├── data ├── .gitignore └── migration │ ├── v0-v1 │ ├── notes.md │ └── migration.sql │ ├── v13-v14 │ └── migration.sql │ ├── v12-13 │ └── moderation.sql │ ├── v10-v11 │ └── migration.sql │ ├── v2-v3 │ └── migration.sql │ ├── v3-v4 │ └── migration.sql │ ├── v4-v5 │ └── migration.sql │ ├── v7-v8 │ └── migration.sql │ └── v6-v7 │ └── migration.sql ├── logs └── .gitignore ├── locales ├── domains │ └── .gitignore ├── templates │ ├── .gitignore │ ├── core_config.pot │ ├── timer-gui.pot │ ├── base.pot │ ├── profile-gui.pot │ ├── test.pot │ ├── config.pot │ ├── exec.pot │ ├── goals-gui.pot │ ├── leaderboard-gui.pot │ ├── wards.pot │ ├── weekly-gui.pot │ └── user_config.pot ├── ceaser │ └── LC_MESSAGES │ │ ├── .gitignore │ │ ├── core_config.po │ │ ├── timer-gui.po │ │ ├── base.po │ │ ├── profile-gui.po │ │ ├── test.po │ │ ├── goals-gui.po │ │ ├── leaderboard-gui.po │ │ ├── config.po │ │ └── exec.po ├── he_IL │ └── LC_MESSAGES │ │ ├── core_config.po │ │ ├── timer-gui.po │ │ ├── base.po │ │ ├── profile-gui.po │ │ └── test.po └── pt_BR │ └── LC_MESSAGES │ ├── core_config.po │ ├── timer-gui.po │ ├── base.po │ ├── profile-gui.po │ └── test.po ├── src ├── modules │ ├── rolemenus │ │ ├── lib.py │ │ ├── ui │ │ │ ├── menu.py │ │ │ ├── msgeditor.py │ │ │ └── roleeditor.py │ │ ├── __init__.py │ │ └── data.py │ ├── test │ │ ├── .gitignore │ │ └── __init__.py │ ├── shop │ │ ├── shops │ │ │ ├── boosts.py │ │ │ ├── roles.py │ │ │ └── __init__.py │ │ └── __init__.py │ ├── statistics │ │ ├── config.py │ │ ├── ui │ │ │ ├── lib.py │ │ │ ├── __init__.py │ │ │ └── summary.py │ │ ├── graphics │ │ │ ├── __init__.py │ │ │ ├── profilestats.py │ │ │ ├── leaderboard.py │ │ │ └── weekly.py │ │ ├── notes.txt │ │ ├── __init__.py │ │ └── lib.py │ ├── skins │ │ ├── editor │ │ │ ├── __init__.py │ │ │ └── pages │ │ │ │ ├── __init__.py │ │ │ │ └── summary.py │ │ ├── __init__.py │ │ └── settings.py │ ├── moderation │ │ ├── tickets │ │ │ ├── __init__.py │ │ │ ├── note.py │ │ │ └── warning.py │ │ └── __init__.py │ ├── pomodoro │ │ ├── ui │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── lib.py │ │ ├── data.py │ │ ├── graphics.py │ │ ├── settings.py │ │ └── settingui.py │ ├── schedule │ │ ├── core │ │ │ ├── __init__.py │ │ │ └── session_member.py │ │ └── __init__.py │ ├── sponsors │ │ ├── data.py │ │ ├── __init__.py │ │ └── settings.py │ ├── ranks │ │ ├── ui │ │ │ └── __init__.py │ │ └── __init__.py │ ├── meta │ │ ├── __init__.py │ │ └── help.txt │ ├── member_admin │ │ ├── data.py │ │ └── __init__.py │ ├── video_channels │ │ ├── data.py │ │ └── __init__.py │ ├── rooms │ │ ├── __init__.py │ │ ├── lib.py │ │ └── data.py │ ├── topgg │ │ ├── __init__.py │ │ └── data.py │ ├── economy │ │ ├── __init__.py │ │ └── settingui.py │ ├── premium │ │ ├── __init__.py │ │ ├── errors.py │ │ └── data.py │ ├── config │ │ └── __init__.py │ ├── tasklist │ │ ├── __init__.py │ │ └── data.py │ ├── user_config │ │ └── __init__.py │ ├── reminders │ │ └── __init__.py │ ├── sysadmin │ │ ├── __init__.py │ │ ├── dash.py │ │ └── leo_group.py │ └── __init__.py ├── tracking │ ├── __init__.py │ ├── text │ │ └── __init__.py │ └── voice │ │ └── __init__.py ├── meta │ ├── ipc │ │ └── __init__.py │ ├── __init__.py │ ├── context.py │ ├── args.py │ ├── sharding.py │ ├── pending-rewrite │ │ └── client.py │ ├── app.py │ ├── errors.py │ └── LionCog.py ├── analytics │ ├── __init__.py │ └── snapshot.py ├── constants.py ├── utils │ ├── __init__.py │ ├── ui │ │ ├── __init__.py │ │ ├── hooked.py │ │ └── transformed.py │ ├── ansi.py │ └── transformers.py ├── babel │ ├── __init__.py │ └── utils.py ├── settings │ ├── mock.py │ └── __init__.py ├── core │ ├── __init__.py │ ├── settings.py │ ├── config.py │ └── lion_user.py └── data │ ├── __init__.py │ ├── base.py │ ├── cursor.py │ ├── database.py │ └── adapted.py ├── tests └── gui │ ├── cards │ ├── __init__.py │ ├── goal_sample.py │ ├── leaderboard_sample.py │ ├── tasklist_sample.py │ ├── tasklist_spec_sample.py │ ├── stats_spec_sample.py │ ├── profile_spec_sample.py │ ├── leaderboard_spec_sample.py │ ├── weekly_spec_sample.py │ ├── monthly_spec_sample.py │ └── weeklygoals_spec_sample.py │ └── output │ ├── statscard.png │ ├── profilecard.png │ ├── example_avatar.png │ ├── statscard_alt.png │ ├── profile_sample.py │ └── stats_sample.py ├── assets └── pomodoro │ ├── break_alert.wav │ ├── focus_alert.wav │ ├── break_alert_0.wav │ ├── break_alert_1.wav │ ├── focus_alert_0.wav │ └── focus_alert_1.wav ├── config ├── example-gui.conf ├── example-secrets.conf ├── example-bot.conf └── emojis.conf ├── .gitmodules ├── requirements.txt ├── transifex.yml ├── scripts ├── start_leo.py ├── start_gui.py ├── makestrings.sh ├── start_analytics.py ├── start_registry.py ├── start_leo_debug.py ├── makedomains.py └── maketestlang.py ├── .github └── FUNDING.yml ├── LICENSE.md └── .gitignore /data/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /locales/domains/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /locales/templates/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/rolemenus/lib.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/test/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tracking/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/gui/cards/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/rolemenus/ui/menu.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/shop/shops/boosts.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/shop/shops/roles.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/statistics/config.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/statistics/ui/lib.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /locales/ceaser/LC_MESSAGES/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/rolemenus/ui/msgeditor.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/rolemenus/ui/roleeditor.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/skins/editor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/statistics/graphics/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/shop/shops/__init__.py: -------------------------------------------------------------------------------- 1 | from . import base 2 | from . import colours 3 | -------------------------------------------------------------------------------- /src/modules/moderation/tickets/__init__.py: -------------------------------------------------------------------------------- 1 | from .note import NoteTicket 2 | from .warning import WarnTicket 3 | -------------------------------------------------------------------------------- /assets/pomodoro/break_alert.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StudyLions/StudyLion/HEAD/assets/pomodoro/break_alert.wav -------------------------------------------------------------------------------- /assets/pomodoro/focus_alert.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StudyLions/StudyLion/HEAD/assets/pomodoro/focus_alert.wav -------------------------------------------------------------------------------- /config/example-gui.conf: -------------------------------------------------------------------------------- 1 | [GUI] 2 | skin_data_path = ../skins/ 3 | process_count = 10 4 | socket_path = gui.sock 5 | -------------------------------------------------------------------------------- /src/meta/ipc/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import AppClient, AppPayload, AppRoute 2 | from .server import AppServer 3 | -------------------------------------------------------------------------------- /tests/gui/output/statscard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StudyLions/StudyLion/HEAD/tests/gui/output/statscard.png -------------------------------------------------------------------------------- /assets/pomodoro/break_alert_0.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StudyLions/StudyLion/HEAD/assets/pomodoro/break_alert_0.wav -------------------------------------------------------------------------------- /assets/pomodoro/break_alert_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StudyLions/StudyLion/HEAD/assets/pomodoro/break_alert_1.wav -------------------------------------------------------------------------------- /assets/pomodoro/focus_alert_0.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StudyLions/StudyLion/HEAD/assets/pomodoro/focus_alert_0.wav -------------------------------------------------------------------------------- /assets/pomodoro/focus_alert_1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StudyLions/StudyLion/HEAD/assets/pomodoro/focus_alert_1.wav -------------------------------------------------------------------------------- /tests/gui/output/profilecard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StudyLions/StudyLion/HEAD/tests/gui/output/profilecard.png -------------------------------------------------------------------------------- /tests/gui/output/example_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StudyLions/StudyLion/HEAD/tests/gui/output/example_avatar.png -------------------------------------------------------------------------------- /tests/gui/output/statscard_alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StudyLions/StudyLion/HEAD/tests/gui/output/statscard_alt.png -------------------------------------------------------------------------------- /src/analytics/__init__.py: -------------------------------------------------------------------------------- 1 | from .cog import Analytics 2 | 3 | 4 | async def setup(bot): 5 | await bot.add_cog(Analytics(bot)) 6 | -------------------------------------------------------------------------------- /src/modules/pomodoro/ui/__init__.py: -------------------------------------------------------------------------------- 1 | from .status import TimerStatusUI 2 | from .edit import TimerEditor 3 | from .config import TimerOptionsUI 4 | -------------------------------------------------------------------------------- /config/example-secrets.conf: -------------------------------------------------------------------------------- 1 | [STUDYLION] 2 | token = 3 | 4 | [DATA] 5 | args = dbname=lion_data 6 | appid = StudyLion 7 | 8 | [TOPGG] 9 | auth = 10 | -------------------------------------------------------------------------------- /src/modules/schedule/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .session_member import SessionMember 2 | from .session import ScheduledSession 3 | from .timeslot import TimeSlot 4 | -------------------------------------------------------------------------------- /src/modules/statistics/ui/__init__.py: -------------------------------------------------------------------------------- 1 | from .profile import ProfileUI 2 | from .weeklymonthly import WeeklyMonthlyUI 3 | from .leaderboard import LeaderboardUI 4 | -------------------------------------------------------------------------------- /src/modules/sponsors/data.py: -------------------------------------------------------------------------------- 1 | from data import Registry, Table 2 | 3 | 4 | class SponsorData(Registry): 5 | sponsor_whitelist = Table('sponsor_guild_whitelist') 6 | -------------------------------------------------------------------------------- /src/modules/statistics/notes.txt: -------------------------------------------------------------------------------- 1 | # Questions 2 | - New GUI designs or sample layouts for each card in general mode? 3 | - New achievements? Should they be customisable? 4 | -------------------------------------------------------------------------------- /data/migration/v0-v1/notes.md: -------------------------------------------------------------------------------- 1 | The purpose of this migration is to remove the guild dependency on tasklist tasks, so that a user's tasklist is available across all guilds. 2 | -------------------------------------------------------------------------------- /src/constants.py: -------------------------------------------------------------------------------- 1 | CONFIG_FILE = "config/bot.conf" 2 | DATA_VERSION = 14 3 | 4 | MAX_COINS = 2147483647 - 1 5 | 6 | HINT_ICON = "https://projects.iamcal.com/emoji-data/img-apple-64/1f4a1.png" 7 | -------------------------------------------------------------------------------- /src/modules/shop/__init__.py: -------------------------------------------------------------------------------- 1 | from babel import LocalBabel 2 | 3 | babel = LocalBabel('shop') 4 | 5 | 6 | async def setup(bot): 7 | from .cog import Shopping 8 | await bot.add_cog(Shopping(bot)) 9 | -------------------------------------------------------------------------------- /src/modules/ranks/ui/__init__.py: -------------------------------------------------------------------------------- 1 | from .editor import RankEditor 2 | from .preview import RankPreviewUI 3 | from .overview import RankOverviewUI 4 | from .config import RankConfigUI 5 | from .refresh import RankRefreshUI 6 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from babel.translator import LocalBabel 2 | 3 | util_babel = LocalBabel('utils') 4 | 5 | 6 | async def setup(bot): 7 | from .cog import MetaUtils 8 | await bot.add_cog(MetaUtils(bot)) 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "bot/gui"] 2 | path = src/gui 3 | url = https://github.com/StudyLions/StudyLion-Plugin-GUI.git 4 | [submodule "skins"] 5 | path = skins 6 | url = https://github.com/StudyLions/StudyLion-Plugin-Skins.git 7 | -------------------------------------------------------------------------------- /src/modules/meta/__init__.py: -------------------------------------------------------------------------------- 1 | from babel.translator import LocalBabel 2 | 3 | babel = LocalBabel('meta') 4 | 5 | 6 | async def setup(bot): 7 | from .cog import MetaCog 8 | 9 | await bot.add_cog(MetaCog(bot)) 10 | -------------------------------------------------------------------------------- /src/modules/member_admin/data.py: -------------------------------------------------------------------------------- 1 | from data import Registry, Table 2 | 3 | 4 | class MemberAdminData(Registry): 5 | autoroles = Table('autoroles') 6 | bot_autoroles = Table('bot_autoroles') 7 | past_roles = Table('past_member_roles') 8 | -------------------------------------------------------------------------------- /src/modules/test/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | async def setup(bot): 4 | # from .test import TestCog 5 | # from .data import test_data 6 | 7 | # bot.db.load_registry(test_data) 8 | # await bot.add_cog(TestCog(bot)) 9 | pass 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.7.4.post0 2 | cachetools==4.2.2 3 | configparser==5.0.2 4 | discord.py [voice] 5 | iso8601==0.1.16 6 | psycopg[pool] 7 | pytz==2021.1 8 | topggpy 9 | psutil 10 | pillow 11 | python-dateutil 12 | bidict 13 | frozendict 14 | -------------------------------------------------------------------------------- /transifex.yml: -------------------------------------------------------------------------------- 1 | git: 2 | filters: 3 | - filter_type: dynamic 4 | file_format: PO 5 | source_language: en 6 | source_files_expression: locales/templates/.pot 7 | translation_files_expression: locales//LC_MESSAGES/.po 8 | -------------------------------------------------------------------------------- /data/migration/v13-v14/migration.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | ALTER TABLE bot_config ADD COLUMN sponsor_prompt TEXT; 4 | ALTER TABLE bot_config ADD COLUMN sponsor_message TEXT; 5 | 6 | INSERT INTO VersionHistory (version, author) VALUES (14, 'v13-v14 migration'); 7 | COMMIT; 8 | -------------------------------------------------------------------------------- /src/babel/__init__.py: -------------------------------------------------------------------------------- 1 | from .translator import SOURCE_LOCALE, LeoBabel, LocalBabel, LazyStr, ctx_locale, ctx_translator 2 | 3 | babel = LocalBabel('babel') 4 | 5 | 6 | async def setup(bot): 7 | from .cog import BabelCog 8 | await bot.add_cog(BabelCog(bot)) 9 | -------------------------------------------------------------------------------- /src/modules/video_channels/data.py: -------------------------------------------------------------------------------- 1 | from data import Registry, Table 2 | 3 | 4 | class VideoData(Registry): 5 | video_channels = Table('video_channels') 6 | video_exempt_roles = Table('video_exempt_roles') 7 | video_blacklist_durations = Table('studyban_durations') 8 | -------------------------------------------------------------------------------- /scripts/start_leo.py: -------------------------------------------------------------------------------- 1 | # !/bin/python3 2 | 3 | import sys 4 | import os 5 | 6 | sys.path.insert(0, os.path.join(os.getcwd())) 7 | sys.path.insert(0, os.path.join(os.getcwd(), "src")) 8 | 9 | 10 | if __name__ == '__main__': 11 | from bot import _main 12 | _main() 13 | -------------------------------------------------------------------------------- /src/settings/mock.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord import app_commands 3 | 4 | 5 | class LocalString: 6 | def __init__(self, string): 7 | self.string = string 8 | 9 | def as_string(self): 10 | return self.string 11 | 12 | 13 | _ = LocalString 14 | -------------------------------------------------------------------------------- /src/modules/rooms/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | logger = logging.getLogger('Rooms') 5 | babel = LocalBabel('rooms') 6 | 7 | 8 | async def setup(bot): 9 | from .cog import RoomCog 10 | await bot.add_cog(RoomCog(bot)) 11 | -------------------------------------------------------------------------------- /src/modules/topgg/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | babel = LocalBabel('topgg') 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | async def setup(bot): 9 | from .cog import TopggCog 10 | await bot.add_cog(TopggCog(bot)) 11 | -------------------------------------------------------------------------------- /src/modules/pomodoro/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | babel = LocalBabel('Pomodoro') 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | async def setup(bot): 9 | from .cog import TimerCog 10 | await bot.add_cog(TimerCog(bot)) 11 | -------------------------------------------------------------------------------- /src/modules/economy/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | logger = logging.getLogger(__name__) 5 | babel = LocalBabel('economy') 6 | 7 | 8 | async def setup(bot): 9 | from .cog import Economy 10 | 11 | await bot.add_cog(Economy(bot)) 12 | -------------------------------------------------------------------------------- /src/modules/premium/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | babel = LocalBabel('premium') 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | async def setup(bot): 9 | from .cog import PremiumCog 10 | await bot.add_cog(PremiumCog(bot)) 11 | -------------------------------------------------------------------------------- /src/modules/sponsors/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | babel = LocalBabel('sponsors') 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | async def setup(bot): 9 | from .cog import SponsorCog 10 | await bot.add_cog(SponsorCog(bot)) 11 | -------------------------------------------------------------------------------- /src/modules/video_channels/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | logger = logging.getLogger(__name__) 5 | babel = LocalBabel('video') 6 | 7 | 8 | async def setup(bot): 9 | from .cog import VideoCog 10 | await bot.add_cog(VideoCog(bot)) 11 | -------------------------------------------------------------------------------- /src/modules/rolemenus/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | logger = logging.getLogger(__name__) 5 | babel = LocalBabel('rolemenus') 6 | 7 | 8 | async def setup(bot): 9 | from .cog import RoleMenuCog 10 | await bot.add_cog(RoleMenuCog(bot)) 11 | -------------------------------------------------------------------------------- /src/modules/schedule/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | logger = logging.getLogger(__name__) 5 | babel = LocalBabel('schedule') 6 | 7 | 8 | async def setup(bot): 9 | from .cog import ScheduleCog 10 | await bot.add_cog(ScheduleCog(bot)) 11 | -------------------------------------------------------------------------------- /src/modules/skins/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | babel = LocalBabel('customskins') 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | async def setup(bot): 9 | from .cog import CustomSkinCog 10 | await bot.add_cog(CustomSkinCog(bot)) 11 | -------------------------------------------------------------------------------- /src/modules/config/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | logger = logging.getLogger(__name__) 5 | babel = LocalBabel('config') 6 | 7 | 8 | async def setup(bot): 9 | from .cog import GuildConfigCog 10 | 11 | await bot.add_cog(GuildConfigCog(bot)) 12 | -------------------------------------------------------------------------------- /src/modules/moderation/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | logger = logging.getLogger(__name__) 5 | babel = LocalBabel('moderation') 6 | 7 | 8 | async def setup(bot): 9 | from .cog import ModerationCog 10 | await bot.add_cog(ModerationCog(bot)) 11 | -------------------------------------------------------------------------------- /src/modules/tasklist/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | 5 | babel = LocalBabel('tasklist') 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | async def setup(bot): 10 | from .cog import TasklistCog 11 | await bot.add_cog(TasklistCog(bot)) 12 | -------------------------------------------------------------------------------- /src/core/__init__.py: -------------------------------------------------------------------------------- 1 | from babel.translator import LocalBabel 2 | 3 | 4 | babel = LocalBabel('lion-core') 5 | 6 | 7 | async def setup(bot): 8 | from .cog import CoreCog 9 | from .config import ConfigCog 10 | 11 | await bot.add_cog(CoreCog(bot)) 12 | await bot.add_cog(ConfigCog(bot)) 13 | -------------------------------------------------------------------------------- /src/modules/member_admin/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | logger = logging.getLogger(__name__) 5 | babel = LocalBabel('member_admin') 6 | 7 | 8 | async def setup(bot): 9 | from .cog import MemberAdminCog 10 | await bot.add_cog(MemberAdminCog(bot)) 11 | -------------------------------------------------------------------------------- /src/tracking/text/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | logger = logging.getLogger(__name__) 5 | babel = LocalBabel('text-tracker') 6 | 7 | 8 | async def setup(bot): 9 | from .cog import TextTrackerCog 10 | 11 | await bot.add_cog(TextTrackerCog(bot)) 12 | -------------------------------------------------------------------------------- /src/modules/user_config/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | logger = logging.getLogger(__name__) 5 | babel = LocalBabel('user_config') 6 | 7 | 8 | async def setup(bot): 9 | from .cog import UserConfigCog 10 | 11 | await bot.add_cog(UserConfigCog(bot)) 12 | -------------------------------------------------------------------------------- /src/tracking/voice/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | 4 | logger = logging.getLogger(__name__) 5 | babel = LocalBabel('voice-tracker') 6 | 7 | 8 | async def setup(bot): 9 | from .cog import VoiceTrackerCog 10 | 11 | await bot.add_cog(VoiceTrackerCog(bot)) 12 | -------------------------------------------------------------------------------- /scripts/start_gui.py: -------------------------------------------------------------------------------- 1 | # !/bin/python3 2 | 3 | import sys 4 | import os 5 | import asyncio 6 | 7 | sys.path.insert(0, os.path.join(os.getcwd())) 8 | sys.path.insert(0, os.path.join(os.getcwd(), "src")) 9 | 10 | 11 | if __name__ == '__main__': 12 | from gui.server import main 13 | asyncio.run(main()) 14 | -------------------------------------------------------------------------------- /src/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from babel.translator import LocalBabel 2 | babel = LocalBabel('settings_base') 3 | 4 | from .data import ModelData, ListData 5 | from .base import BaseSetting 6 | from .ui import SettingWidget, InteractiveSetting 7 | from .groups import SettingDotDict, SettingGroup, ModelSettings, ModelSetting 8 | -------------------------------------------------------------------------------- /scripts/makestrings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for filename in locales/domains/*.txt; do 4 | domain=$(basename "$filename" .txt) 5 | echo "Creating template for domain: $domain" 6 | xgettext -f $filename -o locales/templates/$domain.pot --keyword=_p:1c,2 --keyword=_n:1,2 --keyword=_np:1c,2,3 -c --debug --from-code=utf-8 7 | done 8 | -------------------------------------------------------------------------------- /src/modules/ranks/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from meta import LionBot 3 | from babel.translator import LocalBabel 4 | 5 | 6 | babel = LocalBabel('ranks') 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | async def setup(bot: LionBot): 11 | from .cog import RankCog 12 | await bot.add_cog(RankCog(bot)) 13 | -------------------------------------------------------------------------------- /src/modules/reminders/__init__.py: -------------------------------------------------------------------------------- 1 | from babel.translator import LocalBabel 2 | 3 | babel = LocalBabel('reminders') 4 | 5 | import logging 6 | logger = logging.getLogger(__name__) 7 | logger.debug("Loaded reminders") 8 | 9 | 10 | from .cog import Reminders 11 | 12 | async def setup(bot): 13 | await bot.add_cog(Reminders(bot)) 14 | -------------------------------------------------------------------------------- /src/modules/statistics/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from babel.translator import LocalBabel 3 | from meta.LionBot import LionBot 4 | 5 | babel = LocalBabel('statistics') 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | async def setup(bot: LionBot): 10 | from .cog import StatsCog 11 | 12 | await bot.add_cog(StatsCog(bot)) 13 | -------------------------------------------------------------------------------- /data/migration/v0-v1/migration.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE tasklist DROP COLUMN guildid; 2 | CREATE INDEX tasklist_users ON tasklist (userid); 3 | 4 | ALTER TABLE tasklist_reward_history DROP COLUMN guildid; 5 | CREATE INDEX tasklist_reward_history_users ON tasklist_reward_history (userid, reward_time); 6 | 7 | 8 | INSERT INTO VersionHistory (version, author) VALUES (1, 'Migration v0-v1'); 9 | -------------------------------------------------------------------------------- /scripts/start_analytics.py: -------------------------------------------------------------------------------- 1 | # !/bin/python3 2 | 3 | import sys 4 | import os 5 | import asyncio 6 | 7 | sys.path.insert(0, os.path.join(os.getcwd())) 8 | sys.path.insert(0, os.path.join(os.getcwd(), "src")) 9 | 10 | 11 | if __name__ == '__main__': 12 | from analytics.server import AnalyticsServer 13 | server = AnalyticsServer() 14 | asyncio.run(server.run()) 15 | -------------------------------------------------------------------------------- /tests/gui/cards/goal_sample.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime as dt 3 | from src.cards import WeeklyGoalCard 4 | 5 | 6 | async def get_card(): 7 | card = await WeeklyGoalCard.generate_sample() 8 | with open('samples/weekly-sample.png', 'wb') as image_file: 9 | image_file.write(card.fp.read()) 10 | 11 | if __name__ == '__main__': 12 | asyncio.run(get_card()) 13 | -------------------------------------------------------------------------------- /tests/gui/cards/leaderboard_sample.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime as dt 3 | from src.cards import LeaderboardCard 4 | 5 | 6 | async def get_card(): 7 | card = await LeaderboardCard.generate_sample() 8 | with open('samples/leaderboard-sample.png', 'wb') as image_file: 9 | image_file.write(card.fp.read()) 10 | 11 | if __name__ == '__main__': 12 | asyncio.run(get_card()) 13 | -------------------------------------------------------------------------------- /src/core/settings.py: -------------------------------------------------------------------------------- 1 | from settings.groups import ModelConfig, SettingDotDict 2 | 3 | from .data import CoreData 4 | 5 | 6 | class GuildConfig(ModelConfig): 7 | settings = SettingDotDict() 8 | _model_settings = set() 9 | model = CoreData.Guild 10 | 11 | 12 | class UserConfig(ModelConfig): 13 | settings = SettingDotDict() 14 | _model_settings = set() 15 | model = CoreData.User 16 | -------------------------------------------------------------------------------- /src/modules/topgg/data.py: -------------------------------------------------------------------------------- 1 | from data import Registry, Table, RowModel 2 | from data.columns import Integer, Timestamp 3 | 4 | 5 | class TopggData(Registry): 6 | class TopGG(RowModel): 7 | _tablename_ = 'topgg' 8 | 9 | voteid = Integer(primary=True) 10 | userid = Integer() 11 | boostedtimestamp = Timestamp() 12 | 13 | guild_whitelist = Table('topgg_guild_whitelist') 14 | -------------------------------------------------------------------------------- /src/data/__init__.py: -------------------------------------------------------------------------------- 1 | from .conditions import Condition, condition, NULL 2 | from .database import Database 3 | from .models import RowModel, RowTable, WeakCache 4 | from .table import Table 5 | from .base import Expression, RawExpr 6 | from .columns import ColumnExpr, Column, Integer, String 7 | from .registry import Registry, AttachableClass, Attachable 8 | from .adapted import RegisterEnum 9 | from .queries import ORDER, NULLS, JOINTYPE 10 | -------------------------------------------------------------------------------- /data/migration/v12-13/moderation.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS video_exempt_roles CASCADE; 2 | UPDATE guild_config SET studyban_role = NULL WHERE video_studyban = False; 3 | 4 | CREATE TABLE video_exempt_roles( 5 | guildid BIGINT NOT NULL, 6 | roleid BIGINT NOT NULL, 7 | _timestamp TIMESTAMPTZ NOT NULL DEFAULT now(), 8 | FOREIGN KEY (guildid) REFERENCES guild_config (guildid) ON DELETE CASCADE ON UPDATE CASCADE, 9 | PRIMARY KEY (guildid, roleid) 10 | ); 11 | -------------------------------------------------------------------------------- /scripts/start_registry.py: -------------------------------------------------------------------------------- 1 | # !/bin/python3 2 | 3 | import sys 4 | import os 5 | import asyncio 6 | 7 | sys.path.insert(0, os.path.join(os.getcwd())) 8 | sys.path.insert(0, os.path.join(os.getcwd(), "src")) 9 | 10 | 11 | if __name__ == '__main__': 12 | from meta.ipc.server import AppServer 13 | from meta import conf 14 | address = {'host': conf.appipc['server_host'], 'port': int(conf.appipc['server_port'])} 15 | server = AppServer() 16 | asyncio.run(server.start(address)) 17 | -------------------------------------------------------------------------------- /src/modules/premium/errors.py: -------------------------------------------------------------------------------- 1 | class GemTransactionFailed(Exception): 2 | """ 3 | Base exception class used when a gem transaction failed. 4 | """ 5 | pass 6 | 7 | 8 | class BalanceTooLow(GemTransactionFailed): 9 | """ 10 | Exception raised when transaction results in a negative gem balance. 11 | """ 12 | pass 13 | 14 | 15 | class BalanceTooHigh(GemTransactionFailed): 16 | """ 17 | Exception raised when transaction results in gem balance overflow. 18 | """ 19 | pass 20 | -------------------------------------------------------------------------------- /tests/gui/cards/tasklist_sample.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime as dt 3 | from src.cards import TasklistCard 4 | 5 | 6 | highlight = "mini_profile_badge_text_colour" 7 | highlight_colour = "#E84727" 8 | 9 | async def get_card(): 10 | card = await TasklistCard.generate_sample( 11 | skin={highlight: highlight_colour} 12 | ) 13 | with open('samples/tasklist-sample.png', 'wb') as image_file: 14 | image_file.write(card.fp.read()) 15 | 16 | if __name__ == '__main__': 17 | asyncio.run(get_card()) 18 | -------------------------------------------------------------------------------- /src/modules/skins/editor/pages/__init__.py: -------------------------------------------------------------------------------- 1 | from .stats import stats_page 2 | from .profile import profile_page 3 | from .summary import summary_page 4 | from .weekly import weekly_page 5 | from .monthly import monthly_page 6 | from .weekly_goals import weekly_goal_page 7 | from .monthly_goals import monthly_goal_page 8 | from .leaderboard import leaderboard_page 9 | 10 | 11 | pages = [ 12 | profile_page, stats_page, 13 | weekly_page, monthly_page, 14 | weekly_goal_page, monthly_goal_page, 15 | leaderboard_page, 16 | ] 17 | -------------------------------------------------------------------------------- /src/modules/sysadmin/__init__.py: -------------------------------------------------------------------------------- 1 | from babel.translator import LocalBabel 2 | babel = LocalBabel('sysadmin') 3 | 4 | 5 | async def setup(bot): 6 | from .exec_cog import Exec 7 | from .blacklists import Blacklists 8 | from .guild_log import GuildLog 9 | from .presence import PresenceCtrl 10 | 11 | from .dash import LeoSettings 12 | await bot.add_cog(LeoSettings(bot)) 13 | 14 | await bot.add_cog(Blacklists(bot)) 15 | await bot.add_cog(Exec(bot)) 16 | await bot.add_cog(GuildLog(bot)) 17 | await bot.add_cog(PresenceCtrl(bot)) 18 | -------------------------------------------------------------------------------- /src/babel/utils.py: -------------------------------------------------------------------------------- 1 | from .translator import ctx_translator 2 | from . import babel 3 | 4 | _, _p, _np = babel._, babel._p, babel._np 5 | 6 | 7 | MONTHS = _p( 8 | 'utils|months', 9 | "January,February,March,April,May,June,July,August,September,October,November,December" 10 | ) 11 | 12 | SHORT_MONTHS = _p( 13 | 'utils|short_months', 14 | "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec" 15 | ) 16 | 17 | 18 | def local_month(month, short=False): 19 | string = MONTHS if not short else SHORT_MONTHS 20 | return ctx_translator.get().t(string).split(',')[month-1] 21 | -------------------------------------------------------------------------------- /data/migration/v10-v11/migration.sql: -------------------------------------------------------------------------------- 1 | -- App Config Data {{{ 2 | CREATE TABLE AppConfig( 3 | appid TEXT, 4 | key TEXT, 5 | value TEXT, 6 | PRIMARY KEY(appid, key) 7 | ); 8 | -- }}} 9 | 10 | 11 | -- Sponsor Data {{{ 12 | CREATE TABLE sponsor_guild_whitelist( 13 | appid TEXT, 14 | guildid BIGINT, 15 | PRIMARY KEY(appid, guildid) 16 | ); 17 | -- }}} 18 | 19 | -- Topgg Data {{{ 20 | CREATE TABLE topgg_guild_whitelist( 21 | appid TEXT, 22 | guildid BIGINT, 23 | PRIMARY KEY(appid, guildid) 24 | ); 25 | -- }}} 26 | 27 | INSERT INTO VersionHistory (version, author) VALUES (11, 'v10-v11 migration'); 28 | -------------------------------------------------------------------------------- /src/meta/__init__.py: -------------------------------------------------------------------------------- 1 | from .LionBot import LionBot 2 | from .LionCog import LionCog 3 | from .LionContext import LionContext 4 | from .LionTree import LionTree 5 | 6 | from .logger import logging_context, log_wrap, log_action_stack, log_context, log_app 7 | from .config import conf, configEmoji 8 | from .args import args 9 | from .app import appname, shard_talk, appname_from_shard, shard_from_appname 10 | from .errors import HandledException, UserInputError, ResponseTimedOut, SafeCancellation, UserCancelled 11 | from .context import context, ctx_bot 12 | 13 | from . import sharding 14 | from . import logger 15 | from . import app 16 | -------------------------------------------------------------------------------- /tests/gui/output/profile_sample.py: -------------------------------------------------------------------------------- 1 | from profile import ProfileCard 2 | 3 | 4 | card = ProfileCard( 5 | name='ARI HORESH', 6 | discrim='#0001', 7 | avatar=open('samples/example_avatar.png', 'rb'), 8 | coins=58596, 9 | time=3750 * 3600, 10 | answers=10, 11 | attendance=0.9, 12 | badges=('MEDICINE', 'NEUROSCIENCE', 'BIO', 'MATHS', 'BACHELOR\'S DEGREE', 'VEGAN SOMETIMES', 'EUROPE'), 13 | achievements=(0, 2, 5, 7), 14 | current_rank=('VAMPIRE', 3000, 4000), 15 | next_rank=('WIZARD', 4000, 8000), 16 | draft=False 17 | ) 18 | image = card.draw() 19 | image.save('profilecard.png', dpi=(150, 150)) 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: AriHoresh 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | -------------------------------------------------------------------------------- /src/utils/ui/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from .. import util_babel 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | from .hooked import * 8 | from .leo import * 9 | from .micros import * 10 | from .pagers import * 11 | from .transformed import * 12 | from .config import * 13 | from .msgeditor import * 14 | 15 | 16 | # def create_task_in(coro, context: Context): 17 | # """ 18 | # Transitional. 19 | # Since py3.10 asyncio does not support context instantiation, 20 | # this helper method runs `asyncio.create_task(coro)` inside the given context. 21 | # """ 22 | # return context.run(asyncio.create_task, coro) 23 | -------------------------------------------------------------------------------- /data/migration/v2-v3/migration.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE global_user_blacklist( 2 | userid BIGINT PRIMARY KEY, 3 | ownerid BIGINT NOT NULL, 4 | reason TEXT NOT NULL, 5 | created_at TIMESTAMPTZ DEFAULT now() 6 | ); 7 | 8 | CREATE TABLE global_guild_blacklist( 9 | guildid BIGINT PRIMARY KEY, 10 | ownerid BIGINT NOT NULL, 11 | reason TEXT NOT NULL, 12 | created_at TIMESTAMPTZ DEFAULT now() 13 | ); 14 | 15 | CREATE TABLE ignored_members( 16 | guildid BIGINT NOT NULL, 17 | userid BIGINT NOT NULL 18 | ); 19 | CREATE INDEX ignored_member_guilds ON ignored_members (guildid); 20 | 21 | 22 | INSERT INTO VersionHistory (version, author) VALUES (3, 'v2-v3 Migration'); 23 | -------------------------------------------------------------------------------- /src/meta/context.py: -------------------------------------------------------------------------------- 1 | """ 2 | Namespace for various global context variables. 3 | Allows asyncio callbacks to accurately retrieve information about the current state. 4 | """ 5 | 6 | 7 | from typing import TYPE_CHECKING, Optional 8 | 9 | from contextvars import ContextVar 10 | 11 | if TYPE_CHECKING: 12 | from .LionBot import LionBot 13 | from .LionContext import LionContext 14 | 15 | 16 | # Contains the current command context, if applicable 17 | context: ContextVar[Optional['LionContext']] = ContextVar('context', default=None) 18 | 19 | # Contains the current LionBot instance 20 | ctx_bot: ContextVar[Optional['LionBot']] = ContextVar('bot', default=None) 21 | -------------------------------------------------------------------------------- /src/modules/__init__.py: -------------------------------------------------------------------------------- 1 | this_package = 'modules' 2 | 3 | active = [ 4 | '.sysadmin', 5 | '.config', 6 | '.user_config', 7 | '.skins', 8 | '.schedule', 9 | '.economy', 10 | '.ranks', 11 | '.reminders', 12 | '.shop', 13 | '.statistics', 14 | '.pomodoro', 15 | '.rooms', 16 | '.tasklist', 17 | '.rolemenus', 18 | '.member_admin', 19 | '.moderation', 20 | '.video_channels', 21 | '.meta', 22 | '.sponsors', 23 | '.topgg', 24 | '.premium', 25 | '.test', 26 | ] 27 | 28 | 29 | async def setup(bot): 30 | for ext in active: 31 | await bot.load_extension(ext, package=this_package) 32 | -------------------------------------------------------------------------------- /tests/gui/output/stats_sample.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | from stats import StatsCard 3 | 4 | 5 | card = StatsCard( 6 | (21, 123), 7 | (3600, 5 * 24 * 3600, 1.5 * 24 * 3600, 100 * 24 * 3600), 8 | 50, 9 | [(1, 3), (7, 8), (10, 10), (12, 16), (18, 25), (27, 31)], 10 | date=dt.datetime(2022, 1, 1), 11 | # draft=True 12 | ) 13 | 14 | image = card.draw() 15 | image.save('statscard_alt.png', dpi=(150, 150)) 16 | 17 | card = StatsCard( 18 | (21, 123), 19 | (3600, 5 * 24 * 3600, 1.5 * 24 * 3600, 100 * 24 * 3600), 20 | 50, 21 | [(1, 3), (7, 8), (10, 10), (12, 16), (18, 25), (27, 31)], 22 | date=dt.datetime(2022, 2, 1), 23 | # draft=True 24 | ) 25 | 26 | image = card.draw() 27 | image.save('statscard.png', dpi=(150, 150)) 28 | -------------------------------------------------------------------------------- /src/meta/args.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from constants import CONFIG_FILE 4 | 5 | # ------------------------------ 6 | # Parsed commandline arguments 7 | # ------------------------------ 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument( 10 | '--conf', 11 | dest='config', 12 | default=CONFIG_FILE, 13 | help="Path to configuration file." 14 | ) 15 | parser.add_argument( 16 | '--shard', 17 | dest='shard', 18 | default=None, 19 | type=int, 20 | help="Shard number to run, if applicable." 21 | ) 22 | parser.add_argument( 23 | '--host', 24 | dest='host', 25 | default='127.0.0.1', 26 | help="IP address to run the app listener on." 27 | ) 28 | parser.add_argument( 29 | '--port', 30 | dest='port', 31 | default='5001', 32 | help="Port to run the app listener on." 33 | ) 34 | 35 | args = parser.parse_args() 36 | -------------------------------------------------------------------------------- /tests/gui/cards/tasklist_spec_sample.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime as dt 3 | from src.cards import TasklistCard 4 | 5 | 6 | highlights = [ 7 | 'mini_profile_badge_colour', 8 | 'mini_profile_name_colour', 9 | 'mini_profile_discrim_colour', 10 | 'task_done_number_colour', 11 | 'task_done_text_colour', 12 | 'task_undone_text_colour', 13 | 'task_undone_number_colour', 14 | 'footer_colour' 15 | ] 16 | highlight_colour = "#E84727" 17 | 18 | 19 | async def get_cards(): 20 | for highlight in highlights: 21 | card = await TasklistCard.generate_sample( 22 | skin={highlight: highlight_colour} 23 | ) 24 | with open('../skins/spec/images/tasklist/{}.png'.format(highlight), 'wb') as image_file: 25 | image_file.write(card.fp.read()) 26 | 27 | if __name__ == '__main__': 28 | asyncio.run(get_cards()) 29 | -------------------------------------------------------------------------------- /src/modules/pomodoro/lib.py: -------------------------------------------------------------------------------- 1 | import os 2 | from enum import IntEnum 3 | 4 | from meta import conf 5 | 6 | from . import babel 7 | 8 | _p = babel._p 9 | 10 | 11 | class TimerRole(IntEnum): 12 | ADMIN = 3 13 | OWNER = 2 14 | MANAGER = 1 15 | OTHER = 0 16 | 17 | 18 | channel_name_keys = [ 19 | ("{remaining}", _p('formatstring:channel_name|key:remaining', "{remaining}")), 20 | ("{stage}", _p('formatstring:channel_name|key:stage', "{stage}")), 21 | ("{members}", _p('formatstring:channel_name|key:members', "{members}")), 22 | ("{name}", _p('formatstring:channel_name|key:name', "{name}")), 23 | ("{pattern}", _p('formatstring:channel_name|key:pattern', "{pattern}")), 24 | ] 25 | 26 | focus_alert_path = os.path.join(conf.bot.asset_path, 'pomodoro', 'focus_alert.wav') 27 | break_alert_path = os.path.join(conf.bot.asset_path, 'pomodoro', 'break_alert.wav') 28 | -------------------------------------------------------------------------------- /tests/gui/cards/stats_spec_sample.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime as dt 3 | from src.cards import StatsCard 4 | 5 | 6 | highlights = [ 7 | 'header_colour', 8 | 'stats_subheader_colour', 9 | 'stats_text_colour', 10 | 'col2_date_colour', 11 | 'col2_hours_colour', 12 | 'cal_weekday_colour', 13 | 'cal_number_colour', 14 | 'cal_number_end_colour', 15 | 'cal_streak_end_colour', 16 | 'cal_streak_middle_colour', 17 | ] 18 | highlight_colour = "#E84727" 19 | 20 | 21 | async def get_cards(): 22 | for highlight in highlights: 23 | card = await StatsCard.generate_sample( 24 | skin={highlight: highlight_colour} 25 | ) 26 | with open('../skins/spec/images/stats/{}.png'.format(highlight), 'wb') as image_file: 27 | image_file.write(card.fp.read()) 28 | 29 | if __name__ == '__main__': 30 | asyncio.run(get_cards()) 31 | -------------------------------------------------------------------------------- /src/analytics/snapshot.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple 2 | 3 | from meta.context import ctx_bot 4 | 5 | 6 | class ShardSnapshot(NamedTuple): 7 | guild_count: int 8 | voice_count: int 9 | member_count: int 10 | user_count: int 11 | 12 | 13 | async def shard_snapshot(): 14 | """ 15 | Take a snapshot of the current shard. 16 | """ 17 | bot = ctx_bot.get() 18 | if bot is None or not bot.is_ready(): 19 | # We cannot take a snapshot without Bot 20 | # Just quietly fail 21 | return None 22 | snap = ShardSnapshot( 23 | guild_count=len(bot.guilds), 24 | voice_count=sum(len(channel.members) for guild in bot.guilds for channel in guild.voice_channels), 25 | member_count=sum(guild.member_count for guild in bot.guilds), 26 | user_count=len(set(m.id for guild in bot.guilds for m in guild.members)) 27 | ) 28 | return snap 29 | -------------------------------------------------------------------------------- /locales/templates/core_config.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-10-01 16:01+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/core/config.py:28 21 | msgctxt "group:configure" 22 | msgid "configure" 23 | msgstr "" 24 | 25 | #: src/core/config.py:29 26 | msgctxt "group:configure|desc" 27 | msgid "View and adjust my configuration options." 28 | msgstr "" 29 | -------------------------------------------------------------------------------- /locales/ceaser/LC_MESSAGES/core_config.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 1.0\n" 10 | "Report-Msgid-Bugs-To: you@example.com\n" 11 | "POT-Creation-Date: 2007-10-18 14:00+0100\n" 12 | "PO-Revision-Date: 2007-10-18 14:00+0100\n" 13 | "Last-Translator: you \n" 14 | "Language-Team: English \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: src/core/config.py:28 20 | msgctxt "group:configure" 21 | msgid "configure" 22 | msgstr "dpogjhvsf" 23 | 24 | #: src/core/config.py:29 25 | msgctxt "group:configure|desc" 26 | msgid "View and adjust my configuration options." 27 | msgstr "Wjfx boe bekvtu nz dpogjhvsbujpo pqujpot." 28 | -------------------------------------------------------------------------------- /src/meta/sharding.py: -------------------------------------------------------------------------------- 1 | from .args import args 2 | from .config import conf 3 | 4 | from psycopg import sql 5 | from data.conditions import Condition, Joiner 6 | 7 | 8 | shard_number = args.shard or 0 9 | 10 | shard_count = conf.bot.getint('shard_count', 1) 11 | 12 | sharded = (shard_count > 0) 13 | 14 | 15 | def SHARDID(shard_id: int, guild_column: str = 'guildid', shard_count: int = shard_count) -> Condition: 16 | """ 17 | Condition constructor for filtering by shard id. 18 | 19 | Example Usage 20 | ------------- 21 | Query.where(_shard_condition('guildid', 10, 1)) 22 | """ 23 | return Condition( 24 | sql.SQL("({guildid} >> 22) %% {shard_count}").format( 25 | guildid=sql.Identifier(guild_column), 26 | shard_count=sql.Literal(shard_count) 27 | ), 28 | Joiner.EQUALS, 29 | sql.Placeholder(), 30 | (shard_id,) 31 | ) 32 | 33 | 34 | # Pre-built Condition for filtering by current shard. 35 | THIS_SHARD = SHARDID(shard_number) 36 | -------------------------------------------------------------------------------- /locales/templates/timer-gui.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-10-01 16:01+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/gui/cards/timer.py:73 21 | msgctxt "skin:timer|field:date_text" 22 | msgid "Use /now to show what you are working on!" 23 | msgstr "" 24 | 25 | #: src/gui/cards/timer.py:89 26 | msgctxt "skin:timer|stage:focus|field:stage_text" 27 | msgid "FOCUS" 28 | msgstr "" 29 | 30 | #: src/gui/cards/timer.py:103 31 | msgctxt "skin:timer|stage:break|field:stage_text" 32 | msgid "BREAK" 33 | msgstr "" 34 | -------------------------------------------------------------------------------- /data/migration/v3-v4/migration.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE guild_config 2 | ADD COLUMN greeting_channel BIGINT, 3 | ADD COLUMN greeting_message TEXT, 4 | ADD COLUMN returning_message TEXT, 5 | ADD COLUMN starting_funds INTEGER, 6 | ADD COLUMN persist_roles BOOLEAN; 7 | 8 | CREATE INDEX rented_members_users ON rented_members (userid); 9 | 10 | CREATE TABLE autoroles( 11 | guildid BIGINT NOT NULL , 12 | roleid BIGINT NOT NULL 13 | ); 14 | CREATE INDEX autoroles_guilds ON autoroles (guildid); 15 | 16 | CREATE TABLE bot_autoroles( 17 | guildid BIGINT NOT NULL , 18 | roleid BIGINT NOT NULL 19 | ); 20 | CREATE INDEX bot_autoroles_guilds ON bot_autoroles (guildid); 21 | 22 | CREATE TABLE past_member_roles( 23 | guildid BIGINT NOT NULL, 24 | userid BIGINT NOT NULL, 25 | roleid BIGINT NOT NULL, 26 | _timestamp TIMESTAMPTZ DEFAULT now(), 27 | FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid) 28 | ); 29 | CREATE INDEX member_role_persistence_members ON past_member_roles (guildid, userid); 30 | 31 | INSERT INTO VersionHistory (version, author) VALUES (4, 'v3-v4 Migration'); 32 | -------------------------------------------------------------------------------- /locales/ceaser/LC_MESSAGES/timer-gui.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 1.0\n" 10 | "Report-Msgid-Bugs-To: you@example.com\n" 11 | "POT-Creation-Date: 2007-10-18 14:00+0100\n" 12 | "PO-Revision-Date: 2007-10-18 14:00+0100\n" 13 | "Last-Translator: you \n" 14 | "Language-Team: English \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: src/gui/cards/timer.py:73 20 | msgctxt "skin:timer|field:date_text" 21 | msgid "Use /now to show what you are working on!" 22 | msgstr "Vtf /opx up tipx xibu zpv bsf xpsljoh po!" 23 | 24 | #: src/gui/cards/timer.py:89 25 | msgctxt "skin:timer|stage:focus|field:stage_text" 26 | msgid "FOCUS" 27 | msgstr "GPDVT" 28 | 29 | #: src/gui/cards/timer.py:103 30 | msgctxt "skin:timer|stage:break|field:stage_text" 31 | msgid "BREAK" 32 | msgstr "CSFBL" 33 | -------------------------------------------------------------------------------- /locales/templates/base.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-11-22 13:17+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: bot/modules/sysadmin/exec_cog.py:70 21 | msgid "You cannot use this interface!" 22 | msgstr "" 23 | 24 | #: bot/modules/sysadmin/exec_cog.py:252 bot/modules/sysadmin/exec_cog.py:254 25 | msgid "async" 26 | msgstr "" 27 | 28 | #: bot/modules/sysadmin/exec_cog.py:253 bot/modules/sysadmin/exec_cog.py:255 29 | msgid "Execute arbitrary code with Exec" 30 | msgstr "" 31 | 32 | #: bot/modules/sysadmin/exec_cog.py:265 33 | msgid "eval" 34 | msgstr "" 35 | -------------------------------------------------------------------------------- /src/meta/pending-rewrite/client.py: -------------------------------------------------------------------------------- 1 | from discord import Intents 2 | from cmdClient.cmdClient import cmdClient 3 | 4 | from . import patches 5 | from .interactions import InteractionType 6 | from .config import conf 7 | from .sharding import shard_number, shard_count 8 | from LionContext import LionContext 9 | 10 | 11 | # Initialise client 12 | owners = [int(owner) for owner in conf.bot.getlist('owners')] 13 | intents = Intents.all() 14 | intents.presences = False 15 | client = cmdClient( 16 | prefix=conf.bot['prefix'], 17 | owners=owners, 18 | intents=intents, 19 | shard_id=shard_number, 20 | shard_count=shard_count, 21 | baseContext=LionContext 22 | ) 23 | client.conf = conf 24 | 25 | 26 | # TODO: Could include client id here, or app id, to avoid multiple handling. 27 | NOOP_ID = 'NOOP' 28 | 29 | 30 | @client.add_after_event('interaction_create') 31 | async def handle_noop_interaction(client, interaction): 32 | if interaction.interaction_type in (InteractionType.MESSAGE_COMPONENT, InteractionType.MODAL_SUBMIT): 33 | if interaction.custom_id == NOOP_ID: 34 | interaction.ack() 35 | -------------------------------------------------------------------------------- /src/modules/rooms/lib.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import re 3 | 4 | 5 | def parse_members(memberstr: str) -> list[int]: 6 | """ 7 | Parse a mixed list of ids and mentions into a list of memberids. 8 | """ 9 | if memberstr: 10 | memberids = [int(x) for x in re.findall(r'[<@!\s]*([0-9]{15,20})[>\s,]*', memberstr)] 11 | else: 12 | memberids = [] 13 | return memberids 14 | 15 | 16 | member_overwrite = discord.PermissionOverwrite( 17 | view_channel=True, 18 | send_messages=True, 19 | read_message_history=True, 20 | attach_files=True, 21 | embed_links=True, 22 | add_reactions=True, 23 | connect=True, 24 | speak=True, 25 | stream=True, 26 | use_application_commands=True, 27 | use_embedded_activities=True, 28 | external_emojis=True, 29 | ) 30 | owner_overwrite = discord.PermissionOverwrite.from_pair(*member_overwrite.pair()) 31 | owner_overwrite.update( 32 | manage_webhooks=True, 33 | manage_channels=True, 34 | manage_messages=True, 35 | move_members=True, 36 | ) 37 | bot_overwrite = discord.PermissionOverwrite.from_pair(*owner_overwrite.pair()) 38 | -------------------------------------------------------------------------------- /locales/ceaser/LC_MESSAGES/base.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 1.0\n" 10 | "Report-Msgid-Bugs-To: you@example.com\n" 11 | "POT-Creation-Date: 2007-10-18 14:00+0100\n" 12 | "PO-Revision-Date: 2007-10-18 14:00+0100\n" 13 | "Last-Translator: you \n" 14 | "Language-Team: English \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: bot/modules/sysadmin/exec_cog.py:70 20 | msgid "You cannot use this interface!" 21 | msgstr "Zpv dboopu vtf uijt joufsgbdf!" 22 | 23 | #: bot/modules/sysadmin/exec_cog.py:252 bot/modules/sysadmin/exec_cog.py:254 24 | msgid "async" 25 | msgstr "btzod" 26 | 27 | #: bot/modules/sysadmin/exec_cog.py:253 bot/modules/sysadmin/exec_cog.py:255 28 | msgid "Execute arbitrary code with Exec" 29 | msgstr "Fyfdvuf bscjusbsz dpef xjui Fyfd" 30 | 31 | #: bot/modules/sysadmin/exec_cog.py:265 32 | msgid "eval" 33 | msgstr "fwbm" 34 | -------------------------------------------------------------------------------- /locales/he_IL/LC_MESSAGES/core_config.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Interitio, 2023 8 | # Ari Horesh, 2023 9 | # 10 | #, fuzzy 11 | msgid "" 12 | msgstr "" 13 | "Project-Id-Version: PACKAGE VERSION\n" 14 | "Report-Msgid-Bugs-To: \n" 15 | "POT-Creation-Date: 2023-09-13 08:47+0300\n" 16 | "PO-Revision-Date: 2023-08-28 13:43+0000\n" 17 | "Last-Translator: Ari Horesh, 2023\n" 18 | "Language-Team: Hebrew (Israel) (https://app.transifex.com/leobot/teams/174919/he_IL/)\n" 19 | "MIME-Version: 1.0\n" 20 | "Content-Type: text/plain; charset=UTF-8\n" 21 | "Content-Transfer-Encoding: 8bit\n" 22 | "Language: he_IL\n" 23 | "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" 24 | 25 | #: src/core/config.py:28 26 | msgctxt "group:configure" 27 | msgid "configure" 28 | msgstr "הגדר" 29 | 30 | #: src/core/config.py:29 31 | msgctxt "group:configure|desc" 32 | msgid "View and adjust my configuration options." 33 | msgstr "בחר ושנה את ההגדרות." 34 | -------------------------------------------------------------------------------- /locales/pt_BR/LC_MESSAGES/core_config.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Interitio, 2023 8 | # Bruno Evangelista De Oliveira, 2023 9 | # 10 | #, fuzzy 11 | msgid "" 12 | msgstr "" 13 | "Project-Id-Version: PACKAGE VERSION\n" 14 | "Report-Msgid-Bugs-To: \n" 15 | "POT-Creation-Date: 2023-09-13 08:47+0300\n" 16 | "PO-Revision-Date: 2023-08-28 13:43+0000\n" 17 | "Last-Translator: Bruno Evangelista De Oliveira, 2023\n" 18 | "Language-Team: Portuguese (Brazil) (https://app.transifex.com/leobot/teams/174919/pt_BR/)\n" 19 | "MIME-Version: 1.0\n" 20 | "Content-Type: text/plain; charset=UTF-8\n" 21 | "Content-Transfer-Encoding: 8bit\n" 22 | "Language: pt_BR\n" 23 | "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 24 | 25 | #: src/core/config.py:28 26 | msgctxt "group:configure" 27 | msgid "configure" 28 | msgstr "configurar" 29 | 30 | #: src/core/config.py:29 31 | msgctxt "group:configure|desc" 32 | msgid "View and adjust my configuration options." 33 | msgstr "Ver e ajustar minhas opções de configuração." 34 | -------------------------------------------------------------------------------- /src/modules/meta/help.txt: -------------------------------------------------------------------------------- 1 | Member View / Admin View / SuperAdmin View 2 | Compact / Full 3 | 4 | Compact Member: 5 | ---------------------------- 6 | # Personal Configuration 7 | View or adjust personal settings with the {/my} command. 8 | {/my timezone}: ... 9 | {/my language}: ... 10 | 11 | # Economy 12 | Earn coins through studying/chatting/voice activity 13 | then spend them in the {/shop} or {/send} them to others. 14 | You could also {/room rent} a private room! 15 | {/send}: ... 16 | {/shop}: ... 17 | {/room}: ... 18 | 19 | # Statistics 20 | ... 21 | {/ranks}: ... 22 | {/me}: ... 23 | {/stats}: ... 24 | {/leaderboard}: ... 25 | 26 | # Utilities 27 | ... 28 | {/reminders}: ... 29 | {/tasklist}: ... 30 | {/pomodoro}: ... 31 | {/schedule}: ... 32 | ---------------------------- 33 | 34 | Compact Admin 35 | ---------------------------- 36 | # General server configuration 37 | {/dashboard}: 38 | {/configure general}: 39 | {/configure language}: 40 | 41 | # Economy 42 | {/configure economy}: 43 | {/editshop} 44 | 45 | # Statistics 46 | {/ranks}: 47 | {/configure ranks}: 48 | {/configure statistics}: 49 | {/configure voice_rewards}: 50 | {/configure message_exp}: 51 | 52 | # Utilities 53 | {/configure pomodoro}: 54 | {/configure rooms}: 55 | {/configure tasklist} 56 | {/timer admin} 57 | ---------------------------- 58 | -------------------------------------------------------------------------------- /scripts/start_leo_debug.py: -------------------------------------------------------------------------------- 1 | # !/bin/python3 2 | 3 | from datetime import datetime 4 | import sys 5 | import os 6 | import tracemalloc 7 | import asyncio 8 | import logging 9 | import yappi 10 | import aiomonitor 11 | 12 | 13 | sys.path.insert(0, os.path.join(os.getcwd())) 14 | sys.path.insert(0, os.path.join(os.getcwd(), "src")) 15 | 16 | tracemalloc.start() 17 | 18 | 19 | def loop_exception_handler(loop, context): 20 | print(context) 21 | task: asyncio.Task = context.get('task', None) 22 | if task is not None: 23 | addendum = f"" 24 | message = context.get('message', '') 25 | context['message'] = ' '.join((message, addendum)) 26 | loop.default_exception_handler(context) 27 | 28 | 29 | def main(): 30 | loop = asyncio.get_event_loop() 31 | loop.set_exception_handler(loop_exception_handler) 32 | loop.set_debug(enabled=True) 33 | 34 | yappi.set_clock_type("WALL") 35 | with yappi.run(): 36 | with aiomonitor.start_monitor(loop): 37 | from bot import _main 38 | try: 39 | _main() 40 | finally: 41 | yappi.get_func_stats().save('logs/callgrind.out.' + datetime.utcnow().isoformat(), 'CALLGRIND') 42 | 43 | 44 | if __name__ == '__main__': 45 | main() 46 | -------------------------------------------------------------------------------- /src/meta/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | appname: str 3 | The base identifer for this application. 4 | This identifies which services the app offers. 5 | shardname: str 6 | The specific name of the running application. 7 | Only one process should be connecteded with a given appname. 8 | For the bot apps, usually specifies the shard id and shard number. 9 | """ 10 | # TODO: Find a better schema for these. We use appname for shard_talk, do we need it for data? 11 | 12 | from . import sharding, conf 13 | from .logger import log_app 14 | from .ipc.client import AppClient 15 | from .args import args 16 | 17 | 18 | appname = conf.data['appid'] 19 | appid = appname # backwards compatibility 20 | 21 | 22 | def appname_from_shard(shardid): 23 | appname = f"{conf.data['appid']}_{sharding.shard_count:02}_{shardid:02}" 24 | return appname 25 | 26 | 27 | def shard_from_appname(appname: str): 28 | return int(appname.rsplit('_', maxsplit=1)[-1]) 29 | 30 | 31 | shardname = appname_from_shard(sharding.shard_number) 32 | 33 | log_app.set(shardname) 34 | 35 | 36 | shard_talk = AppClient( 37 | shardname, 38 | appname, 39 | {'host': args.host, 'port': args.port}, 40 | {'host': conf.appipc['server_host'], 'port': int(conf.appipc['server_port'])} 41 | ) 42 | 43 | 44 | @shard_talk.register_route() 45 | async def ping(): 46 | return "Pong!" 47 | -------------------------------------------------------------------------------- /tests/gui/cards/profile_spec_sample.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime as dt 3 | from src.cards import ProfileCard as _Card 4 | 5 | 6 | highlights = [ 7 | "header_colour_1", 8 | "header_colour_2", 9 | "counter_bg_colour", 10 | "counter_colour", 11 | "subheader_colour", 12 | "badge_text_colour", 13 | "badge_blob_colour", 14 | "rank_name_colour", 15 | "rank_hours_colour", 16 | "bar_full_colour", 17 | "bar_empty_colour", 18 | "next_rank_colour" 19 | ] 20 | highlight_colour = "#E84727" 21 | card_name = "profile" 22 | 23 | 24 | async def get_cards(): 25 | strings = [] 26 | for highlight in highlights: 27 | card = await _Card.generate_sample( 28 | skin={highlight: highlight_colour} 29 | ) 30 | with open(f"../skins/spec/images/{card_name}/{highlight}.png", 'wb') as image_file: 31 | image_file.write(card.fp.read()) 32 | 33 | esc_highlight = highlight.replace('_', '\\_') 34 | string = f""" 35 | \\hypertarget{{{card_name}-{highlight.replace('_', '-')}}}{{\\texttt{{{esc_highlight}}}}} & & 36 | \\includegraphics[width=.25\\textwidth,valign=m]{{images/{card_name}/{highlight}.png}} 37 | \\\\ 38 | """ 39 | strings.append(string) 40 | 41 | print('\n'.join(strings)) 42 | 43 | if __name__ == '__main__': 44 | asyncio.run(get_cards()) 45 | -------------------------------------------------------------------------------- /data/migration/v4-v5/migration.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE reaction_role_messages( 2 | messageid BIGINT PRIMARY KEY, 3 | guildid BIGINT NOT NULL REFERENCES guild_config (guildid) ON DELETE CASCADE, 4 | channelid BIGINT NOT NULL, 5 | enabled BOOLEAN DEFAULT TRUE, 6 | required_role BIGINT, 7 | removable BOOLEAN, 8 | maximum INTEGER, 9 | refunds BOOLEAN, 10 | event_log BOOLEAN, 11 | default_price INTEGER 12 | ); 13 | CREATE INDEX reaction_role_guilds ON reaction_role_messages (guildid); 14 | 15 | CREATE TABLE reaction_role_reactions( 16 | reactionid SERIAL PRIMARY KEY, 17 | messageid BIGINT NOT NULL REFERENCES reaction_role_messages (messageid) ON DELETE CASCADE, 18 | roleid BIGINT NOT NULL, 19 | emoji_name TEXT, 20 | emoji_id BIGINT, 21 | emoji_animated BOOLEAN, 22 | price INTEGER, 23 | timeout INTEGER 24 | ); 25 | CREATE INDEX reaction_role_reaction_messages ON reaction_role_reactions (messageid); 26 | 27 | CREATE TABLE reaction_role_expiring( 28 | guildid BIGINT NOT NULL, 29 | userid BIGINT NOT NULL, 30 | roleid BIGINT NOT NULL, 31 | expiry TIMESTAMPTZ NOT NULL, 32 | reactionid INTEGER REFERENCES reaction_role_reactions (reactionid) ON DELETE SET NULL 33 | ); 34 | CREATE UNIQUE INDEX reaction_role_expiry_members ON reaction_role_expiring (guildid, userid, roleid); 35 | 36 | INSERT INTO VersionHistory (version, author) VALUES (5, 'v4-v5 Migration'); 37 | -------------------------------------------------------------------------------- /locales/he_IL/LC_MESSAGES/timer-gui.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Interitio, 2023 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2023-08-27 16:37+0300\n" 15 | "PO-Revision-Date: 2023-08-28 13:43+0000\n" 16 | "Last-Translator: Interitio, 2023\n" 17 | "Language-Team: Hebrew (Israel) (https://app.transifex.com/leobot/teams/174919/he_IL/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: he_IL\n" 22 | "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" 23 | 24 | #: src/gui/cards/timer.py:73 25 | msgctxt "skin:timer|field:date_text" 26 | msgid "Use /now to show what you are working on!" 27 | msgstr "השתמש ב /now כדי להראות לכולם על מה אתה עובד כרגע!" 28 | 29 | #: src/gui/cards/timer.py:89 30 | msgctxt "skin:timer|stage:focus|field:stage_text" 31 | msgid "FOCUS" 32 | msgstr "פוקוס" 33 | 34 | #: src/gui/cards/timer.py:103 35 | msgctxt "skin:timer|stage:break|field:stage_text" 36 | msgid "BREAK" 37 | msgstr "הפסקה" 38 | -------------------------------------------------------------------------------- /locales/pt_BR/LC_MESSAGES/timer-gui.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Bruno Evangelista De Oliveira, 2023 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2023-08-28 22:43+0300\n" 15 | "PO-Revision-Date: 2023-08-28 13:43+0000\n" 16 | "Last-Translator: Bruno Evangelista De Oliveira, 2023\n" 17 | "Language-Team: Portuguese (Brazil) (https://app.transifex.com/leobot/teams/174919/pt_BR/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: pt_BR\n" 22 | "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 23 | 24 | #: src/gui/cards/timer.py:73 25 | msgctxt "skin:timer|field:date_text" 26 | msgid "Use /now to show what you are working on!" 27 | msgstr "Use /now para mostrar a descrição do que você está fazendo!" 28 | 29 | #: src/gui/cards/timer.py:89 30 | msgctxt "skin:timer|stage:focus|field:stage_text" 31 | msgid "FOCUS" 32 | msgstr "FOCO" 33 | 34 | #: src/gui/cards/timer.py:103 35 | msgctxt "skin:timer|stage:break|field:stage_text" 36 | msgid "BREAK" 37 | msgstr "PAUSA" 38 | -------------------------------------------------------------------------------- /config/example-bot.conf: -------------------------------------------------------------------------------- 1 | [STUDYLION] 2 | prefix = !! 3 | 4 | admins = 5 | 6 | admin_guilds = 7 | 8 | shard_count = 1 9 | 10 | ALSO_READ = config/emojis.conf, config/secrets.conf, config/gui.conf 11 | 12 | asset_path = assets 13 | 14 | support_guild = 15 | invite_bot = 16 | 17 | 18 | [ENDPOINTS] 19 | guild_log = 20 | gem_log = 21 | 22 | [LOGGING] 23 | log_file = bot.log 24 | 25 | general_log = 26 | error_log = %(general_log) 27 | critical_log = %(general_log) 28 | warning_log = %(general_log) 29 | warning_prefix = 30 | error_prefix = 31 | critical_prefix = 32 | 33 | [LOGGING_LEVELS] 34 | root = DEBUG 35 | discord = INFO 36 | discord.http = INFO 37 | discord.gateway = INFO 38 | 39 | [APPIPC] 40 | server_host = 127.0.0.1 41 | server_port = 5000 42 | 43 | [ANALYTICS] 44 | appname = Analytics 45 | server_host = 127.0.0.1 46 | server_port = 4999 47 | 48 | [BABEL] 49 | locales = en-GB, ceaser 50 | domains = base, wards, schedule, shop, moderation, economy, user_config, config, member_admin, ranks, tasklist, sysadmin, exec, meta, rooms, rolemenus, test, reminders, video, Pomodoro, statistics, utils, timer-gui, goals-gui, weekly-gui, profile-gui, monthly-gui, leaderboard-gui, stats-gui, settings_base, voice-tracker, text-tracker, lion-core, core_config, babel 51 | 52 | [TEXT_TRACKER] 53 | batchsize = 1 54 | batchtime = 600 55 | 56 | [TOPGG] 57 | enabled = false 58 | route = /dbl 59 | port = 5000 60 | -------------------------------------------------------------------------------- /locales/he_IL/LC_MESSAGES/base.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Interitio, 2023 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2022-11-22 13:17+0200\n" 15 | "PO-Revision-Date: 2023-08-28 13:43+0000\n" 16 | "Last-Translator: Interitio, 2023\n" 17 | "Language-Team: Hebrew (Israel) (https://app.transifex.com/leobot/teams/174919/he_IL/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: he_IL\n" 22 | "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" 23 | 24 | #: bot/modules/sysadmin/exec_cog.py:70 25 | msgid "You cannot use this interface!" 26 | msgstr "אתם לא יכולים להשתמש בתפריט הזה." 27 | 28 | #: bot/modules/sysadmin/exec_cog.py:252 bot/modules/sysadmin/exec_cog.py:254 29 | msgid "async" 30 | msgstr "אסינכרוני" 31 | 32 | #: bot/modules/sysadmin/exec_cog.py:253 bot/modules/sysadmin/exec_cog.py:255 33 | msgid "Execute arbitrary code with Exec" 34 | msgstr "הרץ קוד ארביטררי עם אקסק" 35 | 36 | #: bot/modules/sysadmin/exec_cog.py:265 37 | msgid "eval" 38 | msgstr "eval" 39 | -------------------------------------------------------------------------------- /src/data/base.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import Any, Protocol, runtime_checkable 3 | from itertools import chain 4 | from psycopg import sql 5 | 6 | 7 | @runtime_checkable 8 | class Expression(Protocol): 9 | __slots__ = () 10 | 11 | @abstractmethod 12 | def as_tuple(self) -> tuple[sql.Composable, tuple[Any, ...]]: 13 | raise NotImplementedError 14 | 15 | 16 | class RawExpr(Expression): 17 | __slots__ = ('expr', 'values') 18 | 19 | expr: sql.Composable 20 | values: tuple[Any, ...] 21 | 22 | def __init__(self, expr: sql.Composable, values: tuple[Any, ...] = ()): 23 | self.expr = expr 24 | self.values = values 25 | 26 | def as_tuple(self): 27 | return (self.expr, self.values) 28 | 29 | @classmethod 30 | def join(cls, *expressions: Expression, joiner: sql.SQL = sql.SQL(' ')): 31 | """ 32 | Join a sequence of Expressions into a single RawExpr. 33 | """ 34 | tups = ( 35 | expression.as_tuple() 36 | for expression in expressions 37 | ) 38 | return cls.join_tuples(*tups, joiner=joiner) 39 | 40 | @classmethod 41 | def join_tuples(cls, *tuples: tuple[sql.Composable, tuple[Any, ...]], joiner: sql.SQL = sql.SQL(' ')): 42 | exprs, values = zip(*tuples) 43 | expr = joiner.join(exprs) 44 | value = tuple(chain(*values)) 45 | return cls(expr, value) 46 | -------------------------------------------------------------------------------- /tests/gui/cards/leaderboard_spec_sample.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | import datetime as dt 4 | from src.cards import LeaderboardCard as _Card 5 | 6 | 7 | highlights = [ 8 | "header_text_colour", 9 | "subheader_name_colour", 10 | "subheader_value_colour", 11 | "top_position_colour", 12 | "top_name_colour", 13 | "top_hours_colour", 14 | "entry_position_colour", 15 | "entry_position_highlight_colour", 16 | "entry_name_colour", 17 | "entry_hours_colour", 18 | "entry_bg_colour", 19 | "entry_bg_highlight_colour" 20 | ] 21 | highlight_colour = "#E84727" 22 | card_name = "leaderboard" 23 | 24 | 25 | async def get_cards(): 26 | strings = [] 27 | random.seed(0) 28 | for highlight in highlights: 29 | card = await _Card.generate_sample( 30 | skin={highlight: highlight_colour} 31 | ) 32 | with open(f"../skins/spec/images/{card_name}/{highlight}.png", 'wb') as image_file: 33 | image_file.write(card.fp.read()) 34 | 35 | esc_highlight = highlight.replace('_', '\\_') 36 | string = f"""\ 37 | \\hypertarget{{{card_name}-{highlight.replace('_', '-')}}}{{\\texttt{{{esc_highlight}}}}} & & 38 | \\includegraphics[width=.25\\textwidth,valign=m]{{images/{card_name}/{highlight}.png}} 39 | \\\\""" 40 | strings.append(string) 41 | 42 | print('\n'.join(strings)) 43 | 44 | if __name__ == '__main__': 45 | asyncio.run(get_cards()) 46 | -------------------------------------------------------------------------------- /locales/pt_BR/LC_MESSAGES/base.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Tatiana Tenuto, 2023 8 | # Bruno Evangelista De Oliveira, 2023 9 | # 10 | #, fuzzy 11 | msgid "" 12 | msgstr "" 13 | "Project-Id-Version: PACKAGE VERSION\n" 14 | "Report-Msgid-Bugs-To: \n" 15 | "POT-Creation-Date: 2022-11-22 13:17+0200\n" 16 | "PO-Revision-Date: 2023-08-28 13:43+0000\n" 17 | "Last-Translator: Bruno Evangelista De Oliveira, 2023\n" 18 | "Language-Team: Portuguese (Brazil) (https://app.transifex.com/leobot/teams/174919/pt_BR/)\n" 19 | "MIME-Version: 1.0\n" 20 | "Content-Type: text/plain; charset=UTF-8\n" 21 | "Content-Transfer-Encoding: 8bit\n" 22 | "Language: pt_BR\n" 23 | "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 24 | 25 | #: bot/modules/sysadmin/exec_cog.py:70 26 | msgid "You cannot use this interface!" 27 | msgstr "Você não pode usar essa interface!" 28 | 29 | #: bot/modules/sysadmin/exec_cog.py:252 bot/modules/sysadmin/exec_cog.py:254 30 | msgid "async" 31 | msgstr "async" 32 | 33 | #: bot/modules/sysadmin/exec_cog.py:253 bot/modules/sysadmin/exec_cog.py:255 34 | msgid "Execute arbitrary code with Exec" 35 | msgstr "Executar código arbitrário com Exec" 36 | 37 | #: bot/modules/sysadmin/exec_cog.py:265 38 | msgid "eval" 39 | msgstr "eval" 40 | -------------------------------------------------------------------------------- /src/modules/moderation/tickets/note.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | import datetime as dt 3 | 4 | import discord 5 | from meta import LionBot 6 | from utils.lib import utc_now 7 | 8 | from ..ticket import Ticket, ticket_factory 9 | from ..data import TicketType, TicketState, ModerationData 10 | from .. import logger, babel 11 | 12 | if TYPE_CHECKING: 13 | from ..cog import ModerationCog 14 | 15 | _p = babel._p 16 | 17 | 18 | @ticket_factory(TicketType.NOTE) 19 | class NoteTicket(Ticket): 20 | __slots__ = () 21 | 22 | @classmethod 23 | async def create( 24 | cls, bot: LionBot, guildid: int, userid: int, 25 | moderatorid: int, content: str, expiry=None, 26 | **kwargs 27 | ): 28 | modcog: 'ModerationCog' = bot.get_cog('ModerationCog') 29 | ticket_data = await modcog.data.Ticket.create( 30 | guildid=guildid, 31 | targetid=userid, 32 | ticket_type=TicketType.NOTE, 33 | ticket_state=TicketState.OPEN, 34 | moderator_id=moderatorid, 35 | content=content, 36 | expiry=expiry, 37 | created_at=utc_now().replace(tzinfo=None), 38 | **kwargs 39 | ) 40 | 41 | lguild = await bot.core.lions.fetch_guild(guildid) 42 | new_ticket = cls(lguild, ticket_data) 43 | await new_ticket.post() 44 | 45 | if expiry: 46 | cls.expiring.schedule_task(ticket_data.ticketid, expiry.timestamp()) 47 | 48 | return new_ticket 49 | -------------------------------------------------------------------------------- /tests/gui/cards/weekly_spec_sample.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime as dt 3 | from src.cards import WeeklyStatsCard as _Card 4 | 5 | 6 | highlights = [ 7 | 'title_colour', 8 | 'top_hours_colour', 9 | 'top_hours_bg_colour', 10 | 'top_line_colour', 11 | 'top_weekday_colour', 12 | 'top_date_colour', 13 | 'top_this_colour', 14 | 'top_last_colour', 15 | 'btm_weekly_background_colour', 16 | 'btm_this_colour', 17 | 'btm_last_colour', 18 | 'btm_weekday_colour', 19 | 'btm_day_colour', 20 | 'btm_bar_horiz_colour', 21 | 'btm_bar_vert_colour', 22 | 'this_week_colour', 23 | 'last_week_colour', 24 | 'footer_colour' 25 | ] 26 | highlight_colour = "#E84727" 27 | card_name = "weekly" 28 | 29 | 30 | async def get_cards(): 31 | strings = [] 32 | for highlight in highlights: 33 | card = await _Card.generate_sample( 34 | skin={highlight: highlight_colour} 35 | ) 36 | with open(f"../skins/spec/images/{card_name}/{highlight}.png", 'wb') as image_file: 37 | image_file.write(card.fp.read()) 38 | 39 | esc_highlight = highlight.replace('_', '\\_') 40 | string = f"""\ 41 | \\hypertarget{{{card_name}-{highlight.replace('_', '-')}}}{{\\texttt{{{esc_highlight}}}}} & & 42 | \\includegraphics[width=.25\\textwidth,valign=m]{{images/{card_name}/{highlight}.png}} 43 | \\\\""" 44 | strings.append(string) 45 | 46 | print('\n'.join(strings)) 47 | 48 | if __name__ == '__main__': 49 | asyncio.run(get_cards()) 50 | -------------------------------------------------------------------------------- /locales/templates/profile-gui.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-10-01 16:01+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/gui/cards/profile.py:79 21 | msgctxt "skin:profile|header:profile" 22 | msgid "PROFILE" 23 | msgstr "" 24 | 25 | #: src/gui/cards/profile.py:83 26 | msgctxt "skin:profile|header:achievements" 27 | msgid "ACHIEVEMENTS" 28 | msgstr "" 29 | 30 | #: src/gui/cards/profile.py:134 31 | msgctxt "skin:profile|field:rank_unranked_text" 32 | msgid "UNRANKED" 33 | msgstr "" 34 | 35 | #: src/gui/cards/profile.py:138 36 | #, possible-python-brace-format 37 | msgctxt "skin:profile|field:rank_nextrank_text" 38 | msgid "NEXT RANK: {name} {rangestr}" 39 | msgstr "" 40 | 41 | #: src/gui/cards/profile.py:142 42 | msgctxt "skin:profile|field:rank_noranks_text" 43 | msgid "NO RANKS AVAILABLE" 44 | msgstr "" 45 | 46 | #: src/gui/cards/profile.py:146 47 | msgctxt "skin:profile|field:rank_maxrank_text" 48 | msgid "YOU HAVE REACHED THE MAXIMUM RANK" 49 | msgstr "" 50 | -------------------------------------------------------------------------------- /src/modules/pomodoro/data.py: -------------------------------------------------------------------------------- 1 | from data import Registry, RowModel 2 | from data.columns import Integer, Bool, Timestamp, String 3 | 4 | 5 | class TimerData(Registry): 6 | class Timer(RowModel): 7 | """ 8 | Schema 9 | ------ 10 | CREATE TABLE timers( 11 | channelid BIGINT PRIMARY KEY, 12 | guildid BIGINT NOT NULL REFERENCES guild_config (guildid), 13 | ownerid BIGINT REFERENCES user_config, 14 | manager_roleid BIGINT, 15 | notification_channelid BIGINT, 16 | focus_length INTEGER NOT NULL, 17 | break_length INTEGER NOT NULL, 18 | last_started TIMESTAMPTZ, 19 | last_messageid BIGINT, 20 | voice_alerts BOOLEAN, 21 | inactivity_threshold INTEGER, 22 | auto_restart BOOLEAN, 23 | channel_name TEXT, 24 | pretty_name TEXT 25 | ); 26 | CREATE INDEX timers_guilds ON timers (guildid); 27 | """ 28 | _tablename_ = 'timers' 29 | 30 | channelid = Integer(primary=True) 31 | guildid = Integer() 32 | ownerid = Integer() 33 | manager_roleid = Integer() 34 | 35 | last_started = Timestamp() 36 | focus_length = Integer() 37 | break_length = Integer() 38 | auto_restart = Bool() 39 | 40 | inactivity_threshold = Integer() 41 | notification_channelid = Integer() 42 | last_messageid = Integer() 43 | voice_alerts = Bool() 44 | 45 | channel_name = String() 46 | pretty_name = String() 47 | -------------------------------------------------------------------------------- /src/data/cursor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | from psycopg import AsyncCursor, sql 5 | from psycopg.abc import Query, Params 6 | from psycopg._encodings import pgconn_encoding 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class AsyncLoggingCursor(AsyncCursor): 12 | def mogrify_query(self, query: Query): 13 | if isinstance(query, str): 14 | msg = query 15 | elif isinstance(query, (sql.SQL, sql.Composed)): 16 | msg = query.as_string(self) 17 | elif isinstance(query, bytes): 18 | msg = query.decode(pgconn_encoding(self._conn.pgconn), 'replace') 19 | else: 20 | msg = repr(query) 21 | return msg 22 | 23 | async def execute(self, query: Query, params: Optional[Params] = None, **kwargs): 24 | if logging.DEBUG >= logger.getEffectiveLevel(): 25 | msg = self.mogrify_query(query) 26 | logger.debug( 27 | "Executing query (%s) with values %s", msg, params, 28 | extra={'action': "Query Execute"} 29 | ) 30 | try: 31 | return await super().execute(query, params=params, **kwargs) 32 | except Exception: 33 | msg = self.mogrify_query(query) 34 | logger.exception( 35 | "Exception during query execution. Query (%s) with parameters %s.", 36 | msg, params, 37 | extra={'action': "Query Execute"}, 38 | stack_info=True 39 | ) 40 | else: 41 | # TODO: Possibly log execution time 42 | pass 43 | -------------------------------------------------------------------------------- /src/modules/tasklist/data.py: -------------------------------------------------------------------------------- 1 | from psycopg import sql 2 | 3 | from data import RowModel, Registry, Table 4 | from data.columns import Integer, String, Timestamp, Bool 5 | 6 | 7 | class TasklistData(Registry): 8 | class Task(RowModel): 9 | """ 10 | Row model describing a single task in a tasklist. 11 | 12 | Schema 13 | ------ 14 | CREATE TABLE tasklist( 15 | taskid SERIAL PRIMARY KEY, 16 | userid BIGINT NOT NULL REFERENCES user_config ON DELETE CASCADE, 17 | parentid INTEGER REFERENCES tasklist (taskid) ON DELETE SET NULL, 18 | content TEXT NOT NULL, 19 | rewarded BOOL DEFAULT FALSE, 20 | deleted_at TIMESTAMPTZ, 21 | completed_at TIMESTAMPTZ, 22 | created_at TIMESTAMPTZ, 23 | last_updated_at TIMESTAMPTZ 24 | ); 25 | CREATE INDEX tasklist_users ON tasklist (userid); 26 | 27 | CREATE TABLE tasklist_channels( 28 | guildid BIGINT NOT NULL REFERENCES guild_config (guildid) ON DELETE CASCADE, 29 | channelid BIGINT NOT NULL 30 | ); 31 | CREATE INDEX tasklist_channels_guilds ON tasklist_channels (guildid); 32 | """ 33 | _tablename_ = "tasklist" 34 | 35 | taskid = Integer(primary=True) 36 | userid = Integer() 37 | parentid = Integer() 38 | rewarded = Bool() 39 | content = String() 40 | completed_at = Timestamp() 41 | created_at = Timestamp() 42 | deleted_at = Timestamp() 43 | last_updated_at = Timestamp() 44 | 45 | channels = Table('tasklist_channels') 46 | -------------------------------------------------------------------------------- /src/modules/statistics/lib.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | import pytz 3 | 4 | 5 | def extract_weekid(timestamp) -> int: 6 | """ 7 | Extract a weekid from a given timestamp with timezone. 8 | 9 | Weekids are calculated by first stripping the timezone, 10 | then extracting the UTC timestamp of the start of the week. 11 | """ 12 | day_start = timestamp.replace(hour=0, minute=0, second=0, microsecond=0) 13 | week_start = day_start - timedelta(days=day_start.weekday()) 14 | return int(week_start.replace(tzinfo=pytz.utc).timestamp()) 15 | 16 | 17 | def extract_monthid(timestamp) -> int: 18 | """ 19 | Extract a monthid from a given timestamp with timezone. 20 | 21 | Monthids are calculated by first stripping the timezone, 22 | then extracting the UTC timestamp from the start of the month. 23 | """ 24 | month_start = timestamp.replace(day=1, hour=0, minute=0, second=0, microsecond=0) 25 | return int(month_start.replace(tzinfo=pytz.utc).timestamp()) 26 | 27 | 28 | def apply_week_offset(timestamp, offset): 29 | return timestamp - timedelta(weeks=offset) 30 | 31 | 32 | def apply_month_offset(timestamp, offset): 33 | raw_month = timestamp.month - offset - 1 34 | timestamp = timestamp.replace( 35 | year=timestamp.year + int(raw_month // 12), 36 | month=(raw_month % 12) + 1 37 | ) 38 | return timestamp 39 | 40 | 41 | def week_difference(ts_1, ts_2): 42 | return int((ts_2 - ts_1).total_seconds() // (7*24*3600)) 43 | 44 | 45 | def month_difference(ts_1, ts_2): 46 | return (ts_2.month - ts_1.month) + (ts_2.year - ts_1.year) * 12 47 | -------------------------------------------------------------------------------- /locales/ceaser/LC_MESSAGES/profile-gui.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 1.0\n" 10 | "Report-Msgid-Bugs-To: you@example.com\n" 11 | "POT-Creation-Date: 2007-10-18 14:00+0100\n" 12 | "PO-Revision-Date: 2007-10-18 14:00+0100\n" 13 | "Last-Translator: you \n" 14 | "Language-Team: English \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: src/gui/cards/profile.py:79 20 | msgctxt "skin:profile|header:profile" 21 | msgid "PROFILE" 22 | msgstr "QSPGJMF" 23 | 24 | #: src/gui/cards/profile.py:83 25 | msgctxt "skin:profile|header:achievements" 26 | msgid "ACHIEVEMENTS" 27 | msgstr "BDIJFWFNFOUT" 28 | 29 | #: src/gui/cards/profile.py:134 30 | msgctxt "skin:profile|field:rank_unranked_text" 31 | msgid "UNRANKED" 32 | msgstr "VOSBOLFE" 33 | 34 | #: src/gui/cards/profile.py:138 35 | #, possible-python-brace-format 36 | msgctxt "skin:profile|field:rank_nextrank_text" 37 | msgid "NEXT RANK: {name} {rangestr}" 38 | msgstr "OFYU SBOL: {name} {rangestr}" 39 | 40 | #: src/gui/cards/profile.py:142 41 | msgctxt "skin:profile|field:rank_noranks_text" 42 | msgid "NO RANKS AVAILABLE" 43 | msgstr "OP SBOLT BWBJMBCMF" 44 | 45 | #: src/gui/cards/profile.py:146 46 | msgctxt "skin:profile|field:rank_maxrank_text" 47 | msgid "YOU HAVE REACHED THE MAXIMUM RANK" 48 | msgstr "ZPV IBWF SFBDIFE UIF NBYJNVN SBOL" 49 | -------------------------------------------------------------------------------- /tests/gui/cards/monthly_spec_sample.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | import datetime as dt 4 | from src.cards import MonthlyStatsCard as _Card 5 | 6 | 7 | highlights = [ 8 | 'title_colour', 9 | 'top_hours_colour', 10 | 'top_hours_bg_colour', 11 | 'top_line_colour', 12 | 'top_date_colour', 13 | 'top_this_colour', 14 | 'top_last_colour', 15 | 'top_this_hours_colour', 16 | 'top_last_hours_colour', 17 | 'this_month_colour', 18 | 'last_month_colour', 19 | 'heatmap_empty_colour', 20 | 'weekday_background_colour', 21 | 'weekday_colour', 22 | 'month_background_colour', 23 | 'month_colour', 24 | 'stats_key_colour', 25 | 'stats_value_colour', 26 | 'footer_colour', 27 | ] 28 | highlight_colour = "#E84727" 29 | card_name = "monthly" 30 | 31 | highlights = ['heatmap_colours'] 32 | highlight_colour = ["#E84727"] 33 | 34 | async def get_cards(): 35 | strings = [] 36 | random.seed(0) 37 | for highlight in highlights: 38 | card = await _Card.generate_sample( 39 | skin={highlight: highlight_colour} 40 | ) 41 | with open(f"../skins/spec/images/{card_name}/{highlight}.png", 'wb') as image_file: 42 | image_file.write(card.fp.read()) 43 | 44 | esc_highlight = highlight.replace('_', '\\_') 45 | string = f"""\ 46 | \\hypertarget{{{card_name}-{highlight.replace('_', '-')}}}{{\\texttt{{{esc_highlight}}}}} & & 47 | \\includegraphics[width=.25\\textwidth,valign=m]{{images/{card_name}/{highlight}.png}} 48 | \\\\""" 49 | strings.append(string) 50 | 51 | print('\n'.join(strings)) 52 | 53 | if __name__ == '__main__': 54 | asyncio.run(get_cards()) 55 | -------------------------------------------------------------------------------- /src/data/database.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | import logging 3 | from collections import namedtuple 4 | 5 | # from .cursor import AsyncLoggingCursor 6 | from .registry import Registry 7 | from .connector import Connector 8 | 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | Version = namedtuple('Version', ('version', 'time', 'author')) 13 | 14 | T = TypeVar('T', bound=Registry) 15 | 16 | 17 | class Database(Connector): 18 | # cursor_factory = AsyncLoggingCursor 19 | 20 | def __init__(self, *args, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | 23 | self.registries: dict[str, Registry] = {} 24 | 25 | def load_registry(self, registry: T) -> T: 26 | logger.debug( 27 | f"Loading and binding registry '{registry.name}'.", 28 | extra={'action': f"Reg {registry.name}"} 29 | ) 30 | registry.bind(self) 31 | self.registries[registry.name] = registry 32 | return registry 33 | 34 | async def version(self) -> Version: 35 | """ 36 | Return the current schema version as a Version namedtuple. 37 | """ 38 | async with self.connection() as conn: 39 | async with conn.cursor() as cursor: 40 | # Get last entry in version table, compare against desired version 41 | await cursor.execute("SELECT * FROM VersionHistory ORDER BY time DESC LIMIT 1") 42 | row = await cursor.fetchone() 43 | if row: 44 | return Version(row['version'], row['time'], row['author']) 45 | else: 46 | # No versions in the database 47 | return Version(-1, None, None) 48 | -------------------------------------------------------------------------------- /src/data/adapted.py: -------------------------------------------------------------------------------- 1 | # from enum import Enum 2 | from typing import Optional 3 | from psycopg.types.enum import register_enum, EnumInfo 4 | from psycopg import AsyncConnection 5 | from .registry import Attachable, Registry 6 | 7 | 8 | class RegisterEnum(Attachable): 9 | def __init__(self, enum, name: Optional[str] = None, mapper=None): 10 | super().__init__() 11 | self.enum = enum 12 | self.name = name or enum.__name__ 13 | self.mapping = mapper(enum) if mapper is not None else self._mapper() 14 | 15 | def _mapper(self): 16 | return {m: m.value[0] for m in self.enum} 17 | 18 | def attach_to(self, registry: Registry): 19 | self._registry = registry 20 | registry.init_task(self.on_init) 21 | return self 22 | 23 | async def on_init(self, registry: Registry): 24 | connector = registry._conn 25 | if connector is None: 26 | raise ValueError("Cannot initialise without connector!") 27 | connector.connect_hook(self.connection_hook) 28 | # await connector.refresh_pool() 29 | # The below may be somewhat dangerous 30 | # But adaption should never write to the database 31 | await connector.map_over_pool(self.connection_hook) 32 | # if conn := connector.conn: 33 | # # Ensure the adaption is run in the current context as well 34 | # await self.connection_hook(conn) 35 | 36 | async def connection_hook(self, conn: AsyncConnection): 37 | info = await EnumInfo.fetch(conn, self.name) 38 | if info is None: 39 | raise ValueError(f"Enum {self.name} not found in database.") 40 | register_enum(info, conn, self.enum, mapping=list(self.mapping.items())) 41 | -------------------------------------------------------------------------------- /tests/gui/cards/weeklygoals_spec_sample.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | import datetime as dt 4 | from src.cards import WeeklyGoalCard as _Card 5 | 6 | 7 | highlights = [ 8 | 'title_colour', 9 | 'mini_profile_name_colour', 10 | 'mini_profile_discrim_colour', 11 | 'mini_profile_badge_colour', 12 | 'mini_profile_badge_text_colour', 13 | 'progress_bg_colour', 14 | 'progress_colour', 15 | 'task_count_colour', 16 | 'task_done_colour', 17 | 'task_goal_colour', 18 | 'task_goal_number_colour', 19 | 'attendance_rate_colour', 20 | 'attendance_colour', 21 | 'studied_text_colour', 22 | 'studied_hour_colour', 23 | 'task_header_colour', 24 | 'task_done_number_colour', 25 | 'task_done_text_colour', 26 | 'task_undone_number_colour', 27 | 'task_undone_text_colour', 28 | 'footer_colour' 29 | ] 30 | highlight_colour = "#E84727" 31 | card_name = "weeklygoals" 32 | 33 | 34 | async def get_cards(): 35 | strings = [] 36 | random.seed(0) 37 | for highlight in highlights: 38 | card = await _Card.generate_sample( 39 | skin={highlight: highlight_colour} 40 | ) 41 | with open(f"../skins/spec/images/{card_name}/{highlight}.png", 'wb') as image_file: 42 | image_file.write(card.fp.read()) 43 | 44 | esc_highlight = highlight.replace('_', '\\_') 45 | string = f"""\ 46 | \\hypertarget{{{card_name}-{highlight.replace('_', '-')}}}{{\\texttt{{{esc_highlight}}}}} & & 47 | \\includegraphics[width=.25\\textwidth,valign=m]{{images/{card_name}/{highlight}.png}} 48 | \\\\""" 49 | strings.append(string) 50 | 51 | print('\n'.join(strings)) 52 | 53 | if __name__ == '__main__': 54 | asyncio.run(get_cards()) 55 | -------------------------------------------------------------------------------- /src/modules/rooms/data.py: -------------------------------------------------------------------------------- 1 | from data import Registry, RowModel 2 | from data.columns import Integer, Timestamp, String 3 | 4 | 5 | class RoomData(Registry): 6 | class Room(RowModel): 7 | """ 8 | CREATE TABLE rented_rooms( 9 | channelid BIGINT PRIMARY KEY, 10 | guildid BIGINT NOT NULL, 11 | ownerid BIGINT NOT NULL, 12 | coin_balance INTEGER NOT NULL DEFAULT 0, 13 | name TEXT, 14 | created_at TIMESTAMPTZ DEFAULT now(), 15 | last_tick TIMESTAMPTZ, 16 | deleted_at TIMESTAMPTZ, 17 | FOREIGN KEY (guildid, ownerid) REFERENCES members (guildid, userid) ON DELETE CASCADE 18 | ); 19 | CREATE INDEX rented_owners ON rented (guildid, ownerid); 20 | """ 21 | _tablename_ = 'rented_rooms' 22 | 23 | channelid = Integer(primary=True) 24 | guildid = Integer() 25 | ownerid = Integer() 26 | coin_balance = Integer() 27 | name = String() 28 | created_at = Timestamp() 29 | last_tick = Timestamp() 30 | deleted_at = Timestamp() 31 | 32 | class RoomMember(RowModel): 33 | """ 34 | Schema 35 | ------ 36 | CREATE TABLE rented_members( 37 | channelid BIGINT NOT NULL REFERENCES rented(channelid) ON DELETE CASCADE, 38 | userid BIGINT NOT NULL, 39 | contribution INTEGER NOT NULL DEFAULT 0 40 | ); 41 | CREATE INDEX rented_members_channels ON rented_members (channelid); 42 | CREATE INDEX rented_members_users ON rented_members (userid); 43 | """ 44 | _tablename_ = 'rented_members' 45 | 46 | channelid = Integer(primary=True) 47 | userid = Integer(primary=True) 48 | contribution = Integer() 49 | -------------------------------------------------------------------------------- /src/utils/ui/hooked.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import discord 4 | from discord.ui.item import Item 5 | from discord.ui.button import Button 6 | 7 | from .leo import LeoUI 8 | 9 | __all__ = ( 10 | 'HookedItem', 11 | 'AButton', 12 | 'AsComponents' 13 | ) 14 | 15 | 16 | class HookedItem: 17 | """ 18 | Mixin for Item classes allowing an instance to be used as a callback decorator. 19 | """ 20 | def __init__(self, *args, pass_kwargs={}, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | self.pass_kwargs = pass_kwargs 23 | 24 | def __call__(self, coro): 25 | async def wrapped(interaction, **kwargs): 26 | return await coro(interaction, self, **(self.pass_kwargs | kwargs)) 27 | self.callback = wrapped 28 | return self 29 | 30 | 31 | class AButton(HookedItem, Button): 32 | ... 33 | 34 | 35 | class AsComponents(LeoUI): 36 | """ 37 | Simple container class to accept a number of Items and turn them into an attachable View. 38 | """ 39 | def __init__(self, *items, pass_kwargs={}, **kwargs): 40 | super().__init__(**kwargs) 41 | self.pass_kwargs = pass_kwargs 42 | 43 | for item in items: 44 | self.add_item(item) 45 | 46 | async def _scheduled_task(self, item: Item, interaction: discord.Interaction): 47 | try: 48 | item._refresh_state(interaction, interaction.data) # type: ignore 49 | 50 | allow = await self.interaction_check(interaction) 51 | if not allow: 52 | return 53 | 54 | if self.timeout: 55 | self.__timeout_expiry = time.monotonic() + self.timeout 56 | 57 | await item.callback(interaction, **self.pass_kwargs) 58 | except Exception as e: 59 | return await self.on_error(interaction, e, item) 60 | -------------------------------------------------------------------------------- /locales/he_IL/LC_MESSAGES/profile-gui.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Interitio, 2023 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2023-08-27 16:37+0300\n" 15 | "PO-Revision-Date: 2023-08-28 13:43+0000\n" 16 | "Last-Translator: Interitio, 2023\n" 17 | "Language-Team: Hebrew (Israel) (https://app.transifex.com/leobot/teams/174919/he_IL/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: he_IL\n" 22 | "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" 23 | 24 | #: src/gui/cards/profile.py:79 25 | msgctxt "skin:profile|header:profile" 26 | msgid "PROFILE" 27 | msgstr "פרופיל" 28 | 29 | #: src/gui/cards/profile.py:83 30 | msgctxt "skin:profile|header:achievements" 31 | msgid "ACHIEVEMENTS" 32 | msgstr "השגים" 33 | 34 | #: src/gui/cards/profile.py:134 35 | msgctxt "skin:profile|field:rank_unranked_text" 36 | msgid "UNRANKED" 37 | msgstr "ללא דרגה" 38 | 39 | #: src/gui/cards/profile.py:138 40 | #, possible-python-brace-format 41 | msgctxt "skin:profile|field:rank_nextrank_text" 42 | msgid "NEXT RANK: {name} {rangestr}" 43 | msgstr "הדרגה הבאה: {name} {rangestr}" 44 | 45 | #: src/gui/cards/profile.py:142 46 | msgctxt "skin:profile|field:rank_noranks_text" 47 | msgid "NO RANKS AVAILABLE" 48 | msgstr "אין דרגות זמינות" 49 | 50 | #: src/gui/cards/profile.py:146 51 | msgctxt "skin:profile|field:rank_maxrank_text" 52 | msgid "YOU HAVE REACHED THE MAXIMUM RANK" 53 | msgstr "הגעת לדרגה המקסימלית" 54 | -------------------------------------------------------------------------------- /src/modules/pomodoro/graphics.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from meta import LionBot 4 | from utils.lib import utc_now 5 | 6 | from gui.cards import FocusTimerCard, BreakTimerCard 7 | 8 | if TYPE_CHECKING: 9 | from .timer import Timer, Stage 10 | from tracking.voice.cog import VoiceTrackerCog 11 | 12 | 13 | async def get_timer_card(bot: LionBot, timer: 'Timer', stage: 'Stage'): 14 | voicecog: 'VoiceTrackerCog' = bot.get_cog('VoiceTrackerCog') 15 | 16 | name = timer.base_name 17 | if stage is not None: 18 | duration = stage.duration 19 | remaining = (stage.end - utc_now()).total_seconds() 20 | else: 21 | remaining = duration = timer.data.focus_length 22 | 23 | card_users = [] 24 | guildid = timer.data.guildid 25 | for member in timer.members: 26 | if voicecog is not None: 27 | session = voicecog.get_session(guildid, member.id) 28 | tag = session.tag 29 | if session.start_time: 30 | session_duration = (utc_now() - session.start_time).total_seconds() 31 | else: 32 | session_duration = 0 33 | else: 34 | session_duration = 0 35 | tag = None 36 | 37 | card_user = ( 38 | (member.id, (member.avatar or member.default_avatar).key), 39 | session_duration, 40 | tag, 41 | ) 42 | card_users.append(card_user) 43 | 44 | if stage is None or stage.focused: 45 | card_cls = FocusTimerCard 46 | else: 47 | card_cls = BreakTimerCard 48 | 49 | skin = await bot.get_cog('CustomSkinCog').get_skinargs_for( 50 | timer.data.guildid, None, card_cls.card_id 51 | ) 52 | 53 | return card_cls( 54 | name, 55 | remaining, 56 | duration, 57 | users=card_users, 58 | ) 59 | -------------------------------------------------------------------------------- /src/core/config.py: -------------------------------------------------------------------------------- 1 | from discord import app_commands as appcmds 2 | from discord.ext import commands as cmds 3 | 4 | from meta import LionBot, LionContext, LionCog 5 | from babel.translator import LocalBabel 6 | 7 | babel = LocalBabel('core_config') 8 | 9 | _p = babel._p 10 | 11 | 12 | class ConfigCog(LionCog): 13 | """ 14 | Core guild config cog. 15 | 16 | Primarily used to expose the `configure` base command group at a high level. 17 | """ 18 | def __init__(self, bot: LionBot): 19 | self.bot = bot 20 | 21 | async def cog_load(self): 22 | ... 23 | 24 | async def cog_unload(self): 25 | ... 26 | 27 | @cmds.hybrid_group( 28 | name=_p('group:config', "config"), 29 | description=_p('group:config|desc', "View and adjust moderation-level configuration."), 30 | ) 31 | @appcmds.guild_only 32 | @appcmds.default_permissions(manage_guild=True) 33 | async def config_group(self, ctx: LionContext): 34 | """ 35 | Bare command group, has no function. 36 | """ 37 | return 38 | 39 | @cmds.hybrid_group( 40 | name=_p('group:admin', "admin"), 41 | description=_p('group:admin|desc', "Administrative commands."), 42 | ) 43 | @appcmds.guild_only 44 | @appcmds.default_permissions(administrator=True) 45 | async def admin_group(self, ctx: LionContext): 46 | """ 47 | Bare command group, has no function. 48 | """ 49 | return 50 | 51 | @admin_group.group( 52 | name=_p('group:admin_config', "config"), 53 | description=_p('group:admin_config|desc', "View and adjust admin-level configuration."), 54 | ) 55 | @appcmds.guild_only 56 | async def admin_config_group(self, ctx: LionContext): 57 | """ 58 | Bare command group, has no function. 59 | """ 60 | return 61 | -------------------------------------------------------------------------------- /locales/pt_BR/LC_MESSAGES/profile-gui.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Bruno Evangelista De Oliveira, 2023 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2023-08-28 22:43+0300\n" 15 | "PO-Revision-Date: 2023-08-28 13:43+0000\n" 16 | "Last-Translator: Bruno Evangelista De Oliveira, 2023\n" 17 | "Language-Team: Portuguese (Brazil) (https://app.transifex.com/leobot/teams/174919/pt_BR/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: pt_BR\n" 22 | "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 23 | 24 | #: src/gui/cards/profile.py:79 25 | msgctxt "skin:profile|header:profile" 26 | msgid "PROFILE" 27 | msgstr "PERFIL" 28 | 29 | #: src/gui/cards/profile.py:83 30 | msgctxt "skin:profile|header:achievements" 31 | msgid "ACHIEVEMENTS" 32 | msgstr "CONQUISTAS" 33 | 34 | #: src/gui/cards/profile.py:134 35 | msgctxt "skin:profile|field:rank_unranked_text" 36 | msgid "UNRANKED" 37 | msgstr "NÃO_RANKEADO" 38 | 39 | #: src/gui/cards/profile.py:138 40 | #, possible-python-brace-format 41 | msgctxt "skin:profile|field:rank_nextrank_text" 42 | msgid "NEXT RANK: {name} {rangestr}" 43 | msgstr "PRÓXIMO NÍVEL: {name} {rangestr}" 44 | 45 | #: src/gui/cards/profile.py:142 46 | msgctxt "skin:profile|field:rank_noranks_text" 47 | msgid "NO RANKS AVAILABLE" 48 | msgstr "NÃO HÁ RANKS DISPONÍVEIS" 49 | 50 | #: src/gui/cards/profile.py:146 51 | msgctxt "skin:profile|field:rank_maxrank_text" 52 | msgid "YOU HAVE REACHED THE MAXIMUM RANK" 53 | msgstr "VOCÊ ATINGIU O NÍVEL MÁXIMO" 54 | -------------------------------------------------------------------------------- /locales/templates/test.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-10-01 16:01+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/modules/test/test.py:59 src/modules/test/test.py:66 21 | msgid "test" 22 | msgstr "" 23 | 24 | #: src/modules/test/test.py:67 25 | msgid "Test" 26 | msgstr "" 27 | 28 | #: src/modules/test/test.py:74 29 | msgid "editor" 30 | msgstr "" 31 | 32 | #: src/modules/test/test.py:75 33 | msgid "Test message editor" 34 | msgstr "" 35 | 36 | #: src/modules/test/test.py:101 37 | msgid "test_ephemeral" 38 | msgstr "" 39 | 40 | #: src/modules/test/test.py:102 41 | msgid "Test ephemeral delete and edit" 42 | msgstr "" 43 | 44 | #: src/modules/test/test.py:114 45 | msgid "colours" 46 | msgstr "" 47 | 48 | #: src/modules/test/test.py:115 49 | msgid "Test Ansi colours" 50 | msgstr "" 51 | 52 | #: src/modules/test/test.py:135 53 | msgid "fail" 54 | msgstr "" 55 | 56 | #: src/modules/test/test.py:143 57 | msgid "failui" 58 | msgstr "" 59 | 60 | #: src/modules/test/test.py:150 61 | msgid "pager" 62 | msgstr "" 63 | 64 | #: src/modules/test/test.py:178 65 | msgid "pager2" 66 | msgstr "" 67 | 68 | #: src/modules/test/test.py:209 69 | msgid "prettyusers" 70 | msgstr "" 71 | 72 | #: src/modules/test/test.py:259 73 | msgid "dmview" 74 | msgstr "" 75 | 76 | #: src/modules/test/test.py:270 77 | msgid "multiview" 78 | msgstr "" 79 | 80 | #: src/modules/test/test.py:287 81 | msgid "stats-card" 82 | msgstr "" 83 | -------------------------------------------------------------------------------- /src/modules/skins/settings.py: -------------------------------------------------------------------------------- 1 | from meta.errors import UserInputError 2 | from settings.data import ModelData 3 | from settings.setting_types import StringSetting 4 | from settings.groups import SettingGroup 5 | 6 | from wards import sys_admin_iward 7 | from core.data import CoreData 8 | from gui.base import AppSkin 9 | from babel.translator import ctx_translator 10 | 11 | from . import babel 12 | 13 | _p = babel._p 14 | 15 | 16 | class GlobalSkinSettings(SettingGroup): 17 | class DefaultSkin(ModelData, StringSetting): 18 | setting_id = 'default_app_skin' 19 | _event = 'botset_skin' 20 | _write_ward = sys_admin_iward 21 | 22 | _display_name = _p( 23 | 'botset:default_app_skin', "default_skin" 24 | ) 25 | _desc = _p( 26 | 'botset:default_app_skin|desc', 27 | "The skin name of the app skin to use as the global default." 28 | ) 29 | _long_desc = _p( 30 | 'botset:default_app_skin|long_desc', 31 | "The skin name, as given in the `skins.json` file," 32 | " of the client default interface skin." 33 | " Guilds and users will be able to apply this skin" 34 | "regardless of whether it is set as visible in the skin configuration." 35 | ) 36 | _accepts = _p( 37 | 'botset:default_app_skin|accepts', 38 | "A valid skin name as given in skins.json" 39 | ) 40 | 41 | _model = CoreData.BotConfig 42 | _column = CoreData.BotConfig.default_skin.name 43 | 44 | @classmethod 45 | async def _parse_string(cls, parent_id, string, **kwargs): 46 | t = ctx_translator.get().t 47 | if string and not AppSkin.get_skin_path(string): 48 | raise UserInputError( 49 | t(_p( 50 | 'botset:default_app_skin|parse|error:invalid', 51 | "Provided `{string}` is not a valid skin id!" 52 | )).format(string=string) 53 | ) 54 | return string or None 55 | 56 | -------------------------------------------------------------------------------- /src/modules/statistics/ui/summary.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import asyncio 3 | 4 | import discord 5 | 6 | from utils.lib import MessageArgs 7 | 8 | from .. import babel 9 | from .base import StatsUI 10 | 11 | from gui.cards import StatsCard, ProfileCard 12 | from ..graphics.stats import get_stats_card 13 | 14 | _p = babel._p 15 | 16 | 17 | class SummaryUI(StatsUI): 18 | _ui_name = _p('ui:SummaryUI|name', 'Summary') 19 | 20 | def __init__(self, *args, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | 23 | self._rendered = False 24 | self._stats_card: Optional[StatsCard] = None 25 | self._profile_card: Optional[ProfileCard] = None 26 | 27 | async def redraw(self): 28 | if self.guild is not None: 29 | self._layout = [ 30 | (*self._switcher_buttons, self.toggle_pressed) 31 | ] 32 | else: 33 | self._layout = [ 34 | self._switcher_buttons 35 | ] 36 | await super().redraw() 37 | 38 | async def make_message(self) -> MessageArgs: 39 | if not self._rendered: 40 | await self._render() 41 | 42 | stats_file = self._stats_card.as_file('stats.png') 43 | profile_file = self._profile_card.as_file('profile.png') 44 | 45 | # TODO: Refresh peer timeouts on interaction usage 46 | # TODO: Write close and cleanup 47 | return MessageArgs(files=[profile_file, stats_file]) 48 | 49 | async def _render(self): 50 | await asyncio.gather(self._render_stats(), self._render_profile()) 51 | self._rendered = True 52 | 53 | async def _render_stats(self): 54 | card = await get_stats_card(self.bot, self.data, self.user.id, self.guild.id if self.guild else None) 55 | await card.render() 56 | self._stats_card = card 57 | return card 58 | 59 | async def _render_profile(self): 60 | args = await ProfileCard.sample_args(None) 61 | card = ProfileCard(**args) 62 | await card.render() 63 | self._profile_card = card 64 | return card 65 | -------------------------------------------------------------------------------- /locales/ceaser/LC_MESSAGES/test.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 1.0\n" 10 | "Report-Msgid-Bugs-To: you@example.com\n" 11 | "POT-Creation-Date: 2007-10-18 14:00+0100\n" 12 | "PO-Revision-Date: 2007-10-18 14:00+0100\n" 13 | "Last-Translator: you \n" 14 | "Language-Team: English \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: src/modules/test/test.py:59 src/modules/test/test.py:66 20 | msgid "test" 21 | msgstr "uftu" 22 | 23 | #: src/modules/test/test.py:67 24 | msgid "Test" 25 | msgstr "Uftu" 26 | 27 | #: src/modules/test/test.py:74 28 | msgid "editor" 29 | msgstr "fejups" 30 | 31 | #: src/modules/test/test.py:75 32 | msgid "Test message editor" 33 | msgstr "Uftu nfttbhf fejups" 34 | 35 | #: src/modules/test/test.py:101 36 | msgid "test_ephemeral" 37 | msgstr "uftu_fqifnfsbm" 38 | 39 | #: src/modules/test/test.py:102 40 | msgid "Test ephemeral delete and edit" 41 | msgstr "Uftu fqifnfsbm efmfuf boe feju" 42 | 43 | #: src/modules/test/test.py:114 44 | msgid "colours" 45 | msgstr "dpmpvst" 46 | 47 | #: src/modules/test/test.py:115 48 | msgid "Test Ansi colours" 49 | msgstr "Uftu Botj dpmpvst" 50 | 51 | #: src/modules/test/test.py:135 52 | msgid "fail" 53 | msgstr "gbjm" 54 | 55 | #: src/modules/test/test.py:143 56 | msgid "failui" 57 | msgstr "gbjmvj" 58 | 59 | #: src/modules/test/test.py:150 60 | msgid "pager" 61 | msgstr "qbhfs" 62 | 63 | #: src/modules/test/test.py:178 64 | msgid "pager2" 65 | msgstr "qbhfs2" 66 | 67 | #: src/modules/test/test.py:209 68 | msgid "prettyusers" 69 | msgstr "qsfuuzvtfst" 70 | 71 | #: src/modules/test/test.py:259 72 | msgid "dmview" 73 | msgstr "enwjfx" 74 | 75 | #: src/modules/test/test.py:270 76 | msgid "multiview" 77 | msgstr "nvmujwjfx" 78 | 79 | #: src/modules/test/test.py:287 80 | msgid "stats-card" 81 | msgstr "tubut-dbse" 82 | -------------------------------------------------------------------------------- /src/modules/sysadmin/dash.py: -------------------------------------------------------------------------------- 1 | """ 2 | The dashboard shows a summary of the various registered global bot settings. 3 | """ 4 | 5 | import discord 6 | import discord.ext.commands as cmds 7 | import discord.app_commands as appcmd 8 | 9 | from meta import LionBot, LionCog, LionContext, conf 10 | from meta.app import appname 11 | from wards import sys_admin_ward 12 | 13 | from settings.groups import SettingGroup 14 | 15 | 16 | class LeoSettings(LionCog): 17 | depends = {'CoreCog'} 18 | 19 | admin_guilds = conf.bot.getintlist('admin_guilds') 20 | 21 | def __init__(self, bot: LionBot): 22 | self.bot = bot 23 | 24 | self.bot_setting_groups: list[SettingGroup] = [] 25 | 26 | @cmds.hybrid_group( 27 | name="leo" 28 | ) 29 | @appcmd.guilds(*admin_guilds) 30 | @sys_admin_ward 31 | async def leo_group(self, ctx: LionContext): 32 | """ 33 | Base command group for global leo-only functions. 34 | Only accessible by sysadmins. 35 | """ 36 | ... 37 | 38 | @leo_group.command( 39 | name='dashboard', 40 | description="Global setting dashboard" 41 | ) 42 | @sys_admin_ward 43 | async def dash_cmd(self, ctx: LionContext): 44 | embed = discord.Embed( 45 | title="System Admin Dashboard", 46 | colour=discord.Colour.orange() 47 | ) 48 | for group in self.bot_setting_groups: 49 | table = await group.make_setting_table(appname) 50 | description = group.description.format(ctx=ctx, bot=ctx.bot).strip() 51 | embed.add_field( 52 | name=group.title.format(ctx=ctx, bot=ctx.bot), 53 | value=f"{description}\n{table}", 54 | inline=False 55 | ) 56 | 57 | await ctx.reply(embed=embed) 58 | 59 | @leo_group.group( 60 | name='configure', 61 | description="Leo Configuration Group" 62 | ) 63 | @sys_admin_ward 64 | async def leo_configure_group(self, ctx: LionContext): 65 | """ 66 | Base command group for global configuration of Leo. 67 | """ 68 | ... 69 | -------------------------------------------------------------------------------- /src/core/lion_user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import discord 3 | import pytz 4 | 5 | from meta import LionBot 6 | from utils.lib import utc_now, Timezoned 7 | from settings.groups import ModelConfig, SettingDotDict 8 | 9 | from .data import CoreData 10 | 11 | 12 | class UserConfig(ModelConfig): 13 | settings = SettingDotDict() 14 | _model_settings = set() 15 | model = CoreData.User 16 | 17 | @property 18 | def timezone(self) -> pytz.timezone: 19 | return self.get('timezone') 20 | 21 | 22 | class LionUser(Timezoned): 23 | """ 24 | Represents a User in the LionBot paradigm. 25 | 26 | Provides central access to cached data and configuration for a User. 27 | 28 | No guarantee is made that the client has access to this User. 29 | """ 30 | __slots__ = ('bot', 'data', 'userid', '_user', 'config', '__weakref__') 31 | 32 | Config = UserConfig 33 | settings = Config.settings 34 | 35 | def __init__(self, bot: LionBot, data: CoreData.User, user: Optional[discord.User] = None): 36 | self.bot = bot 37 | self.data = data 38 | self.userid = data.userid 39 | 40 | self._user = user 41 | 42 | self.config = self.Config(self.userid, data) 43 | 44 | @property 45 | def user(self): 46 | if self._user is None: 47 | self._user = self.bot.get_user(self.userid) 48 | return self._user 49 | 50 | @property 51 | def timezone(self) -> pytz.timezone: 52 | return self.config.timezone.value or pytz.UTC 53 | 54 | async def touch_discord_model(self, user: discord.User, seen=True): 55 | """ 56 | Updated stored Discord model attributes for this user. 57 | """ 58 | to_update = {} 59 | 60 | avatar_key = user.avatar.key if user.avatar else None 61 | if self.data.avatar_hash != avatar_key: 62 | to_update['avatar_hash'] = avatar_key 63 | 64 | if self.data.name != user.name: 65 | to_update['name'] = user.name 66 | 67 | if seen: 68 | to_update['last_seen'] = utc_now() 69 | 70 | if to_update: 71 | await self.data.update(**to_update) 72 | -------------------------------------------------------------------------------- /config/emojis.conf: -------------------------------------------------------------------------------- 1 | [EMOJIS] 2 | lionyay = <:lionyay:933610591388581890> 3 | lionlove = <:lionlove:933610591459872868> 4 | 5 | progress_left_empty = <:1dark:933826583934959677> 6 | progress_left_full = <:3_:933820201840021554> 7 | progress_middle_full = <:5_:933823492221173860> 8 | progress_middle_transition = <:6_:933824739414278184> 9 | progress_middle_empty = <:7_:933825425631752263> 10 | progress_right_empty = <:8fixed:933827434950844468> 11 | progress_right_full = <:9full:933828043548524634> 12 | 13 | inactive_achievement_1 = <:1:936107534748635166> 14 | inactive_achievement_2 = <:2:936107534815735879> 15 | inactive_achievement_3 = <:3:936107534782193704> 16 | inactive_achievement_4 = <:4:936107534358577203> 17 | inactive_achievement_5 = <:5:936107534715088926> 18 | inactive_achievement_6 = <:6:936107534169833483> 19 | inactive_achievement_7 = <:7:936107534723448832> 20 | inactive_achievement_8 = <:8:936107534706688070> 21 | 22 | active_achievement_1 = <:a1_:936107582786011196> 23 | active_achievement_2 = <:a2_:936107582693720104> 24 | active_achievement_3 = <:a3_:936107582760824862> 25 | active_achievement_4 = <:a4_:936107582614028348> 26 | active_achievement_5 = <:a5_:936107582630809620> 27 | active_achievement_6 = <:a6_:936107582609821726> 28 | active_achievement_7 = <:a7_:936107582546935818> 29 | active_achievement_8 = <:a8_:936107582626623498> 30 | 31 | 32 | gem = <:gem:975880967891845210> 33 | 34 | skin_equipped = <:checkmarkthinbigger:975880828494151711> 35 | skin_owned = <:greycheckmarkthinbigger:975880828485767198> 36 | skin_unowned = <:newbigger:975880828712288276> 37 | 38 | backward = <:arrowbackwardbigger:975880828460597288> 39 | forward = <:arrowforwardbigger:975880828624199710> 40 | person = <:person01:975880828481581096> 41 | 42 | question = <:questionmarkbigger:975880828645167154> 43 | cancel = <:xbigger:975880828653568012> 44 | refresh = <:cyclebigger:975880828611600404> 45 | loading = 46 | 47 | tick = :✅: 48 | clock = :⏱️: 49 | warning = :⚠️: 50 | config = :⚙️: 51 | stats = :📊: 52 | utility = :⏱️: 53 | 54 | coin = <:coin:975880967485022239> 55 | 56 | task_checked = :🟢: 57 | task_unchecked = :⚫: 58 | task_new = :➕: 59 | task_save = :💾: 60 | -------------------------------------------------------------------------------- /src/modules/moderation/tickets/warning.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Optional 2 | import datetime as dt 3 | 4 | import discord 5 | from meta import LionBot 6 | from utils.lib import utc_now 7 | 8 | from ..ticket import Ticket, ticket_factory 9 | from ..data import TicketType, TicketState, ModerationData 10 | from .. import logger, babel 11 | 12 | if TYPE_CHECKING: 13 | from ..cog import ModerationCog 14 | 15 | _p = babel._p 16 | 17 | 18 | @ticket_factory(TicketType.WARNING) 19 | class WarnTicket(Ticket): 20 | __slots__ = () 21 | 22 | @classmethod 23 | async def create( 24 | cls, bot: LionBot, guildid: int, userid: int, 25 | moderatorid: int, content: Optional[str], expiry=None, 26 | **kwargs 27 | ): 28 | modcog: 'ModerationCog' = bot.get_cog('ModerationCog') 29 | ticket_data = await modcog.data.Ticket.create( 30 | guildid=guildid, 31 | targetid=userid, 32 | ticket_type=TicketType.WARNING, 33 | ticket_state=TicketState.OPEN, 34 | moderator_id=moderatorid, 35 | content=content, 36 | expiry=expiry, 37 | created_at=utc_now().replace(tzinfo=None), 38 | **kwargs 39 | ) 40 | 41 | lguild = await bot.core.lions.fetch_guild(guildid) 42 | new_ticket = cls(lguild, ticket_data) 43 | await new_ticket.post() 44 | 45 | if expiry: 46 | cls.expiring.schedule_task(ticket_data.ticketid, expiry.timestamp()) 47 | 48 | return new_ticket 49 | 50 | @classmethod 51 | async def count_warnings_for( 52 | cls, bot: LionBot, guildid: int, userid: int, **kwargs 53 | ): 54 | modcog: 'ModerationCog' = bot.get_cog('ModerationCog') 55 | Ticket = modcog.data.Ticket 56 | record = await Ticket.table.select_one_where( 57 | (Ticket.ticket_state != TicketState.PARDONED), 58 | guildid=guildid, 59 | targetid=userid, 60 | ticket_type=TicketType.WARNING, 61 | **kwargs 62 | ).select(ticket_count='COUNT(*)').with_no_adapter() 63 | return (record[0]['ticket_count'] or 0) if record else 0 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/modules/schedule/core/session_member.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from collections import defaultdict 3 | import datetime as dt 4 | import asyncio 5 | import itertools 6 | 7 | import discord 8 | 9 | from meta import LionBot 10 | from utils.lib import utc_now 11 | from core.lion_member import LionMember 12 | 13 | from .. import babel, logger 14 | from ..data import ScheduleData as Data 15 | from ..lib import slotid_to_utc 16 | 17 | _p = babel._p 18 | 19 | 20 | class SessionMember: 21 | """ 22 | Member context for a scheduled session timeslot. 23 | 24 | Intended to keep track of members for ongoing and upcoming sessions. 25 | Primarily used to track clock time and set attended status. 26 | """ 27 | # TODO: slots 28 | 29 | def __init__(self, 30 | bot: LionBot, data: Data.ScheduleSessionMember, 31 | lion: LionMember): 32 | self.bot = bot 33 | self.data = data 34 | self.lion = lion 35 | 36 | self.slotid = data.slotid 37 | self.slot_start = slotid_to_utc(self.slotid) 38 | self.slot_end = slotid_to_utc(self.slotid + 3600) 39 | self.userid = data.userid 40 | self.guildid = data.guildid 41 | 42 | self.clock_start = None 43 | self.clocked = 0 44 | 45 | @property 46 | def total_clock(self): 47 | clocked = self.clocked 48 | if self.clock_start is not None: 49 | end = min(utc_now(), self.slot_end) 50 | clocked += (end - self.clock_start).total_seconds() 51 | return clocked 52 | 53 | def clock_on(self, at: dt.datetime): 54 | """ 55 | Mark this member as attending the scheduled session. 56 | """ 57 | if self.clock_start: 58 | self.clock_off(at) 59 | self.clock_start = max(self.slot_start, at) 60 | 61 | def clock_off(self, at: dt.datetime): 62 | """ 63 | Mark this member as no longer attending. 64 | """ 65 | if not self.clock_start: 66 | raise ValueError("Member clocking off while already off.") 67 | end = min(at, self.slot_end) 68 | self.clocked += (end - self.clock_start).total_seconds() 69 | self.clock_start = None 70 | -------------------------------------------------------------------------------- /src/meta/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from string import Template 3 | 4 | 5 | class SafeCancellation(Exception): 6 | """ 7 | Raised to safely cancel execution of the current operation. 8 | 9 | If not caught, is expected to be propagated to the Tree and safely ignored there. 10 | If a `msg` is provided, a context-aware error handler should catch and send the message to the user. 11 | The error handler should then set the `msg` to None, to avoid double handling. 12 | Debugging information should go in `details`, to be logged by a top-level error handler. 13 | """ 14 | default_message = "" 15 | 16 | @property 17 | def msg(self): 18 | return self._msg if self._msg is not None else self.default_message 19 | 20 | def __init__(self, _msg: Optional[str] = None, details: Optional[str] = None, **kwargs): 21 | self._msg: Optional[str] = _msg 22 | self.details: str = details if details is not None else self.msg 23 | super().__init__(**kwargs) 24 | 25 | 26 | class UserInputError(SafeCancellation): 27 | """ 28 | A SafeCancellation induced from unparseable user input. 29 | """ 30 | default_message = "Could not understand your input." 31 | 32 | @property 33 | def msg(self): 34 | return Template(self._msg).substitute(**self.info) if self._msg is not None else self.default_message 35 | 36 | def __init__(self, _msg: Optional[str] = None, info: dict[str, str] = {}, **kwargs): 37 | self.info = info 38 | super().__init__(_msg, **kwargs) 39 | 40 | 41 | class UserCancelled(SafeCancellation): 42 | """ 43 | A SafeCancellation induced from manual user cancellation. 44 | 45 | Usually silent. 46 | """ 47 | default_msg = None 48 | 49 | 50 | class ResponseTimedOut(SafeCancellation): 51 | """ 52 | A SafeCancellation induced from a user interaction time-out. 53 | """ 54 | default_msg = "Session timed out waiting for input." 55 | 56 | 57 | class HandledException(SafeCancellation): 58 | """ 59 | Sentinel class to indicate to error handlers that this exception has been handled. 60 | Required because discord.ext breaks the exception stack, so we can't just catch the error in a lower handler. 61 | """ 62 | def __init__(self, exc=None, **kwargs): 63 | self.exc = exc 64 | super().__init__(**kwargs) 65 | -------------------------------------------------------------------------------- /src/modules/rolemenus/data.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from data import Registry, RowModel, RegisterEnum, Column, NULL 4 | from data.columns import Integer, Timestamp, String, Bool 5 | 6 | 7 | class MenuType(Enum): 8 | REACTION = 'REACTION', 9 | BUTTON = 'BUTTON', 10 | DROPDOWN = 'DROPDOWN', 11 | 12 | 13 | class RoleMenuData(Registry): 14 | MenuType = RegisterEnum(MenuType, name='RoleMenuType') 15 | 16 | class RoleMenu(RowModel): 17 | _tablename_ = 'role_menus' 18 | _cache_ = {} 19 | 20 | menuid = Integer(primary=True) 21 | guildid = Integer() 22 | 23 | channelid = Integer() 24 | messageid = Integer() 25 | 26 | name = String() 27 | enabled = Bool() 28 | 29 | required_roleid = Integer() 30 | sticky = Bool() 31 | refunds = Bool() 32 | obtainable = Integer() 33 | 34 | menutype: Column[MenuType] = Column() 35 | templateid = Integer() 36 | rawmessage = String() 37 | 38 | default_price = Integer() 39 | event_log = Bool() 40 | 41 | class RoleMenuRole(RowModel): 42 | _tablename_ = 'role_menu_roles' 43 | _cache_ = {} 44 | 45 | menuroleid = Integer(primary=True) 46 | 47 | menuid = Integer() 48 | roleid = Integer() 49 | 50 | label = String() 51 | emoji = String() 52 | description = String() 53 | 54 | price = Integer() 55 | duration = Integer() 56 | 57 | rawreply = String() 58 | 59 | class RoleMenuHistory(RowModel): 60 | _tablename_ = 'role_menu_history' 61 | _cache_ = None 62 | 63 | equipid = Integer(primary=True) 64 | 65 | menuid = Integer() 66 | roleid = Integer() 67 | userid = Integer() 68 | 69 | obtained_at = Timestamp() 70 | transactionid = Integer() 71 | expires_at = Timestamp() 72 | removed_at = Timestamp() 73 | 74 | @classmethod 75 | def fetch_expiring_where(cls, *args, **kwargs): 76 | """ 77 | Fetch expiring equip rows. 78 | 79 | This returns an awaitable and chainable Select Query. 80 | """ 81 | return cls.fetch_where( 82 | (cls.expires_at != NULL), 83 | (cls.removed_at == NULL), 84 | *args, **kwargs 85 | ) 86 | -------------------------------------------------------------------------------- /locales/pt_BR/LC_MESSAGES/test.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Bruno Evangelista De Oliveira, 2023 8 | # 9 | #, fuzzy 10 | msgid "" 11 | msgstr "" 12 | "Project-Id-Version: PACKAGE VERSION\n" 13 | "Report-Msgid-Bugs-To: \n" 14 | "POT-Creation-Date: 2023-09-24 12:21+0300\n" 15 | "PO-Revision-Date: 2023-08-28 13:43+0000\n" 16 | "Last-Translator: Bruno Evangelista De Oliveira, 2023\n" 17 | "Language-Team: Portuguese (Brazil) (https://app.transifex.com/leobot/teams/174919/pt_BR/)\n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Language: pt_BR\n" 22 | "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" 23 | 24 | #: src/modules/test/test.py:59 src/modules/test/test.py:66 25 | msgid "test" 26 | msgstr "teste" 27 | 28 | #: src/modules/test/test.py:67 29 | msgid "Test" 30 | msgstr "Teste" 31 | 32 | #: src/modules/test/test.py:74 33 | msgid "editor" 34 | msgstr "editor" 35 | 36 | #: src/modules/test/test.py:75 37 | msgid "Test message editor" 38 | msgstr "Teste do editor de mensagem" 39 | 40 | #: src/modules/test/test.py:101 41 | msgid "test_ephemeral" 42 | msgstr "teste_provisório" 43 | 44 | #: src/modules/test/test.py:102 45 | msgid "Test ephemeral delete and edit" 46 | msgstr "Apagar e editar Teste Provisório" 47 | 48 | #: src/modules/test/test.py:114 49 | msgid "colours" 50 | msgstr "cores" 51 | 52 | #: src/modules/test/test.py:115 53 | msgid "Test Ansi colours" 54 | msgstr "Teste cores Ansi" 55 | 56 | #: src/modules/test/test.py:135 57 | msgid "fail" 58 | msgstr "falha" 59 | 60 | #: src/modules/test/test.py:143 61 | msgid "failui" 62 | msgstr "falha" 63 | 64 | #: src/modules/test/test.py:150 65 | msgid "pager" 66 | msgstr "pager" 67 | 68 | #: src/modules/test/test.py:178 69 | msgid "pager2" 70 | msgstr "pager2" 71 | 72 | #: src/modules/test/test.py:209 73 | msgid "prettyusers" 74 | msgstr "usuáriosfofos" 75 | 76 | #: src/modules/test/test.py:259 77 | msgid "dmview" 78 | msgstr "ver_md" 79 | 80 | #: src/modules/test/test.py:270 81 | msgid "multiview" 82 | msgstr "multivisao" 83 | 84 | #: src/modules/test/test.py:287 85 | msgid "stats-card" 86 | msgstr "stats-cartões" 87 | -------------------------------------------------------------------------------- /locales/he_IL/LC_MESSAGES/test.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | # Translators: 7 | # Interitio, 2023 8 | # Ari Horesh, 2023 9 | # 10 | #, fuzzy 11 | msgid "" 12 | msgstr "" 13 | "Project-Id-Version: PACKAGE VERSION\n" 14 | "Report-Msgid-Bugs-To: \n" 15 | "POT-Creation-Date: 2023-09-13 08:47+0300\n" 16 | "PO-Revision-Date: 2023-08-28 13:43+0000\n" 17 | "Last-Translator: Ari Horesh, 2023\n" 18 | "Language-Team: Hebrew (Israel) (https://app.transifex.com/leobot/teams/174919/he_IL/)\n" 19 | "MIME-Version: 1.0\n" 20 | "Content-Type: text/plain; charset=UTF-8\n" 21 | "Content-Transfer-Encoding: 8bit\n" 22 | "Language: he_IL\n" 23 | "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" 24 | 25 | #: src/modules/test/test.py:59 src/modules/test/test.py:66 26 | msgid "test" 27 | msgstr "test" 28 | 29 | #: src/modules/test/test.py:67 30 | msgid "Test" 31 | msgstr "Test" 32 | 33 | #: src/modules/test/test.py:74 34 | msgid "editor" 35 | msgstr "עורך" 36 | 37 | #: src/modules/test/test.py:75 38 | msgid "Test message editor" 39 | msgstr "Test message editor" 40 | 41 | #: src/modules/test/test.py:101 42 | msgid "test_ephemeral" 43 | msgstr "test_ephemeral" 44 | 45 | #: src/modules/test/test.py:102 46 | msgid "Test ephemeral delete and edit" 47 | msgstr "Test ephemeral delete and edit" 48 | 49 | #: src/modules/test/test.py:114 50 | msgid "colours" 51 | msgstr "צבעים" 52 | 53 | #: src/modules/test/test.py:115 54 | msgid "Test Ansi colours" 55 | msgstr "Test Ansi colours" 56 | 57 | #: src/modules/test/test.py:135 58 | msgid "fail" 59 | msgstr "נכשל" 60 | 61 | #: src/modules/test/test.py:143 62 | msgid "failui" 63 | msgstr "failui" 64 | 65 | #: src/modules/test/test.py:150 66 | msgid "pager" 67 | msgstr "pager" 68 | 69 | #: src/modules/test/test.py:178 70 | msgid "pager2" 71 | msgstr "pager2" 72 | 73 | #: src/modules/test/test.py:209 74 | msgid "prettyusers" 75 | msgstr "prettyusers" 76 | 77 | #: src/modules/test/test.py:259 78 | msgid "dmview" 79 | msgstr "dmview" 80 | 81 | #: src/modules/test/test.py:270 82 | msgid "multiview" 83 | msgstr "multiview" 84 | 85 | #: src/modules/test/test.py:287 86 | msgid "stats-card" 87 | msgstr "כרטיס-סטטיסטיקה" 88 | -------------------------------------------------------------------------------- /src/modules/economy/settingui.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import discord 4 | from discord.ui.select import select, Select, ChannelSelect 5 | from discord.ui.button import button, Button, ButtonStyle 6 | 7 | from meta import LionBot 8 | 9 | from utils.ui import ConfigUI, DashboardSection 10 | from utils.lib import MessageArgs 11 | 12 | from .settings import EconomySettings 13 | from . import babel 14 | 15 | _p = babel._p 16 | 17 | 18 | class EconomyConfigUI(ConfigUI): 19 | setting_classes = ( 20 | EconomySettings.StartingFunds, 21 | EconomySettings.CoinsPerXP, 22 | EconomySettings.AllowTransfers, 23 | ) 24 | 25 | def __init__(self, bot: LionBot, 26 | guildid: int, channelid: int, **kwargs): 27 | self.settings = bot.get_cog('Economy').settings 28 | super().__init__(bot, guildid, channelid, **kwargs) 29 | 30 | async def make_message(self) -> MessageArgs: 31 | t = self.bot.translator.t 32 | title = t(_p( 33 | 'ui:economy_config|embed|title', 34 | "Economy Configuration Panel" 35 | )) 36 | embed = discord.Embed( 37 | colour=discord.Colour.orange(), 38 | title=title 39 | ) 40 | for setting in self.instances: 41 | embed.add_field(**setting.embed_field, inline=False) 42 | 43 | args = MessageArgs(embed=embed) 44 | return args 45 | 46 | async def reload(self): 47 | lguild = await self.bot.core.lions.fetch_guild(self.guildid) 48 | self.instances = tuple( 49 | lguild.config.get(cls.setting_id) 50 | for cls in self.setting_classes 51 | ) 52 | 53 | async def refresh_components(self): 54 | await asyncio.gather( 55 | self.edit_button_refresh(), 56 | self.close_button_refresh(), 57 | self.reset_button_refresh(), 58 | ) 59 | self.set_layout( 60 | (self.edit_button, self.reset_button, self.close_button), 61 | ) 62 | 63 | 64 | class EconomyDashboard(DashboardSection): 65 | section_name = _p( 66 | 'dash:economy|title', 67 | "Economy Configuration ({commands[config economy]})" 68 | ) 69 | _option_name = _p( 70 | "dash:economy|dropdown|placeholder", 71 | "Economy Panel" 72 | ) 73 | configui = EconomyConfigUI 74 | setting_classes = EconomyConfigUI.setting_classes 75 | -------------------------------------------------------------------------------- /scripts/makedomains.py: -------------------------------------------------------------------------------- 1 | # Iterate through each file in the project 2 | # Look for LocalBabel("text") in each file, mark it as the "text" domain 3 | # module files inherit from their __init__ files if they don't specify a specific domain 4 | import re 5 | import os 6 | from collections import defaultdict 7 | 8 | babel_regex = re.compile( 9 | r".*LocalBabel\(\s*['\"](.*)['\"]\s*\).*" 10 | ) 11 | 12 | 13 | ignored_dirs = { 14 | 'RCS', 15 | '__pycache__', 16 | 'pending-rewrite', 17 | '.git' 18 | } 19 | 20 | 21 | def parse_domain(path): 22 | """ 23 | Parse a file to check for domain specifications. 24 | 25 | Currently just looks for a LocalBabel("domain") specification. 26 | """ 27 | with open(path, 'r') as file: 28 | for line in file: 29 | match = babel_regex.match(line) 30 | if match: 31 | return match.groups()[0] 32 | 33 | 34 | def read_directory(path, domain_map, domain='base'): 35 | init_path = None 36 | files = [] 37 | dirs = [] 38 | for entry in os.scandir(path): 39 | if entry.is_file(follow_symlinks=False) and entry.name.endswith('.py'): 40 | if entry.name == '__init__.py': 41 | init_path = entry.path 42 | else: 43 | files.append(entry.path) 44 | elif entry.is_dir(follow_symlinks=False) and entry.name not in ignored_dirs: 45 | dirs.append(entry.path) 46 | 47 | if init_path: 48 | domain = parse_domain(init_path) or domain 49 | print( 50 | f"{domain:<20} | {path}" 51 | ) 52 | 53 | for file_path in files: 54 | file_domain = parse_domain(file_path) or domain 55 | print( 56 | f"{file_domain:<20} | {file_path}" 57 | ) 58 | domain_map[file_domain].append(file_path) 59 | 60 | for dir_path in dirs: 61 | read_directory(dir_path, domain_map, domain) 62 | 63 | 64 | def write_domains(domain_map): 65 | for domain, files in domain_map.items(): 66 | domain_path = os.path.join('locales', 'domains', f"{domain}.txt") 67 | with open(domain_path, 'w') as domain_file: 68 | domain_file.write('\n'.join(files)) 69 | print(f"Wrote {len(files)} source files to {domain_path}") 70 | 71 | 72 | if __name__ == '__main__': 73 | domain_map = defaultdict(list) 74 | read_directory('src/', domain_map, domain='base') 75 | write_domains(domain_map) 76 | print("Supported domains: {}".format(', '.join(domain_map.keys()))) 77 | -------------------------------------------------------------------------------- /data/migration/v7-v8/migration.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE guild_config ADD COLUMN pomodoro_channel BIGINT; 2 | 3 | -- Timer Data {{{ 4 | create TABLE timers( 5 | channelid BIGINT PRIMARY KEY, 6 | guildid BIGINT NOT NULL REFERENCES guild_config (guildid), 7 | text_channelid BIGINT, 8 | focus_length INTEGER NOT NULL, 9 | break_length INTEGER NOT NULL, 10 | last_started TIMESTAMPTZ NOT NULL, 11 | inactivity_threshold INTEGER, 12 | channel_name TEXT, 13 | pretty_name TEXT 14 | ); 15 | CREATE INDEX timers_guilds ON timers (guildid); 16 | -- }}} 17 | 18 | -- Session tags {{{ 19 | ALTER TABLE current_sessions 20 | ADD COLUMN rating INTEGER, 21 | ADD COLUMN tag TEXT; 22 | 23 | ALTER TABLE session_history 24 | ADD COLUMN rating INTEGER, 25 | ADD COLUMN tag TEXT; 26 | 27 | DROP FUNCTION close_study_session(_guildid BIGINT, _userid BIGINT); 28 | 29 | CREATE FUNCTION close_study_session(_guildid BIGINT, _userid BIGINT) 30 | RETURNS SETOF members 31 | AS $$ 32 | BEGIN 33 | RETURN QUERY 34 | WITH 35 | current_sesh AS ( 36 | DELETE FROM current_sessions 37 | WHERE guildid=_guildid AND userid=_userid 38 | RETURNING 39 | *, 40 | EXTRACT(EPOCH FROM (NOW() - start_time)) AS total_duration, 41 | stream_duration + COALESCE(EXTRACT(EPOCH FROM (NOW() - stream_start)), 0) AS total_stream_duration, 42 | video_duration + COALESCE(EXTRACT(EPOCH FROM (NOW() - video_start)), 0) AS total_video_duration, 43 | live_duration + COALESCE(EXTRACT(EPOCH FROM (NOW() - live_start)), 0) AS total_live_duration 44 | ), saved_sesh AS ( 45 | INSERT INTO session_history ( 46 | guildid, userid, channelid, rating, tag, channel_type, start_time, 47 | duration, stream_duration, video_duration, live_duration, 48 | coins_earned 49 | ) SELECT 50 | guildid, userid, channelid, rating, tag, channel_type, start_time, 51 | total_duration, total_stream_duration, total_video_duration, total_live_duration, 52 | (total_duration * hourly_coins + live_duration * hourly_live_coins) / 3600 53 | FROM current_sesh 54 | RETURNING * 55 | ) 56 | UPDATE members 57 | SET 58 | tracked_time=(tracked_time + saved_sesh.duration), 59 | coins=(coins + saved_sesh.coins_earned) 60 | FROM saved_sesh 61 | WHERE members.guildid=saved_sesh.guildid AND members.userid=saved_sesh.userid 62 | RETURNING members.*; 63 | END; 64 | $$ LANGUAGE PLPGSQL; 65 | -- }}} 66 | 67 | INSERT INTO VersionHistory (version, author) VALUES (8, 'v7-v8 migration'); 68 | -------------------------------------------------------------------------------- /locales/templates/config.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-10-01 16:01+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/modules/config/general.py:41 21 | msgctxt "guildset:timezone" 22 | msgid "timezone" 23 | msgstr "" 24 | 25 | #: src/modules/config/general.py:44 26 | msgctxt "guildset:timezone|desc" 27 | msgid "Guild timezone for statistics display." 28 | msgstr "" 29 | 30 | #: src/modules/config/general.py:48 31 | msgctxt "guildset:timezone|long_desc" 32 | msgid "" 33 | "Guild-wide timezone. Used to determine start of the day for the " 34 | "leaderboards, and as the default statistics timezone for members who have " 35 | "not set one." 36 | msgstr "" 37 | 38 | #: src/modules/config/general.py:62 39 | #, possible-python-brace-format 40 | msgctxt "guildset:timezone|response" 41 | msgid "The guild timezone has been set to `{timezone}`." 42 | msgstr "" 43 | 44 | #: src/modules/config/general.py:94 45 | msgctxt "cmd:configure_general" 46 | msgid "general" 47 | msgstr "" 48 | 49 | #: src/modules/config/general.py:95 50 | msgctxt "cmd:configure_general|desc" 51 | msgid "General configuration panel" 52 | msgstr "" 53 | 54 | #: src/modules/config/general.py:129 55 | msgctxt "cmd:configure_general|parse_failure:timezone" 56 | msgid "Could not set the timezone!" 57 | msgstr "" 58 | 59 | #: src/modules/config/general.py:150 60 | msgctxt "cmd:configure_general|success" 61 | msgid "Settings Updated!" 62 | msgstr "" 63 | 64 | #: src/modules/config/general.py:165 65 | msgctxt "cmd:configure_general|panel|title" 66 | msgid "General Configuration Panel" 67 | msgstr "" 68 | 69 | #: src/modules/config/dashboard.py:98 70 | #, possible-python-brace-format 71 | msgctxt "ui:dashboard|title" 72 | msgid "Guild Dashboard (Page {page}/{total})" 73 | msgstr "" 74 | 75 | #: src/modules/config/dashboard.py:109 76 | msgctxt "ui:dashboard|footer" 77 | msgid "Hover over setting names for a brief description" 78 | msgstr "" 79 | 80 | #: src/modules/config/dashboard.py:172 81 | msgctxt "ui:dashboard|menu:config|placeholder" 82 | msgid "Open Configuration Panel" 83 | msgstr "" 84 | -------------------------------------------------------------------------------- /src/utils/ansi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Minimal library for making Discord Ansi colour codes. 3 | """ 4 | from enum import StrEnum 5 | 6 | 7 | PREFIX = u'\u001b' 8 | 9 | 10 | class TextColour(StrEnum): 11 | Gray = '30' 12 | Red = '31' 13 | Green = '32' 14 | Yellow = '33' 15 | Blue = '34' 16 | Pink = '35' 17 | Cyan = '36' 18 | White = '37' 19 | 20 | def __str__(self) -> str: 21 | return AnsiColour(fg=self).as_str() 22 | 23 | def __call__(self): 24 | return AnsiColour(fg=self) 25 | 26 | 27 | class BgColour(StrEnum): 28 | FireflyDarkBlue = '40' 29 | Orange = '41' 30 | MarbleBlue = '42' 31 | GrayTurq = '43' 32 | Gray = '44' 33 | Indigo = '45' 34 | LightGray = '46' 35 | White = '47' 36 | 37 | def __str__(self) -> str: 38 | return AnsiColour(bg=self).as_str() 39 | 40 | def __call__(self): 41 | return AnsiColour(bg=self) 42 | 43 | 44 | class Format(StrEnum): 45 | NORMAL = '0' 46 | BOLD = '1' 47 | UNDERLINE = '4' 48 | NOOP = '9' 49 | 50 | def __str__(self) -> str: 51 | return AnsiColour(self).as_str() 52 | 53 | def __call__(self): 54 | return AnsiColour(self) 55 | 56 | 57 | class AnsiColour: 58 | def __init__(self, *flags, fg=None, bg=None): 59 | self.text_colour = fg 60 | self.background_colour = bg 61 | self.reset = (Format.NORMAL in flags) 62 | self._flags = set(flags) 63 | self._flags.discard(Format.NORMAL) 64 | 65 | @property 66 | def flags(self): 67 | return (*((Format.NORMAL,) if self.reset else ()), *self._flags) 68 | 69 | def as_str(self): 70 | parts = [] 71 | if self.reset: 72 | parts.append(Format.NORMAL) 73 | elif not self.flags: 74 | parts.append(Format.NOOP) 75 | 76 | parts.extend(self._flags) 77 | 78 | for c in (self.text_colour, self.background_colour): 79 | if c is not None: 80 | parts.append(c) 81 | 82 | partstr = ';'.join(part.value for part in parts) 83 | return f"{PREFIX}[{partstr}m" # ] 84 | 85 | def __str__(self): 86 | return self.as_str() 87 | 88 | def __add__(self, obj: 'AnsiColour'): 89 | text_colour = obj.text_colour or self.text_colour 90 | background_colour = obj.background_colour or self.background_colour 91 | flags = (*self.flags, *obj.flags) 92 | return AnsiColour(*flags, fg=text_colour, bg=background_colour) 93 | 94 | 95 | RESET = AnsiColour(Format.NORMAL) 96 | BOLD = AnsiColour(Format.BOLD) 97 | UNDERLINE = AnsiColour(Format.UNDERLINE) 98 | -------------------------------------------------------------------------------- /src/modules/statistics/graphics/profilestats.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from io import BytesIO 3 | 4 | from PIL import Image 5 | 6 | from meta import LionBot 7 | from gui.base import CardMode 8 | 9 | from .stats import get_stats_card 10 | from .profile import get_profile_card 11 | 12 | 13 | card_gap = 10 14 | 15 | 16 | async def get_full_profile(bot: LionBot, userid: int, guildid: int, mode: CardMode) -> BytesIO: 17 | """ 18 | Render both profile and stats for the target member in the given mode. 19 | 20 | Combines the resulting cards into a single image and returns the image data. 21 | """ 22 | # Prepare cards for rendering 23 | get_tasks = ( 24 | asyncio.create_task(get_stats_card(bot, userid, guildid, mode), name='get-stats-for-combined'), 25 | asyncio.create_task(get_profile_card(bot, userid, guildid), name='get-profile-for-combined'), 26 | ) 27 | stats_card, profile_card = await asyncio.gather(*get_tasks) 28 | 29 | # Render cards 30 | render_tasks = ( 31 | asyncio.create_task(stats_card.render(), name='render-stats-for-combined'), 32 | asyncio.create_task(profile_card.render(), name='render=profile-for-combined'), 33 | ) 34 | 35 | # Load the card data into images 36 | stats_data, profile_data = await asyncio.gather(*render_tasks) 37 | with BytesIO(stats_data) as stats_stream, BytesIO(profile_data) as profile_stream: 38 | with Image.open(stats_stream) as stats_image, Image.open(profile_stream) as profile_image: 39 | # Create a new blank image of the correct dimenstions 40 | stats_bbox = stats_image.getbbox(alpha_only=False) 41 | profile_bbox = profile_image.getbbox(alpha_only=False) 42 | 43 | if stats_bbox is None or profile_bbox is None: 44 | # Should be impossible, image is already checked by GUI client 45 | raise ValueError("Could not combine, empty stats or profile image.") 46 | 47 | combined = Image.new( 48 | 'RGBA', 49 | ( 50 | max(stats_bbox[2], profile_bbox[2]), 51 | stats_bbox[3] + card_gap + profile_bbox[3] 52 | ), 53 | color=None 54 | ) 55 | with combined: 56 | combined.alpha_composite(profile_image) 57 | combined.alpha_composite(stats_image, (0, profile_bbox[3] + card_gap)) 58 | 59 | results = BytesIO() 60 | combined.save(results, format='PNG', compress_type=3, compress_level=1) 61 | results.seek(0) 62 | return results 63 | -------------------------------------------------------------------------------- /scripts/maketestlang.py: -------------------------------------------------------------------------------- 1 | import os 2 | import string 3 | import polib 4 | 5 | templates = os.path.join('locales', 'templates') 6 | test_target = os.path.join('locales', 'ceaser', 'LC_MESSAGES') 7 | 8 | 9 | def translate_string(msgid: str) -> str: 10 | tokens = [] 11 | lifted = False 12 | for c in msgid: 13 | if c in ('{', '}'): 14 | lifted = not lifted 15 | elif not lifted and c.isalpha(): 16 | if c.isupper(): 17 | letters = string.ascii_uppercase 18 | else: 19 | letters = string.ascii_lowercase 20 | index = letters.index(c) 21 | c = letters[(index + 1) % len(letters)] 22 | tokens.append(c) 23 | translated = ''.join(tokens) 24 | return translated 25 | 26 | 27 | def translate_entry(entry: polib.POEntry): 28 | if entry.msgctxt and ('regex' in entry.msgctxt): 29 | # Ignore 30 | ... 31 | else: 32 | if entry.msgid_plural: 33 | entry.msgstr_plural = { 34 | '0': translate_string(entry.msgid), 35 | '1': translate_string(entry.msgid_plural) 36 | } 37 | elif entry.msgid: 38 | entry.msgstr = translate_string(entry.msgid) 39 | 40 | 41 | def process_pot(domain, path): 42 | print(f"Processing pot for {domain}") 43 | entries = 0 44 | po = polib.pofile(path, encoding="UTF-8") 45 | po.metadata = { 46 | 'Project-Id-Version': '1.0', 47 | 'Report-Msgid-Bugs-To': 'you@example.com', 48 | 'POT-Creation-Date': '2007-10-18 14:00+0100', 49 | 'PO-Revision-Date': '2007-10-18 14:00+0100', 50 | 'Last-Translator': 'you ', 51 | 'Language-Team': 'English ', 52 | 'MIME-Version': '1.0', 53 | 'Content-Type': 'text/plain; charset=utf-8', 54 | 'Content-Transfer-Encoding': '8bit', 55 | } 56 | for entry in po.untranslated_entries(): 57 | entries += 1 58 | translate_entry(entry) 59 | # Now save 60 | targetpo = os.path.join(test_target, f"{domain}.po") 61 | targetmo = os.path.join(test_target, f"{domain}.mo") 62 | po.save(targetpo) 63 | po.save_as_mofile(targetmo) 64 | print(f"Processed {entries} from POT {domain}.") 65 | return entries 66 | 67 | 68 | def process_all(): 69 | total = 0 70 | for file in os.scandir(templates): 71 | if file.name.endswith('pot'): 72 | print(f"Processing pot: {file.name[:-4]}") 73 | total += process_pot(file.name[:-4], file.path) 74 | print(f"Total strings: {total}") 75 | 76 | 77 | if __name__ == '__main__': 78 | process_all() 79 | -------------------------------------------------------------------------------- /locales/templates/exec.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-10-01 16:01+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/modules/sysadmin/exec_cog.py:269 21 | msgctxt "ward:sys_admin|failed" 22 | msgid "You must be a bot owner to do this!" 23 | msgstr "" 24 | 25 | #: src/modules/sysadmin/exec_cog.py:274 26 | msgid "async" 27 | msgstr "" 28 | 29 | #: src/modules/sysadmin/exec_cog.py:275 30 | msgid "Execute arbitrary code with Exec" 31 | msgstr "" 32 | 33 | #: src/modules/sysadmin/exec_cog.py:337 34 | msgctxt "command" 35 | msgid "eval" 36 | msgstr "" 37 | 38 | #: src/modules/sysadmin/exec_cog.py:338 39 | msgctxt "command:eval" 40 | msgid "Execute arbitrary code with Eval" 41 | msgstr "" 42 | 43 | #: src/modules/sysadmin/exec_cog.py:341 44 | msgctxt "command:eval|param:string" 45 | msgid "Code to evaluate." 46 | msgstr "" 47 | 48 | #: src/modules/sysadmin/exec_cog.py:348 49 | msgctxt "command" 50 | msgid "asyncall" 51 | msgstr "" 52 | 53 | #: src/modules/sysadmin/exec_cog.py:349 54 | msgctxt "command:asyncall|desc" 55 | msgid "Execute arbitrary code on all shards." 56 | msgstr "" 57 | 58 | #: src/modules/sysadmin/exec_cog.py:352 59 | msgctxt "command:asyncall|param:string" 60 | msgid "Cross-shard code to execute. Cannot reference ctx!" 61 | msgstr "" 62 | 63 | #: src/modules/sysadmin/exec_cog.py:353 64 | msgctxt "command:asyncall|param:target" 65 | msgid "Target shard app name, see autocomplete for options." 66 | msgstr "" 67 | 68 | #: src/modules/sysadmin/exec_cog.py:396 69 | msgid "reload" 70 | msgstr "" 71 | 72 | #: src/modules/sysadmin/exec_cog.py:397 73 | msgid "Reload a given LionBot extension. Launches an ExecUI." 74 | msgstr "" 75 | 76 | #: src/modules/sysadmin/exec_cog.py:400 77 | msgid "Name of the extension to reload. See autocomplete for options." 78 | msgstr "" 79 | 80 | #: src/modules/sysadmin/exec_cog.py:401 81 | msgid "Whether to force an extension reload even if it doesn't exist." 82 | msgstr "" 83 | 84 | #: src/modules/sysadmin/exec_cog.py:437 85 | msgid "shutdown" 86 | msgstr "" 87 | 88 | #: src/modules/sysadmin/exec_cog.py:438 89 | msgid "Shutdown (or restart) the client." 90 | msgstr "" 91 | -------------------------------------------------------------------------------- /src/meta/LionCog.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from discord.ext.commands import Cog 4 | from discord.ext import commands as cmds 5 | 6 | 7 | class LionCog(Cog): 8 | # A set of other cogs that this cog depends on 9 | depends_on: set['LionCog'] = set() 10 | _placeholder_groups_: set[str] 11 | 12 | def __init_subclass__(cls, **kwargs): 13 | super().__init_subclass__(**kwargs) 14 | 15 | cls._placeholder_groups_ = set() 16 | 17 | for base in reversed(cls.__mro__): 18 | for elem, value in base.__dict__.items(): 19 | if isinstance(value, cmds.HybridGroup) and hasattr(value, '_placeholder_group_'): 20 | cls._placeholder_groups_.add(value.name) 21 | 22 | def __new__(cls, *args: Any, **kwargs: Any): 23 | # Patch to ensure no placeholder groups are in the command list 24 | self = super().__new__(cls) 25 | self.__cog_commands__ = [ 26 | command for command in self.__cog_commands__ if command.name not in cls._placeholder_groups_ 27 | ] 28 | return self 29 | 30 | async def _inject(self, bot, *args, **kwargs): 31 | if self.depends_on: 32 | not_found = {cogname for cogname in self.depends_on if not bot.get_cog(cogname)} 33 | raise ValueError(f"Could not load cog '{self.__class__.__name__}', dependencies missing: {not_found}") 34 | 35 | return await super()._inject(bot, *args, *kwargs) 36 | 37 | @classmethod 38 | def placeholder_group(cls, group: cmds.HybridGroup): 39 | group._placeholder_group_ = True 40 | return group 41 | 42 | def crossload_group(self, placeholder_group: cmds.HybridGroup, target_group: cmds.HybridGroup): 43 | """ 44 | Crossload a placeholder group's commands into the target group 45 | """ 46 | if not isinstance(placeholder_group, cmds.HybridGroup) or not isinstance(target_group, cmds.HybridGroup): 47 | raise ValueError("Placeholder and target groups my be HypridGroups.") 48 | if placeholder_group.name not in self._placeholder_groups_: 49 | raise ValueError("Placeholder group was not registered! Stopping to avoid duplicates.") 50 | if target_group.app_command is None: 51 | raise ValueError("Target group has no app_command to crossload into.") 52 | 53 | for command in placeholder_group.commands: 54 | placeholder_group.remove_command(command.name) 55 | target_group.remove_command(command.name) 56 | acmd = command.app_command._copy_with(parent=target_group.app_command, binding=self) 57 | command.app_command = acmd 58 | target_group.add_command(command) 59 | -------------------------------------------------------------------------------- /src/modules/pomodoro/settings.py: -------------------------------------------------------------------------------- 1 | from settings import ModelData 2 | from settings.groups import SettingGroup 3 | from settings.setting_types import ChannelSetting 4 | 5 | from core.data import CoreData 6 | from babel.translator import ctx_translator 7 | from wards import low_management_iward 8 | 9 | from . import babel 10 | 11 | _p = babel._p 12 | 13 | 14 | class TimerSettings(SettingGroup): 15 | class PomodoroChannel(ModelData, ChannelSetting): 16 | setting_id = 'pomodoro_channel' 17 | _event = 'guildset_pomodoro_channel' 18 | _set_cmd = 'config pomodoro' 19 | _write_ward = low_management_iward 20 | 21 | _display_name = _p('guildset:pomodoro_channel', "pomodoro_channel") 22 | _desc = _p( 23 | 'guildset:pomodoro_channel|desc', 24 | "Default central notification channel for pomodoro timers." 25 | ) 26 | _long_desc = _p( 27 | 'guildset:pomodoro_channel|long_desc', 28 | "Pomodoro timers which do not have a custom notification channel set will send " 29 | "timer notifications in this channel. " 30 | "If this setting is not set, pomodoro notifications will default to the " 31 | "timer voice channel itself." 32 | ) 33 | _notset_str = _p( 34 | 'guildset:pomodoro_channel|formatted|notset', 35 | "Not Set (Will use timer voice channel.)" 36 | ) 37 | _accepts = _p( 38 | 'guildset:pomodoro_channel|accepts', 39 | "Timer notification channel name or id." 40 | ) 41 | 42 | _model = CoreData.Guild 43 | _column = CoreData.Guild.pomodoro_channel.name 44 | _allow_object = False 45 | 46 | @property 47 | def update_message(self) -> str: 48 | t = ctx_translator.get().t 49 | value = self.value 50 | if value is not None: 51 | resp = t(_p( 52 | 'guildset:pomodoro_channel|set_response|set', 53 | "Pomodoro timer notifications will now default to {channel}" 54 | )).format(channel=value.mention) 55 | else: 56 | resp = t(_p( 57 | 'guildset:pomodoro_channel|set_response|unset', 58 | "Pomodoro timer notifications will now default to their voice channel." 59 | )) 60 | return resp 61 | 62 | @property 63 | def set_str(self) -> str: 64 | cmdstr = super().set_str 65 | t = ctx_translator.get().t 66 | return t(_p( 67 | 'guildset:pomdoro_channel|set_using', 68 | "{cmd} or channel selector below." 69 | )).format(cmd=cmdstr) 70 | -------------------------------------------------------------------------------- /src/modules/sysadmin/leo_group.py: -------------------------------------------------------------------------------- 1 | from discord.app_commands import Group, Command 2 | from discord.ext.commands import HybridCommand 3 | 4 | from meta import LionCog 5 | 6 | 7 | class LeoGroup(Group, name='leo'): 8 | """ 9 | Base command group for all Leo system admin commands. 10 | """ 11 | ... 12 | 13 | 14 | """ 15 | TODO: 16 | This will take some work to get working. 17 | We want to be able to specify a command in a cog 18 | as a subcommand of a group command in a different cog, 19 | or even a different extension. 20 | Unfortunately, this really messes with the hotloading and unloading, 21 | and may require overriding LionCog.__new__. 22 | 23 | We also have to answer some implementation decisions, 24 | such as what happens when the child command cog gets unloaded/reloaded? 25 | What happens when the group command gets unloaded/reloaded? 26 | 27 | Well, if the child cog gets unloaded, it makes sense to detach the commands. 28 | The commands should keep their binding to the defining cog, 29 | the parent command is mainly relevant for the CommandTree, which we have control of anyway.. 30 | 31 | If the parent cog gets unloaded, it makes sense to unload all the subcommands, if possible. 32 | 33 | Now technically, it shouldn't _matter_ where the child command is defined. 34 | The Tree is in charge (or should be) of arranging parent commands and subcommands. 35 | The Group class should just specify some optional extra properties or wrappers 36 | to apply to the subcommands. 37 | So perhaps we can just extend Hybrid command to actually pass in a parent... 38 | Or specify a _string_ as the parent, which gets mapped with a group class 39 | if it exists.. but it doesn't need to exist. 40 | """ 41 | 42 | 43 | class LeoCog(LionCog): 44 | """ 45 | Abstract container cog acting as a manager for the LeoGroup above. 46 | """ 47 | def __init__(self, bot): 48 | self.bot = bot 49 | self.commands = [] 50 | self.group = LeoGroup() 51 | 52 | def attach(self, *commands): 53 | """ 54 | Attach the given commands to the LeoGroup group. 55 | """ 56 | for command in commands: 57 | if isinstance(command, Command): 58 | # Classic app command, attach as-is 59 | cmd = command 60 | elif isinstance(command, HybridCommand): 61 | cmd = command.app_command 62 | else: 63 | raise ValueError( 64 | f"Command must by 'app_commands.Command' or 'commands.HybridCommand' not {cmd.__class_}" 65 | ) 66 | self.group.add_command(cmd) 67 | 68 | self.commands.extend(commands) 69 | -------------------------------------------------------------------------------- /src/utils/transformers.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord import app_commands as appcmds 3 | from discord.app_commands import Transformer 4 | from discord.enums import AppCommandOptionType 5 | 6 | from meta.errors import UserInputError 7 | 8 | from babel.translator import ctx_translator 9 | 10 | from .lib import parse_duration, strfdur 11 | from . import util_babel 12 | 13 | 14 | _, _p = util_babel._, util_babel._p 15 | 16 | 17 | class DurationTransformer(Transformer): 18 | """ 19 | Duration parameter, with included autocompletion. 20 | """ 21 | 22 | def __init__(self, multiplier=1): 23 | # Multiplier used for a raw integer value 24 | self.multiplier = multiplier 25 | 26 | @property 27 | def type(self): 28 | return AppCommandOptionType.string 29 | 30 | async def transform(self, interaction: discord.Interaction, value: str) -> int: 31 | """ 32 | Returns the number of seconds in the parsed duration. 33 | Raises UserInputError if the duration cannot be parsed. 34 | """ 35 | translator = ctx_translator.get() 36 | t = translator.t 37 | 38 | if value.isdigit(): 39 | return int(value) * self.multiplier 40 | duration = parse_duration(value) 41 | if duration is None: 42 | raise UserInputError( 43 | t(_p( 44 | 'utils:parse_dur|error', 45 | "Cannot parse `{value}` as a duration." 46 | )).format( 47 | value=value 48 | ) 49 | ) 50 | return duration or 0 51 | 52 | async def autocomplete(self, interaction: discord.Interaction, partial: str): 53 | """ 54 | Default autocomplete for Duration parameters. 55 | 56 | Attempts to parse the partial value as a duration, and reformat it as an autocomplete choice. 57 | If not possible, displays an error message. 58 | """ 59 | translator = ctx_translator.get() 60 | t = translator.t 61 | 62 | if partial.isdigit(): 63 | duration = int(partial) * self.multiplier 64 | else: 65 | duration = parse_duration(partial) 66 | 67 | if duration is None: 68 | choice = appcmds.Choice( 69 | name=t(_p( 70 | 'util:Duration|acmpl|error', 71 | "Cannot extract duration from \"{partial}\"" 72 | )).format(partial=partial)[:100], 73 | value=partial 74 | ) 75 | else: 76 | choice = appcmds.Choice( 77 | name=strfdur(duration, short=False, show_days=True)[:100], 78 | value=partial 79 | ) 80 | return [choice] 81 | -------------------------------------------------------------------------------- /src/utils/ui/transformed.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Type, TYPE_CHECKING 2 | from enum import Enum 3 | 4 | import discord 5 | import discord.app_commands as appcmd 6 | from discord.app_commands.transformers import AppCommandOptionType 7 | 8 | 9 | __all__ = ( 10 | 'ChoicedEnum', 11 | 'ChoicedEnumTransformer', 12 | 'Transformed', 13 | ) 14 | 15 | 16 | class ChoicedEnum(Enum): 17 | @property 18 | def choice_name(self): 19 | return self.name 20 | 21 | @property 22 | def choice_value(self): 23 | return self.value 24 | 25 | @property 26 | def choice(self): 27 | return appcmd.Choice( 28 | name=self.choice_name, value=self.choice_value 29 | ) 30 | 31 | @classmethod 32 | def choices(self): 33 | return [item.choice for item in self] 34 | 35 | @classmethod 36 | def make_choice_map(cls): 37 | return {item.choice_value: item for item in cls} 38 | 39 | @classmethod 40 | async def transform(cls, transformer: 'ChoicedEnumTransformer', interaction: discord.Interaction, value: Any): 41 | return transformer._choice_map[value] 42 | 43 | @classmethod 44 | def option_type(cls) -> AppCommandOptionType: 45 | return AppCommandOptionType.string 46 | 47 | @classmethod 48 | def transformer(cls, *args) -> appcmd.Transformer: 49 | return ChoicedEnumTransformer(cls, *args) 50 | 51 | 52 | class ChoicedEnumTransformer(appcmd.Transformer): 53 | # __discord_app_commands_is_choice__ = True 54 | 55 | def __init__(self, enum: Type[ChoicedEnum], opt_type) -> None: 56 | super().__init__() 57 | 58 | self._type = opt_type 59 | self._enum = enum 60 | self._choices = enum.choices() 61 | self._choice_map = enum.make_choice_map() 62 | 63 | @property 64 | def _error_display_name(self) -> str: 65 | return self._enum.__name__ 66 | 67 | @property 68 | def type(self) -> AppCommandOptionType: 69 | return self._type 70 | 71 | @property 72 | def choices(self): 73 | return self._choices 74 | 75 | async def transform(self, interaction: discord.Interaction, value: Any, /) -> Any: 76 | return await self._enum.transform(self, interaction, value) 77 | 78 | 79 | if TYPE_CHECKING: 80 | from typing_extensions import Annotated as Transformed 81 | else: 82 | 83 | class Transformed: 84 | def __class_getitem__(self, items): 85 | cls = items[0] 86 | options = items[1:] 87 | 88 | if not hasattr(cls, 'transformer'): 89 | raise ValueError("Tranformed class must have a transformer classmethod.") 90 | transformer = cls.transformer(*options) 91 | return appcmd.Transform[cls, transformer] 92 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | StudyLion Open Source License 2 | 3 | Copyright (c) 2023, Ari Horesh. All rights reserved. 4 | 5 | ### 1. Definitions 6 | 7 | - "Software": Refers to the Discord bot named "StudyLion" and associated documentation, source code, scripts, assets, and other related materials. 8 | - "Educational Use": Means utilization of the Software primarily for learning, teaching, training, research, or development. 9 | - "Non-commercial Use": Describes an application of the Software where there's no expectation or realization of direct or indirect monetary compensation. 10 | 11 | ### 2. Grant of License 12 | 13 | - Under the terms and conditions of this license, the licensor grants a worldwide, non-exclusive, royalty-free, non-transferable license to: 14 | - Use the Software. 15 | - Reproduce the Software. 16 | - Modify the Software, creating derivative works based on the Software. 17 | - Distribute the unmodified Software for Educational and Non-commercial Use. 18 | 19 | ### 3. Restrictions 20 | 21 | - Redistribution, whether modified or unmodified, must: 22 | - Preserve the above copyright notice. 23 | - Incorporate this list of conditions. 24 | - Include the following disclaimers. 25 | 26 | - You must not: 27 | - Use the name, trademarks, service marks, or names of the Software or its contributors to endorse or promote derivative products without prior written consent. 28 | - Deploy the Software or any derivative works thereof for commercial purposes or any context leading to financial gain. 29 | - Assert proprietary rights or assign authorship of the original Software to any entity other than the original authors. 30 | - Grant sublicenses for the Software. 31 | 32 | ### 4. Contributions 33 | 34 | - Any contributions made to the Software by third parties shall be subject to this license. The contributor grants the licensor a non-exclusive, perpetual, irrevocable license to any such contributions. 35 | 36 | ### 5. Termination 37 | 38 | - If you breach any terms of this license, your rights will terminate automatically. Once terminated, you must: 39 | - Halt all utilization of the Software. 40 | - Destroy or delete all copies of the Software in your possession or control. 41 | 42 | ### 6. Disclaimer and Limitation of Liability 43 | 44 | - THE SOFTWARE IS OFFERED "AS IS", WITHOUT ANY GUARANTEES OR CLAIMS OF EFFICACY. NO WARRANTIES, IMPLIED OR EXPLICIT, ARE PROVIDED. THIS INCLUDES, BUT IS NOT RESTRICTED TO, WARRANTIES OF MERCHANDISE, FITNESS FOR A SPECIFIC PURPOSE, AND NON-INFRINGEMENT. 45 | - UNDER NO CONDITION SHALL THE AUTHORS, COPYRIGHT HOLDERS, OR CONTRIBUTORS BE ACCOUNTABLE FOR ANY CLAIMS, DAMAGES, OR OTHER LIABILITIES, WHETHER RESULTING FROM CONTRACT, TORT, NEGLIGENCE, OR ANY OTHER LEGAL THEORY, EMERGING FROM, OUT OF, OR CONNECTED WITH THE SOFTWARE OR ITS USE. 46 | -------------------------------------------------------------------------------- /src/modules/skins/editor/pages/summary.py: -------------------------------------------------------------------------------- 1 | from gui.cards import ProfileCard 2 | 3 | from ... import babel 4 | from ..skinsetting import ColourSetting, SkinSetting, ColoursSetting 5 | from ..layout import Page, SettingGroup 6 | 7 | from . import stats, profile 8 | 9 | _p = babel._p 10 | 11 | 12 | summary_page = Page( 13 | display_name=_p('skinsettings|page:summary|display_name', "Setting Summary"), 14 | editing_description=_p( 15 | 'skinsettings|page:summary|edit_desc', 16 | "Simple setup for creating a unified interface theme." 17 | ), 18 | preview_description=_p( 19 | 'skinsettings|page:summary|preview_desc', 20 | "Summary of common settings across the entire interface." 21 | ), 22 | visible_in_preview=True, 23 | render_card=ProfileCard 24 | ) 25 | 26 | name_colours = ColoursSetting( 27 | profile.header_colour_1, 28 | display_name=_p( 29 | 'skinsettings|page:summary|set:name_colours|display_name', 30 | "username colour" 31 | ), 32 | description=_p( 33 | 'skinsettings|page:summary|set:name_colours|desc', 34 | "Author username colour." 35 | ) 36 | ) 37 | 38 | discrim_colours = ColoursSetting( 39 | profile.header_colour_2, 40 | display_name=_p( 41 | 'skinsettings|page:summary|set:discrim_colours|display_name', 42 | "discrim colour" 43 | ), 44 | description=_p( 45 | 'skinsettings|page:summary|set:discrim_colours|desc', 46 | "Author discriminator colour." 47 | ) 48 | ) 49 | 50 | subheader_colour = ColoursSetting( 51 | stats.header_colour, 52 | profile.subheader_colour, 53 | display_name=_p( 54 | 'skinsettings|page:summary|set:subheader_colour|display_name', 55 | "subheadings" 56 | ), 57 | description=_p( 58 | 'skinsettings|page:summary|set:subheader_colour|desc', 59 | "Colour of subheadings and column headings." 60 | ) 61 | ) 62 | 63 | header_colour_group = SettingGroup( 64 | _p('skinsettings|page:summary|grp:header_colour', "Title Colours"), 65 | description=_p( 66 | 'skinsettings|page:summary|grp:header_colour|desc', 67 | "Title and header text colours." 68 | ), 69 | custom_id='shared-titles', 70 | settings=( 71 | subheader_colour, 72 | ) 73 | ) 74 | 75 | profile_colour_group = SettingGroup( 76 | _p('skinsettings|page:summary|grp:profile_colour', "Profile Colours"), 77 | description=_p( 78 | 'skinsettings|page:summary|grp:profile_colour|desc', 79 | "Profile elements shared across various cards." 80 | ), 81 | custom_id='shared-profile', 82 | settings=( 83 | name_colours, 84 | discrim_colours 85 | ) 86 | ) 87 | 88 | summary_page.groups = [header_colour_group, profile_colour_group] 89 | 90 | -------------------------------------------------------------------------------- /src/modules/statistics/graphics/leaderboard.py: -------------------------------------------------------------------------------- 1 | from meta import LionBot 2 | 3 | from gui.cards import LeaderboardCard 4 | from gui.base import CardMode 5 | 6 | 7 | async def get_leaderboard_card( 8 | bot: LionBot, highlightid: int, guildid: int, 9 | mode: CardMode, 10 | entry_data: list[tuple[int, int, int]], # userid, position, time 11 | ): 12 | """ 13 | Render a leaderboard card with given parameters. 14 | """ 15 | guild = bot.get_guild(guildid) 16 | if guild is None: 17 | raise ValueError("Attempting to build leaderboard for non-existent guild!") 18 | 19 | # Need to do two passes here in case we need to do a db request for the avatars or names 20 | avatars = {} 21 | names = {} 22 | missing = [] 23 | for userid, _, _ in entry_data: 24 | if guild and (member := guild.get_member(userid)): 25 | avatars[userid] = member.avatar.key if member.avatar else None 26 | names[userid] = member.display_name 27 | elif (user := bot.get_user(userid)): 28 | avatars[userid] = user.avatar.key if user.avatar else None 29 | names[userid] = user.display_name 30 | elif (user_data := bot.core.data.User._cache_.get((userid,))): 31 | avatars[userid] = user_data.avatar_hash 32 | names[userid] = user_data.name 33 | else: 34 | missing.append(userid) 35 | 36 | if missing: 37 | # We were unable to retrieve information for some userids 38 | # Bulk-fetch missing users from data 39 | data = await bot.core.data.User.fetch_where(userid=missing) 40 | for user_data in data: 41 | avatars[user_data.userid] = user_data.avatar_hash 42 | names[user_data.userid] = user_data.name or 'Unknown' 43 | missing.remove(user_data.userid) 44 | 45 | if missing: 46 | # Some of the users were missing from data 47 | # This should be impossible (by FKEY constraints on sessions) 48 | # But just in case... 49 | for userid in missing: 50 | avatars[userid] = None 51 | names[userid] = str(userid) 52 | 53 | highlight = None 54 | entries = [] 55 | for userid, position, duration in entry_data: 56 | entries.append( 57 | (userid, position, duration, names[userid], (userid, avatars[userid])) 58 | ) 59 | if userid == highlightid: 60 | highlight = position 61 | 62 | # Request Card 63 | 64 | skin = await bot.get_cog('CustomSkinCog').get_skinargs_for( 65 | guildid, None, LeaderboardCard.card_id 66 | ) 67 | card = LeaderboardCard( 68 | skin=skin | {'mode': mode}, 69 | server_name=guild.name, 70 | entries=entries, 71 | highlight=highlight 72 | ) 73 | return card 74 | -------------------------------------------------------------------------------- /locales/templates/goals-gui.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-10-01 16:01+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/gui/cards/goals.py:97 21 | msgctxt "skin:goals|goal:tasks|name" 22 | msgid "TASKS DONE" 23 | msgstr "" 24 | 25 | #: src/gui/cards/goals.py:101 26 | msgctxt "skin:goals|goal:tasks|goal" 27 | msgid "GOAL: " 28 | msgstr "" 29 | 30 | #: src/gui/cards/goals.py:127 31 | msgctxt "skin:goals|goal:attendance|name" 32 | msgid "" 33 | "ATTENDANCE\n" 34 | "RATE" 35 | msgstr "" 36 | 37 | #: src/gui/cards/goals.py:141 38 | msgctxt "skin:goals|mode:study|goal:middle|above" 39 | msgid "STUDIED" 40 | msgstr "" 41 | 42 | #: src/gui/cards/goals.py:145 43 | msgctxt "skin:goals|mode:study|goal:middle|below" 44 | msgid "HOURS" 45 | msgstr "" 46 | 47 | #: src/gui/cards/goals.py:149 48 | msgctxt "skin:goals|mode:voice|goal:middle|above" 49 | msgid "CHATTED" 50 | msgstr "" 51 | 52 | #: src/gui/cards/goals.py:153 53 | msgctxt "skin:goals|mode:voice|goal:middle|below" 54 | msgid "HOURS" 55 | msgstr "" 56 | 57 | #: src/gui/cards/goals.py:157 58 | msgctxt "skin:goals|mode:text|goal:middle|above" 59 | msgid "SENT" 60 | msgstr "" 61 | 62 | #: src/gui/cards/goals.py:161 63 | msgctxt "skin:goals|mode:text|goal:middle|below" 64 | msgid "MESSAGES" 65 | msgstr "" 66 | 67 | #: src/gui/cards/goals.py:165 68 | msgctxt "skin:goals|mode:anki|goal:middle|above" 69 | msgid "REVIEWED" 70 | msgstr "" 71 | 72 | #: src/gui/cards/goals.py:169 73 | msgctxt "skin:goals|mode:anki|goal:middle|below" 74 | msgid "CARDS" 75 | msgstr "" 76 | 77 | #: src/gui/cards/goals.py:230 78 | #, possible-python-brace-format 79 | msgctxt "skin:goals|footer" 80 | msgid "As of {day} {month} • {name}" 81 | msgstr "" 82 | 83 | #: src/gui/cards/goals.py:242 84 | msgctxt "ui:goals|weekly|title" 85 | msgid "WEEKLY STATISTICS" 86 | msgstr "" 87 | 88 | #: src/gui/cards/goals.py:246 89 | msgctxt "ui:goals|weekly|task_header" 90 | msgid "GOALS OF THE WEEK" 91 | msgstr "" 92 | 93 | #: src/gui/cards/goals.py:256 94 | msgctxt "ui:goals|monthly|title" 95 | msgid "MONTHLY STATISTICS" 96 | msgstr "" 97 | 98 | #: src/gui/cards/goals.py:260 99 | msgctxt "ui:goals|monthly|task_header" 100 | msgid "GOALS OF THE MONTH" 101 | msgstr "" 102 | -------------------------------------------------------------------------------- /locales/templates/leaderboard-gui.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-10-01 16:01+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/gui/cards/leaderboard.py:68 21 | msgctxt "skin:leaderboard|mode:study|header_text" 22 | msgid "STUDY TIME LEADERBOARD" 23 | msgstr "" 24 | 25 | #: src/gui/cards/leaderboard.py:72 26 | msgctxt "skin:leaderboard|mode:text|header_text" 27 | msgid "MESSAGE LEADERBOARD" 28 | msgstr "" 29 | 30 | #: src/gui/cards/leaderboard.py:76 31 | msgctxt "skin:leaderboard|mode:voice|header_text" 32 | msgid "VOICE LEADERBOARD" 33 | msgstr "" 34 | 35 | #: src/gui/cards/leaderboard.py:80 36 | msgctxt "skin:leaderboard|mode:anki|header_text" 37 | msgid "ANKI REVIEW LEADERBOARD" 38 | msgstr "" 39 | 40 | #: src/gui/cards/leaderboard.py:98 41 | msgctxt "skin:leaderboard|field:subheader_text" 42 | msgid "SERVER: " 43 | msgstr "" 44 | 45 | #: src/gui/cards/leaderboard.py:128 46 | #, possible-python-brace-format 47 | msgctxt "skin:leaderboard|mode:study|top_hours_text" 48 | msgid "{amount} hours" 49 | msgstr "" 50 | 51 | #: src/gui/cards/leaderboard.py:132 52 | #, possible-python-brace-format 53 | msgctxt "skin:leaderboard|mode:text|top_hours_text" 54 | msgid "{amount} XP" 55 | msgstr "" 56 | 57 | #: src/gui/cards/leaderboard.py:136 58 | #, possible-python-brace-format 59 | msgctxt "skin:leaderboard|mode:voice|top_hours_text" 60 | msgid "{amount} hours" 61 | msgstr "" 62 | 63 | #: src/gui/cards/leaderboard.py:140 64 | #, possible-python-brace-format 65 | msgctxt "skin:leaderboard|mode:anki|top_hours_text" 66 | msgid "{amount} cards" 67 | msgstr "" 68 | 69 | #: src/gui/cards/leaderboard.py:167 70 | #, possible-python-brace-format 71 | msgctxt "skin:leaderboard|mode:study|entry_hours_text" 72 | msgid "{HH:02d}:{MM:02d}" 73 | msgstr "" 74 | 75 | #: src/gui/cards/leaderboard.py:171 76 | #, possible-python-brace-format 77 | msgctxt "skin:leaderboard|mode:text|entry_hours_text" 78 | msgid "{amount} XP" 79 | msgstr "" 80 | 81 | #: src/gui/cards/leaderboard.py:175 82 | #, possible-python-brace-format 83 | msgctxt "skin:leaderboard|mode:voice|entry_hours_text" 84 | msgid "{HH:02d}:{MM:02d}" 85 | msgstr "" 86 | 87 | #: src/gui/cards/leaderboard.py:179 88 | #, possible-python-brace-format 89 | msgctxt "skin:leaderboard|mode:anki|entry_hours_text" 90 | msgid "{amount} cards" 91 | msgstr "" 92 | -------------------------------------------------------------------------------- /locales/templates/wards.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-10-01 16:01+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/wards.py:83 21 | msgctxt "ward:sys_admin|failed" 22 | msgid "You must be a bot owner to do this!" 23 | msgstr "" 24 | 25 | #: src/wards.py:99 26 | msgctxt "ward:high_management|failed" 27 | msgid "You must have the `ADMINISTRATOR` permission in this server to do this!" 28 | msgstr "" 29 | 30 | #: src/wards.py:115 31 | msgctxt "ward:low_management|failed" 32 | msgid "You must have the `MANAGE_GUILD` permission in this server to do this!" 33 | msgstr "" 34 | 35 | #: src/wards.py:127 36 | msgctxt "ward:moderator|failed" 37 | msgid "" 38 | "You must have the configured moderator role, or `MANAGE_GUILD` permissions " 39 | "to do this." 40 | msgstr "" 41 | 42 | #: src/wards.py:153 43 | #, possible-python-brace-format 44 | msgctxt "ward:equippable_role|error:bot_managed" 45 | msgid "I cannot manage {role} because it is managed by another bot!" 46 | msgstr "" 47 | 48 | #: src/wards.py:160 49 | #, possible-python-brace-format 50 | msgctxt "ward:equippable_role|error:integration" 51 | msgid "I cannot manage {role} because it is managed by a server integration." 52 | msgstr "" 53 | 54 | #: src/wards.py:167 55 | msgctxt "ward:equippable_role|error:default_role" 56 | msgid "I cannot manage the server's default role." 57 | msgstr "" 58 | 59 | #: src/wards.py:174 60 | msgctxt "ward:equippable_role|error:no_perms" 61 | msgid "I need the `MANAGE_ROLES` permission before I can manage roles!" 62 | msgstr "" 63 | 64 | #: src/wards.py:181 65 | #, possible-python-brace-format 66 | msgctxt "ward:equippable_role|error:my_top_role" 67 | msgid "I cannot assign or remove {role} because it is above my top role!" 68 | msgstr "" 69 | 70 | #: src/wards.py:188 71 | #, possible-python-brace-format 72 | msgctxt "ward:equippable_role|error:not_assignable" 73 | msgid "I don't have sufficient permissions to assign or remove {role}." 74 | msgstr "" 75 | 76 | #: src/wards.py:196 77 | msgctxt "ward:equippable_role|error:actor_perms" 78 | msgid "You need the `MANAGE_ROLES` permission before you can configure roles!" 79 | msgstr "" 80 | 81 | #: src/wards.py:203 82 | #, possible-python-brace-format 83 | msgctxt "ward:equippable_role|error:actor_top_role" 84 | msgid "You cannot configure {role} because it is above your top role!" 85 | msgstr "" 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/modules/test/* 2 | 3 | pending-rewrite/ 4 | logs/* 5 | notes/* 6 | tmp/* 7 | output/* 8 | locales/domains 9 | 10 | .idea/* 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | *.py,cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | cover/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | .pybuilder/ 87 | target/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # IPython 93 | profile_default/ 94 | ipython_config.py 95 | 96 | # pyenv 97 | # For a library or package, you might want to ignore these files since the code is 98 | # intended to run in multiple environments; otherwise, check them in: 99 | # .python-version 100 | 101 | # pipenv 102 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 103 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 104 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 105 | # install all needed dependencies. 106 | #Pipfile.lock 107 | 108 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 109 | __pypackages__/ 110 | 111 | # Celery stuff 112 | celerybeat-schedule 113 | celerybeat.pid 114 | 115 | # SageMath parsed files 116 | *.sage.py 117 | 118 | # Environments 119 | .env 120 | .venv 121 | env/ 122 | venv/ 123 | ENV/ 124 | env.bak/ 125 | venv.bak/ 126 | 127 | # Spyder project settings 128 | .spyderproject 129 | .spyproject 130 | 131 | # Rope project settings 132 | .ropeproject 133 | 134 | # mkdocs documentation 135 | /site 136 | 137 | # mypy 138 | .mypy_cache/ 139 | .dmypy.json 140 | dmypy.json 141 | 142 | # Pyre type checker 143 | .pyre/ 144 | 145 | # pytype static type analyzer 146 | .pytype/ 147 | 148 | # Cython debug symbols 149 | cython_debug/ 150 | 151 | config/** 152 | -------------------------------------------------------------------------------- /src/modules/premium/data.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from psycopg import sql 4 | from meta.logger import log_wrap 5 | from data import Registry, RowModel, RegisterEnum, Table 6 | from data.columns import Integer, Bool, Column, Timestamp, String 7 | 8 | 9 | class GemTransactionType(Enum): 10 | """ 11 | Schema 12 | ------ 13 | CREATE TYPE GemTransactionType AS ENUM ( 14 | 'ADMIN', 15 | 'GIFT', 16 | 'PURCHASE', 17 | 'AUTOMATIC' 18 | ); 19 | """ 20 | ADMIN = 'ADMIN', 21 | GIFT = 'GIFT', 22 | PURCHASE = 'PURCHASE', 23 | AUTOMATIC = 'AUTOMATIC', 24 | 25 | 26 | class PremiumData(Registry): 27 | GemTransactionType = RegisterEnum(GemTransactionType, 'GemTransactionType') 28 | 29 | class GemTransaction(RowModel): 30 | """ 31 | Schema 32 | ------ 33 | 34 | CREATE TABLE gem_transactions( 35 | transactionid SERIAL PRIMARY KEY, 36 | transaction_type GemTransactionType NOT NULL, 37 | actorid BIGINT NOT NULL, 38 | from_account BIGINT, 39 | to_account BIGINT, 40 | amount INTEGER NOT NULL, 41 | description TEXT NOT NULL, 42 | note TEXT, 43 | reference TEXT, 44 | _timestamp TIMESTAMPTZ DEFAULT now() 45 | ); 46 | CREATE INDEX gem_transactions_from ON gem_transactions (from_account); 47 | """ 48 | _tablename_ = 'gem_transactions' 49 | 50 | transactionid = Integer(primary=True) 51 | transaction_type: Column[GemTransactionType] = Column() 52 | actorid = Integer() 53 | from_account = Integer() 54 | to_account = Integer() 55 | amount = Integer() 56 | description = String() 57 | note = String() 58 | reference = String() 59 | 60 | _timestamp = Timestamp() 61 | 62 | class PremiumGuild(RowModel): 63 | """ 64 | Schema 65 | ------ 66 | CREATE TABLE premium_guilds( 67 | guildid BIGINT PRIMARY KEY REFERENCES guild_config, 68 | premium_since TIMESTAMPTZ NOT NULL DEFAULT now(), 69 | premium_until TIMESTAMPTZ NOT NULL DEFAULT now(), 70 | custom_skin_id INTEGER REFERENCES customised_skins 71 | ); 72 | """ 73 | _tablename_ = "premium_guilds" 74 | _cache_ = {} 75 | 76 | guildid = Integer(primary=True) 77 | premium_since = Timestamp() 78 | premium_until = Timestamp() 79 | custom_skin_id = Integer() 80 | 81 | """ 82 | CREATE TABLE premium_guild_contributions( 83 | contributionid SERIAL PRIMARY KEY, 84 | userid BIGINT NOT NULL REFERENCES user_config, 85 | guildid BIGINT NOT NULL REFERENCES premium_guilds, 86 | transactionid INTEGER REFERENCES gem_transactions, 87 | duration INTEGER NOT NULL, 88 | _timestamp TIMESTAMPTZ DEFAULT now() 89 | ); 90 | """ 91 | premium_guild_contributions = Table('premium_guild_contributions') 92 | 93 | -------------------------------------------------------------------------------- /locales/ceaser/LC_MESSAGES/goals-gui.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 1.0\n" 10 | "Report-Msgid-Bugs-To: you@example.com\n" 11 | "POT-Creation-Date: 2007-10-18 14:00+0100\n" 12 | "PO-Revision-Date: 2007-10-18 14:00+0100\n" 13 | "Last-Translator: you \n" 14 | "Language-Team: English \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: src/gui/cards/goals.py:97 20 | msgctxt "skin:goals|goal:tasks|name" 21 | msgid "TASKS DONE" 22 | msgstr "UBTLT EPOF" 23 | 24 | #: src/gui/cards/goals.py:101 25 | msgctxt "skin:goals|goal:tasks|goal" 26 | msgid "GOAL: " 27 | msgstr "HPBM: " 28 | 29 | #: src/gui/cards/goals.py:127 30 | msgctxt "skin:goals|goal:attendance|name" 31 | msgid "" 32 | "ATTENDANCE\n" 33 | "RATE" 34 | msgstr "" 35 | "BUUFOEBODF\n" 36 | "SBUF" 37 | 38 | #: src/gui/cards/goals.py:141 39 | msgctxt "skin:goals|mode:study|goal:middle|above" 40 | msgid "STUDIED" 41 | msgstr "TUVEJFE" 42 | 43 | #: src/gui/cards/goals.py:145 44 | msgctxt "skin:goals|mode:study|goal:middle|below" 45 | msgid "HOURS" 46 | msgstr "IPVST" 47 | 48 | #: src/gui/cards/goals.py:149 49 | msgctxt "skin:goals|mode:voice|goal:middle|above" 50 | msgid "CHATTED" 51 | msgstr "DIBUUFE" 52 | 53 | #: src/gui/cards/goals.py:153 54 | msgctxt "skin:goals|mode:voice|goal:middle|below" 55 | msgid "HOURS" 56 | msgstr "IPVST" 57 | 58 | #: src/gui/cards/goals.py:157 59 | msgctxt "skin:goals|mode:text|goal:middle|above" 60 | msgid "SENT" 61 | msgstr "TFOU" 62 | 63 | #: src/gui/cards/goals.py:161 64 | msgctxt "skin:goals|mode:text|goal:middle|below" 65 | msgid "MESSAGES" 66 | msgstr "NFTTBHFT" 67 | 68 | #: src/gui/cards/goals.py:165 69 | msgctxt "skin:goals|mode:anki|goal:middle|above" 70 | msgid "REVIEWED" 71 | msgstr "SFWJFXFE" 72 | 73 | #: src/gui/cards/goals.py:169 74 | msgctxt "skin:goals|mode:anki|goal:middle|below" 75 | msgid "CARDS" 76 | msgstr "DBSET" 77 | 78 | #: src/gui/cards/goals.py:230 79 | #, possible-python-brace-format 80 | msgctxt "skin:goals|footer" 81 | msgid "As of {day} {month} • {name}" 82 | msgstr "Bt pg {day} {month} • {name}" 83 | 84 | #: src/gui/cards/goals.py:242 85 | msgctxt "ui:goals|weekly|title" 86 | msgid "WEEKLY STATISTICS" 87 | msgstr "XFFLMZ TUBUJTUJDT" 88 | 89 | #: src/gui/cards/goals.py:246 90 | msgctxt "ui:goals|weekly|task_header" 91 | msgid "GOALS OF THE WEEK" 92 | msgstr "HPBMT PG UIF XFFL" 93 | 94 | #: src/gui/cards/goals.py:256 95 | msgctxt "ui:goals|monthly|title" 96 | msgid "MONTHLY STATISTICS" 97 | msgstr "NPOUIMZ TUBUJTUJDT" 98 | 99 | #: src/gui/cards/goals.py:260 100 | msgctxt "ui:goals|monthly|task_header" 101 | msgid "GOALS OF THE MONTH" 102 | msgstr "HPBMT PG UIF NPOUI" 103 | -------------------------------------------------------------------------------- /locales/ceaser/LC_MESSAGES/leaderboard-gui.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 1.0\n" 10 | "Report-Msgid-Bugs-To: you@example.com\n" 11 | "POT-Creation-Date: 2007-10-18 14:00+0100\n" 12 | "PO-Revision-Date: 2007-10-18 14:00+0100\n" 13 | "Last-Translator: you \n" 14 | "Language-Team: English \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: src/gui/cards/leaderboard.py:69 20 | msgctxt "skin:leaderboard|mode:study|header_text" 21 | msgid "STUDY TIME LEADERBOARD" 22 | msgstr "TUVEZ UJNF MFBEFSCPBSE" 23 | 24 | #: src/gui/cards/leaderboard.py:73 25 | msgctxt "skin:leaderboard|mode:text|header_text" 26 | msgid "MESSAGE LEADERBOARD" 27 | msgstr "NFTTBHF MFBEFSCPBSE" 28 | 29 | #: src/gui/cards/leaderboard.py:77 30 | msgctxt "skin:leaderboard|mode:voice|header_text" 31 | msgid "VOICE LEADERBOARD" 32 | msgstr "WPJDF MFBEFSCPBSE" 33 | 34 | #: src/gui/cards/leaderboard.py:81 35 | msgctxt "skin:leaderboard|mode:anki|header_text" 36 | msgid "ANKI REVIEW LEADERBOARD" 37 | msgstr "BOLJ SFWJFX MFBEFSCPBSE" 38 | 39 | #: src/gui/cards/leaderboard.py:99 40 | msgctxt "skin:leaderboard|field:subheader_text" 41 | msgid "SERVER: " 42 | msgstr "TFSWFS: " 43 | 44 | #: src/gui/cards/leaderboard.py:129 45 | #, possible-python-brace-format 46 | msgctxt "skin:leaderboard|mode:study|top_hours_text" 47 | msgid "{amount} hours" 48 | msgstr "{amount} ipvst" 49 | 50 | #: src/gui/cards/leaderboard.py:133 51 | #, possible-python-brace-format 52 | msgctxt "skin:leaderboard|mode:text|top_hours_text" 53 | msgid "{amount} XP" 54 | msgstr "{amount} YQ" 55 | 56 | #: src/gui/cards/leaderboard.py:137 57 | #, possible-python-brace-format 58 | msgctxt "skin:leaderboard|mode:voice|top_hours_text" 59 | msgid "{amount} hours" 60 | msgstr "{amount} ipvst" 61 | 62 | #: src/gui/cards/leaderboard.py:141 63 | #, possible-python-brace-format 64 | msgctxt "skin:leaderboard|mode:anki|top_hours_text" 65 | msgid "{amount} cards" 66 | msgstr "{amount} dbset" 67 | 68 | #: src/gui/cards/leaderboard.py:168 69 | #, possible-python-brace-format 70 | msgctxt "skin:leaderboard|mode:study|entry_hours_text" 71 | msgid "{HH:02d}:{MM:02d}" 72 | msgstr "{HH:02d}:{MM:02d}" 73 | 74 | #: src/gui/cards/leaderboard.py:172 75 | #, possible-python-brace-format 76 | msgctxt "skin:leaderboard|mode:text|entry_hours_text" 77 | msgid "{amount} XP" 78 | msgstr "{amount} YQ" 79 | 80 | #: src/gui/cards/leaderboard.py:176 81 | #, possible-python-brace-format 82 | msgctxt "skin:leaderboard|mode:voice|entry_hours_text" 83 | msgid "{HH:02d}:{MM:02d}" 84 | msgstr "{HH:02d}:{MM:02d}" 85 | 86 | #: src/gui/cards/leaderboard.py:180 87 | #, possible-python-brace-format 88 | msgctxt "skin:leaderboard|mode:anki|entry_hours_text" 89 | msgid "{amount} cards" 90 | msgstr "{amount} dbset" 91 | -------------------------------------------------------------------------------- /locales/templates/weekly-gui.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-10-01 16:01+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/gui/cards/weekly.py:52 21 | msgctxt "skin:weeklystats|mode:study|title" 22 | msgid "STUDY HOURS" 23 | msgstr "" 24 | 25 | #: src/gui/cards/weekly.py:56 26 | msgctxt "skin:weeklystats|mode:voice|title" 27 | msgid "VOICE CHANNEL ACTIVITY" 28 | msgstr "" 29 | 30 | #: src/gui/cards/weekly.py:60 31 | msgctxt "skin:weeklystats|mode:text|title" 32 | msgid "MESSAGE ACTIVITY" 33 | msgstr "" 34 | 35 | #: src/gui/cards/weekly.py:64 36 | msgctxt "skin::weeklystats|mode:anki|title" 37 | msgid "CARDS REVIEWED" 38 | msgstr "" 39 | 40 | #: src/gui/cards/weekly.py:137 41 | msgctxt "skin:weeklystats|weekdays" 42 | msgid "M,T,W,T,F,S,S" 43 | msgstr "" 44 | 45 | #: src/gui/cards/weekly.py:207 46 | #, possible-python-brace-format 47 | msgctxt "skin:weeklystats|mode:study|summary:this_week" 48 | msgid "THIS WEEK: {amount} HOURS" 49 | msgstr "" 50 | 51 | #: src/gui/cards/weekly.py:211 52 | #, possible-python-brace-format 53 | msgctxt "skin:weeklystats|mode:voice|summary:this_week" 54 | msgid "THIS WEEK: {amount} HOURS" 55 | msgstr "" 56 | 57 | #: src/gui/cards/weekly.py:215 58 | #, possible-python-brace-format 59 | msgctxt "skin:weeklystats|mode:text|summary:this_week" 60 | msgid "THIS WEEK: {amount} MESSAGES" 61 | msgstr "" 62 | 63 | #: src/gui/cards/weekly.py:219 64 | #, possible-python-brace-format 65 | msgctxt "skin:weeklystats|mode:text|summary:this_week" 66 | msgid "THIS WEEK: {amount} CARDS" 67 | msgstr "" 68 | 69 | #: src/gui/cards/weekly.py:240 70 | #, possible-python-brace-format 71 | msgctxt "skin:weeklystats|mode:study|summary:last_week" 72 | msgid "LAST WEEK: {amount} HOURS" 73 | msgstr "" 74 | 75 | #: src/gui/cards/weekly.py:244 76 | #, possible-python-brace-format 77 | msgctxt "skin:weeklystats|mode:voice|summary:last_week" 78 | msgid "LAST WEEK: {amount} HOURS" 79 | msgstr "" 80 | 81 | #: src/gui/cards/weekly.py:248 82 | #, possible-python-brace-format 83 | msgctxt "skin:weeklystats|mode:text|summary:last_week" 84 | msgid "LAST WEEK: {amount} MESSAGES" 85 | msgstr "" 86 | 87 | #: src/gui/cards/weekly.py:252 88 | #, possible-python-brace-format 89 | msgctxt "skin:weeklystats|mode:text|summary:last_week" 90 | msgid "LAST WEEK: {amount} CARDS" 91 | msgstr "" 92 | 93 | #: src/gui/cards/weekly.py:272 94 | #, possible-python-brace-format 95 | msgctxt "skin:weeklystats|footer" 96 | msgid "Weekly Statistics • As of {day} {month} • {name} {discrim}" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /src/modules/statistics/graphics/weekly.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from datetime import timedelta 3 | 4 | from data import ORDER 5 | from meta import LionBot 6 | from gui.cards import WeeklyStatsCard 7 | from gui.base import CardMode 8 | from tracking.text.data import TextTrackerData 9 | 10 | from ..data import StatsData 11 | 12 | 13 | async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int, mode: CardMode) -> WeeklyStatsCard: 14 | data: StatsData = bot.get_cog('StatsCog').data 15 | 16 | if guildid: 17 | lion = await bot.core.lions.fetch_member(guildid, userid) 18 | user = await lion.fetch_member() 19 | else: 20 | lion = await bot.core.lions.fetch_user(userid) 21 | user = await bot.fetch_user(userid) 22 | today = lion.today 23 | week_start = today - timedelta(days=today.weekday()) - timedelta(weeks=offset) 24 | days = [week_start + timedelta(i) for i in range(-7, 8 if offset else (today.weekday() + 2))] 25 | 26 | # TODO: Select statistics model based on mode 27 | if mode is CardMode.VOICE: 28 | model = data.VoiceSessionStats 29 | day_stats = await model.study_times_between(guildid or None, userid, *days) 30 | day_stats = list(map(lambda n: n // 3600, day_stats)) 31 | elif mode is CardMode.TEXT: 32 | model = TextTrackerData.TextSessions 33 | if guildid: 34 | day_stats = await model.member_messages_between(guildid, userid, *days) 35 | else: 36 | day_stats = await model.user_messages_between(userid, *days) 37 | else: 38 | # TODO: ANKI 39 | model = data.VoiceSessionStats 40 | day_stats = await model.study_times_between(guildid or None, userid, *days) 41 | day_stats = list(map(lambda n: n // 3600, day_stats)) 42 | 43 | # Get user session rows 44 | query = model.table.select_where(model.start_time >= days[0]) 45 | if guildid: 46 | query = query.where(userid=userid, guildid=guildid).order_by('start_time', ORDER.ASC) 47 | else: 48 | query = query.where(userid=userid) 49 | sessions = await query 50 | 51 | # Extract quantities per-day 52 | for i in range(14 - len(day_stats)): 53 | day_stats.append(0) 54 | 55 | # Get member profile 56 | if user: 57 | username = (user.display_name, user.discriminator) 58 | else: 59 | username = (lion.data.display_name, '#????') 60 | 61 | skin = await bot.get_cog('CustomSkinCog').get_skinargs_for( 62 | guildid, userid, WeeklyStatsCard.card_id 63 | ) 64 | 65 | card = WeeklyStatsCard( 66 | user=username, 67 | timezone=str(lion.timezone), 68 | now=lion.now.timestamp(), 69 | week=week_start.timestamp(), 70 | daily=tuple(map(int, day_stats)), 71 | sessions=[ 72 | (int(session['start_time'].timestamp()), int(session['start_time'].timestamp() + int(session['duration']))) 73 | for session in sessions 74 | ], 75 | skin=skin | {'mode': mode} 76 | ) 77 | return card 78 | -------------------------------------------------------------------------------- /locales/ceaser/LC_MESSAGES/config.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 1.0\n" 10 | "Report-Msgid-Bugs-To: you@example.com\n" 11 | "POT-Creation-Date: 2007-10-18 14:00+0100\n" 12 | "PO-Revision-Date: 2007-10-18 14:00+0100\n" 13 | "Last-Translator: you \n" 14 | "Language-Team: English \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: src/modules/config/general.py:41 20 | msgctxt "guildset:timezone" 21 | msgid "timezone" 22 | msgstr "ujnfapof" 23 | 24 | #: src/modules/config/general.py:44 25 | msgctxt "guildset:timezone|desc" 26 | msgid "Guild timezone for statistics display." 27 | msgstr "Hvjme ujnfapof gps tubujtujdt ejtqmbz." 28 | 29 | #: src/modules/config/general.py:48 30 | msgctxt "guildset:timezone|long_desc" 31 | msgid "" 32 | "Guild-wide timezone. Used to determine start of the day for the " 33 | "leaderboards, and as the default statistics timezone for members who have " 34 | "not set one." 35 | msgstr "" 36 | "Hvjme-xjef ujnfapof. Vtfe up efufsnjof tubsu pg uif ebz gps uif " 37 | "mfbefscpbset, boe bt uif efgbvmu tubujtujdt ujnfapof gps nfncfst xip ibwf " 38 | "opu tfu pof." 39 | 40 | #: src/modules/config/general.py:62 41 | #, possible-python-brace-format 42 | msgctxt "guildset:timezone|response" 43 | msgid "The guild timezone has been set to `{timezone}`." 44 | msgstr "Uif hvjme ujnfapof ibt cffo tfu up `{timezone}`." 45 | 46 | #: src/modules/config/general.py:94 47 | msgctxt "cmd:configure_general" 48 | msgid "general" 49 | msgstr "hfofsbm" 50 | 51 | #: src/modules/config/general.py:95 52 | msgctxt "cmd:configure_general|desc" 53 | msgid "General configuration panel" 54 | msgstr "Hfofsbm dpogjhvsbujpo qbofm" 55 | 56 | #: src/modules/config/general.py:129 57 | msgctxt "cmd:configure_general|parse_failure:timezone" 58 | msgid "Could not set the timezone!" 59 | msgstr "Dpvme opu tfu uif ujnfapof!" 60 | 61 | #: src/modules/config/general.py:150 62 | msgctxt "cmd:configure_general|success" 63 | msgid "Settings Updated!" 64 | msgstr "Tfuujoht Vqebufe!" 65 | 66 | #: src/modules/config/general.py:165 67 | msgctxt "cmd:configure_general|panel|title" 68 | msgid "General Configuration Panel" 69 | msgstr "Hfofsbm Dpogjhvsbujpo Qbofm" 70 | 71 | #: src/modules/config/dashboard.py:98 72 | #, possible-python-brace-format 73 | msgctxt "ui:dashboard|title" 74 | msgid "Guild Dashboard (Page {page}/{total})" 75 | msgstr "Hvjme Ebticpbse (Qbhf {page}/{total})" 76 | 77 | #: src/modules/config/dashboard.py:109 78 | msgctxt "ui:dashboard|footer" 79 | msgid "Hover over setting names for a brief description" 80 | msgstr "Ipwfs pwfs tfuujoh obnft gps b csjfg eftdsjqujpo" 81 | 82 | #: src/modules/config/dashboard.py:172 83 | msgctxt "ui:dashboard|menu:config|placeholder" 84 | msgid "Open Configuration Panel" 85 | msgstr "Pqfo Dpogjhvsbujpo Qbofm" 86 | -------------------------------------------------------------------------------- /locales/templates/user_config.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-10-01 16:01+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: src/modules/user_config/cog.py:38 21 | msgctxt "userset:timezone" 22 | msgid "timezone" 23 | msgstr "" 24 | 25 | #: src/modules/user_config/cog.py:41 26 | msgctxt "userset:timezone|desc" 27 | msgid "Timezone in which to display statistics." 28 | msgstr "" 29 | 30 | #: src/modules/user_config/cog.py:45 31 | msgctxt "userset:timezone|long_desc" 32 | msgid "" 33 | "All personal time-related features of StudyLion will use this timezone for " 34 | "you, including personal statistics. Note that leaderboards will still be " 35 | "shown in the server's own timezone." 36 | msgstr "" 37 | 38 | #: src/modules/user_config/cog.py:60 39 | #, possible-python-brace-format 40 | msgctxt "userset:timezone|response:set" 41 | msgid "Your personal timezone has been set to `{timezone}`." 42 | msgstr "" 43 | 44 | #: src/modules/user_config/cog.py:65 45 | msgctxt "userset:timezone|response:unset" 46 | msgid "" 47 | "You have unset your timezone. Statistics will be displayed in the server " 48 | "timezone." 49 | msgstr "" 50 | 51 | #: src/modules/user_config/cog.py:81 52 | msgctxt "cmd:userconfig" 53 | msgid "my" 54 | msgstr "" 55 | 56 | #: src/modules/user_config/cog.py:82 57 | msgctxt "cmd:userconfig|desc" 58 | msgid "User configuration commands." 59 | msgstr "" 60 | 61 | #: src/modules/user_config/cog.py:89 62 | msgctxt "cmd:userconfig_timezone" 63 | msgid "timezone" 64 | msgstr "" 65 | 66 | #: src/modules/user_config/cog.py:92 67 | msgctxt "cmd:userconfig_timezone|desc" 68 | msgid "" 69 | "Set your personal timezone, used for displaying stats and setting reminders." 70 | msgstr "" 71 | 72 | #: src/modules/user_config/cog.py:96 73 | msgctxt "cmd:userconfig_timezone|param:timezone" 74 | msgid "timezone" 75 | msgstr "" 76 | 77 | #: src/modules/user_config/cog.py:101 78 | msgctxt "cmd:userconfig_timezone|param:timezone|desc" 79 | msgid "What timezone are you in? Try typing your country or continent." 80 | msgstr "" 81 | 82 | #: src/modules/user_config/cog.py:117 83 | #, possible-python-brace-format 84 | msgctxt "cmd:userconfig_timezone|response:set" 85 | msgid "Your timezone is currently set to {timezone}" 86 | msgstr "" 87 | 88 | #: src/modules/user_config/cog.py:121 89 | msgctxt "cmd:userconfig_timezone|button:reset|label" 90 | msgid "Reset" 91 | msgstr "" 92 | 93 | #: src/modules/user_config/cog.py:133 94 | #, possible-python-brace-format 95 | msgctxt "cmd:userconfig_timezone|response:unset" 96 | msgid "Your timezone is not set. Using the server timezone `{timezone}`." 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /locales/ceaser/LC_MESSAGES/exec.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: 1.0\n" 10 | "Report-Msgid-Bugs-To: you@example.com\n" 11 | "POT-Creation-Date: 2007-10-18 14:00+0100\n" 12 | "PO-Revision-Date: 2007-10-18 14:00+0100\n" 13 | "Last-Translator: you \n" 14 | "Language-Team: English \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: src/modules/sysadmin/exec_cog.py:257 20 | msgctxt "ward:sys_admin|failed" 21 | msgid "You must be a bot owner to do this!" 22 | msgstr "Zpv nvtu cf b cpu pxofs up ep uijt!" 23 | 24 | #: src/modules/sysadmin/exec_cog.py:262 25 | msgid "async" 26 | msgstr "btzod" 27 | 28 | #: src/modules/sysadmin/exec_cog.py:263 29 | msgid "Execute arbitrary code with Exec" 30 | msgstr "Fyfdvuf bscjusbsz dpef xjui Fyfd" 31 | 32 | #: src/modules/sysadmin/exec_cog.py:325 33 | msgctxt "command" 34 | msgid "eval" 35 | msgstr "fwbm" 36 | 37 | #: src/modules/sysadmin/exec_cog.py:326 38 | msgctxt "command:eval" 39 | msgid "Execute arbitrary code with Eval" 40 | msgstr "Fyfdvuf bscjusbsz dpef xjui Fwbm" 41 | 42 | #: src/modules/sysadmin/exec_cog.py:329 43 | msgctxt "command:eval|param:string" 44 | msgid "Code to evaluate." 45 | msgstr "Dpef up fwbmvbuf." 46 | 47 | #: src/modules/sysadmin/exec_cog.py:336 48 | msgctxt "command" 49 | msgid "asyncall" 50 | msgstr "btzodbmm" 51 | 52 | #: src/modules/sysadmin/exec_cog.py:337 53 | msgctxt "command:asyncall|desc" 54 | msgid "Execute arbitrary code on all shards." 55 | msgstr "Fyfdvuf bscjusbsz dpef po bmm tibset." 56 | 57 | #: src/modules/sysadmin/exec_cog.py:340 58 | msgctxt "command:asyncall|param:string" 59 | msgid "Cross-shard code to execute. Cannot reference ctx!" 60 | msgstr "Dsptt-tibse dpef up fyfdvuf. Dboopu sfgfsfodf duy!" 61 | 62 | #: src/modules/sysadmin/exec_cog.py:341 63 | msgctxt "command:asyncall|param:target" 64 | msgid "Target shard app name, see autocomplete for options." 65 | msgstr "Ubshfu tibse bqq obnf, tff bvupdpnqmfuf gps pqujpot." 66 | 67 | #: src/modules/sysadmin/exec_cog.py:384 68 | msgid "reload" 69 | msgstr "sfmpbe" 70 | 71 | #: src/modules/sysadmin/exec_cog.py:385 72 | msgid "Reload a given LionBot extension. Launches an ExecUI." 73 | msgstr "Sfmpbe b hjwfo MjpoCpu fyufotjpo. Mbvodift bo FyfdVJ." 74 | 75 | #: src/modules/sysadmin/exec_cog.py:388 76 | msgid "Name of the extension to reload. See autocomplete for options." 77 | msgstr "Obnf pg uif fyufotjpo up sfmpbe. Tff bvupdpnqmfuf gps pqujpot." 78 | 79 | #: src/modules/sysadmin/exec_cog.py:389 80 | msgid "Whether to force an extension reload even if it doesn't exist." 81 | msgstr "Xifuifs up gpsdf bo fyufotjpo sfmpbe fwfo jg ju epfto'u fyjtu." 82 | 83 | #: src/modules/sysadmin/exec_cog.py:425 84 | msgid "shutdown" 85 | msgstr "tivuepxo" 86 | 87 | #: src/modules/sysadmin/exec_cog.py:426 88 | msgid "Shutdown (or restart) the client." 89 | msgstr "Tivuepxo (ps sftubsu) uif dmjfou." 90 | -------------------------------------------------------------------------------- /src/modules/sponsors/settings.py: -------------------------------------------------------------------------------- 1 | from settings.data import ListData, ModelData 2 | from settings.groups import SettingGroup 3 | from settings.setting_types import GuildIDListSetting 4 | 5 | from core.setting_types import MessageSetting 6 | from core.data import CoreData 7 | from wards import sys_admin_iward 8 | from . import babel 9 | from .data import SponsorData 10 | 11 | _p = babel._p 12 | 13 | 14 | class SponsorSettings(SettingGroup): 15 | class Whitelist(ListData, GuildIDListSetting): 16 | setting_id = 'sponsor_whitelist' 17 | _write_ward = sys_admin_iward 18 | 19 | _display_name = _p( 20 | 'botset:sponsor_whitelist', "sponsor_whitelist" 21 | ) 22 | _desc = _p( 23 | 'botset:sponsor_whitelist|desc', 24 | "List of guildids where the sponsor prompt is not shown." 25 | ) 26 | _long_desc = _p( 27 | 'botset:sponsor_whitelist|long_desc', 28 | "The sponsor prompt will not appear in the set guilds." 29 | ) 30 | _accepts = _p( 31 | 'botset:sponsor_whitelist|accetps', 32 | "Comma separated list of guildids." 33 | ) 34 | 35 | _table_interface = SponsorData.sponsor_whitelist 36 | _id_column = 'appid' 37 | _data_column = 'guildid' 38 | _order_column = 'guildid' 39 | 40 | class SponsorPrompt(ModelData, MessageSetting): 41 | setting_id = 'sponsor_prompt' 42 | _set_cmd = 'leo sponsors' 43 | _write_ward = sys_admin_iward 44 | 45 | _display_name = _p( 46 | 'botset:sponsor_prompt', "sponsor_prompt" 47 | ) 48 | _desc = _p( 49 | 'botset:sponsor_prompt|desc', 50 | "Message to add underneath core commands." 51 | ) 52 | _long_desc = _p( 53 | 'botset:sponsor_prompt|long_desc', 54 | "Content of the message to send after core commands such as stats," 55 | " reminding users to check the sponsors command." 56 | ) 57 | 58 | _model = CoreData.BotConfig 59 | _column = CoreData.BotConfig.sponsor_prompt.name 60 | 61 | async def editor_callback(self, editor_data): 62 | self.value = editor_data 63 | await self.write() 64 | 65 | class SponsorMessage(ModelData, MessageSetting): 66 | setting_id = 'sponsor_message' 67 | _set_cmd = 'leo sponsors' 68 | _write_ward = sys_admin_iward 69 | 70 | _display_name = _p( 71 | 'botset:sponsor_message', "sponsor_message" 72 | ) 73 | _desc = _p( 74 | 'botset:sponsor_message|desc', 75 | "Message to send in response to /sponsors command." 76 | ) 77 | _long_desc = _p( 78 | 'botset:sponsor_message|long_desc', 79 | "Content of the message to send when a user runs the `/sponsors` command." 80 | ) 81 | 82 | _model = CoreData.BotConfig 83 | _column = CoreData.BotConfig.sponsor_message.name 84 | 85 | async def editor_callback(self, editor_data): 86 | self.value = editor_data 87 | await self.write() 88 | -------------------------------------------------------------------------------- /src/modules/pomodoro/settingui.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import discord 4 | from discord.ui.select import select, ChannelSelect 5 | 6 | from meta import LionBot 7 | 8 | from utils.ui import ConfigUI, DashboardSection 9 | from utils.lib import MessageArgs 10 | 11 | from .settings import TimerSettings 12 | from . import babel 13 | 14 | _p = babel._p 15 | 16 | 17 | class TimerConfigUI(ConfigUI): 18 | setting_classes = ( 19 | TimerSettings.PomodoroChannel, 20 | ) 21 | 22 | def __init__(self, bot: LionBot, guildid: int, channelid: int, **kwargs): 23 | self.settings = bot.get_cog('TimerCog').settings 24 | super().__init__(bot, guildid, channelid, **kwargs) 25 | 26 | # ----- UI Components ----- 27 | @select(cls=ChannelSelect, channel_types=[discord.ChannelType.text, discord.ChannelType.voice], 28 | placeholder="CHANNEL_SELECT_PLACEHOLDER", 29 | min_values=0, max_values=1) 30 | async def channel_menu(self, selection: discord.Interaction, selected: ChannelSelect): 31 | await selection.response.defer() 32 | setting = self.instances[0] 33 | await setting.interaction_check(setting.parent_id, selection) 34 | setting.value = selected.values[0] if selected.values else None 35 | await setting.write() 36 | 37 | async def refresh_channel_menu(self): 38 | self.channel_menu.placeholder = self.bot.translator.t(_p( 39 | 'ui:timer_config|menu:channels|placeholder', 40 | "Select Pomodoro Notification Channel" 41 | )) 42 | 43 | # ----- UI Flow ----- 44 | async def make_message(self) -> MessageArgs: 45 | t = self.bot.translator.t 46 | title = t(_p( 47 | 'ui:timer_config|embed|title', 48 | "Timer Configuration Panel" 49 | )) 50 | embed = discord.Embed( 51 | colour=discord.Colour.orange(), 52 | title=title 53 | ) 54 | for setting in self.instances: 55 | embed.add_field(**setting.embed_field, inline=False) 56 | 57 | args = MessageArgs(embed=embed) 58 | return args 59 | 60 | async def reload(self): 61 | lguild = await self.bot.core.lions.fetch_guild(self.guildid) 62 | self.instances = ( 63 | lguild.config.get(TimerSettings.PomodoroChannel.setting_id), 64 | ) 65 | 66 | async def refresh_components(self): 67 | await asyncio.gather( 68 | self.refresh_channel_menu(), 69 | self.edit_button_refresh(), 70 | self.close_button_refresh(), 71 | self.reset_button_refresh(), 72 | ) 73 | self.set_layout( 74 | (self.channel_menu,), 75 | (self.edit_button, self.reset_button, self.close_button) 76 | ) 77 | 78 | 79 | class TimerDashboard(DashboardSection): 80 | section_name = _p( 81 | 'dash:pomodoro|title', 82 | "Pomodoro Configuration ({commands[config pomodoro]})" 83 | ) 84 | _option_name = _p( 85 | "dash:stats|dropdown|placeholder", 86 | "Pomodoro Timer Panel" 87 | ) 88 | configui = TimerConfigUI 89 | setting_classes = TimerConfigUI.setting_classes 90 | -------------------------------------------------------------------------------- /data/migration/v6-v7/migration.sql: -------------------------------------------------------------------------------- 1 | -- Improved tasklist statistics 2 | ALTER TABLE tasklist 3 | ADD COLUMN completed_at TIMESTAMPTZ, 4 | ADD COLUMN deleted_at TIMESTAMPTZ, 5 | ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC', 6 | ALTER COLUMN last_updated_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC'; 7 | 8 | UPDATE tasklist SET deleted_at = NOW() WHERE last_updated_at < NOW() - INTERVAL '24h'; 9 | UPDATE tasklist SET completed_at = last_updated_at WHERE complete; 10 | 11 | ALTER TABLE tasklist 12 | DROP COLUMN complete; 13 | 14 | 15 | -- New member profile tags 16 | CREATE TABLE member_profile_tags( 17 | tagid SERIAL PRIMARY KEY, 18 | guildid BIGINT NOT NULL, 19 | userid BIGINT NOT NULL, 20 | tag TEXT NOT NULL, 21 | _timestamp TIMESTAMPTZ DEFAULT now(), 22 | FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid) 23 | ); 24 | CREATE INDEX member_profile_tags_members ON member_profile_tags (guildid, userid); 25 | 26 | 27 | -- New member weekly and monthly goals 28 | CREATE TABLE member_weekly_goals( 29 | guildid BIGINT NOT NULL, 30 | userid BIGINT NOT NULL, 31 | weekid INTEGER NOT NULL, -- Epoch time of the start of the UTC week 32 | study_goal INTEGER, 33 | task_goal INTEGER, 34 | _timestamp TIMESTAMPTZ DEFAULT now(), 35 | PRIMARY KEY (guildid, userid, weekid), 36 | FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid) ON DELETE CASCADE 37 | ); 38 | CREATE INDEX member_weekly_goals_members ON member_weekly_goals (guildid, userid); 39 | 40 | CREATE TABLE member_weekly_goal_tasks( 41 | taskid SERIAL PRIMARY KEY, 42 | guildid BIGINT NOT NULL, 43 | userid BIGINT NOT NULL, 44 | weekid INTEGER NOT NULL, 45 | content TEXT NOT NULL, 46 | completed BOOLEAN NOT NULL DEFAULT FALSE, 47 | _timestamp TIMESTAMPTZ DEFAULT now(), 48 | FOREIGN KEY (weekid, guildid, userid) REFERENCES member_weekly_goals (weekid, guildid, userid) ON DELETE CASCADE 49 | ); 50 | CREATE INDEX member_weekly_goal_tasks_members_weekly ON member_weekly_goal_tasks (guildid, userid, weekid); 51 | 52 | CREATE TABLE member_monthly_goals( 53 | guildid BIGINT NOT NULL, 54 | userid BIGINT NOT NULL, 55 | monthid INTEGER NOT NULL, -- Epoch time of the start of the UTC month 56 | study_goal INTEGER, 57 | task_goal INTEGER, 58 | _timestamp TIMESTAMPTZ DEFAULT now(), 59 | PRIMARY KEY (guildid, userid, monthid), 60 | FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid) ON DELETE CASCADE 61 | ); 62 | CREATE INDEX member_monthly_goals_members ON member_monthly_goals (guildid, userid); 63 | 64 | CREATE TABLE member_monthly_goal_tasks( 65 | taskid SERIAL PRIMARY KEY, 66 | guildid BIGINT NOT NULL, 67 | userid BIGINT NOT NULL, 68 | monthid INTEGER NOT NULL, 69 | content TEXT NOT NULL, 70 | completed BOOLEAN NOT NULL DEFAULT FALSE, 71 | _timestamp TIMESTAMPTZ DEFAULT now(), 72 | FOREIGN KEY (monthid, guildid, userid) REFERENCES member_monthly_goals (monthid, guildid, userid) ON DELETE CASCADE 73 | ); 74 | CREATE INDEX member_monthly_goal_tasks_members_monthly ON member_monthly_goal_tasks (guildid, userid, monthid); 75 | 76 | INSERT INTO VersionHistory (version, author) VALUES (7, 'v6-v7 migration'); 77 | --------------------------------------------------------------------------------