├── .documentation_pictures └── test_cases │ ├── .a.hide_xp.png │ ├── .a.remove_level_name 1.png │ ├── .announce follow-up input.png │ ├── .announce follow-up result.png │ ├── .announce input.png │ ├── .announce result.png │ ├── .b.remove_level_name 1.png │ ├── .b.show_xp.png │ ├── .deletereminder 8.png │ ├── .em "description" "title" "field".png │ ├── .em "title" "field".png │ ├── .emojispeak 1234_abcd.png │ ├── .emojispeak.png │ ├── .exc ls -l.png │ ├── .frequency command.png │ ├── .frequency.png │ ├── .help here.png │ ├── .help nothing.png │ ├── .help.png │ ├── .here wall.png │ ├── .here.png │ ├── .hide_xp.png │ ├── .levels.png │ ├── .load nothing.png │ ├── .load reminders.png │ ├── .outline blah.png │ ├── .outline cmpt 300 spring d200.png │ ├── .outline cmpt 300.png │ ├── .outline cmpt300 d200 next.png │ ├── .outline cmpt300 next.png │ ├── .outline cmpt300 spring d200.png │ ├── .outline cmpt300 summer d200 next.png │ ├── .outline cmpt300.png │ ├── .outline cmpt666.png │ ├── .outline.png │ ├── .poll "go to the moon?" "yes" "no" "boye you crazy??".png │ ├── .poll 1 2 3 4 5 6 7 8 9 10 11 12 13.png │ ├── .poll avengers?.png │ ├── .poll.png │ ├── .rank @Micah.png │ ├── .rank.png │ ├── .ranks.png │ ├── .reload nothing.png │ ├── .reload reminders.png │ ├── .remindmeat tomorrow at 5:00pm Canada|Eastern to turn in my assignment.png │ ├── .remindmein 10 minutes to turn in my assignment.png │ ├── .remindmein a day after tomorrow to turn in my assignment.png │ ├── .remindmein.png │ ├── .remindmeon Oct 4 at 6:23 am to turn in my assignment.png │ ├── .set_level_name 1 XP.png │ ├── .set_level_name 1 XP_level_1.png │ ├── .sfu blah.png │ ├── .sfu cmpt 300.png │ ├── .sfu cmpt300.png │ ├── .sfu cmpt666.png │ ├── .sfu.png │ ├── .show_xp.png │ ├── .showreminders.png │ ├── .sync.png │ ├── .unload nothing.png │ ├── .unload reminders.png │ ├── .urban DevelopersDevelopersDevelopers.png │ ├── .urban girl.png │ ├── .warn behold my mod powers and be scarred.png │ ├── .wolfram Marvel.png │ ├── .wolfram giberasdfasdfadfasdf.png │ ├── a.remindmein 10 seconds to turn in my assignment.png │ ├── a.showreminders.png │ ├── b.remindmein 10 seconds to turn in my assignment.png │ ├── b.showreminders.png │ ├── courses.png │ ├── courses_department_blah.png │ ├── courses_department_math.png │ ├── courses_department_math_level_200.png │ ├── courses_department_phys_term_summer_year_2020.png │ ├── courses_department_stat_level_300_term_fall_year_2024.png │ ├── courses_level_1.png │ ├── courses_level_200.png │ ├── courses_level_300_term_spring_year_2021.png │ ├── courses_term_and_year_set.png │ ├── courses_term_blah.png │ ├── courses_term_or_year_unset.png │ ├── deleterole 1158444206990299208.png │ ├── echo "this is the test case".png │ ├── echo 'this is the test case'.png │ ├── echo this is the test case.png │ ├── iam 1159103657120387167.png │ ├── iamn 1159103657120387167.png │ ├── newrole hello.png │ ├── newrole hello_5.png │ ├── ping.png │ ├── purge_messages.png │ ├── purgeroles.png │ ├── roles.png │ ├── roles_assignable.png │ ├── tex e^{i\theta} = \cos x + i \sin x.png │ └── whois 1007425263879069736.png ├── .github └── pull_request_template.md ├── .gitignore ├── .gitmodules ├── .run_walle.py ├── .wiki ├── Discord.py_Features_and_Functionalities │ ├── auto_complete_menu.png │ ├── commandtree.png │ ├── interaction.png │ └── registered_guild_commands.png ├── Managing_CSSS_Discord_Bots │ ├── add_new_application.png │ ├── create_discord_guild.png │ ├── create_permission_integer.png │ ├── get_client_id.png │ └── granting_intents.png ├── Misc_Features_and_Functionalities │ ├── down_arrow.png │ ├── specify_env_in_pycharm.png │ └── up_arrow.png ├── Setting_up_local_environment_for_repo │ ├── add_new_interpreter_1.png │ ├── add_new_interpreter_2.png │ ├── add_new_interpreter_3.png │ ├── run_configuration_1.png │ └── run_configuration_2.png ├── wall_e_PROD_infrastructure │ └── console_output_instructions.png └── wall_e_discordpy_coding_tips │ ├── slash_command_argument_help_indicator.png │ ├── slash_command_help_indicator.png │ ├── text_command_argument_help_indicator.png │ └── text_command_help_indicator.png ├── CI └── validate_and_deploy │ ├── 1_validate │ ├── Dockerfile.test │ ├── run_local_formatting_test.sh │ ├── test_config_files │ │ ├── pytest.ini │ │ ├── setup.cfg │ │ ├── test-requirements.txt │ │ └── validate_line_endings.sh │ └── validate_formatting.sh │ ├── 2_deploy │ ├── create-database.ddl │ ├── destroy-dev-env.sh │ ├── server_scripts │ │ ├── Dockerfile.wall_e │ │ ├── Dockerfile.wall_e_base │ │ ├── deploy_to_prod_discord_guild.sh │ │ ├── docker-compose.yml │ │ └── wait-for-postgres.sh │ └── user_scripts │ │ ├── Dockerfile.walle.mount │ │ ├── create-dev-docker-image.sh │ │ ├── docker-compose-mount.yml │ │ ├── set_env.sh │ │ └── setup-dev-env.sh │ └── Jenkinsfile ├── CODEOWNERS ├── LICENSE ├── README.rst ├── Test_Cases.md ├── download_repo.sh ├── run_walle.sh ├── wall_e ├── create-database.ddl ├── django_manage.py ├── django_settings.py ├── extensions │ ├── administration.py │ ├── ban.py │ ├── custom_commands.py │ ├── frosh.py │ ├── health_checks.py │ ├── help_commands.py │ ├── here.py │ ├── leveling.py │ ├── misc.py │ ├── mod.py │ ├── reminders.py │ ├── role_commands.py │ └── sfu.py ├── main.py ├── overriden_coroutines │ ├── __init__.py │ ├── delete_help_messages.py │ ├── detect_reactions.py │ └── error_handlers.py ├── requirements.txt ├── utilities │ ├── autocomplete │ │ ├── __init__.py │ │ ├── banned_users_choices.py │ │ ├── examples_command.py │ │ ├── extensions_load_choices.py │ │ └── role_commands_choices.py │ ├── bot_channel_manager.py │ ├── config │ │ ├── config.py │ │ ├── local.ini │ │ └── production.ini │ ├── create_github_issue.py │ ├── discordpy_stream_handler.py │ ├── embed.py │ ├── error_reporter.py │ ├── file_uploading.py │ ├── global_vars.py │ ├── log_channel.py │ ├── paginate.py │ ├── send.py │ ├── setup_logger.py │ ├── slash_command_examples.json │ └── wall_e_bot.py ├── wait-for-postgres.sh └── wall_e_models └── wall_e_pic.jpg /.documentation_pictures/test_cases/.a.hide_xp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.a.hide_xp.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.a.remove_level_name 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.a.remove_level_name 1.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.announce follow-up input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.announce follow-up input.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.announce follow-up result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.announce follow-up result.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.announce input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.announce input.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.announce result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.announce result.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.b.remove_level_name 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.b.remove_level_name 1.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.b.show_xp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.b.show_xp.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.deletereminder 8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.deletereminder 8.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.em "description" "title" "field".png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.em "description" "title" "field".png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.em "title" "field".png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.em "title" "field".png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.emojispeak 1234_abcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.emojispeak 1234_abcd.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.emojispeak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.emojispeak.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.exc ls -l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.exc ls -l.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.frequency command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.frequency command.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.frequency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.frequency.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.help here.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.help here.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.help nothing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.help nothing.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.help.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.here wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.here wall.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.here.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.here.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.hide_xp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.hide_xp.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.levels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.levels.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.load nothing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.load nothing.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.load reminders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.load reminders.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.outline blah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.outline blah.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.outline cmpt 300 spring d200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.outline cmpt 300 spring d200.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.outline cmpt 300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.outline cmpt 300.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.outline cmpt300 d200 next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.outline cmpt300 d200 next.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.outline cmpt300 next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.outline cmpt300 next.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.outline cmpt300 spring d200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.outline cmpt300 spring d200.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.outline cmpt300 summer d200 next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.outline cmpt300 summer d200 next.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.outline cmpt300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.outline cmpt300.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.outline cmpt666.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.outline cmpt666.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.outline.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.poll "go to the moon?" "yes" "no" "boye you crazy??".png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.poll "go to the moon?" "yes" "no" "boye you crazy??".png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.poll 1 2 3 4 5 6 7 8 9 10 11 12 13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.poll 1 2 3 4 5 6 7 8 9 10 11 12 13.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.poll avengers?.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.poll avengers?.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.poll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.poll.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.rank @Micah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.rank @Micah.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.rank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.rank.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.ranks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.ranks.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.reload nothing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.reload nothing.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.reload reminders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.reload reminders.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.remindmeat tomorrow at 5:00pm Canada|Eastern to turn in my assignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.remindmeat tomorrow at 5:00pm Canada|Eastern to turn in my assignment.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.remindmein 10 minutes to turn in my assignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.remindmein 10 minutes to turn in my assignment.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.remindmein a day after tomorrow to turn in my assignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.remindmein a day after tomorrow to turn in my assignment.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.remindmein.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.remindmein.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.remindmeon Oct 4 at 6:23 am to turn in my assignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.remindmeon Oct 4 at 6:23 am to turn in my assignment.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.set_level_name 1 XP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.set_level_name 1 XP.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.set_level_name 1 XP_level_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.set_level_name 1 XP_level_1.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.sfu blah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.sfu blah.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.sfu cmpt 300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.sfu cmpt 300.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.sfu cmpt300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.sfu cmpt300.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.sfu cmpt666.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.sfu cmpt666.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.sfu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.sfu.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.show_xp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.show_xp.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.showreminders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.showreminders.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.sync.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.unload nothing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.unload nothing.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.unload reminders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.unload reminders.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.urban DevelopersDevelopersDevelopers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.urban DevelopersDevelopersDevelopers.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.urban girl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.urban girl.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.warn behold my mod powers and be scarred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.warn behold my mod powers and be scarred.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.wolfram Marvel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.wolfram Marvel.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/.wolfram giberasdfasdfadfasdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/.wolfram giberasdfasdfadfasdf.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/a.remindmein 10 seconds to turn in my assignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/a.remindmein 10 seconds to turn in my assignment.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/a.showreminders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/a.showreminders.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/b.remindmein 10 seconds to turn in my assignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/b.remindmein 10 seconds to turn in my assignment.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/b.showreminders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/b.showreminders.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/courses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/courses.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/courses_department_blah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/courses_department_blah.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/courses_department_math.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/courses_department_math.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/courses_department_math_level_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/courses_department_math_level_200.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/courses_department_phys_term_summer_year_2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/courses_department_phys_term_summer_year_2020.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/courses_department_stat_level_300_term_fall_year_2024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/courses_department_stat_level_300_term_fall_year_2024.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/courses_level_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/courses_level_1.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/courses_level_200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/courses_level_200.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/courses_level_300_term_spring_year_2021.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/courses_level_300_term_spring_year_2021.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/courses_term_and_year_set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/courses_term_and_year_set.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/courses_term_blah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/courses_term_blah.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/courses_term_or_year_unset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/courses_term_or_year_unset.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/deleterole 1158444206990299208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/deleterole 1158444206990299208.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/echo "this is the test case".png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/echo "this is the test case".png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/echo 'this is the test case'.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/echo 'this is the test case'.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/echo this is the test case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/echo this is the test case.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/iam 1159103657120387167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/iam 1159103657120387167.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/iamn 1159103657120387167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/iamn 1159103657120387167.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/newrole hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/newrole hello.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/newrole hello_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/newrole hello_5.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/ping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/ping.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/purge_messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/purge_messages.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/purgeroles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/purgeroles.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/roles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/roles.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/roles_assignable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/roles_assignable.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/tex e^{i\theta} = \cos x + i \sin x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/tex e^{i\theta} = \cos x + i \sin x.png -------------------------------------------------------------------------------- /.documentation_pictures/test_cases/whois 1007425263879069736.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.documentation_pictures/test_cases/whois 1007425263879069736.png -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | > insert PR description here 4 | 5 | ## PR Checklist 6 | 7 | > Make sure to account for all the items in the [PR checklist](https://github.com/CSSS/wall_e/wiki/4.-PR-Checklist) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | ### JetBrains template 103 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 104 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 105 | 106 | # User-specific stuff: 107 | .idea/**/workspace.xml 108 | .idea/**/tasks.xml 109 | .idea/dictionaries 110 | 111 | # Sensitive or high-churn files: 112 | .idea/**/dataSources/ 113 | .idea/**/dataSources.ids 114 | .idea/**/dataSources.xml 115 | .idea/**/dataSources.local.xml 116 | .idea/**/sqlDataSources.xml 117 | .idea/**/dynamic.xml 118 | .idea/**/uiDesigner.xml 119 | 120 | # Gradle: 121 | .idea/**/gradle.xml 122 | .idea/**/libraries 123 | 124 | # CMake 125 | cmake-build-debug/ 126 | 127 | # Mongo Explorer plugin: 128 | .idea/**/mongoSettings.xml 129 | 130 | ## File-based project format: 131 | *.iws 132 | 133 | ## Plugin-specific files: 134 | 135 | # IntelliJ 136 | out/ 137 | container_settings.json 138 | 139 | # mpeltonen/sbt-idea plugin 140 | .idea_modules/ 141 | 142 | # JIRA plugin 143 | atlassian-ide-plugin.xml 144 | 145 | # Cursive Clojure plugin 146 | .idea/replstate.xml 147 | 148 | # Crashlytics plugin (for Android Studio and IntelliJ) 149 | com_crashlytics_export_strings.xml 150 | crashlytics.properties 151 | crashlytics-build.properties 152 | fabric.properties 153 | token 154 | 155 | 156 | .pytest_cache 157 | @eaDir 158 | notes/ 159 | sftp-config.json 160 | test-bot.iml 161 | logs/ 162 | 163 | 164 | .idea/ 165 | 166 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "wall_e_models"] 2 | path = .wall_e_models 3 | url = https://github.com/CSSS/wall_e_models.git 4 | -------------------------------------------------------------------------------- /.wiki/Discord.py_Features_and_Functionalities/auto_complete_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Discord.py_Features_and_Functionalities/auto_complete_menu.png -------------------------------------------------------------------------------- /.wiki/Discord.py_Features_and_Functionalities/commandtree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Discord.py_Features_and_Functionalities/commandtree.png -------------------------------------------------------------------------------- /.wiki/Discord.py_Features_and_Functionalities/interaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Discord.py_Features_and_Functionalities/interaction.png -------------------------------------------------------------------------------- /.wiki/Discord.py_Features_and_Functionalities/registered_guild_commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Discord.py_Features_and_Functionalities/registered_guild_commands.png -------------------------------------------------------------------------------- /.wiki/Managing_CSSS_Discord_Bots/add_new_application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Managing_CSSS_Discord_Bots/add_new_application.png -------------------------------------------------------------------------------- /.wiki/Managing_CSSS_Discord_Bots/create_discord_guild.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Managing_CSSS_Discord_Bots/create_discord_guild.png -------------------------------------------------------------------------------- /.wiki/Managing_CSSS_Discord_Bots/create_permission_integer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Managing_CSSS_Discord_Bots/create_permission_integer.png -------------------------------------------------------------------------------- /.wiki/Managing_CSSS_Discord_Bots/get_client_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Managing_CSSS_Discord_Bots/get_client_id.png -------------------------------------------------------------------------------- /.wiki/Managing_CSSS_Discord_Bots/granting_intents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Managing_CSSS_Discord_Bots/granting_intents.png -------------------------------------------------------------------------------- /.wiki/Misc_Features_and_Functionalities/down_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Misc_Features_and_Functionalities/down_arrow.png -------------------------------------------------------------------------------- /.wiki/Misc_Features_and_Functionalities/specify_env_in_pycharm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Misc_Features_and_Functionalities/specify_env_in_pycharm.png -------------------------------------------------------------------------------- /.wiki/Misc_Features_and_Functionalities/up_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Misc_Features_and_Functionalities/up_arrow.png -------------------------------------------------------------------------------- /.wiki/Setting_up_local_environment_for_repo/add_new_interpreter_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Setting_up_local_environment_for_repo/add_new_interpreter_1.png -------------------------------------------------------------------------------- /.wiki/Setting_up_local_environment_for_repo/add_new_interpreter_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Setting_up_local_environment_for_repo/add_new_interpreter_2.png -------------------------------------------------------------------------------- /.wiki/Setting_up_local_environment_for_repo/add_new_interpreter_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Setting_up_local_environment_for_repo/add_new_interpreter_3.png -------------------------------------------------------------------------------- /.wiki/Setting_up_local_environment_for_repo/run_configuration_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Setting_up_local_environment_for_repo/run_configuration_1.png -------------------------------------------------------------------------------- /.wiki/Setting_up_local_environment_for_repo/run_configuration_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/Setting_up_local_environment_for_repo/run_configuration_2.png -------------------------------------------------------------------------------- /.wiki/wall_e_PROD_infrastructure/console_output_instructions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/wall_e_PROD_infrastructure/console_output_instructions.png -------------------------------------------------------------------------------- /.wiki/wall_e_discordpy_coding_tips/slash_command_argument_help_indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/wall_e_discordpy_coding_tips/slash_command_argument_help_indicator.png -------------------------------------------------------------------------------- /.wiki/wall_e_discordpy_coding_tips/slash_command_help_indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/wall_e_discordpy_coding_tips/slash_command_help_indicator.png -------------------------------------------------------------------------------- /.wiki/wall_e_discordpy_coding_tips/text_command_argument_help_indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/wall_e_discordpy_coding_tips/text_command_argument_help_indicator.png -------------------------------------------------------------------------------- /.wiki/wall_e_discordpy_coding_tips/text_command_help_indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/.wiki/wall_e_discordpy_coding_tips/text_command_help_indicator.png -------------------------------------------------------------------------------- /CI/validate_and_deploy/1_validate/Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM python:3.8.5-alpine 2 | 3 | ARG CONTAINER_HOME_DIR 4 | 5 | ENV CONTAINER_HOME_DIR=$CONTAINER_HOME_DIR 6 | 7 | ARG UNIT_TEST_RESULTS 8 | 9 | ENV UNIT_TEST_RESULTS=$UNIT_TEST_RESULTS 10 | 11 | ARG TEST_RESULT_FILE_NAME 12 | 13 | ENV TEST_RESULT_FILE_NAME=$TEST_RESULT_FILE_NAME 14 | 15 | 16 | 17 | WORKDIR $CONTAINER_HOME_DIR 18 | 19 | COPY CI/validate_and_deploy/1_validate/test_config_files/test-requirements.txt ./ 20 | 21 | COPY wall_e ./ 22 | 23 | COPY CI/validate_and_deploy/1_validate/test_config_files/pytest.ini ./ 24 | 25 | COPY CI/validate_and_deploy/1_validate/test_config_files/setup.cfg ./ 26 | 27 | RUN pip install --no-cache-dir -r test-requirements.txt 28 | 29 | RUN mkdir -p $UNIT_TEST_RESULTS 30 | 31 | CMD ["sh" , "-c", "py.test --junitxml=${UNIT_TEST_RESULTS}/${TEST_RESULT_FILE_NAME}" ] 32 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/1_validate/run_local_formatting_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # PURPOSE: to be used by the user when they want to test their code against the linter 5 | 6 | docker stop wall_e_py_test || true 7 | docker rm wall_e_py_test || true 8 | docker image rm wall_e_py_test || true 9 | docker build -t wall_e_py_test -f CI/validate_and_deploy/1_validate/Dockerfile.test \ 10 | --build-arg CONTAINER_HOME_DIR=/usr/src/app --build-arg UNIT_TEST_RESULTS=/usr/src/app/tests \ 11 | --build-arg TEST_RESULT_FILE_NAME=all-unit-tests.xml . 12 | docker run -d --name wall_e_py_test wall_e_py_test 13 | 14 | while [ "$(docker inspect -f '{{.State.Running}}' wall_e_py_test)" = "true" ] 15 | do 16 | echo "waiting for testing to complete" 17 | sleep 1 18 | done 19 | 20 | docker logs wall_e_py_test 21 | 22 | docker rm -f wall_e_py_test || true 23 | docker image rm wall_e_py_test 24 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/1_validate/test_config_files/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --flake8 3 | norecursedirs = local_development 4 | junit_family=legacy 5 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/1_validate/test_config_files/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 118 3 | extend-ignore = SFS301 -------------------------------------------------------------------------------- /CI/validate_and_deploy/1_validate/test_config_files/test-requirements.txt: -------------------------------------------------------------------------------- 1 | flake8==3.9.2 2 | pytest==8.2.2 3 | pytest-flake8==1.1.1 4 | pep8-naming==0.14.1 5 | flake8-sfs==1.0.0 6 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/1_validate/test_config_files/validate_line_endings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # PURPOSE: used by both the user and jenkins to verify that all the plain test files in the repo used linux 4 | # line endings 5 | traverse_files(){ 6 | echo "layer="$1 7 | local files_in_current_folder 8 | mapfile -t files_in_current_folder < <( ls | tr '\n' '\n' ) 9 | local index 10 | for (( index=0; index < ${#files_in_current_folder[@]}; index++ )) 11 | do 12 | echo ${files_in_current_folder[$index]} 13 | if [ -d "${files_in_current_folder[$index]}" ] 14 | then 15 | echo -e "\tits a directory" 16 | local current_directory=$(pwd) 17 | echo "going to ${files_in_current_folder[$index]} from $current_directory" 18 | cd "${files_in_current_folder[$index]}" 19 | traverse_files $(expr $1 + 1) 20 | if [ $? -eq 1 ]; then 21 | return 1 22 | fi 23 | echo "going back to $current_directory" 24 | cd "$current_directory" 25 | echo "back at ${files_in_current_folder[$index]}" 26 | 27 | else 28 | echo -e "\tits a regular file" 29 | fileType=$(file -i ${files_in_current_folder[$index]} | cut -d' ' -f2) 30 | if [ "$fileType" == "text/x-python;" ] || [ "$fileType" == "text/plain;" ]; then 31 | result=$(dos2unix < ${files_in_current_folder[$index]} | cmp - ${files_in_current_folder[$index]}) 32 | if [ "$result" != "" ]; then 33 | echo ${files_in_current_folder[$index]} is not using linux line endings! 34 | return 1 35 | fi 36 | fi 37 | fi 38 | 39 | done 40 | } 41 | pushd wall_e 42 | traverse_files 1 43 | popd -------------------------------------------------------------------------------- /CI/validate_and_deploy/1_validate/validate_formatting.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # PURPOSE: used by jenkins to run the code past the linter 4 | 5 | set -e -o xtrace 6 | 7 | rm ${DISCORD_NOTIFICATION_MESSAGE_FILE} || true 8 | 9 | docker_test_image_lower_case=$(echo "$DOCKER_TEST_IMAGE" | awk '{print tolower($0)}') 10 | 11 | docker rm -f ${DOCKER_TEST_CONTAINER} || true 12 | docker image rm -f ${docker_test_image_lower_case} || true 13 | 14 | rm -r ${LOCALHOST_TEST_DIR} || true 15 | mkdir -p ${LOCALHOST_TEST_DIR} 16 | 17 | 18 | docker build --no-cache -t ${docker_test_image_lower_case} \ 19 | -f CI/validate_and_deploy/1_validate/Dockerfile.test \ 20 | --build-arg CONTAINER_HOME_DIR=${CONTAINER_HOME_DIR} \ 21 | --build-arg UNIT_TEST_RESULTS=${CONTAINER_TEST_DIR} \ 22 | --build-arg TEST_RESULT_FILE_NAME=${TEST_RESULT_FILE_NAME} . 23 | 24 | docker run -d --name ${DOCKER_TEST_CONTAINER} ${docker_test_image_lower_case} 25 | while [ "$(docker inspect -f '{{.State.Running}}' ${DOCKER_TEST_CONTAINER})" == "true" ] 26 | do 27 | echo "waiting for the python formatting validation to finish" 28 | sleep 1 29 | done 30 | sudo docker cp ${DOCKER_TEST_CONTAINER}:${CONTAINER_TEST_DIR}/${TEST_RESULT_FILE_NAME} ${LOCALHOST_TEST_DIR}/${TEST_RESULT_FILE_NAME} 31 | 32 | test_container_failed=$(docker inspect ${DOCKER_TEST_CONTAINER} --format='{{.State.ExitCode}}') 33 | 34 | if [ "${test_container_failed}" -eq "1" ]; then 35 | docker logs ${DOCKER_TEST_CONTAINER} 36 | docker logs ${DOCKER_TEST_CONTAINER} --tail 12 &> ${DISCORD_NOTIFICATION_MESSAGE_FILE} 37 | docker stop ${DOCKER_TEST_CONTAINER} || true 38 | docker rm ${DOCKER_TEST_CONTAINER} || true 39 | docker image rm -f ${docker_test_image_lower_case} || true 40 | exit 1 41 | fi 42 | 43 | docker stop ${DOCKER_TEST_CONTAINER} || true 44 | docker rm ${DOCKER_TEST_CONTAINER} || true 45 | docker image rm -f ${docker_test_image_lower_case} || true 46 | exit 0 47 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/2_deploy/create-database.ddl: -------------------------------------------------------------------------------- 1 | 2 | CREATE USER :WALL_E_DB_USER WITH PASSWORD :'WALL_E_DB_PASSWORD'; 3 | CREATE DATABASE :WALL_E_DB_DBNAME; 4 | ALTER DATABASE :WALL_E_DB_DBNAME OWNER TO :WALL_E_DB_USER; 5 | GRANT CONNECT ON DATABASE :WALL_E_DB_DBNAME TO :WALL_E_DB_USER; 6 | GRANT ALL PRIVILEGES ON DATABASE :WALL_E_DB_DBNAME TO :WALL_E_DB_USER; 7 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/2_deploy/destroy-dev-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o xtrace 4 | # https://stackoverflow.com/a/5750463/7734535 5 | 6 | if [ -z "${COMPOSE_PROJECT_NAME}" ]; then 7 | echo "COMPOSE_PROJECT_NAME needs to be set" 8 | exit 1 9 | fi 10 | 11 | export image_name=$(echo "${COMPOSE_PROJECT_NAME}"_wall_e | awk '{print tolower($0)}') 12 | export network_name=$(echo "${COMPOSE_PROJECT_NAME}"_default | awk '{print tolower($0)}') 13 | export volume_name="${COMPOSE_PROJECT_NAME}_logs" 14 | 15 | 16 | pushd CI 17 | cp validate_and_deploy/2_deploy/user_scripts/docker-compose-mount.yml docker-compose.yml 18 | touch wall_e.env 19 | docker-compose rm -f -s -v || true 20 | docker volume rm "${volume_name}" || true 21 | docker image rm "${image_name}" || true 22 | docker network rm "${network_name}" || true 23 | rm docker-compose.yml 24 | popd 25 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/2_deploy/server_scripts/Dockerfile.wall_e: -------------------------------------------------------------------------------- 1 | ARG ORIGIN_IMAGE 2 | 3 | FROM $ORIGIN_IMAGE 4 | 5 | COPY wall_e ./ 6 | 7 | COPY .wall_e_models/wall_e_models wall_e_models 8 | 9 | COPY CI/validate_and_deploy/2_deploy/create-database.ddl . 10 | 11 | COPY CI/validate_and_deploy/2_deploy/server_scripts/wait-for-postgres.sh . 12 | 13 | RUN apk add --no-cache tzdata # https://github.com/docker-library/postgres/issues/220 14 | 15 | CMD ["./wait-for-postgres.sh", "db", "python", "./main.py" ] 16 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/2_deploy/server_scripts/Dockerfile.wall_e_base: -------------------------------------------------------------------------------- 1 | ARG WALL_E_BASE_ORIGIN_NAME 2 | 3 | FROM $WALL_E_BASE_ORIGIN_NAME 4 | 5 | ARG CONTAINER_HOME_DIR 6 | 7 | ENV CONTAINER_HOME_DIR=$CONTAINER_HOME_DIR 8 | 9 | WORKDIR $CONTAINER_HOME_DIR 10 | 11 | COPY wall_e/requirements.txt . 12 | 13 | RUN pip install --no-cache-dir -r requirements.txt 14 | 15 | COPY .wall_e_models/requirements.txt wall_e_models_requirements.txt 16 | 17 | RUN pip install --no-cache-dir -r wall_e_models_requirements.txt -------------------------------------------------------------------------------- /CI/validate_and_deploy/2_deploy/server_scripts/deploy_to_prod_discord_guild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # PURPOSE: used be jenkins to launch Wall_e to the CSSS PROD Discord Guild 4 | 5 | set -e -o xtrace 6 | # https://stackoverflow.com/a/5750463/7734535 7 | 8 | 9 | if [ -z "${DOCKER_HUB_PASSWORD}" ]; then 10 | echo "DOCKER_HUB_PASSWORD is not set" 11 | exit 1 12 | fi 13 | 14 | 15 | if [ -z "${DOCKER_HUB_USER_NAME}" ]; then 16 | echo "DOCKER_HUB_USER_NAME is not set" 17 | exit 1 18 | fi 19 | 20 | rm ${DISCORD_NOTIFICATION_MESSAGE_FILE} || true 21 | 22 | export compose_project_name=$(echo "$COMPOSE_PROJECT_NAME" | awk '{print tolower($0)}') 23 | 24 | export docker_compose_file="CI/validate_and_deploy/2_deploy/server_scripts/docker-compose.yml" 25 | 26 | export prod_container_name="${COMPOSE_PROJECT_NAME}_wall_e" 27 | export prod_image_name_lower_case=$(echo "$prod_container_name" | awk '{print tolower($0)}') 28 | 29 | export prod_container_db_name="${COMPOSE_PROJECT_NAME}_wall_e_db" 30 | 31 | export docker_registry="sfucsssorg" 32 | export wall_e_top_base_image="wall_e_base" 33 | export ORIGIN_IMAGE="${docker_registry}/wall_e" 34 | export WALL_E_PYTHON_BASE_IMAGE="${docker_registry}/wall_e_python" 35 | 36 | export wall_e_top_base_image_dockerfile="CI/validate_and_deploy/2_deploy/server_scripts/Dockerfile.wall_e_base" 37 | 38 | docker rm -f ${prod_container_name} || true 39 | docker image rm -f ${prod_image_name_lower_case} || true 40 | docker volume create --name="${COMPOSE_PROJECT_NAME}_logs" 41 | 42 | git submodule update --init --recursive 43 | 44 | re_create_top_base_image () { 45 | docker image rm -f "${prod_image_name_lower_case}" "${wall_e_top_base_image}" "${ORIGIN_IMAGE}" 46 | docker system prune -f --all 47 | docker build --no-cache -t ${wall_e_top_base_image} -f ${wall_e_top_base_image_dockerfile} \ 48 | --build-arg CONTAINER_HOME_DIR=${CONTAINER_HOME_DIR} \ 49 | --build-arg WALL_E_BASE_ORIGIN_NAME=${WALL_E_PYTHON_BASE_IMAGE} . 50 | docker tag ${wall_e_top_base_image} ${ORIGIN_IMAGE} 51 | echo "${DOCKER_HUB_PASSWORD}" | docker login --username=${DOCKER_HUB_USER_NAME} --password-stdin 52 | docker push ${ORIGIN_IMAGE} 53 | docker image rm -f "${prod_image_name_lower_case}" "${wall_e_top_base_image}" "${ORIGIN_IMAGE}" 54 | docker system prune -f --all 55 | } 56 | 57 | re_create_top_base_image 58 | 59 | docker-compose -f "${docker_compose_file}" up -d 60 | sleep 20 61 | 62 | container_failed=$(docker ps -a -f name=${prod_container_name} --format "{{.Status}}" | head -1) 63 | container_db_failed=$(docker ps -a -f name=${prod_container_db_name} --format "{{.Status}}" | head -1) 64 | 65 | if [[ "${container_failed}" != *"Up"* ]]; then 66 | docker logs ${prod_container_name} 67 | docker logs ${prod_container_name} --tail 12 &> ${DISCORD_NOTIFICATION_MESSAGE_FILE} 68 | exit 1 69 | fi 70 | 71 | if [[ "${container_db_failed}" != *"Up"* ]]; then 72 | docker logs ${prod_container_db_name} 73 | docker logs ${prod_container_name} --tail 12 &> ${DISCORD_NOTIFICATION_MESSAGE_FILE} 74 | exit 1 75 | fi 76 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/2_deploy/server_scripts/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' #docker-compose version 2 | services: #Services that are needed for the wall_e app 3 | wall_e: #the wall_e app, the name will of the image will be ${COMPOSE_PROJECT_NAME}_wall_e 4 | build: 5 | context: ../../../../ #Saying that all of my source files are at the root path 6 | dockerfile: CI/validate_and_deploy/2_deploy/server_scripts/Dockerfile.wall_e 7 | args: 8 | - ORIGIN_IMAGE 9 | environment: 10 | - basic_config__TOKEN 11 | - BOT_LOG_CHANNEL 12 | - BOT_GENERAL_CHANNEL 13 | - basic_config__BRANCH_NAME 14 | - basic_config__ENVIRONMENT 15 | - basic_config__COMPOSE_PROJECT_NAME 16 | - basic_config__WOLFRAM_API_TOKEN 17 | - POSTGRES_DB_USER 18 | - POSTGRES_DB_DBNAME 19 | - POSTGRES_PASSWORD 20 | - database_config__WALL_E_DB_USER 21 | - database_config__WALL_E_DB_DBNAME 22 | - database_config__WALL_E_DB_PASSWORD 23 | - basic_config__MEE6_AUTHORIZATION 24 | - github__TOKEN 25 | depends_on: # used to ensure that docker wont start wall_e until after it has started the database container 26 | - "db" 27 | image: "${compose_project_name}_wall_e" 28 | container_name: "${COMPOSE_PROJECT_NAME}_wall_e" 29 | networks: 30 | - wall_e_network 31 | db: #declaration of the postgres container 32 | environment: 33 | - POSTGRES_PASSWORD 34 | image: postgres:alpine #using postgres image 35 | container_name: "${COMPOSE_PROJECT_NAME}_wall_e_db" 36 | networks: 37 | - wall_e_network 38 | 39 | networks: 40 | wall_e_network: -------------------------------------------------------------------------------- /CI/validate_and_deploy/2_deploy/server_scripts/wait-for-postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # wait-for-postgres.sh 3 | 4 | # aquired from https://docs.docker.com/compose/startup-order/ 5 | set -e -o xtrace 6 | 7 | host="$1" 8 | shift 9 | cmd="$@" 10 | 11 | until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$host" -U "postgres" -c '\q'; do 12 | >&2 echo "Postgres is unavailable - sleeping" 13 | sleep 1 14 | done 15 | 16 | >&2 echo "Postgres is up - executing command" 17 | 18 | if [[ "${basic_config__ENVIRONMENT}" == "TEST" ]]; then 19 | # setup database 20 | HOME_DIR=`pwd` 21 | rm -r /wall_e || true 22 | git clone https://github.com/CSSS/wall_e.git /wall_e 23 | cd /wall_e/wall_e/ 24 | git checkout HEAD^ # temporarily checkout previous commit 25 | PGPASSWORD=$POSTGRES_PASSWORD psql --set=WALL_E_DB_USER="${database_config__WALL_E_DB_USER}" \ 26 | --set=WALL_E_DB_PASSWORD="${database_config__WALL_E_DB_PASSWORD}" \ 27 | --set=WALL_E_DB_DBNAME="${database_config__WALL_E_DB_DBNAME}" \ 28 | -h "$host" -U "postgres" -f "${HOME_DIR}"/create-database.ddl 29 | python3 django_manage.py migrate 30 | wget https://dev.sfucsss.org/wall_e/fixtures/banrecords.json 31 | wget https://dev.sfucsss.org/wall_e/fixtures/commandstats.json 32 | wget https://dev.sfucsss.org/wall_e/fixtures/levels.json 33 | wget https://dev.sfucsss.org/wall_e/fixtures/profilebucketsinprogress.json 34 | wget https://dev.sfucsss.org/wall_e/fixtures/reminders.json 35 | wget https://dev.sfucsss.org/wall_e/fixtures/userpoints.json 36 | python3 django_manage.py loaddata banrecords.json 37 | python3 django_manage.py loaddata commandstats.json 38 | python3 django_manage.py loaddata levels.json 39 | python3 django_manage.py loaddata profilebucketsinprogress.json 40 | python3 django_manage.py loaddata reminders.json 41 | python3 django_manage.py loaddata userpoints.json 42 | rm banrecords.json commandstats.json levels.json profilebucketsinprogress.json reminders.json userpoints.json 43 | cd "${HOME_DIR}" 44 | rm -r /wall_e || true 45 | fi 46 | 47 | python3 django_manage.py migrate 48 | 49 | exec $cmd 50 | 51 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/2_deploy/user_scripts/Dockerfile.walle.mount: -------------------------------------------------------------------------------- 1 | ARG ORIGIN_IMAGE 2 | 3 | FROM $ORIGIN_IMAGE 4 | 5 | CMD ["./wait-for-postgres.sh", "db", "python", "./main.py" ] 6 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/2_deploy/user_scripts/create-dev-docker-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # PURPOSE: TO BE USED BY THE USER WHEN THEY HAVE MADE CHANGES TO EITHER THE DOCKERFILE.BASE OR REQUIREMENTS FILE 5 | # WHICH MEANS THEY CAN NO LONGE RELY ON THE REPO STORED AT SFUCSSSORG/WALL_E FOR THEIR LOCAL DEV AND NEED TO CREATE THEIR OWN 6 | ## DOCKER WALL_E BASE IMAGE TO WORK OFF OF 7 | 8 | set -e -o xtrace 9 | # https://stackoverflow.com/a/5750463/7734535 10 | 11 | if [ -z "${COMPOSE_PROJECT_NAME}" ]; then 12 | echo "COMPOSE_PROJECT_NAME needs to be set" 13 | exit 1 14 | fi 15 | 16 | export test_base_image_name_lower_case=$(echo "${COMPOSE_PROJECT_NAME}"_wall_e_base | awk '{print tolower($0)}') 17 | export DOCKERFILE="CI/server_scripts/build_wall_e/Dockerfile.wall_e_base" 18 | export CONTAINER_HOME_DIR="/usr/src/app" 19 | 20 | ./CI/validate_and_deploy/2_deploy/destroy-dev-env.sh 21 | 22 | docker image rm -f "${test_base_image_name_lower_case}" || true 23 | docker build --no-cache -t ${test_base_image_name_lower_case} -f "${DOCKERFILE}" --build-arg WALL_E_BASE_ORIGIN_NAME="sfucsssorg/wall_e_python" --build-arg CONTAINER_HOME_DIR="${CONTAINER_HOME_DIR}" . -------------------------------------------------------------------------------- /CI/validate_and_deploy/2_deploy/user_scripts/docker-compose-mount.yml: -------------------------------------------------------------------------------- 1 | version: '3' #docker-compose version 2 | services: #Services that are needed for the wall_e app 3 | wall_e: #the wall_e app, the name will of the image will be ${COMPOSE_PROJECT_NAME}_wall_e 4 | env_file: 5 | - wall_e.env 6 | build: 7 | context: ../../../../ #root path to start at just for the Dockerfile 8 | dockerfile: CI/validate_and_deploy/2_deploy/user_scripts/Dockerfile.walle.mount 9 | args: 10 | - ORIGIN_IMAGE 11 | volumes: #volume are for hot reload 12 | - logs:/usr/src/app/logs 13 | - ../../../../wall_e:/usr/src/app #volumes use the current directory, not the context directory for file paths 14 | depends_on: # used to ensure that docker wont start wall_e until after it has started the database container 15 | - "db" 16 | container_name: "${COMPOSE_PROJECT_NAME}_wall_e" 17 | #needed in order to allow debugging to happen when using CMD ash 18 | #stdin_open: true 19 | #tty: true 20 | db: #declaration of the postgres container 21 | env_file: 22 | - wall_e.env 23 | image: postgres:alpine #using postgres image 24 | container_name: "${COMPOSE_PROJECT_NAME}_wall_e_db" 25 | volumes: 26 | logs: 27 | external: 28 | name: "${COMPOSE_PROJECT_NAME}_logs" 29 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/2_deploy/user_scripts/set_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ENV_FILE="wall_e.env" 4 | 5 | if [ "$#" -eq 1 ] 6 | then 7 | ENV_FILE=$1 8 | fi 9 | 10 | 11 | 12 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 13 | 14 | set -o allexport 15 | source "${DIR}"/"${ENV_FILE}" 16 | set +o allexport 17 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/2_deploy/user_scripts/setup-dev-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # PURPOSE: TO BE USED BY THE USER WHEN THEY WANT TO DEPLOY WALL_E TO THEIR OWN DISCORD GUILD 5 | # WITH THE USE OF A DATABASE 6 | 7 | docker_compose=$(which docker-compose) 8 | if [ -z "${docker_compose}" ]; then 9 | docker_compose="$(which docker) compose" 10 | fi 11 | 12 | 13 | set -e -o xtrace 14 | # https://stackoverflow.com/a/5750463/7734535 15 | 16 | if [ -z "${COMPOSE_PROJECT_NAME}" ]; then 17 | echo "COMPOSE_PROJECT_NAME needs to be set" 18 | exit 1 19 | fi 20 | 21 | if [ -z "${POSTGRES_PASSWORD}" ]; then 22 | echo "POSTGRES_PASSWORD needs to be set" 23 | exit 1 24 | fi 25 | 26 | if [ -z "${ORIGIN_IMAGE}" ]; then 27 | echo "ORIGIN_IMAGE needs to be set" 28 | exit 1 29 | fi 30 | 31 | ./CI/validate_and_deploy/2_deploy/destroy-dev-env.sh 32 | 33 | docker volume create --name="${COMPOSE_PROJECT_NAME}_logs" 34 | ${docker_compose} -f CI/user_scripts/docker-compose-mount.yml up --force-recreate -d 35 | 36 | wall_e_container_name="${COMPOSE_PROJECT_NAME}_wall_e" 37 | wall_e_db_container_name="${COMPOSE_PROJECT_NAME}_wall_e_db" 38 | 39 | while [ "$(docker inspect -f '{{.State.Running}}' ${wall_e_db_container_name})" != "true" ] 40 | do 41 | echo "waiting for wall_e's database to launch" 42 | sleep 1 43 | done 44 | 45 | while [ "$(docker inspect -f '{{.State.Running}}' ${wall_e_container_name})" != "true" ] 46 | do 47 | echo "waiting for wall_e to launch" 48 | sleep 1 49 | done 50 | 51 | echo "wall_e with database succesfully launched!" 52 | -------------------------------------------------------------------------------- /CI/validate_and_deploy/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | options { 4 | disableConcurrentBuilds() 5 | buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '10')) 6 | } 7 | stages { 8 | stage('Validate Formatting') { 9 | steps { 10 | sh(''' 11 | export ENVIRONMENT=TEST; 12 | export COMPOSE_PROJECT_NAME=TEST_${BRANCH_NAME}; 13 | 14 | export POSTGRES_DB_USER=postgres; 15 | export POSTGRES_DB_DBNAME=postgres; 16 | export WALL_E_DB_USER=wall_e; 17 | export WALL_E_DB_DBNAME=csss_discord_db; 18 | 19 | export CONTAINER_HOME_DIR=/usr/src/app; 20 | export CONTAINER_TEST_DIR=\${CONTAINER_HOME_DIR}/tests; 21 | export CONTAINER_SRC_DIR=\${CONTAINER_HOME_DIR}/src; 22 | 23 | export LOCALHOST_SRC_DIR=${WORKSPACE}/wall_e/src/; 24 | export LOCALHOST_TEST_DIR=test_results; 25 | export TEST_RESULT_FILE_NAME=all-unit-tests.xml; 26 | export LOCALHOST_TEST_DIR=${WORKSPACE}/\${LOCALHOST_TEST_DIR}; 27 | 28 | export DOCKER_TEST_IMAGE=\${COMPOSE_PROJECT_NAME}_wall_e_pytest; 29 | export DOCKER_TEST_CONTAINER=\${COMPOSE_PROJECT_NAME}_pytest; 30 | export DISCORD_NOTIFICATION_MESSAGE_FILE=OUTPUT; 31 | 32 | ./CI/validate_and_deploy/1_validate/test_config_files/validate_line_endings.sh; 33 | ./CI/validate_and_deploy/1_validate/validate_formatting.sh; 34 | ''') 35 | } 36 | } 37 | stage('Deploy to PROD Guild') { 38 | when { 39 | branch 'master' 40 | } 41 | steps { 42 | withCredentials([string(credentialsId: 'WALL_E_PROD_DISCORD_BOT_TOKEN', variable: 'WALL_E_PROD_DISCORD_BOT_TOKEN'), 43 | string(credentialsId: 'WOLFRAM_API_TOKEN', variable: 'WOLFRAM_API_TOKEN'), 44 | string(credentialsId: 'MEE6_AUTHORIZATION', variable: 'MEE6_AUTHORIZATION'), 45 | string(credentialsId: 'POSTGRES_PASSWORD', variable: 'POSTGRES_PASSWORD'), 46 | string(credentialsId: 'WALL_E_DB_PASSWORD', variable: 'WALL_E_DB_PASSWORD'), 47 | usernamePassword(credentialsId: 'docker-hub-perms',passwordVariable: 'DOCKER_HUB_PASSWORD',usernameVariable: 'DOCKER_HUB_USER_NAME'), 48 | usernamePassword(credentialsId: 'csss-admin',passwordVariable: 'GITHUB_ACCESS_TOKEN',usernameVariable: 'ignored')]) { 49 | sh(''' 50 | export basic_config__ENVIRONMENT=PRODUCTION; 51 | export basic_config__BRANCH_NAME=${BRANCH_NAME}; 52 | export basic_config__COMPOSE_PROJECT_NAME=PRODUCTION_MASTER; 53 | export COMPOSE_PROJECT_NAME=PRODUCTION_MASTER; 54 | 55 | export CONTAINER_HOME_DIR=/usr/src/app; 56 | export LOCAL_PATH_TO_SRC_DIR=wall_e/src/; 57 | 58 | export DOCKER_HUB_PASSWORD=${DOCKER_HUB_PASSWORD}; 59 | export DOCKER_HUB_USER_NAME=${DOCKER_HUB_USER_NAME}; 60 | 61 | export POSTGRES_DB_USER=postgres; 62 | export POSTGRES_DB_DBNAME=postgres; 63 | export POSTGRES_PASSWORD=${POSTGRES_PASSWORD}; 64 | 65 | export database_config__WALL_E_DB_USER=wall_e; 66 | export database_config__WALL_E_DB_DBNAME=csss_discord_db; 67 | export database_config__WALL_E_DB_PASSWORD=${WALL_E_DB_PASSWORD}; 68 | 69 | export basic_config__WOLFRAM_API_TOKEN=${WOLFRAM_API_TOKEN}; 70 | export basic_config__MEE6_AUTHORIZATION=${MEE6_AUTHORIZATION}; 71 | export basic_config__TOKEN=${WALL_E_PROD_DISCORD_BOT_TOKEN}; 72 | 73 | export github__TOKEN=${GITHUB_ACCESS_TOKEN}; 74 | 75 | export DISCORD_NOTIFICATION_MESSAGE_FILE=OUTPUT; 76 | 77 | ./CI/validate_and_deploy/2_deploy/server_scripts/deploy_to_prod_discord_guild.sh; 78 | ''') 79 | } 80 | } 81 | } 82 | } 83 | post { 84 | always { 85 | script { 86 | if (fileExists('test_results/all-unit-tests.xml')){ 87 | junit 'test_results/all-unit-tests.xml' 88 | } 89 | def summary = '' 90 | if (fileExists('OUTPUT')){ 91 | summary=readFile('OUTPUT').trim() 92 | theTitle = "ISSUE DETECTED" 93 | status = false 94 | }else{ 95 | if (currentBuild.currentResult == "SUCCESS"){ 96 | theTitle = "SUCCESS" 97 | summary = "No issues detected" 98 | status = true 99 | }else{ 100 | theTitle = "ISSUE DETECTED" 101 | summary = "Please look at Jenkins for more info" 102 | status = false 103 | } 104 | } 105 | 106 | withCredentials([string(credentialsId: 'DISCORD_WEBHOOK', variable: 'WEBHOOKURL')]) { 107 | discordSend description: "Branch or PR Name: " + BRANCH_NAME + '\n' + summary, footer: env.GIT_COMMIT, link: env.BUILD_URL, successful: status, title: theTitle, webhookURL: '$WEBHOOKURL' 108 | } 109 | } 110 | cleanWs( 111 | cleanWhenAborted: true, 112 | cleanWhenFailure: true, 113 | cleanWhenNotBuilt: false, 114 | cleanWhenSuccess: true, 115 | cleanWhenUnstable: true, 116 | deleteDirs: true, 117 | disableDeferredWipeout: true 118 | ) 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in the repo. 5 | * @CSSS/wall_e 6 | 7 | # Order is important. The last matching pattern has the most precedence. 8 | # So if a pull request only touches javascript files, only these owners 9 | # will be requested to review. 10 | #*.js @octocat @github/js 11 | 12 | # You can also use email addresses if you prefer. 13 | #docs/* docs@example.com 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | CSSS Discord Bot (Wall-E) 2 | ============================ 3 | 4 | .. image:: https://discord.com/api/guilds/228761314644852736/embed.png 5 | :target: http://discord.sfucsss.org 6 | :alt: Discord server invite 7 | 8 | - `Documentation `_ 9 | 10 | .. image:: wall_e_pic.jpg 11 | :alt: The One and Only, Lovable Wall-E 12 | 13 | Wall-E, named after the lovable character `Wall-E `_, is the CSSS Discord Bot. This bot is owned by the CSSS and will be maintained by the Bot_manager team. 14 | 15 | What commands do we have? You can find this out by logging onto the `discord server `_ and run the command :code:`.help` to view the text commands or entering :code:`/` to view the slash comand 16 | -------------------------------------------------------------------------------- /Test_Cases.md: -------------------------------------------------------------------------------- 1 | # Test Cases 2 | 3 | ## Administration 4 | 1. `/delete_log_channels` 5 | 1. `/purge_messages` 6 | ![](.documentation_pictures/test_cases/purge_messages.png) 7 | 1. `.sync` 8 | ![](.documentation_pictures/test_cases/.sync.png) 9 | 1. `.announce "message"` 10 | ![](.documentation_pictures/test_cases/.announce%20input.png) 11 | Result: 12 | ![](.documentation_pictures/test_cases/.announce%20result.png) 13 | 1. `.announce "message" ` 14 | ![](.documentation_pictures/test_cases/.announce%20follow-up%20input.png) 15 | Result: 16 | ![](.documentation_pictures/test_cases/.announce%20follow-up%20result.png) 17 | 1. `/unload reminders` 18 | ![](.documentation_pictures/test_cases/.unload%20reminders.png) 19 | 1. `/unload nothing` 20 | ![](.documentation_pictures/test_cases/.unload%20nothing.png) 21 | 1. `/load reminders` 22 | ![](.documentation_pictures/test_cases/.load%20reminders.png) 23 | 1. `/load nothing` 24 | ![](.documentation_pictures/test_cases/.load%20nothing.png) 25 | 1. `/reload reminders` 26 | ![](.documentation_pictures/test_cases/.reload%20reminders.png) 27 | 1. `/reload nothing` 28 | ![](.documentation_pictures/test_cases/.reload%20nothing.png) 29 | 1. `.exc ls -l` 30 | ![](.documentation_pictures/test_cases/.exc%20ls%20-l.png) 31 | 1. `.frequency` 32 | ![](.documentation_pictures/test_cases/.frequency.png) 33 | 1. `.frequency command` 34 | ![](.documentation_pictures/test_cases/.frequency%20command.png) 35 | ## Ban 36 | 1. `/convertbans` 37 | 1. `/ban @user reason for my ban` 38 | 1. `/ban @user "reason for my ban"` 39 | 1. `/unban ` 40 | 1. `/unban ` 41 | 1. `/unban ` 42 | 1. `/bans` 43 | 1. `/purgebans` 44 | ## Frosh 45 | 1. `.team` 46 | 1. `.team "JL" "Super Tag" "Jon, Bruce, Clark, Diana, Barry"` 47 | 1. `.team "team 1337" "PacMacro" "Jeffrey, Harry, Noble, Ali" "#E8C100"` 48 | 1. `.team "Z fighters" "Cell Games" "Goku, Vegeta, Uub, Beerus" "4CD100"` 49 | 1. `.team "spaces #1" "musical voice channels" "Billy, Bob, Megan, Cary" "notAHexCode"` 50 | 1. `.reportwin` 51 | 1. `.reportwin "team 1337" "Jeffrey, Harry, Noble, Ali"` 52 | ## HealthChecks 53 | 1. `/ping` 54 | ![](.documentation_pictures/test_cases/ping.png) 55 | 1. `/echo this is the test case` 56 | ![](.documentation_pictures/test_cases/echo%20this%20is%20the%20test%20case.png) 57 | 1. `/echo "this is the test case"` 58 | ![](.documentation_pictures/test_cases/echo%20%22this%20is%20the%20test%20case%22.png) 59 | 1. `/echo 'this is the test case'` 60 | ![](.documentation_pictures/test_cases/echo%20'this%20is%20the%20test%20case'.png) 61 | ## Help 62 | 1. `.help` 63 | ![](.documentation_pictures/test_cases/.help.png) 64 | 1. `.help here` 65 | ![](.documentation_pictures/test_cases/.help%20here.png) 66 | 1. `.help nothing` 67 | ![](.documentation_pictures/test_cases/.help%20nothing.png) 68 | ## Here 69 | 1. `.here` 70 | ![](.documentation_pictures/test_cases/.here.png) 71 | 1. `.here wall` 72 | ![](.documentation_pictures/test_cases/.here%20wall.png) 73 | ## Leveling 74 | 1. `.set_level_name 1 XP_level_1` 75 | > if role `XP_level_1` exists 76 | 77 | ![](.documentation_pictures/test_cases/.set_level_name%201%20XP_level_1.png) 78 | 1. `.set_level_name 1 XP` 79 | > where role `XP` does not exist 80 | 81 | ![](.documentation_pictures/test_cases/.set_level_name%201%20XP.png) 82 | 1. `.remove_level_name 1` 83 | > when level 1 has a role 84 | 85 | ![](.documentation_pictures/test_cases/.a.remove_level_name%201.png) 86 | 1. `.remove_level_name 1` 87 | > when level 1 does not have a role 88 | 89 | ![](.documentation_pictures/test_cases/.b.remove_level_name%201.png) 90 | 1. `.rank` 91 | ![](.documentation_pictures/test_cases/.rank.png) 92 | 3. `.rank @other_user` 93 | ![](.documentation_pictures/test_cases/.rank%20%40Micah.png) 94 | 1. `.levels` 95 | ![](.documentation_pictures/test_cases/.levels.png) 96 | 1. `.ranks` 97 | ![](.documentation_pictures/test_cases/.ranks.png) 98 | 1. `.hide_xp` 99 | ![](.documentation_pictures/test_cases/.a.hide_xp.png) 100 | 1. `.hide_xp` 101 | > when your XP is already hidden 102 | 103 | ![](.documentation_pictures/test_cases/.hide_xp.png) 104 | 1. `.show_xp` 105 | ![](.documentation_pictures/test_cases/.b.show_xp.png) 106 | 1. `.show_xp` 107 | > when your XP is already visible 108 | 109 | ![](.documentation_pictures/test_cases/.show_xp.png) 110 | ## Misc 111 | 1. `.poll avengers?` 112 | ![](.documentation_pictures/test_cases/.poll%20avengers%3F.png) 113 | 1. `.poll` 114 | ![](.documentation_pictures/test_cases/.poll.png) 115 | 1. `.poll “go to the moon?” “yes” “no” “boye you crazy??”` 116 | ![](.documentation_pictures/test_cases/.poll%20%22go%20to%20the%20moon%3F%22%20%22yes%22%20%22no%22%20%22boye%20you%20crazy%3F%3F%22.png) 117 | 1. `.poll 1 2 3 4 5 6 7 8 9 10 11 12 13` 118 | ![](.documentation_pictures/test_cases/.poll%201%202%203%204%205%206%207%208%209%2010%2011%2012%2013.png) 119 | 1. `.urban girl` 120 | ![](.documentation_pictures/test_cases/.urban%20girl.png) 121 | 1. `.urban DevelopersDevelopersDevelopers` 122 | ![](.documentation_pictures/test_cases/.urban%20DevelopersDevelopersDevelopers.png) 123 | 1. `.wolfram Marvel` 124 | ![](.documentation_pictures/test_cases/.wolfram%20Marvel.png) 125 | 1. `.wolfram giberasdfasdfadfasdf` 126 | ![](.documentation_pictures/test_cases/.wolfram%20giberasdfasdfadfasdf.png) 127 | 1. `.emojispeak` 128 | ![](.documentation_pictures/test_cases/.emojispeak.png) 129 | 1. `.emojispeak 1234_abcd` 130 | ![](.documentation_pictures/test_cases/.emojispeak%201234_abcd.png) 131 | 1. `/tex e^{i\theta} = \cos x + i \sin x.png` 132 | ![](.documentation_pictures/test_cases/tex%20e%5E%7Bi%5Ctheta%7D%20%3D%20%5Ccos%20x%20%2B%20i%20%5Csin%20x.png) 133 | ## Mod 134 | 1. `.em` 135 | 1. `.em "description" "title" "field"` 136 | ![](.documentation_pictures/test_cases/.em%20%22description%22%20%22title%22%20%22field%22.png) 137 | 1. `.em "title" "field"` 138 | ![](.documentation_pictures/test_cases/.em%20%22title%22%20%22field%22.png) 139 | 1. `.warn` 140 | 1. `.warn behold my mod powers and be scarred` 141 | ![](.documentation_pictures/test_cases/.warn%20behold%20my%20mod%20powers%20and%20be%20scarred.png) 142 | ## Reminders 143 | 1. `.remindmein` 144 | ![](.documentation_pictures/test_cases/.remindmein.png) 145 | 1. `.remindmein 10 seconds to turn in my assignment` 146 | ![](.documentation_pictures/test_cases/a.remindmein%2010%20seconds%20to%20turn%20in%20my%20assignment.png) 147 | 1. *wait 10 seconds* 148 | ![](.documentation_pictures/test_cases/b.remindmein%2010%20seconds%20to%20turn%20in%20my%20assignment.png) 149 | 1. `.remindmein 10 minutes to turn in my assignment` 150 | ![](.documentation_pictures/test_cases/.remindmein%2010%20minutes%20to%20turn%20in%20my%20assignment.png) 151 | 1. `.showreminders` 152 | ![](.documentation_pictures/test_cases/a.showreminders.png) 153 | 1. `.deletereminder ` 154 | ![](.documentation_pictures/test_cases/.deletereminder%208.png) 155 | 1. `.showreminders` 156 | ![](.documentation_pictures/test_cases/b.showreminders.png) 157 | 1. `.remindmeon to turn in my assignment` 158 | ![](.documentation_pictures/test_cases/.remindmeon%20Oct%204%20at%206%3A23%20am%20to%20turn%20in%20my%20assignment.png) 159 | 1. `.remindmeat tomorrow at 5:00pm Canada/Eastern to turn in my assignment` 160 | ![](.documentation_pictures/test_cases/.remindmeat%20tomorrow%20at%205%3A00pm%20Canada%7CEastern%20to%20turn%20in%20my%20assignment.png) 161 | 1. `.remindmein a day after tomorrow to turn in my assignment` 162 | ![](.documentation_pictures/test_cases/.remindmein%20a%20day%20after%20tomorrow%20to%20turn%20in%20my%20assignment.png) 163 | 1. `.showreminders` 164 | ![](.documentation_pictures/test_cases/.showreminders.png) 165 | ## RoleCommands 166 | 1. `/newrole ` 167 | ![](.documentation_pictures/test_cases/newrole%20hello.png) 168 | 1. `/newrole ` 169 | ![](.documentation_pictures/test_cases/newrole%20hello_5.png) 170 | 1. `/iam ` 171 | ![](.documentation_pictures/test_cases/iam%201159103657120387167.png) 172 | 1. `/iamn ` 173 | ![](.documentation_pictures/test_cases/iamn%201159103657120387167.png) 174 | 1. `/deleterole ` 175 | ![](.documentation_pictures/test_cases/deleterole%201158444206990299208.png) 176 | 1. `/whois ` 177 | ![](.documentation_pictures/test_cases/whois%201007425263879069736.png) 178 | 1. `/roles_assignable` 179 | ![](.documentation_pictures/test_cases/roles_assignable.png) 180 | 1. `/roles` 181 | ![](.documentation_pictures/test_cases/roles.png) 182 | 1. `/purgeroles` 183 | ![](.documentation_pictures/test_cases/purgeroles.png) 184 | ## SFU 185 | 1. `.sfu cmpt 300` 186 | ![](.documentation_pictures/test_cases/.sfu%20cmpt%20300.png) 187 | 1. `.sfu cmpt300` 188 | ![](.documentation_pictures/test_cases/.sfu%20cmpt300.png) 189 | 1. `.sfu cmpt666` 190 | ![](.documentation_pictures/test_cases/.sfu%20cmpt666.png) 191 | 1. `.sfu blah` 192 | ![](.documentation_pictures/test_cases/.sfu%20blah.png) 193 | 1. `.sfu` 194 | ![](.documentation_pictures/test_cases/.sfu.png) 195 | 1. `.outline cmpt300` 196 | ![](.documentation_pictures/test_cases/.outline%20cmpt300.png) 197 | 1. `.outline cmpt 300` 198 | ![](.documentation_pictures/test_cases/.outline%20cmpt%20300.png) 199 | 1. `.outline cmpt300 spring d200` 200 | ![](.documentation_pictures/test_cases/.outline%20cmpt300%20spring%20d200.png) 201 | 1. `.outline cmpt 300 spring d200` 202 | ![](.documentation_pictures/test_cases/.outline%20cmpt%20300%20spring%20d200.png) 203 | 1. `.outline cmpt300 next` 204 | ![](.documentation_pictures/test_cases/.outline%20cmpt300%20next.png) 205 | 1. `.outline cmpt300 d200 next` 206 | ![](.documentation_pictures/test_cases/.outline%20cmpt300%20d200%20next.png) 207 | 1. `.outline cmpt300 summer d200 next` 208 | ![](.documentation_pictures/test_cases/.outline%20cmpt300%20summer%20d200%20next.png) 209 | 1. `.outline cmpt666` 210 | ![](.documentation_pictures/test_cases/.outline%20cmpt666.png) 211 | 1. `.outline blah` 212 | ![](.documentation_pictures/test_cases/.outline%20blah.png) 213 | 1. `.outline` 214 | ![](.documentation_pictures/test_cases/.outline.png) 215 | 1. `/courses` 216 | ![](.documentation_pictures/test_cases/courses.png) 217 | 1. `/courses department:` 218 | ![](.documentation_pictures/test_cases/courses_department_math.png) 219 | 1. `/courses department:` 220 | ![](.documentation_pictures/test_cases/courses_department_blah.png) 221 | 1. `/courses level:` 222 | ![](.documentation_pictures/test_cases/courses_level_200.png) 223 | 1. `/courses level:` 224 | ![](.documentation_pictures/test_cases/courses_level_1.png) 225 | 1. `/courses term: year:` 226 | ![](.documentation_pictures/test_cases/courses_term_and_year_set.png) 227 | 1. `/courses term: year:` 228 | ![](.documentation_pictures/test_cases/courses_term_blah.png) 229 | 1. `/courses term: year:` OR `/courses term: year:` 230 | ![](.documentation_pictures/test_cases/courses_term_or_year_unset.png) 231 | 1. `/courses department: level:` 232 | ![](.documentation_pictures/test_cases/courses_department_math_level_200.png) 233 | 1. `/courses department: term: year:` 234 | ![](.documentation_pictures/test_cases/courses_department_phys_term_summer_year_2020.png) 235 | 1. `/courses level: term: year:` 236 | ![](.documentation_pictures/test_cases/courses_level_300_term_spring_year_2021.png) 237 | 1. `/courses department: level: term: year:` 238 | ![](.documentation_pictures/test_cases/courses_department_stat_level_300_term_fall_year_2024.png) 239 | -------------------------------------------------------------------------------- /download_repo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "Please enter the HTTPS clone URL for your forked repo" 6 | read forked_repo_https_clone_url 7 | while [ "${forked_repo_https_clone_url}" == "https://github.com/CSSS/wall_e.git" ]; 8 | do 9 | echo "This is not a forked REPO url...Please enter the HTTPS clone URL for your forked repo" 10 | read forked_repo_https_clone_url 11 | done 12 | 13 | git clone --recurse-submodules "${forked_repo_https_clone_url}" 14 | cd wall_e/ 15 | ./run_walle.sh 16 | -------------------------------------------------------------------------------- /run_walle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o xtrace 4 | # https://stackoverflow.com/a/5750463/7734535 5 | 6 | if [ -z "${VIRTUAL_ENV}" ]; then 7 | echo "please active a python virtual environment before using this script" 8 | exit 1 9 | fi 10 | 11 | # need to delete and re-create so that the if statement that tries to detected if the 12 | # help menu was invoked can work correctly 13 | if [ -f "CI/validate_and_deploy/2_deploy/user_scripts/run_wall_e.env" ]; then 14 | cat ./CI/validate_and_deploy/2_deploy/user_scripts/run_wall_e.env | grep -v HELP_SELECTED > ./CI/validate_and_deploy/2_deploy/user_scripts/run_wall_e.env.2 15 | mv ./CI/validate_and_deploy/2_deploy/user_scripts/run_wall_e.env.2 ./CI/validate_and_deploy/2_deploy/user_scripts/run_wall_e.env 16 | fi 17 | 18 | ./.run_walle.py $@ 19 | 20 | while [ "$#" -gt 0 ] 21 | do 22 | shift 23 | done 24 | 25 | . ./CI/validate_and_deploy/2_deploy/user_scripts/set_env.sh 26 | . ./CI/validate_and_deploy/2_deploy/user_scripts/set_env.sh run_wall_e.env 27 | 28 | if [ -z "${HELP_SELECTED}" ]; then 29 | exit 0 30 | fi 31 | 32 | if [[ "${basic_config__DOCKERIZED}" == "1" ]]; then 33 | export COMPOSE_PROJECT_NAME="${basic_config__COMPOSE_PROJECT_NAME}" 34 | ./CI/validate_and_deploy/2_deploy/user_scripts/setup-dev-env.sh 35 | docker logs -f "${COMPOSE_PROJECT_NAME}_wall_e" 36 | else 37 | pushd wall_e 38 | 39 | if [[ "${INSTALL_REQUIREMENTS}" == "True" ]]; then 40 | rm layer-1-requirements.txt layer-2-requirements.txt || true 41 | wget https://raw.githubusercontent.com/CSSS/wall_e_python_base/master/layer-1-requirements.txt 42 | wget https://raw.githubusercontent.com/CSSS/wall_e_python_base/master/layer-2-requirements.txt 43 | python3 -m pip install -r layer-1-requirements.txt 44 | python3 -m pip install -r layer-2-requirements.txt 45 | rm layer-1-requirements.txt layer-2-requirements.txt 46 | python3 -m pip install -r requirements.txt 47 | python3 -m pip install -r ../.wall_e_models/requirements.txt 48 | fi 49 | 50 | if [[ "${SETUP_DATABASE}" == "True" ]]; then 51 | if [[ "${database_config__TYPE}" == "sqlite3" ]]; then 52 | rm ../db.sqlite3 || true 53 | else 54 | dpkg -s postgresql-client &> /dev/null 55 | if [[ $? -eq 1 ]]; 56 | then 57 | sudo apt-get install postgresql-client 58 | fi 59 | docker rm -f "${basic_config__COMPOSE_PROJECT_NAME}_wall_e_db" 60 | sleep 4 61 | docker run -d --env POSTGRES_PASSWORD=${POSTGRES_PASSWORD} -p \ 62 | "${database_config__DB_PORT}":5432 --name "${basic_config__COMPOSE_PROJECT_NAME}_wall_e_db" \ 63 | postgres:alpine 64 | sleep 4 65 | PGPASSWORD=$POSTGRES_PASSWORD psql --set=WALL_E_DB_USER="${database_config__WALL_E_DB_USER}" \ 66 | --set=WALL_E_DB_PASSWORD="${database_config__WALL_E_DB_PASSWORD}" \ 67 | --set=WALL_E_DB_DBNAME="${database_config__WALL_E_DB_DBNAME}" \ 68 | -h "${database_config__HOST}" -p "${database_config__DB_PORT}" -U "postgres" \ 69 | -f ../CI/validate_and_deploy/2_deploy/create-database.ddl 70 | fi 71 | python3 django_manage.py migrate 72 | rm banrecords.json commandstats.json levels.json profilebucketsinprogress.json reminders.json userpoints.json || true 73 | wget -r --no-parent -nd https://dev.sfucsss.org/wall_e/fixtures/ -A 'json' 74 | python3 django_manage.py loaddata banrecords.json 75 | python3 django_manage.py loaddata commandstats.json 76 | python3 django_manage.py loaddata levels.json 77 | python3 django_manage.py loaddata profilebucketsinprogress.json 78 | python3 django_manage.py loaddata reminders.json 79 | python3 django_manage.py loaddata userpoints.json 80 | rm banrecords.json commandstats.json levels.json profilebucketsinprogress.json reminders.json userpoints.json || true 81 | fi 82 | popd 83 | 84 | fi 85 | if [[ "${LAUNCH_WALL_E}" == "True" ]]; then 86 | echo "Launching the wall_e." 87 | sleep 3 88 | cd wall_e 89 | python3 main.py 90 | fi 91 | -------------------------------------------------------------------------------- /wall_e/create-database.ddl: -------------------------------------------------------------------------------- 1 | ../CI/create-database.ddl -------------------------------------------------------------------------------- /wall_e/django_manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django # noqa 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /wall_e/django_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from utilities.config.config import WallEConfig 4 | 5 | ENVIRONMENT = os.environ['basic_config__ENVIRONMENT'] 6 | wall_e_config = WallEConfig(ENVIRONMENT, wall_e=False) 7 | database_type = wall_e_config.get_config_value("database_config", "TYPE") 8 | 9 | if database_type == "postgreSQL": 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.postgresql', 13 | 'NAME': wall_e_config.get_config_value("database_config", "WALL_E_DB_DBNAME"), 14 | 'USER': wall_e_config.get_config_value("database_config", "WALL_E_DB_USER"), 15 | 'PASSWORD': wall_e_config.get_config_value("database_config", "WALL_E_DB_PASSWORD") 16 | } 17 | } 18 | 19 | if wall_e_config.enabled("basic_config", "DOCKERIZED"): 20 | DATABASES['default']['HOST'] = ( 21 | f'{wall_e_config.get_config_value("basic_config", "COMPOSE_PROJECT_NAME")}_wall_e_db' 22 | ) 23 | else: 24 | DATABASES['default']['PORT'] = wall_e_config.get_config_value("database_config", "DB_PORT") 25 | DATABASES['default']['HOST'] = wall_e_config.get_config_value("database_config", "HOST") 26 | 27 | else: 28 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 29 | DATABASES = { 30 | 'default': { 31 | 'ENGINE': 'django.db.backends.sqlite3', 32 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 33 | } 34 | } 35 | 36 | INSTALLED_APPS = ( 37 | 'wall_e_models', 38 | ) 39 | 40 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 41 | 42 | TIME_ZONE = 'Canada/Pacific' 43 | 44 | USE_TZ = True 45 | 46 | # Write a random secret key here 47 | SECRET_KEY = '4e&6aw+(5&cg^_!05r(&7_#dghg_pdgopq(yk)xa^bog7j)^*j' 48 | -------------------------------------------------------------------------------- /wall_e/extensions/custom_commands.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from discord.ext import commands 4 | 5 | 6 | class CustomCommands(commands.Cog): 7 | 8 | def __init__(self): 9 | pass 10 | 11 | @commands.command() 12 | async def cmpt276(self, ctx): 13 | await ctx.send("NOT LIKE THIS NAZ, NOT LIKE THIS") 14 | 15 | @commands.command() 16 | async def cmpt361(self, ctx): 17 | await ctx.send("CAN'T HELP YOU TOO DAMN HARD") 18 | 19 | @commands.command() 20 | async def cmpt376(self, ctx): 21 | await ctx.send("HOW DARE YOU CALL UPON ME, REWRITE THAT COMMAND FILTHY PEASANT") 22 | 23 | @commands.command() 24 | async def f(self, ctx): 25 | await ctx.send("<:F_Eggplant:313248902021120000> <:F_Eggplant:313248902021120000> " 26 | "<:F_Eggplant:313248902021120000> <:F_Eggplant:313248902021120000>\n" 27 | "<:F_Eggplant:313248902021120000>\n<:F_Eggplant:313248902021120000> " 28 | "<:F_Eggplant:313248902021120000> <:F_Eggplant:313248902021120000>\n" 29 | "<:F_Eggplant:313248902021120000>\n<:F_Eggplant:313248902021120000>" 30 | ) 31 | 32 | @commands.command() 33 | async def gnu(self, ctx): 34 | await ctx.send( 35 | "```I'd just like to interject for moment. What you're refering to as Linux, is in fact, GNU/Linux, or " 36 | "as I've recently taken to calling it, GNU plus Linux. Linux is not an operating system unto itself, but " 37 | "rather another free component of a fully functioning GNU system made useful by the GNU corelibs, shell " 38 | "utilities and vital system components comprising a full OS as defined by POSIX.\n\nMany computer users " 39 | "run a modified version of the GNU system every day, without realizing it. Through a peculiar turn of " 40 | "events, the version of GNU which is widely used today is often called Linux, and many of its users are " 41 | "not aware that it is basically the GNU system, developed by the GNU Project. \n\nThere really is a " 42 | "Linux, and these people are using it, but it is just a part of the system they use. Linux is the " 43 | "kernel: the program in the system that allocates the machine's resources to the other programs that " 44 | "you run. The kernel is an essential part of an operating system, but useless by itself; it can only " 45 | "function in the context of a complete operating system. Linux is normally used in combination with the " 46 | "GNU operating system: the whole system is basically GNU with Linux added, or GNU/Linux. All the " 47 | "so-called Linux distributions are really distributions of GNU/Linux!```" 48 | ) 49 | 50 | @commands.command() 51 | async def impeach(self, ctx): 52 | await ctx.send( 53 | "https://theawesomedaily.com/wp-content/uploads/2018/06/you-have-no-power-here-meme-feat-good-1.jpg") 54 | 55 | @commands.command() 56 | async def macm101(self, ctx): 57 | await ctx.send("Easy GPA booster, ~~study hard~~ no studying needed.") 58 | 59 | @commands.command() 60 | async def macm316(self, ctx): 61 | await ctx.send("TEARS OF SALT") 62 | 63 | @commands.command() 64 | async def math150(self, ctx): 65 | await ctx.send("like 151 but on steroids") 66 | 67 | @commands.command() 68 | async def math152(self, ctx): 69 | await ctx.send("makes you wish you failed calc 1") 70 | 71 | @commands.command() 72 | async def medipack(self, ctx): 73 | await ctx.send("WHO IS HE?!?!?!?") 74 | 75 | @commands.command() 76 | async def monty(self, ctx): 77 | await ctx.send( 78 | "RIP Monty Oum.\n`\"I believe that the human spirit is indomitable. If you endeavor to achieve, it will " 79 | "happen given enough resolve. It may not be immediate, and often your greater dreams is something you " 80 | "will not achieve within your own lifetime. The effort you put forth to anything transcends yourself, " 81 | "for there is no futility even in death.\"`" 82 | ) 83 | 84 | @commands.command() 85 | async def prettygood(self, ctx): 86 | await ctx.send("https://pm1.narvii.com/6455/cfc754b99032891e8fce5ef346ddd96c828bf6be_hq.jpg") 87 | 88 | @commands.command() 89 | async def psyduck(self, ctx): 90 | num = random.randint(0, 2) 91 | if num == 0: 92 | await ctx.send("https://media.giphy.com/media/fxIiymxLITF0QMZXWH/giphy.gif") 93 | elif num == 1: 94 | await ctx.send("https://media.giphy.com/media/xTv6kG7GUXfj2/giphy.gif ") 95 | elif num == 2: 96 | await ctx.send("https://tenor.com/view/pokemon-trip-nintendo-psy-duck-camera-gif-5709088") 97 | 98 | @commands.command() 99 | async def thebest(self, ctx): 100 | await ctx.send("404: Best not found.") 101 | 102 | 103 | async def setup(bot): 104 | await bot.add_cog(CustomCommands()) 105 | -------------------------------------------------------------------------------- /wall_e/extensions/frosh.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from discord.ext import commands 4 | 5 | from utilities.global_vars import bot, wall_e_config 6 | 7 | from utilities.embed import embed as em, WallEColour 8 | from utilities.file_uploading import start_file_uploading 9 | from utilities.setup_logger import Loggers 10 | 11 | 12 | class Frosh(commands.Cog): 13 | 14 | def __init__(self): 15 | log_info = Loggers.get_logger(logger_name="Frosh") 16 | self.logger = log_info[0] 17 | self.debug_log_file_absolute_path = log_info[1] 18 | self.error_log_file_absolute_path = log_info[2] 19 | self.logger.info("[Frosh __init__()] initializing Frosh") 20 | self.guild = None 21 | 22 | @commands.Cog.listener(name="on_ready") 23 | async def get_guild(self): 24 | self.guild = bot.guilds[0] 25 | 26 | @commands.Cog.listener(name="on_ready") 27 | async def upload_debug_logs(self): 28 | while self.guild is None: 29 | await asyncio.sleep(2) 30 | await start_file_uploading( 31 | self.logger, self.guild, bot, wall_e_config, self.debug_log_file_absolute_path, "frosh_debug" 32 | ) 33 | 34 | @commands.Cog.listener(name="on_ready") 35 | async def upload_error_logs(self): 36 | while self.guild is None: 37 | await asyncio.sleep(2) 38 | await start_file_uploading( 39 | self.logger, self.guild, bot, wall_e_config, self.error_log_file_absolute_path, "frosh_error" 40 | ) 41 | 42 | @commands.command( 43 | brief="Creates an embed that holds details about your Frosh game team.", 44 | help=( 45 | 'Need help picking a colour?\n[HTML Colour Codes](https://htmlcolorcodes.com/color-picker/)\n\n' 46 | 'Arguments:\n' 47 | '---team name: the name for the team\n' 48 | '---game name: the name of the game\n' 49 | '---team member names: a comma separate list of the team names' 50 | '---[colour]: the hex code/value for embed colour\n\n' 51 | 'Examples:\n' 52 | '---.team "JL" "Super Tag" "Jon, Bruce, Clark, Diana, Barry"\n' 53 | '---.team "team 1337" "PacMacro" "Jeffrey, Harry, Noble, Ali" "#E8C100"\n' 54 | '---.team "Z fighters" "Cell Games" "Goku, Vegeta, Uub, Beerus" "4CD100"\n\n' 55 | ), 56 | usage='"team name "game name" "team member names" [hex color]', 57 | aliases=["team"] 58 | ) 59 | async def froshteam(self, ctx, *info): 60 | self.logger.info( 61 | f'[Frosh froshteam()] team command detected from user {ctx.author} with arguments: {info}' 62 | ) 63 | 64 | if len(info) < 3: 65 | e_obj = await em( 66 | self.logger, 67 | ctx=ctx, 68 | title='Missing Arguments', 69 | author=ctx.me, 70 | colour=WallEColour.ERROR, 71 | content=[('Error', 'You are missing arguments. Call `.help team` for how to use the command')], 72 | footer_text='Team Error' 73 | ) 74 | await ctx.send(embed=e_obj) 75 | self.logger.debug('[Frosh froshteam()] Missing arguments, command ended') 76 | return 77 | 78 | # just gonna assume the provided stuff is all good 79 | e_obj = await em( 80 | self.logger, 81 | ctx=ctx, 82 | title='CSSS Frosh 2020 Gaming Arena', 83 | author=ctx.author, 84 | content=[ 85 | ('Team Name', info[0]), 86 | ('Game', info[1]), 87 | ('Contact', ctx.author.mention), 88 | ('Team Members', '\n'.join(list(map(lambda str: str.strip(), info[2].split(','))))) 89 | ], 90 | footer_text='Frosh 2020' 91 | ) 92 | 93 | try: 94 | if len(info) >= 4: 95 | color = info[3] 96 | if color[0] == '#': 97 | color = color[1:] 98 | e_obj.colour = int('0x' + color, base=16) 99 | except Exception: 100 | pass 101 | 102 | self.logger.debug(f'[Frosh froshteam()] team embed created with the following fields: {e_obj.fields}') 103 | 104 | await ctx.send(embed=e_obj) 105 | 106 | @commands.command( 107 | brief="Creates an embed that report a win for your team.", 108 | help=( 109 | 'Arguments:\n' 110 | '---team name: the name for the team\n' 111 | '---team member names: a comma separate list of the team names\n\n' 112 | 'Examples:\n' 113 | '---.reportwin "team 1337" "Jeffrey, Harry, Noble, Ali"' 114 | ), 115 | usage='"team name "team member names"', 116 | ) 117 | async def reportwin(self, ctx, *info): 118 | self.logger.info(f'[Frosh reportwin()] team command detected from user {ctx.author} with arguments: {info}') 119 | if len(info) < 2: 120 | e_obj = await em( 121 | self.logger, 122 | ctx=ctx, 123 | title='Missing Arguments', 124 | author=ctx.me, 125 | colour=WallEColour.ERROR, 126 | content=[('Error', 'You are missing arguments. Call `.help reportwin` for how to use the command')], 127 | footer_text='ReportWin Error' 128 | ) 129 | await ctx.send(embed=e_obj) 130 | self.logger.debug('[Frosh reportwin()] Missing arguments, command ended') 131 | return 132 | 133 | e_obj = await em( 134 | self.logger, 135 | ctx=ctx, 136 | title='CSSS Frosh 2020 Gaming Arena Winner', 137 | author=ctx.author, 138 | colour=WallEColour.FROSH_2020_THEME, 139 | content=[ 140 | ('Team Name', info[0]), 141 | ('Team Members', '\n'.join(list(map(lambda str: str.strip(), info[1].split(','))))) 142 | ], 143 | footer_text='Frosh 2020' 144 | ) 145 | 146 | self.logger.debug(f'[Frosh reportwin()] winner announcement embed made with following fields: {e_obj.fields}') 147 | await ctx.send(embed=e_obj) 148 | 149 | 150 | async def setup(bot): 151 | await bot.add_cog(Frosh()) 152 | -------------------------------------------------------------------------------- /wall_e/extensions/health_checks.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import discord 4 | from discord import app_commands 5 | from discord.ext import commands 6 | 7 | from utilities.global_vars import bot, wall_e_config 8 | 9 | from utilities.embed import embed 10 | from utilities.file_uploading import start_file_uploading 11 | from utilities.setup_logger import Loggers 12 | 13 | 14 | class HealthChecks(commands.Cog): 15 | 16 | def __init__(self): 17 | log_info = Loggers.get_logger(logger_name="HealthChecks") 18 | self.logger = log_info[0] 19 | self.debug_log_file_absolute_path = log_info[1] 20 | self.warn_log_file_absolute_path = log_info[2] 21 | self.error_log_file_absolute_path = log_info[3] 22 | self.logger.info("[HealthChecks __init__()] initializing HealthChecks") 23 | self.guild = None 24 | 25 | @commands.Cog.listener(name="on_ready") 26 | async def get_guild(self): 27 | self.guild = bot.guilds[0] 28 | 29 | @commands.Cog.listener(name="on_ready") 30 | async def upload_debug_logs(self): 31 | while self.guild is None: 32 | await asyncio.sleep(2) 33 | await start_file_uploading( 34 | self.logger, self.guild, bot, wall_e_config, self.debug_log_file_absolute_path, 35 | "health_checks_debug" 36 | ) 37 | 38 | @commands.Cog.listener(name="on_ready") 39 | async def upload_warn_logs(self): 40 | while self.guild is None: 41 | await asyncio.sleep(2) 42 | await start_file_uploading( 43 | self.logger, self.guild, bot, wall_e_config, self.warn_log_file_absolute_path, "health_checks_warn" 44 | ) 45 | 46 | @commands.Cog.listener(name="on_ready") 47 | async def upload_error_logs(self): 48 | while self.guild is None: 49 | await asyncio.sleep(2) 50 | await start_file_uploading( 51 | self.logger, self.guild, bot, wall_e_config, self.error_log_file_absolute_path, 52 | "health_checks_error" 53 | ) 54 | 55 | @app_commands.command(name="ping", description="return pong!") 56 | @app_commands.checks.has_role("Bot_manager") 57 | async def ping(self, interaction: discord.Interaction): 58 | self.logger.info(f"[HealthChecks ping()] ping command detected from {interaction.user}") 59 | e_obj = await embed( 60 | self.logger, 61 | interaction=interaction, 62 | description='Pong!', 63 | author=interaction.client.user, 64 | ) 65 | if e_obj is not False: 66 | await interaction.response.send_message(embed=e_obj) 67 | 68 | @app_commands.command(name="echo", description="repeats what the user said back at them") 69 | @app_commands.describe(string="string to echo") 70 | async def echo(self, interaction: discord.Interaction, string: str): 71 | self.logger.info( 72 | f"[HealthChecks echo()] echo command detected from {interaction.user} with argument {string}" 73 | ) 74 | e_obj = await embed( 75 | self.logger, interaction=interaction, author=interaction.user, 76 | description=string 77 | ) 78 | if e_obj is not False: 79 | await interaction.response.send_message(embed=e_obj) 80 | 81 | 82 | async def setup(bot): 83 | await bot.add_cog(HealthChecks()) 84 | -------------------------------------------------------------------------------- /wall_e/extensions/help_commands.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from wall_e_models.models import HelpMessage 4 | from utilities.embed import WallEColour, COLOUR_MAPPING 5 | 6 | 7 | class EmbedHelpCommand(commands.DefaultHelpCommand): 8 | # Set the embed colour here 9 | 10 | def __init__(self): 11 | super(EmbedHelpCommand, self).__init__( 12 | show_parameter_descriptions=False, sort_commands=False) 13 | 14 | async def send_bot_help(self, mapping): 15 | embed = discord.Embed(title='Bot Text Commands', colour=COLOUR_MAPPING[WallEColour.INFO]) 16 | description = self.context.bot.description 17 | if description: 18 | embed.description = description 19 | embed.set_footer(text=self.get_ending_note()) 20 | 21 | cogs_ordered_by_value_length = {} 22 | filtered = await self.filter_commands(self.context.bot.commands) 23 | for mapped_cog, mapped_commands in mapping.items(): 24 | commands_display = [ 25 | f"\n.{command.name}: {command.brief}\n" if command.brief else f".{command.name}\u2002" 26 | for command in mapped_commands 27 | if command in filtered 28 | ] 29 | value = ''.join(commands_display) 30 | 31 | command_under_cog = mapped_cog is not None 32 | cog_has_commands_user_can_run = value.strip() != "" 33 | 34 | if cog_has_commands_user_can_run and command_under_cog: 35 | length_of_value = len(value) 36 | if length_of_value in commands_display: 37 | cogs_ordered_by_value_length[length_of_value].append( 38 | {"cog_name": mapped_cog.qualified_name, "cog_commands": value} 39 | ) 40 | else: 41 | cogs_ordered_by_value_length[length_of_value] = [ 42 | {"cog_name": mapped_cog.qualified_name, "cog_commands": value} 43 | ] 44 | 45 | sorted_cogs_keys = sorted(list(cogs_ordered_by_value_length.keys())) 46 | for sorted_cogs_key in sorted_cogs_keys: 47 | for mapped_cog in cogs_ordered_by_value_length[sorted_cogs_key]: 48 | embed.add_field(name=mapped_cog['cog_name'], value=mapped_cog['cog_commands']) 49 | msg = await self.get_destination().send(content=None, embed=embed, reference=self.context.message) 50 | 51 | await HelpMessage.insert_record( 52 | HelpMessage( 53 | message_id=msg.id, channel_name=msg.channel.name, channel_id=msg.channel.id, 54 | time_created=msg.created_at.timestamp() 55 | ) 56 | ) 57 | 58 | async def send_cog_help(self, cog): 59 | embed = discord.Embed(title=f'{cog.qualified_name} Commands', colour=COLOUR_MAPPING[WallEColour.INFO]) 60 | 61 | for command in cog.get_commands(): 62 | embed.add_field(name=self.get_command_signature(command), value=command.short_doc or '', inline=False) 63 | 64 | embed.set_footer(text=self.get_ending_note()) 65 | msg = await self.get_destination().send(embed=embed, reference=self.context.message) 66 | await HelpMessage.insert_record( 67 | HelpMessage( 68 | message_id=msg.id, channel_name=msg.channel.name, channel_id=msg.channel.id, 69 | time_created=msg.created_at.timestamp() 70 | ) 71 | ) 72 | 73 | async def send_command_help(self, command): 74 | embed = discord.Embed( 75 | title=f".{command}{f' {command.usage}' if command.usage else ''}", 76 | colour=COLOUR_MAPPING[WallEColour.INFO], 77 | description=command.help, 78 | ) 79 | embed.set_footer(text=self.get_ending_note()) 80 | msg = await self.get_destination().send(embed=embed, reference=self.context.message) 81 | await HelpMessage.insert_record( 82 | HelpMessage( 83 | message_id=msg.id, channel_name=msg.channel.name, channel_id=msg.channel.id, 84 | time_created=msg.created_at.timestamp() 85 | ) 86 | ) 87 | 88 | async def send_error_message(self, error: str, /) -> None: 89 | embed = discord.Embed( 90 | title="ERROR", colour=COLOUR_MAPPING[WallEColour.INFO], 91 | description=error, 92 | ) 93 | embed.set_footer(text=self.get_ending_note()) 94 | msg = await self.get_destination().send(embed=embed, reference=self.context.message) 95 | await HelpMessage.insert_record( 96 | HelpMessage( 97 | message_id=msg.id, channel_name=msg.channel.name, channel_id=msg.channel.id, 98 | time_created=msg.created_at.timestamp() 99 | ) 100 | ) 101 | -------------------------------------------------------------------------------- /wall_e/extensions/here.py: -------------------------------------------------------------------------------- 1 | # Commands for finding who has access to certain channels. 2 | # Useful since the server size does not allow offline users to be listed 3 | # in the sidebar 4 | import asyncio 5 | 6 | import discord 7 | from discord import Thread 8 | from discord.ext import commands 9 | 10 | from utilities.global_vars import bot, wall_e_config 11 | 12 | from utilities.file_uploading import start_file_uploading 13 | from utilities.setup_logger import Loggers 14 | 15 | 16 | class Here(commands.Cog): 17 | 18 | def __init__(self): 19 | log_info = Loggers.get_logger(logger_name="Here") 20 | self.logger = log_info[0] 21 | self.debug_log_file_absolute_path = log_info[1] 22 | self.warn_log_file_absolute_path = log_info[2] 23 | self.error_log_file_absolute_path = log_info[3] 24 | self.logger.info("[Here __init__()] initializing Here") 25 | self.guild = None 26 | 27 | @commands.Cog.listener(name="on_ready") 28 | async def get_guild(self): 29 | self.guild = bot.guilds[0] 30 | 31 | @commands.Cog.listener(name="on_ready") 32 | async def upload_debug_logs(self): 33 | while self.guild is None: 34 | await asyncio.sleep(2) 35 | await start_file_uploading( 36 | self.logger, self.guild, bot, wall_e_config, self.debug_log_file_absolute_path, "here_debug" 37 | ) 38 | 39 | @commands.Cog.listener(name="on_ready") 40 | async def upload_warn_logs(self): 41 | while self.guild is None: 42 | await asyncio.sleep(2) 43 | await start_file_uploading( 44 | self.logger, self.guild, bot, wall_e_config, self.warn_log_file_absolute_path, 45 | "here_warn" 46 | ) 47 | 48 | @commands.Cog.listener(name="on_ready") 49 | async def upload_error_logs(self): 50 | while self.guild is None: 51 | await asyncio.sleep(2) 52 | await start_file_uploading( 53 | self.logger, self.guild, bot, wall_e_config, self.error_log_file_absolute_path, "here_error" 54 | ) 55 | 56 | def build_embed(self, members, channel, thread=False): 57 | # build response 58 | 59 | title = f"Users in **#{channel.name}**" 60 | 61 | self.logger.debug(f"[Here build_embed()] creating an embed with title \"{title}\"") 62 | embed = discord.Embed(type="rich") 63 | embed.title = title 64 | embed.color = discord.Color.blurple() 65 | embed.set_footer(text="brenfan", icon_url="https://i.imgur.com/vlpCuu2.jpg") 66 | if len(members) == 0: 67 | string = "I couldnt find anyone.\n" 68 | elif len(members) > 50: 69 | string = "There's a lot of people here.\n" 70 | else: 71 | string = f"The following ({len(members)}) users have permission for this channel.\n" 72 | 73 | # newline separated lists of members and their nicknames 74 | nicks = "\n".join([member.display_name for member in members]) 75 | names = "\n".join([str(member) for member in members]) 76 | 77 | embed.add_field(name="Name", value=nicks, inline=True) 78 | embed.add_field(name="Account", value=names, inline=True) 79 | 80 | if not thread: 81 | # comma separated list of role names for each role in the channel 82 | # if they can read messages 83 | # like how it says... 84 | roles = ", ".join([role.name 85 | for role in channel.changed_roles 86 | if role.permissions.read_messages]) 87 | roles += "\n*This message will self-destruct in 5 minutes*\n" 88 | 89 | embed.add_field(name="Channel Specific Roles", value=roles, inline=False) 90 | embed.description = string 91 | return embed 92 | 93 | @commands.command( 94 | brief="Displays users with permission to view the current channel.", 95 | help=( 96 | 'Results can be filtered by looking for users whose useraliases or nickaliases on the ' 97 | 'server contains the substring indicated with any of the included strings or all ' 98 | 'users if no args are given. Multiple may be entered.\n\n' 99 | 'Arguments:\n' 100 | '---the filter: the filter to apply the users through\n\n' 101 | 'Example:\n' 102 | '---.here ab\n\n' 103 | ), 104 | usage='"the filter"' 105 | ) 106 | async def here(self, ctx, *search): 107 | self.logger.info( 108 | f"[Here here()] {ctx.message.author} called here with {len(search)} arguments: {', '.join(search)}" 109 | ) 110 | 111 | # find people in the channel 112 | channel = ctx.channel 113 | if isinstance(channel, Thread): 114 | thread = True 115 | members = await channel.fetch_members() 116 | members = [ 117 | await ctx.guild.fetch_member(member.id) 118 | for member in members 119 | ] 120 | else: 121 | thread = False 122 | members = channel.members 123 | 124 | # optional filtering 125 | if len(search) > 0: 126 | # don't ask 127 | allowed = [m for m in members 128 | if len([query for query in search 129 | if query.lower() in m.display_name.lower() or query.lower() in str(m).lower()]) > 0] 130 | members = allowed 131 | 132 | self.logger.debug(f"[Here here()] found {len(members)} users in {channel.name}") 133 | 134 | embed = self.build_embed(members, channel, thread=thread) 135 | 136 | await ctx.send(embed=embed, delete_after=300, reference=ctx.message) 137 | 138 | 139 | async def setup(bot): 140 | await bot.add_cog(Here()) 141 | -------------------------------------------------------------------------------- /wall_e/extensions/mod.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import discord 4 | from discord.ext import commands 5 | 6 | from utilities.global_vars import bot, wall_e_config 7 | 8 | from utilities.embed import embed, WallEColour 9 | from utilities.file_uploading import start_file_uploading 10 | from utilities.setup_logger import Loggers 11 | 12 | 13 | class Mod(commands.Cog): 14 | 15 | def __init__(self): 16 | log_info = Loggers.get_logger(logger_name="Mod") 17 | self.logger = log_info[0] 18 | self.debug_log_file_absolute_path = log_info[1] 19 | self.warn_log_file_absolute_path = log_info[2] 20 | self.error_log_file_absolute_path = log_info[3] 21 | self.logger.info("[Mod __init__()] initializing Mod") 22 | self.guild = None 23 | 24 | @commands.Cog.listener(name="on_ready") 25 | async def get_guild(self): 26 | self.guild = bot.guilds[0] 27 | 28 | @commands.Cog.listener(name="on_ready") 29 | async def upload_debug_logs(self): 30 | while self.guild is None: 31 | await asyncio.sleep(2) 32 | await start_file_uploading( 33 | self.logger, self.guild, bot, wall_e_config, self.debug_log_file_absolute_path, "mod_debug" 34 | ) 35 | 36 | @commands.Cog.listener(name="on_ready") 37 | async def upload_warn_logs(self): 38 | while self.guild is None: 39 | await asyncio.sleep(2) 40 | await start_file_uploading( 41 | self.logger, self.guild, bot, wall_e_config, self.warn_log_file_absolute_path, 42 | "mod_warn" 43 | ) 44 | 45 | @commands.Cog.listener(name="on_ready") 46 | async def upload_error_logs(self): 47 | while self.guild is None: 48 | await asyncio.sleep(2) 49 | await start_file_uploading( 50 | self.logger, self.guild, bot, wall_e_config, self.error_log_file_absolute_path, "mod_error" 51 | ) 52 | 53 | @commands.command( 54 | brief="Allows Minions to post embed messages.", 55 | help=( 56 | 'For odd number of arguments the first arg will be used as description in the embed and the rest as ' 57 | 'field title and content.\n' 58 | 'For even number there will be no description.\n\n' 59 | 'Arguments:\n' 60 | '---[description]: the description of the embed\n' 61 | '---title: a title in the embed\n' 62 | '---content; the content that correspond to the above title in the embed\n\n' 63 | 'Example: \n' 64 | '---.embed "title1" "content1"' 65 | '---.embed "title1" "content1"\n' 66 | '---.embed "the description" "title1" "content1"\n' 67 | '---.embed "title1" "content1" "title2" "content2"\n' 68 | '---.embed "the description" "title1" "content1" "title2" "content2"\n' 69 | ), 70 | usage='["the description"] ["title"] ["corresponding content"]', 71 | aliases=['em'] 72 | ) 73 | @commands.has_any_role("Minions", "Moderator") 74 | async def embed(self, ctx, *arg): 75 | self.logger.info(f'[Mod embed()] embed function detected by user {ctx.message.author}') 76 | await ctx.message.delete() 77 | self.logger.debug('[Mod embed()] invoking message deleted') 78 | 79 | if not arg: 80 | self.logger.debug("[Mod embed()] no args, so command ended") 81 | return 82 | 83 | if ctx.message.author not in discord.utils.get(ctx.guild.roles, name="Minions").members: 84 | self.logger.debug('[Mod embed()] unathorized command attempt detected. Being handled.') 85 | await self.rekt(ctx) 86 | return 87 | 88 | self.logger.debug('[Mod embed()] minion confirmed') 89 | fields = [] 90 | desc = '' 91 | arg = list(arg) 92 | arg_len = len(arg) 93 | # odd number of args means description plus fields 94 | if not arg_len % 2 == 0: 95 | desc = arg[0] 96 | arg.pop(0) 97 | arg_len = len(arg) 98 | 99 | i = 0 100 | while i < arg_len: 101 | fields.append([arg[i], arg[i + 1]]) 102 | i += 2 103 | 104 | e_obj = await embed( 105 | self.logger, ctx=ctx, description=desc, author=ctx.author, colour=WallEColour.WARNING, 106 | content=fields 107 | ) 108 | if e_obj is not False: 109 | await ctx.send(embed=e_obj) 110 | 111 | async def rekt(self, ctx): 112 | self.logger.debug('[Mod rekt()] sending troll to unauthorized user') 113 | lol = '[secret](https://www.youtube.com/watch?v=dQw4w9WgXcQ)' 114 | e_obj = await embed( 115 | self.logger, 116 | ctx=ctx, 117 | title='Minion Things', 118 | author=ctx.me, 119 | description=lol 120 | ) 121 | if e_obj is not False: 122 | msg = await ctx.send(f'<@{ctx.message.author.id}>', embed=e_obj) 123 | await asyncio.sleep(5) 124 | await msg.delete() 125 | self.logger.debug('[Mod rekt()] troll message deleted') 126 | 127 | @commands.command( 128 | brief="Posts the warning message in embed format.", 129 | help=( 130 | 'Arguments:\n' 131 | '---warning message: message that will be posted in the warning embed in the channel\n\n' 132 | 'Example: \n' 133 | '---.modspeak warning message\n\n' 134 | ), 135 | usage="warning message", 136 | aliases=['warn'], 137 | ) 138 | @commands.has_any_role("Minions", "Moderator") 139 | async def modspeak(self, ctx, *arg): 140 | self.logger.info(f'[Mod modspeak()] modspeack function detected by minion {ctx.message.author}') 141 | await ctx.message.delete() 142 | self.logger.debug('[Mod modspeak()] invoking message deleted') 143 | 144 | if not arg: 145 | self.logger.debug("[Mod modspeak()] no args, so command ended") 146 | return 147 | 148 | if ctx.message.author not in discord.utils.get(ctx.guild.roles, name="Minions").members: 149 | self.logger.debug('[Mod modspeak()] unathorized command attempt detected. Being handled.') 150 | await self.rekt(ctx) 151 | return 152 | 153 | msg = '' 154 | for wrd in arg: 155 | msg += f'{wrd} ' 156 | 157 | e_obj = await embed( 158 | self.logger, ctx=ctx, title='ATTENTION:', author=ctx.author, colour=WallEColour.ERROR, 159 | description=msg, footer_text='Moderator Warning' 160 | ) 161 | if e_obj is not False: 162 | await ctx.send(embed=e_obj) 163 | 164 | 165 | async def setup(bot): 166 | await bot.add_cog(Mod()) 167 | -------------------------------------------------------------------------------- /wall_e/main.py: -------------------------------------------------------------------------------- 1 | from utilities.global_vars import bot, wall_e_config 2 | 3 | if __name__ == "__main__": 4 | bot.run(wall_e_config.get_config_value("basic_config", "TOKEN")) 5 | -------------------------------------------------------------------------------- /wall_e/overriden_coroutines/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/wall_e/overriden_coroutines/__init__.py -------------------------------------------------------------------------------- /wall_e/overriden_coroutines/delete_help_messages.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import tasks 3 | 4 | from wall_e_models.models import HelpMessage 5 | from utilities.setup_logger import print_wall_e_exception 6 | 7 | 8 | @tasks.loop(seconds=1) 9 | async def delete_help_command_messages(): 10 | from utilities.global_vars import bot, logger 11 | try: 12 | help_messages = await HelpMessage.get_messages_to_delete() 13 | for help_message in help_messages: 14 | channel = bot.get_channel(int(help_message.channel_id)) 15 | if channel is not None: 16 | successful = False 17 | try: 18 | message = await channel.fetch_message(int(help_message.message_id)) 19 | try: 20 | invocator_message = await channel.fetch_message(int(message.reference.message_id)) 21 | await invocator_message.delete() 22 | except discord.NotFound: 23 | # means the original invocating message has since been deleted so the code can move on 24 | pass 25 | await message.delete() 26 | successful = True 27 | except discord.NotFound: 28 | logger.error( 29 | "[delete_help_messages.py delete_help_command_messages()] " 30 | f"could not find the message that contains the help command with obj " 31 | f"{help_message}" 32 | ) 33 | # setting successful True since the message seems to already be deleted 34 | successful = True 35 | except discord.Forbidden: 36 | logger.error( 37 | "[delete_help_messages.py delete_help_command_messages()] " 38 | f"wall_e does not seem to have permissions to view/delete the message that " 39 | f"contains the help command with obj {help_message}" 40 | ) 41 | # if wall_e does not have the permission to delete the message, 42 | # a retry would not fix that anyways 43 | successful = True 44 | except discord.HTTPException: 45 | logger.error( 46 | "[delete_help_messages.py delete_help_command_messages()] " 47 | f"some sort of HTTP prevented wall_e from deleting the message that " 48 | f"contains the help command with obj {help_message}" 49 | ) 50 | # there might be a momentary network glitch, best to try again 51 | if successful: 52 | await HelpMessage.delete_message(help_message) 53 | except Exception as error: 54 | print_wall_e_exception(error, error.__traceback__, error_logger=logger.error) 55 | -------------------------------------------------------------------------------- /wall_e/overriden_coroutines/detect_reactions.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import discord 4 | 5 | from utilities.bot_channel_manager import wall_e_category_name 6 | from utilities.embed import embed 7 | from wall_e_models.customFields import pstdatetime 8 | 9 | 10 | async def get_message_after_up_arrow_emoji(channel, message_with_up_arrow_emoji): 11 | messages = [message async for message in channel.history(limit=1, after=message_with_up_arrow_emoji)] 12 | return messages[0] if len(messages) > 0 else None 13 | 14 | 15 | async def get_message_before_down_arrow_emoji(channel, reaction, message_after_message_with_up_arrow_emoji): 16 | message_before_message_with_down_arrow_emoji = message_after_message_with_up_arrow_emoji 17 | number_of_messages_crawled = 0 18 | upper_bound_traceback_message_index = -1 19 | while upper_bound_traceback_message_index == -1: 20 | messages = [message async for message in 21 | channel.history(limit=100, before=message_before_message_with_down_arrow_emoji)] 22 | number_of_messages_crawled += 100 23 | iteration = 0 24 | if len(messages) > 0: 25 | while upper_bound_traceback_message_index == -1 and iteration < len(messages): 26 | upper_bound_emoji_detected = '⬇️' in [reaction.emoji for reaction in messages[iteration].reactions] 27 | if upper_bound_emoji_detected and reaction.message_id != messages[iteration].id: 28 | upper_bound_traceback_message_index = iteration 29 | iteration += 1 30 | else: 31 | # could not find the original traceback message string 32 | # will just clear the whole channel instead I guess 33 | upper_bound_traceback_message_index = None 34 | 35 | if upper_bound_traceback_message_index == -1: 36 | # traceback message not found but there is more potential message to look through 37 | message_before_message_with_down_arrow_emoji = messages[len(messages) - 1] 38 | elif upper_bound_traceback_message_index is None: 39 | # whole channel has to be cleared 40 | message_before_message_with_down_arrow_emoji = None 41 | else: 42 | # message with down arrow found, will now try to find the message before it as that message is needed 43 | # for the "after" parameter when getting the whole block of messages to delete 44 | if len(messages) == upper_bound_traceback_message_index + 1: 45 | 46 | # seems the message before the traceback message was not retrieved in the messages list, so 47 | # another call needs to be made just for that message 48 | previous_messages = [ 49 | message async for message in 50 | channel.history(limit=1, before=messages[upper_bound_traceback_message_index]) 51 | ] 52 | # either a previous message exists and was obtained, indicating that there 53 | # is a suitable message for the "after" cursor or there is no previous 54 | # message, so "None" should be used 55 | message_before_message_with_down_arrow_emoji = previous_messages[0] if len( 56 | previous_messages) > 0 else None 57 | else: 58 | # if the code was lucky, the message right before the Traceback is in the list of 59 | # messages that were already retrieved so the code just need to look at the next 60 | # message in the list for the "after" parameter 61 | message_before_message_with_down_arrow_emoji = messages[upper_bound_traceback_message_index + 1] 62 | return number_of_messages_crawled, message_before_message_with_down_arrow_emoji 63 | 64 | 65 | async def reaction_detected(reaction): 66 | """ 67 | Adding a listener method that allows the Bot_manager to delete a stack trace in an error text channel if 68 | a Bot_manager has fixed the error in the stack trace. Just a nice way to keep a clean error text channel and a 69 | quick visual indicator of whether an error has been fixed 70 | 71 | :param reaction: 72 | :return: 73 | """ 74 | from utilities.global_vars import bot, logger 75 | guild = bot.guilds[0] 76 | delete_debug_log_reaction_detected = reaction.emoji.name == '⬆️' 77 | 78 | users_roles = [role.name for role in discord.utils.get(guild.members, id=reaction.user_id).roles] 79 | reaction_is_from_bot_manager = "Bot_manager" in users_roles 80 | 81 | channel_with_reaction = discord.utils.get(guild.channels, id=reaction.channel_id) 82 | reaction_not_sent_in_regular_channel = channel_with_reaction is None 83 | if reaction_not_sent_in_regular_channel: 84 | return 85 | channel_category = channel_with_reaction.category 86 | if channel_category is None: 87 | return 88 | text_channel_is_in_log_channel_category = channel_category.name == wall_e_category_name 89 | 90 | error_log_channel = channel_with_reaction.name[-6:] == '_error' 91 | warn_log_channel = channel_with_reaction.name[-5:] == '_warn' 92 | valid_error_channel = ( 93 | delete_debug_log_reaction_detected and reaction_is_from_bot_manager and 94 | text_channel_is_in_log_channel_category and (error_log_channel or warn_log_channel) 95 | ) 96 | if not valid_error_channel: 97 | return 98 | channel = channel_with_reaction 99 | message_with_up_arrow_emoji = await channel.fetch_message(reaction.message_id) 100 | 101 | message_after_message_with_up_arrow_emoji = await get_message_after_up_arrow_emoji( 102 | channel, message_with_up_arrow_emoji 103 | ) 104 | ( 105 | number_of_messages_crawled, message_before_message_with_down_arrow_emoji 106 | ) = await get_message_before_down_arrow_emoji(channel, reaction, message_after_message_with_up_arrow_emoji) 107 | 108 | messages_to_delete = [ 109 | message async for message in channel.history( 110 | after=message_before_message_with_down_arrow_emoji, before=message_after_message_with_up_arrow_emoji, 111 | oldest_first=False, limit=number_of_messages_crawled 112 | ) 113 | ] 114 | number_of_messages_deleted = len(messages_to_delete) 115 | todays_date = pstdatetime.now() 116 | messages_that_cant_be_bulk_deleted = [] 117 | while len(messages_to_delete) > 0: 118 | number_of_messages = len(messages_to_delete) 119 | messages_that_can_be_deleted = [ 120 | message_to_delete 121 | for message_to_delete in messages_to_delete[:100] 122 | if (todays_date - message_to_delete.created_at).days < 14 123 | ] 124 | messages_that_cant_be_bulk_deleted.extend([ 125 | message_to_delete 126 | for message_to_delete in messages_to_delete[:100] 127 | if (todays_date - message_to_delete.created_at).days >= 14 128 | ]) 129 | logger.info( 130 | f"[detect_reactions.py reaction_detected()] bulk deleting " 131 | f"{len(messages_that_can_be_deleted)}/{number_of_messages} messages " 132 | ) 133 | await channel.delete_messages(messages_that_can_be_deleted, reason="issue fixed") 134 | messages_to_delete = messages_to_delete[100:] 135 | if len(messages_that_cant_be_bulk_deleted) > 0: 136 | logger.info( 137 | f"[detect_reactions.py reaction_detected()] attempting to manually delete " 138 | f"{len(messages_that_cant_be_bulk_deleted)} messages " 139 | ) 140 | for message_that_cant_be_bulk_deleted in messages_that_cant_be_bulk_deleted: 141 | await message_that_cant_be_bulk_deleted.delete() 142 | message = ( 143 | 'Last' + 144 | (f" {number_of_messages_deleted} messages" if number_of_messages_deleted > 1 else " message") + 145 | " deleted" 146 | ) 147 | e_obj = await embed( 148 | logger, 149 | ctx=channel, 150 | author=bot.user, 151 | description=message, 152 | ) 153 | if e_obj is not False: 154 | message = await channel.send(embed=e_obj) 155 | await asyncio.sleep(10) 156 | await message.delete() 157 | -------------------------------------------------------------------------------- /wall_e/overriden_coroutines/error_handlers.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import re 3 | 4 | import discord 5 | from discord.ext import commands 6 | 7 | from utilities.embed import WallEColour, embed 8 | from utilities.setup_logger import print_wall_e_exception 9 | 10 | 11 | async def report_text_command_error(ctx, error): 12 | """ 13 | Function that gets called when the script cant understand the command that the user invoked 14 | :param ctx: the ctx object that is part of command parameters that are not slash commands 15 | :param error: the error that was encountered 16 | :return: 17 | """ 18 | from utilities.global_vars import logger 19 | handled_errors = ( 20 | commands.errors.ArgumentParsingError, commands.errors.MemberNotFound, commands.MissingRequiredArgument, 21 | commands.errors.BadArgument, discord.errors.HTTPException 22 | ) 23 | if isinstance(error, handled_errors): 24 | message_footer = ( 25 | "\n\n**You have 20 seconds to copy your input to do a retry before I ensure it is wiped from the" 26 | " channel**" 27 | ) 28 | if isinstance(error, commands.errors.ArgumentParsingError): 29 | description = ( 30 | f"Uh-oh, seem like you have entered a badly formed string and wound up with error:" 31 | f"\n'{error.args[0]}'\n\n[Technical Details link if you care to look]" 32 | f"(https://discordpy.readthedocs.io/en/latest/ext/commands/api.html?" 33 | f"highlight=argumentparsingerror#exceptions){message_footer}" 34 | ) 35 | elif ctx.command.name == 'unban' and isinstance(error, commands.errors.BadArgument): 36 | description = f'Please enter a numerical Discord ID.{message_footer}' 37 | else: 38 | description = f"{error.args[0]}{message_footer}" 39 | error_type = f"{type(error)}"[8:-2] 40 | embed_obj = await embed( 41 | logger=ctx.cog.logger, ctx=ctx, title=f"Error {error_type} encountered", 42 | description=description, colour=WallEColour.ERROR 43 | ) 44 | if embed_obj is not False: 45 | message = await ctx.channel.send(embed=embed_obj, reference=ctx.message) 46 | await asyncio.sleep(20) 47 | try: 48 | await ctx.message.delete() 49 | except discord.errors.NotFound: 50 | pass 51 | try: 52 | await message.delete() 53 | except discord.errors.NotFound: 54 | pass 55 | else: 56 | await report_command_errors(error, logger, ctx=ctx) 57 | 58 | 59 | async def report_slash_command_error(interaction: discord.Interaction, error): 60 | """ 61 | Function that gets called when the script cant understand the slash command that the user invoked 62 | :param interaction: 63 | :param error: 64 | :return: 65 | """ 66 | from utilities.global_vars import logger 67 | await report_command_errors(error, logger, interaction=interaction) 68 | 69 | 70 | async def report_command_errors(error, logger, interaction=None, ctx=None): 71 | privilege_errors = ( 72 | discord.ext.commands.errors.MissingAnyRole, discord.ext.commands.errors.MissingRole, 73 | discord.ext.commands.errors.MissingPermissions, discord.app_commands.errors.MissingPermissions, 74 | discord.app_commands.errors.MissingRole, discord.app_commands.errors.MissingAnyRole, 75 | 76 | ) 77 | if isinstance(error, privilege_errors): 78 | from utilities.global_vars import incident_report_logger 79 | author = ctx.author if interaction is None else interaction.user 80 | bot = ctx.me if interaction is None else interaction.client.user 81 | command = ctx.command if interaction is None else interaction.command.name 82 | channel = ctx.channel if ctx is not None else interaction.channel 83 | 84 | if interaction is not None and interaction.message is not None: 85 | await interaction.message.delete() 86 | if ctx is not None: 87 | await ctx.message.delete() 88 | incident_report_logger.info(f"<@{author.id}> tried to run command `{command}`") 89 | e_obj = await embed( 90 | logger, 91 | interaction=interaction, 92 | ctx=ctx, 93 | title='INCIDENT REPORT', 94 | colour=WallEColour.ERROR, 95 | author=bot, 96 | description=( 97 | "You do not have adequate permission to run this command.\n\n" 98 | "Incident has been reported" 99 | ) 100 | ) 101 | if e_obj is not False: 102 | try: 103 | await author.send(embed=e_obj) 104 | except discord.errors.Forbidden: 105 | msg = await channel.send(f'<@{author.id}>', embed=e_obj) 106 | await asyncio.sleep(10) 107 | await msg.delete() 108 | elif isinstance(error, discord.app_commands.commands.CommandInvokeError): 109 | description = error.args[0] 110 | error_type = f"{type(error)}"[8:-2] 111 | embed_obj = await embed( 112 | logger=logger, interaction=interaction, title=f"Error {error_type} encountered", 113 | description=description, colour=WallEColour.ERROR 114 | ) 115 | error.command.binding.logger.error( 116 | 'Encountered exception in command %r', interaction.command.name, exc_info=error 117 | ) 118 | errors = [] 119 | msg = None 120 | if embed_obj is not False: 121 | deferred_interaction = interaction.response.type is not None 122 | if deferred_interaction: 123 | send_func = interaction.followup.send 124 | else: 125 | send_func = interaction.response.send_message 126 | try: 127 | await send_func(embed=embed_obj) 128 | except discord.errors.NotFound as e: 129 | if isinstance(e, discord.errors.NotFound): 130 | errors.append(e) 131 | try: 132 | logger.error( 133 | "[error_handlers.py report_command_errors()] experienced below error" 134 | f" when trying to alert user of error '{description}' when using interaction " 135 | f"response follow up attempt {type(e)}\n{e}" 136 | ) 137 | # if responding to the interaction in any way failed, let's try and just send a 138 | # general message to the channel 139 | msg = await interaction.channel.send(embed=embed_obj) 140 | except Exception as e: 141 | errors.append(e) 142 | logger.error( 143 | "[error_handlers.py report_command_errors()] unable to send error embed" 144 | f" to channel due to error {description} via any routes due to" 145 | f" {type(e)}\n{errors}" 146 | ) 147 | else: 148 | logger.error( 149 | f"[error_handlers.py report_command_errors()] experienced unexpected error below when trying " 150 | f"to send a response to the interaction due to error {description}: {type(e)}/\n{e}" 151 | ) 152 | await asyncio.sleep(20) 153 | await interaction.delete_original_response() 154 | if msg is not None: 155 | await msg.delete() 156 | elif isinstance(error, commands.errors.CommandNotFound): 157 | return 158 | elif isinstance(error, discord.errors.NotFound): 159 | try: 160 | error.command.binding.logger.warn( 161 | 'Encountered exception in command %r', interaction.command.name, exc_info=error 162 | ) 163 | except Exception: 164 | logger.warn( 165 | 'Encountered exception in command %r', interaction.command.name, exc_info=error 166 | ) 167 | else: 168 | # only prints out an error to the log if the string that was entered doesnt contain just "." 169 | pattern = r'[^\.]' 170 | if re.search(pattern, f"{error}"[9:-14]): 171 | if type(error) is discord.ext.commands.errors.CheckFailure: 172 | author = ctx.author if ctx is not None else f"{interaction.user.name}({interaction.user.id})" 173 | logger.warning( 174 | f"[error_handlers.py on_command_error()] user {author} " 175 | "probably tried to access a command they arent supposed to" 176 | ) 177 | else: 178 | try: 179 | print_wall_e_exception( 180 | error, error.__traceback__, error_logger=error.command.binding.logger.error 181 | ) 182 | except Exception: 183 | print_wall_e_exception(error, error.__traceback__, error_logger=logger.error) 184 | -------------------------------------------------------------------------------- /wall_e/requirements.txt: -------------------------------------------------------------------------------- 1 | pytz==2018.5 2 | wolframalpha==5.0.0 3 | configparser==5.2.0 4 | 5 | # needed for DB interactions 6 | Django==4.2.22 7 | asgiref==3.6.0 -------------------------------------------------------------------------------- /wall_e/utilities/autocomplete/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/wall_e/utilities/autocomplete/__init__.py -------------------------------------------------------------------------------- /wall_e/utilities/autocomplete/banned_users_choices.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import discord 4 | from discord import app_commands 5 | 6 | 7 | async def get_banned_users(interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: 8 | """ 9 | Gets a list of banned users that can be viewed with /bans 10 | 11 | :param interaction: 12 | :param current: the query for either a username or user id 13 | :return: an array of the app_commands.Choices to return where the name is the name of the banned username and the 14 | value is the corresponding user id 15 | """ 16 | user_roles = [role.name for role in interaction.user.roles] 17 | if not ('Bot_manager' in user_roles or 'Moderator' in user_roles or 'Minions' in user_roles): 18 | return [] 19 | from extensions.ban import Ban 20 | current = current.lower() 21 | banned_users = [ 22 | app_commands.Choice(name=f"{banned_user_name}({banned_user_id})", value=f"{banned_user_id}") 23 | for banned_user_id, banned_user_name in Ban.ban_list.items() 24 | if current in banned_user_name.lower() or current in f"{banned_user_id}" 25 | ] 26 | if len(banned_users) == 0: 27 | if len(current) > 0: 28 | banned_users = [ 29 | app_commands.Choice(name=f"No banned users could be found that contain {current}", value="-1") 30 | ] 31 | else: 32 | banned_users = [ 33 | app_commands.Choice(name="No banner user could be found", value="-1") 34 | ] 35 | if len(banned_users) > 25: 36 | banned_users = banned_users[:24] 37 | banned_users.append(app_commands.Choice(name="Start typing to get better results", value="-1")) 38 | return banned_users 39 | -------------------------------------------------------------------------------- /wall_e/utilities/autocomplete/examples_command.py: -------------------------------------------------------------------------------- 1 | EXAMPLES_AUTO_COMPLETE_MENU_CHOICES = { 2 | "tex": "tex option" 3 | } 4 | -------------------------------------------------------------------------------- /wall_e/utilities/autocomplete/extensions_load_choices.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import discord 4 | from discord import app_commands 5 | 6 | 7 | from utilities.global_vars import wall_e_config 8 | from utilities.wall_e_bot import extension_location_python_path 9 | 10 | 11 | def user_has_permission_to_load_or_unload_extension(interaction, extension): 12 | """ 13 | Indicates if the user has access to the specified extension 14 | 15 | :param interaction: the interaction object that contains the roles 16 | :param extension: the name of the extension being loaded or unloaded 17 | :return: True or False to indicate if the user using a `load/unload/reload` has the rights to the 18 | specified extension 19 | """ 20 | roles = [role.name for role in sorted(interaction.user.roles, key=lambda x: int(x.position), reverse=True)] 21 | user_is_bot_manager = 'Bot_manager' in roles 22 | user_is_moderator = 'Minions' in roles or 'Moderators' in roles 23 | valid_load_call = user_is_bot_manager or user_is_moderator \ 24 | if extension in ['ban', 'mod'] else user_is_bot_manager 25 | return valid_load_call 26 | 27 | 28 | async def get_extension_that_can_be_loaded( 29 | interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: 30 | """ 31 | Gets a list of extensions that a user can load with /load command 32 | 33 | :param interaction: the interaction object that contains the list of loaded extensions 34 | :param current: the substring that the user has entered into the search box on discord 35 | :return: an array of the app_commands.Choices to return where the name is the name of the class in the 36 | specified extension and the value is the name of the extension that can be passed to `bot.load_extension` 37 | """ 38 | from extensions.administration import extension_mapping 39 | current = current.strip().lower() 40 | extensions = wall_e_config.get_extensions() 41 | loaded_extensions = list(interaction.client.extensions) 42 | extensions = [ 43 | app_commands.Choice( 44 | name=extension_mapping[extension], value=f"{extension}" 45 | ) 46 | for extension in extensions 47 | if f"{extension_location_python_path}{extension}" not in loaded_extensions and 48 | current in extension_mapping[extension].lower() and 49 | user_has_permission_to_load_or_unload_extension(interaction, extension) 50 | ] 51 | if len(extensions) == 0: 52 | if len(current) > 0: 53 | extensions.append( 54 | app_commands.Choice( 55 | name=f'No unloaded extensions could be found that contain "{current}"', value="-1" 56 | ) 57 | ) 58 | else: 59 | extensions.append(app_commands.Choice(name="No unloaded extensions could be found", value="-1")) 60 | if len(extensions) > 25: 61 | extensions = extensions[:24] 62 | extensions.append(app_commands.Choice(name="Start typing to get better results", value="-1")) 63 | return extensions 64 | 65 | 66 | async def get_extension_that_can_be_unloaded( 67 | interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: 68 | """ 69 | Gets a list of extensions that a user can unload with /unload command 70 | 71 | :param interaction: the interaction object that contains the list of loaded extensions 72 | :param current: the substring that the user has entered into the search box on discord 73 | :return: an array of the app_commands.Choices to return where the name is the name of the class in the 74 | specified extension and the value is the name of the extension that can be passed to `bot.unload_extension` 75 | """ 76 | from extensions.administration import extension_mapping 77 | current = current.strip().lower() 78 | extensions = wall_e_config.get_extensions() 79 | loaded_extensions = list(interaction.client.extensions) 80 | extensions = [ 81 | app_commands.Choice( 82 | name=extension_mapping[extension], value=extension 83 | ) 84 | for extension in extensions 85 | if f"{extension_location_python_path}{extension}" in loaded_extensions and 86 | current in extension_mapping[extension].lower() and 87 | user_has_permission_to_load_or_unload_extension(interaction, extension) 88 | ] 89 | if len(extensions) == 0: 90 | if len(current) > 0: 91 | extensions.append( 92 | app_commands.Choice( 93 | name=f'No loaded extensions could be found that contain "{current}"', value="-1" 94 | ) 95 | ) 96 | else: 97 | extensions.append(app_commands.Choice(name="No loaded extensions could be found", value="-1")) 98 | if len(extensions) > 25: 99 | extensions = extensions[:24] 100 | extensions.append(app_commands.Choice(name="Start typing to get better results", value="-1")) 101 | return extensions 102 | -------------------------------------------------------------------------------- /wall_e/utilities/autocomplete/role_commands_choices.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List 3 | 4 | import discord 5 | from discord import app_commands 6 | 7 | 8 | def get_lowercase_roles(current: str): 9 | """ 10 | Gets the latest assign-able roles that contain the substring "current" 11 | :param current: the substring that the user has entered into the search box on discord 12 | :return: the list of assign-able roles that match the substring "current" 13 | """ 14 | roles = [] 15 | from extensions.role_commands import RoleCommands 16 | if not RoleCommands.roles_list_being_updated: 17 | roles = [role for role in list(RoleCommands.lowercase_roles.values()) if current.lower() in role.name.lower()] 18 | return roles 19 | 20 | 21 | async def get_assigned_or_unassigned_roles( 22 | interaction: discord.Interaction, current: str, error_message: List[str], 23 | assigned_roles=True) -> List[app_commands.Choice[str]]: 24 | """ 25 | Get the assigned or unassigned roles for the user that is using /iam or /iamn 26 | :param interaction: the interaction object that contains the roles and the user using the command 27 | :param current: the substring that the user has entered into the search box on discord 28 | :param error_message: the list of error message to use 29 | 0 -> If no assignable roles could be found that contain the "current" substring 30 | 1 -> No assignable roles could be found 31 | 2 -> If no assignable roles could be found that contain the "current" substring that the user does or 32 | does not have [dependent on assigned_roles Flag] 33 | 3 -> If no assignable roles could be found that the user does or does not have [dependent on assigned_roles Flag] 34 | 4-> string to place for the last element if there are more than 25 results 35 | :param assigned_roles: flag to indicate if the roles to get should be 36 | True -> roles that the user already has 37 | False -> roles that the user does not have 38 | :return: an array of the app_commands.Choices to return where the name is the name of the role and the value 39 | is the role's ID in string format 40 | cause an int version of the role ID was too big a number for discord to be able to handle 41 | """ 42 | current = current.strip() 43 | roles = get_lowercase_roles(current) 44 | if len(roles) == 0: 45 | if len(current) > 0: 46 | roles = [app_commands.Choice(name=error_message[0], value="-1")] 47 | else: 48 | roles = [app_commands.Choice(name=error_message[1], value="-1")] 49 | else: 50 | roles = [ 51 | app_commands.Choice(name=role.name, value=f"{role.id}") 52 | for role in roles if ( 53 | role in interaction.user.roles if assigned_roles 54 | else role not in interaction.user.roles 55 | ) 56 | ] 57 | if len(roles) == 0: 58 | if len(current) > 0: 59 | roles.append( 60 | app_commands.Choice(name=error_message[2], value="-1")) 61 | else: 62 | roles.append(app_commands.Choice(name=error_message[3], value="-1")) 63 | if len(roles) > 25: 64 | roles = roles[:24] 65 | roles.append(app_commands.Choice(name=error_message[4], value="-1")) 66 | return roles 67 | 68 | 69 | async def get_assignable_roles(interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: 70 | """ 71 | Gets the roles that the user can assign to themselves. Involved if the user uses /iam command 72 | :param interaction: the interaction object that contains the roles and the user using the command 73 | :param current: the substring that the user has entered into the search box on discord 74 | :return: an array of the app_commands.Choices to return where the name is the name of the role and the 75 | value is the role's ID in string format cause an int version of the role ID was too big a number for 76 | discord to be able to handle 77 | """ 78 | error_message = [ 79 | ( 80 | f"No assignable roles found with '{current[:13]}{'...' if current != current[:13] else ''}'. " 81 | f"Maybe try after /sync_roles if you know it exists" 82 | ), 83 | "No assignable roles found. Maybe re-try after /sync_roles if you know it exists", 84 | ( 85 | f"You are in all the assignable roles with '{current[:3]}{'...' if current != current[:3] else ''}'. " 86 | f"Maybe try after /sync_roles if you know it exists" 87 | ), 88 | "You are in all the assignable roles. Maybe re-try after /sync_roles if you know it exists", 89 | "Start typing to get better results" 90 | ] 91 | logger = logging.getLogger("RoleCommands") 92 | logger.debug( 93 | "[role_commands_autocomplete_functions.py get_assignable_roles()] getting list of assignable roles" 94 | ) 95 | roles = await get_assigned_or_unassigned_roles(interaction, current, error_message, assigned_roles=False) 96 | logger.debug( 97 | "[role_commands_autocomplete_functions.py get_assignable_roles()] retrieved list of assignable roles" 98 | ) 99 | return roles 100 | 101 | 102 | async def get_assigned_roles(interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: 103 | """ 104 | Gets the roles that the user can remove from themselves. Involved if the user uses /iamn command 105 | :param interaction: the interaction object that contains the roles and the user using the command 106 | :param current: the substring that the user has entered into the search box on discord 107 | :return: an array of the app_commands.Choices to return where the name is the name of the role and the 108 | value is the role's ID in string format cause an int version of the role ID was too big a number for 109 | discord to be able to handle 110 | """ 111 | error_message = [ 112 | f"No assigned roles found with '{current[:66]}{'...' if current != current[:66] else ''}", 113 | "No assigned roles could be found. Maybe try after /sync_roles if you know it exists", 114 | ( 115 | f"You aren't in any assignable roles with '{current[:4]}{'...' if current != current[:4] else ''}'. " 116 | f"Maybe try after /sync_roles if you know it exists" 117 | ), 118 | "You are not in any assignable roles", 119 | "Start typing to get better results" 120 | ] 121 | logger = logging.getLogger("RoleCommands") 122 | logger.debug( 123 | "[role_commands_autocomplete_functions.py get_assigned_roles()] getting list of assigned roles" 124 | ) 125 | roles = await get_assigned_or_unassigned_roles(interaction, current, error_message) 126 | logger.debug( 127 | "[role_commands_autocomplete_functions.py get_assigned_roles()] retrieved list of assigned roles" 128 | ) 129 | return roles 130 | 131 | 132 | async def get_roles_that_can_be_deleted(interaction: discord.Interaction, 133 | current: str) -> List[app_commands.Choice[str]]: 134 | """ 135 | Gets a list of assign-able roles that a user can delete with /delete_role command 136 | :param interaction: the interaction object that contains the roles 137 | :param current: the substring that the user has entered into the search box on discord 138 | :return: an array of the app_commands.Choices to return where the name is the name of the role and the 139 | value is the role's ID in string format cause an int version of the role ID was too big a number for 140 | discord to be able to handle 141 | """ 142 | logger = logging.getLogger("RoleCommands") 143 | logger.debug( 144 | "[role_commands_autocomplete_functions.py get_roles_that_can_be_deleted()] getting list of " 145 | "roles that can be deleted" 146 | ) 147 | current = current.strip() 148 | roles = get_lowercase_roles(current) 149 | roles = [ 150 | app_commands.Choice(name=role.name, value=f"{role.id}") 151 | for role in roles 152 | if len(role.members) == 0 153 | ] 154 | if len(roles) == 0: 155 | if len(current) > 0: 156 | roles.append( 157 | app_commands.Choice( 158 | name=( 159 | "No empty assignable roles found with " 160 | f"'{current[:10]}{'...' if current != current[:10] else ''}'. " 161 | "Maybe try after /sync_roles if you know it exists" 162 | ), value="-1" 163 | ) 164 | ) 165 | else: 166 | roles.append( 167 | app_commands.Choice( 168 | name=( 169 | "No empty assignable roles could be found. " 170 | "Maybe re-try after /sync_roles if you know it exists" 171 | ), 172 | value="-1" 173 | ) 174 | ) 175 | if len(roles) > 25: 176 | roles = roles[:24] 177 | roles.append(app_commands.Choice(name="Start typing to get better results", value="-1")) 178 | logger.debug( 179 | "[role_commands_autocomplete_functions.py get_roles_that_can_be_deleted()] obtained list of " 180 | "roles that can be deleted" 181 | ) 182 | return roles 183 | 184 | 185 | async def get_roles_with_members(interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: 186 | """ 187 | Get a list of all the roles that have members where the user can use with the /whois command 188 | :param interaction: the interaction object that contains the roles 189 | :param current: the substring that the user has entered into the search box on discord 190 | :return:an array of the app_commands.Choices to return where the name is the name of the role and the 191 | value is the role's ID in string format cause an int version of the role ID was too big a number for 192 | discord to be able to handle 193 | """ 194 | logger = logging.getLogger("RoleCommands") 195 | logger.debug( 196 | "[role_commands_autocomplete_functions.py get_roles_with_members()] getting list of " 197 | "roles with members" 198 | ) 199 | current = current.strip() 200 | roles = [] 201 | from extensions.role_commands import RoleCommands 202 | if not RoleCommands.roles_list_being_updated: 203 | roles = [ 204 | app_commands.Choice(name=role.name, value=f"{role.id}") 205 | for role in list(RoleCommands.roles_with_members.values()) 206 | if current.lower() in role.name.lower() 207 | ] 208 | if len(roles) == 0: 209 | if len(current) > 0: 210 | roles.append( 211 | app_commands.Choice( 212 | name=( 213 | "No roles found with a member with " 214 | f"'{current[:10]}{'...' if current != current[:10] else ''}'. " 215 | "Maybe try after /sync_roles if you know it exists" 216 | ), value="-1" 217 | ) 218 | ) 219 | else: 220 | roles.append( 221 | app_commands.Choice( 222 | name=( 223 | "No roles could be found with a member. " 224 | "maybe be-try after /sync_roles if you know it exists" 225 | ), 226 | value="-1" 227 | ) 228 | ) 229 | if len(roles) > 25: 230 | roles = roles[:24] 231 | roles.append(app_commands.Choice(name="Start typing to get better results", value="-1")) 232 | logger.debug( 233 | "[role_commands_autocomplete_functions.py get_roles_with_members()] obtained list of " 234 | "roles with members" 235 | ) 236 | return roles 237 | -------------------------------------------------------------------------------- /wall_e/utilities/config/config.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import configparser 4 | 5 | 6 | config_file_location_local = "utilities/config/local.ini" 7 | config_file_location_production = "utilities/config/production.ini" 8 | 9 | 10 | class WallEConfig: 11 | def __init__(self, environment, wall_e=True): 12 | # wall_e flag is needed to ensure that the environment variables aren't wiped clean until after 13 | # they have been used by the django-orm for the database migrations and connection 14 | 15 | self.config = configparser.ConfigParser(interpolation=None) 16 | self.config.optionxform = str 17 | if environment == "LOCALHOST": 18 | self.config.read(config_file_location_local) 19 | elif environment == "PRODUCTION": 20 | self.config.read(config_file_location_production) 21 | else: 22 | raise Exception(f"[WallEConfig __init__()] incorrect environment specified {environment}") 23 | 24 | for each_section in self.config.sections(): 25 | for (key, value) in self.config.items(each_section): 26 | environment_var = f"{each_section}__{key}" 27 | if environment_var in os.environ: 28 | self.set_config_value(each_section, key, os.environ[environment_var]) 29 | if wall_e: 30 | os.environ[environment_var] = ' ' 31 | 32 | def get_config_value(self, section, option): 33 | 34 | if self.config.has_option(section, option) and self.config.get(section, option) != '': 35 | return self.config.get(section, option) 36 | 37 | print( 38 | f"[WallEConfig get_config_value()] no key found for option {option} under section {section}" 39 | ) 40 | return None 41 | 42 | def enabled(self, section, option="ENABLED"): 43 | 44 | return self.config.get(section, option) == "1" 45 | 46 | def set_config_value(self, section, option, value): 47 | if self.config.has_option(section, option): 48 | print( 49 | f"[WallEConfig set_config_value()] setting value for section " 50 | f"[{section}] option [{option}]" 51 | ) 52 | self.config.set(section, option, fr'{value}') 53 | else: 54 | raise KeyError(f"Section '{section}' or Option '{option}' does not exist") 55 | 56 | def get_extensions(self): 57 | 58 | extensions = [ 59 | extension 60 | for extension in self.config['extensions'] 61 | if self.enabled("extensions", extension) == 1 62 | ] 63 | return extensions 64 | -------------------------------------------------------------------------------- /wall_e/utilities/config/local.ini: -------------------------------------------------------------------------------- 1 | [basic_config] 2 | TOKEN = 3 | BRANCH_NAME = unused 4 | ENVIRONMENT = 5 | COMPOSE_PROJECT_NAME = 6 | WOLFRAM_API_TOKEN = 7 | MEE6_AUTHORIZATION = 8 | GUILD_ID = 9 | DOCKERIZED = 10 | 11 | [channel_names] 12 | BOT_GENERAL_CHANNEL = 13 | MOD_CHANNEL = 14 | LEVELLING_CHANNEL = 15 | ANNOUNCEMENTS_CHANNEL = 16 | EMBED_AVATAR_CHANNEL = 17 | LEVELLING_WEBSITE_AVATAR_IMAGE_CHANNEL = 18 | INCIDENT_REPORT_CHANNEL = 19 | BOT_MANAGEMENT_CHANNEL = 20 | 21 | [database_config] 22 | WALL_E_DB_DBNAME = 23 | WALL_E_DB_USER = 24 | WALL_E_DB_PASSWORD = 25 | TYPE = 26 | HOST = 27 | DB_PORT = 28 | 29 | [frequency] 30 | ENABLED = 1 31 | 32 | [github] 33 | TOKEN = 34 | 35 | [extensions] 36 | administration = 1 37 | ban = 1 38 | custom_commands = 1 39 | frosh = 0 40 | health_checks = 1 41 | here = 1 42 | leveling = 1 43 | manage_test_guild = 0 44 | misc = 1 45 | mod = 1 46 | reminders = 1 47 | role_commands = 1 48 | sfu = 1 49 | -------------------------------------------------------------------------------- /wall_e/utilities/config/production.ini: -------------------------------------------------------------------------------- 1 | [basic_config] 2 | TOKEN = needs to be declared via Env variable 3 | BRANCH_NAME = needs to be declared via Env variable 4 | ENVIRONMENT = needs to be declared via Env variable 5 | COMPOSE_PROJECT_NAME = needs to be declared via Env variable 6 | WOLFRAM_API_TOKEN = needs to be declared via Env variable 7 | MEE6_AUTHORIZATION = needs to be declared via Env variable 8 | GUILD_ID = 228761314644852736 9 | DOCKERIZED = 1 10 | 11 | [channel_names] 12 | BOT_GENERAL_CHANNEL = bot-commands-and-misc 13 | MOD_CHANNEL = council-summary 14 | LEVELLING_CHANNEL = council 15 | ANNOUNCEMENTS_CHANNEL = announcements 16 | EMBED_AVATAR_CHANNEL = embed_avatars 17 | LEVELLING_WEBSITE_AVATAR_IMAGE_CHANNEL = leveling_website_avatar_images 18 | INCIDENT_REPORT_CHANNEL = incident_reports 19 | BOT_MANAGEMENT_CHANNEL = bot-management 20 | 21 | [database_config] 22 | WALL_E_DB_DBNAME = needs to be declared via Env variable 23 | WALL_E_DB_USER = needs to be declared via Env variable 24 | WALL_E_DB_PASSWORD = needs to be declared via Env variable 25 | TYPE = postgreSQL 26 | 27 | 28 | 29 | [frequency] 30 | ENABLED = 1 31 | 32 | [github] 33 | TOKEN = 34 | 35 | [extensions] 36 | administration = 1 37 | ban = 1 38 | custom_commands = 1 39 | frosh = 0 40 | health_checks = 1 41 | here = 1 42 | leveling = 1 43 | manage_test_guild = 0 44 | misc = 1 45 | mod = 1 46 | reminders = 1 47 | role_commands = 1 48 | sfu = 1 -------------------------------------------------------------------------------- /wall_e/utilities/create_github_issue.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import requests 4 | 5 | 6 | def create_github_issue(error_messages, config): 7 | """ 8 | Files a wall_e error as an issue under the repo 9 | 10 | :param error_messages: the error stack trace to include in the body of the github issue 11 | :param config: used to determine the github csss-admin credentials 12 | :return: 13 | """ 14 | open_issues = requests.get( 15 | url="https://api.github.com/repos/csss/wall_e/issues?state=open&creator=csss-admin", 16 | headers={ 17 | "Accept": "application/vnd.github+json" 18 | } 19 | ).json() 20 | if len(open_issues) > 0: 21 | # exiting the function so that multiple identical issues don't get created by the bot everytime. 22 | # this once led to 1500 issues being created on the wall_e repo for the exact same problem 23 | return 24 | last_message = None 25 | error_message_body = "".join(error_messages) 26 | if "/usr/src/app/" in error_message_body or '= REPORTABLE =' in error_message_body: # if the directory that 27 | # contains the WALL_E code is in the stacktrace then it is probably a guarantee that the issue is due 28 | # to WALL_E and not a problem with discord.py or a network glitch 29 | last_line = len(error_messages) - 1 30 | while last_line > -1: 31 | if error_messages[last_line] != "\n": 32 | last_message = error_messages[last_line] 33 | last_line = -1 34 | else: 35 | last_line -= 1 36 | beginning_of_error_message = re.match( 37 | r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} = ERROR = ", last_message 38 | ) 39 | beginning_of_error_message = beginning_of_error_message.regs[0][1] if beginning_of_error_message else 0 40 | last_message = last_message[beginning_of_error_message:] 41 | discord_internet_issues = [ 42 | "503 Service Unavailable (error code: 0): upstream connect error or disconnect/reset before headers. " 43 | "reset reason: remote connection failure, transport failure reason: immediate connect error: No such " 44 | "file or directory", 45 | "503 Service Unavailable (error code: 0): upstream connect error or disconnect/reset before headers. " 46 | "reset reason: connection termination", 47 | "discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 1000" 48 | ] 49 | if last_message not in discord_internet_issues: 50 | requests.post( 51 | url="https://api.github.com/repos/csss/wall_e/issues", 52 | headers={ 53 | "Accept": "application/vnd.github+json", 54 | "Authorization": f"Bearer {config.get_config_value('github', 'TOKEN')}" 55 | }, 56 | json={ 57 | "title": last_message, 58 | "body": f"```\n{error_message_body}\n```" 59 | } 60 | ) 61 | -------------------------------------------------------------------------------- /wall_e/utilities/discordpy_stream_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from utilities.global_vars import discordpy_logger, discordpy_logger_name 4 | 5 | 6 | class DiscordPyDebugStreamHandler(logging.StreamHandler): 7 | def __init__(self): 8 | super(DiscordPyDebugStreamHandler, self).__init__() 9 | 10 | def emit(self, record): 11 | if record.name != discordpy_logger_name: 12 | for handler in discordpy_logger.handlers: 13 | if record.levelno >= handler.level: 14 | handler.emit(record) 15 | -------------------------------------------------------------------------------- /wall_e/utilities/embed.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from enum import Enum 4 | 5 | import discord 6 | import requests 7 | from discord.ext import commands 8 | 9 | from utilities.global_vars import wall_e_config 10 | 11 | 12 | class WallEColour(Enum): 13 | INFO = 1, 14 | WARNING = 2, 15 | ERROR = 3, 16 | FROSH_2020_THEME = 4 17 | BAN = 5 18 | 19 | 20 | COLOUR_MAPPING = { 21 | WallEColour.INFO: 0x00bfbd, 22 | WallEColour.WARNING: 0xffc61d, 23 | WallEColour.ERROR: 0xA6192E, 24 | WallEColour.FROSH_2020_THEME: 0x00FF61, 25 | WallEColour.BAN: discord.Color.red() 26 | } 27 | 28 | 29 | async def send_func_helper(message, send_func, text_command, reference): 30 | if text_command: 31 | await send_func(message, reference=reference) 32 | else: 33 | await send_func(message) 34 | 35 | 36 | async def embed(logger, ctx: commands.context = None, interaction: discord.Interaction = None, title: str = '', 37 | content: list = None, description: str = '', author: discord.Member = None, author_name: str = '', 38 | author_icon_url: str = '', colour: WallEColour = WallEColour.INFO, thumbnail: str = '', 39 | footer_text: str = '', footer_icon=None, timestamp=None, channels=None, ban_related_message=False, 40 | bot_management_channel=None, validate=True): 41 | """ 42 | Embed creation helper function that validates the input to ensure it does not exceed the discord limits 43 | :param logger: the logger instance from the service 44 | :param ctx: the ctx object that is in the command's arguments if it was a dot command [need to be specified if 45 | no interaction is detected] 46 | :param interaction: the interaction object that is in the command's arguments if it was a slash command [need to 47 | be specified if no ctx is detected] 48 | :param title: the title to assign to the embed [Optional] 49 | 99% of the time it'll be the command name, exceptions when it makes sense like 50 | with the sfu command. 51 | :param content: array of tuples that are the content for the embed that is set to the add_field 52 | part of the embed [Optional] 53 | Tuple per field of the embed. Field name at index 0 and value at index 1. 54 | :param description: the description to assign to the embed [Optional] 55 | Appears under the title. 56 | :param author: the discord Member whose name and avatar has to be used as part of the 57 | author section of the embed [Optional]. 58 | Examples of how to access the Member object with text command 59 | - author = ctx.author # individual who invoked command 60 | - author = ctx.me # bot will be used as author 61 | 62 | Examples of how to access the Member object with slash command 63 | - author = interaction.user # individual who invoked command 64 | - author = interaction.client.user # bot will be used as author 65 | :param author_name: the name to assign to the name part of the embed's author [Optional] 66 | Used to indicate user who invoked the command or the bot itself when it makes sense like with the 67 | echo command. 68 | :param author_icon_url: the avatar to assign to the icon_url part of embed's author [Optional] 69 | Used to set avatar next to author's name. Must be url. 70 | :param colour: the message level to assign to the embed [Optional] 71 | Used to set the coloured strip on the left side of the embed, by default set to a nice blue colour. 72 | :param thumbnail: the thumbnail to assign to the embed [Optional] 73 | Url to image to be used in the embed. Thumbnail appears top right corner of the embed. 74 | :param footer_text: the footer text to assign to the embed [Optional] 75 | :param footer_icon: the icon to assign to the footer [Optional] 76 | :param timestamp: the timestamp to assign to the footer [Optional] 77 | :param channels: the channels in the guild, necessary for the embed that are created from the intercept and 78 | watchdog methods in ban class 79 | :param ban_related_message: indicates if the embed function was called from the ban_related messages which have no 80 | context or interaction object 81 | :param bot_management_channel: provides a way for the non-context and non-interaction classes in the ban class 82 | to send their error messages somewhere 83 | :param validate: boolean to indicate whether or not input needs to be validated or the function should just 84 | construct the embed object and therefore reduce run-time 85 | :return: 86 | """ 87 | if content is None: 88 | content = [] 89 | # these are put in place cause of the limits on embed described here 90 | # https://discord.com/developers/docs/resources/message#embed-object-embed-limits 91 | 92 | if ctx is not None: 93 | # added below ternary because of detect_reaction calls this function without context, but rather passes in 94 | # a channel object 95 | reference = ctx.message if hasattr(ctx, "message") else None 96 | text_command = True 97 | send_func = ctx.send 98 | elif interaction is not None: 99 | reference = None 100 | text_command = False 101 | deferred_interaction = interaction.response.type is not None 102 | if deferred_interaction: 103 | send_func = interaction.followup.send 104 | else: 105 | send_func = interaction.response.send_message 106 | elif ban_related_message: 107 | reference = False 108 | text_command = False 109 | send_func = bot_management_channel.send 110 | else: 111 | raise Exception("did not detect a ctx or interaction method") 112 | if channels is None and ctx is None and interaction is None: 113 | raise Exception("Unable to get the channels on this guild") 114 | 115 | if validate and len(title) > 256: 116 | title = f"{title}" 117 | await send_func_helper( 118 | "Embed Error:\nlength of the title " 119 | f"being added to the title field is {len(title) - 256} characters " 120 | "too big, please cut down to a size of 256", 121 | send_func, text_command, reference 122 | ) 123 | logger.debug(f"[embed.py embed()] length of title [{title}] being added to the field is too big") 124 | return False 125 | if validate and description is None and content is None: 126 | await send_func_helper( 127 | "Embed Error:\nThere need to be either a description or fields specified", 128 | send_func, text_command, reference 129 | ) 130 | logger.debug("[embed.py embed()] There need to be either a description or fields specified") 131 | return False 132 | if validate and description is not None and len(description) > 2048: 133 | await send_func_helper( 134 | f"Embed Error:\nlength of description being added to the " 135 | f"description field is {len(description) - 2048} characters too big, please cut " 136 | "down to a size of 2048", 137 | send_func, text_command, reference 138 | ) 139 | logger.debug(f"[embed.py embed()] length of description [{description}] being added to the " 140 | "field is too big") 141 | return False 142 | 143 | if validate and content is not None and len(content) > 25: 144 | await send_func_helper( 145 | "Embed Error:\nlength of content being added to the content field " 146 | f"is {(len(content) - 25)} indices too big, please cut down to a size of 25", 147 | send_func, text_command, reference 148 | ) 149 | logger.debug("[embed.py embed()] length of content array will be added to the fields is too big") 150 | return False 151 | if validate and content is not None: 152 | for idx, record in enumerate(content): 153 | if len(record[0]) > 256: 154 | await send_func_helper( 155 | f"Embed Error:\nlength of record[0] for content index {idx} being added to the name " 156 | f"field is {(len(record[0]) - 256)} characters too big, please cut down to a size of 256", 157 | send_func, text_command, reference 158 | ) 159 | logger.debug("[embed.py embed()] length of following record being added to the field is too big") 160 | logger.debug(f"[embed.py embed()] {record[0]}") 161 | return False 162 | if len(record[1]) > 1024: 163 | await send_func_helper( 164 | f"Embed Error:\nlength of record[1] for content index {idx} being added to the value " 165 | f"field is {(len(record[1]) - 1024)} characters too big, please cut down to a " 166 | "size of 1024", send_func, text_command, reference 167 | ) 168 | logger.debug("[embed.py embed()] length of following record being added to the field is too big") 169 | logger.debug(f"[embed.py embed()] {record[1]}") 170 | return False 171 | 172 | if validate and len(footer_text) > 2048: 173 | await send_func_helper( 174 | f"Embed Error:\nlength of footer being added to the footer field is " 175 | f"{len(footer_text) - 2048} characters too big, please cut down to a size of 2048", send_func, 176 | text_command, reference 177 | ) 178 | logger.debug(f"[embed.py embed()] length of footer [{footer_text}] being added to the field is too big") 179 | return False 180 | 181 | emb_obj = discord.Embed(title=title, type='rich') 182 | if description is not None: 183 | emb_obj.description = description 184 | if author is not None: 185 | author_name = author.display_name 186 | author_icon_url = author.display_avatar.url 187 | if author_icon_url != "": 188 | if channels is None: 189 | channels = interaction.guild.channels if interaction is not None else ctx.guild.channels 190 | embed_avatar_chan_name = wall_e_config.get_config_value('channel_names', 'EMBED_AVATAR_CHANNEL') 191 | embed_avatar_chan: discord.TextChannel = discord.utils.get(channels, name=embed_avatar_chan_name) 192 | from wall_e_models.models import EmbedAvatar 193 | # the below is needed in case the avatar url that was passed in to this function is deleted at some point 194 | # in the future, which will result in an embed that has a broken avatar 195 | avatar_obj = await EmbedAvatar.get_avatar_by_url(author_icon_url) 196 | if avatar_obj is None: 197 | avatar_file_name = f'avatar-{time.time()*1000}.png' 198 | with open(avatar_file_name, "wb") as file: 199 | file.write(requests.get(author_icon_url).content) 200 | avatar_msg = await embed_avatar_chan.send(file=discord.File(avatar_file_name)) 201 | os.remove(avatar_file_name) 202 | avatar_obj = EmbedAvatar( 203 | avatar_discord_url=author_icon_url, 204 | avatar_discord_permanent_url=avatar_msg.attachments[0].url 205 | ) 206 | await EmbedAvatar.insert_record(avatar_obj) 207 | author_icon_url = avatar_obj.avatar_discord_permanent_url 208 | emb_obj.set_author(name=author_name, icon_url=author_icon_url) 209 | emb_obj.colour = COLOUR_MAPPING[colour] 210 | emb_obj.set_thumbnail(url=thumbnail) 211 | if footer_text or footer_icon: 212 | if footer_icon is None: 213 | emb_obj.set_footer(text=footer_text) 214 | elif footer_text is None: 215 | emb_obj.set_footer(icon_url=footer_icon) 216 | else: 217 | emb_obj.set_footer(text=footer_text, icon_url=footer_icon) 218 | if timestamp: 219 | emb_obj.timestamp = timestamp 220 | # emb_obj.url = link 221 | # parse content to add fields 222 | if content is not None: 223 | for x in content: 224 | inline = x[2] if len(x) > 2 else True 225 | emb_obj.add_field(name=x[0], value=x[1], inline=inline) 226 | return emb_obj 227 | -------------------------------------------------------------------------------- /wall_e/utilities/error_reporter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import re 3 | 4 | from utilities.create_github_issue import create_github_issue 5 | 6 | 7 | async def error_reporter(config, file_path): 8 | """ 9 | Handles detecting any error stack traces in the sys debug log and reporting them both to github and emailing them 10 | to the bot-managers 11 | :param config: used to determine the gmail and github credentials 12 | :param file_path: the path of the file to scan for errors and upload to the text channel 13 | :return: 14 | """ 15 | sys_debug_file = re.match(r"logs/sys/\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2}_debug.log", file_path) 16 | if sys_debug_file: 17 | f = open(file_path, 'r') 18 | f.seek(0) 19 | error_lines = [] 20 | error_pattern = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} = ERROR = ") 21 | non_error_pattern = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} = (INFO|DEBUG) = ") 22 | error_encountered = False 23 | log_issue = False 24 | while True: 25 | f.flush() 26 | lines = f.readlines() 27 | for line in lines: 28 | if error_pattern.match(line): 29 | error_encountered = True 30 | error_lines.append(line) 31 | elif non_error_pattern.match(line) and error_encountered: 32 | log_issue = True 33 | elif error_encountered: 34 | error_lines.append(line) 35 | elif non_error_pattern.match(line): 36 | pass 37 | else: 38 | pass 39 | if log_issue: 40 | log_issue = False 41 | error_encountered = False 42 | create_github_issue(error_lines, config) 43 | error_lines.clear() 44 | if len(lines) == 0 and error_encountered: 45 | log_issue = True 46 | if log_issue: 47 | log_issue = False 48 | error_encountered = False 49 | create_github_issue(error_lines, config) 50 | error_lines.clear() 51 | await asyncio.sleep(5) 52 | -------------------------------------------------------------------------------- /wall_e/utilities/file_uploading.py: -------------------------------------------------------------------------------- 1 | from utilities.error_reporter import error_reporter 2 | from utilities.log_channel import write_to_bot_log_channel 3 | 4 | 5 | async def start_file_uploading(logger, guild, bot, config, file_path, channel_name, categorized_channel=True): 6 | """ 7 | Handles getting the necessary ID of the channel that is then used to upload the log file entries to 8 | :param logger: the logger instance of the calling service 9 | :param guild: the guild that wall_e is running in 10 | :param bot: necessary for getting the channel id and creating the task of uploading to the discord channel 11 | :param config: used to determine the gmail credentials 12 | :param file_path: the path of the file to upload to the text channel 13 | :param channel_name: the name to set for the file log channel 14 | :param categorized_channel: flag to indicate whether the channel that will be created is under the Logs category 15 | :return: 16 | """ 17 | logger.debug(f"[file_uploading.py start_file_uploading()] trying to open {file_path} to be able to send " 18 | f"its output to #{channel_name} channel") 19 | if categorized_channel: 20 | chan_id = await bot.bot_channel_manager.create_or_get_channel_id_for_service_logs( 21 | logger, guild, config, channel_name 22 | ) 23 | else: 24 | chan_id = await bot.bot_channel_manager.create_or_get_channel_id( 25 | logger, guild, config.get_config_value('basic_config', 'ENVIRONMENT'), channel_name 26 | ) 27 | bot.loop.create_task( 28 | write_to_bot_log_channel( 29 | logger, config, bot, file_path, chan_id, channel_name 30 | ) 31 | ) 32 | bot.loop.create_task( 33 | error_reporter(config, file_path) 34 | ) 35 | logger.debug( 36 | f"[file_uploading.py start_file_uploading()] {file_path} successfully opened and connection to " 37 | f"{channel_name} channel has been made" 38 | ) 39 | -------------------------------------------------------------------------------- /wall_e/utilities/global_vars.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import django 4 | from django.core.wsgi import get_wsgi_application 5 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_settings") 7 | django.setup() 8 | 9 | application = get_wsgi_application() 10 | 11 | from utilities.config.config import WallEConfig # noqa E402 12 | from utilities.setup_logger import Loggers # noqa E402 13 | 14 | wall_e_config = WallEConfig(os.environ['basic_config__ENVIRONMENT']) 15 | 16 | log_info = Loggers.get_logger(logger_name="sys") 17 | sys_debug_log_file_absolute_path = log_info[1] 18 | sys_warn_log_file_absolute_path = log_info[2] 19 | sys_error_log_file_absolute_path = log_info[3] 20 | 21 | discordpy_logger_name = "discord.py" 22 | discordpy_log_info = Loggers.get_logger(logger_name=discordpy_logger_name) 23 | discordpy_logger = discordpy_log_info[0] 24 | discordpy_debug_log_file_absolute_path = discordpy_log_info[1] 25 | discordpy_warn_log_file_absolute_path = discordpy_log_info[2] 26 | discordpy_error_log_file_absolute_path = discordpy_log_info[3] 27 | 28 | log_info = Loggers.get_logger(logger_name="wall_e") 29 | 30 | logger = log_info[0] 31 | wall_e_debug_log_file_absolute_path = log_info[1] 32 | wall_e_warn_log_file_absolute_path = log_info[2] 33 | wall_e_error_log_file_absolute_path = log_info[3] 34 | 35 | incident_report_log_info = Loggers.get_logger(logger_name="incident_report") 36 | 37 | incident_report_logger = incident_report_log_info[0] 38 | incident_report_debug_log_file_absolute_path = incident_report_log_info[1] 39 | 40 | from utilities.wall_e_bot import WalleBot # noqa E402 41 | 42 | bot = WalleBot() 43 | -------------------------------------------------------------------------------- /wall_e/utilities/log_channel.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import discord 4 | 5 | 6 | async def write_to_bot_log_channel(logger, config, bot, file_path, chan_id, channel_name): 7 | """ 8 | Takes care of opening a file and keeping it opening while reading from it and uploading it's contents 9 | to the specified channel 10 | :param logger: the service's logger instance 11 | :param config: used to determine the gmail credentials 12 | :param bot: needed to get the channel ID for the channel that the logs will be sent to and ensure the 13 | while loop only runs while bot.is_closed() is False 14 | :param file_path: the path of the log file to upload to the text channel 15 | :param chan_id: the ID of the channel that the log file lines will be uploaded to 16 | :param channel_name: the name set for the file log channel 17 | :return: 18 | """ 19 | channel = discord.utils.get( 20 | bot.guilds[0].channels, id=chan_id 21 | ) 22 | logger.debug( 23 | f"[log_channel.py write_to_bot_log_channel()] {channel} channel " 24 | f"with id {chan_id} successfully retrieved." 25 | ) 26 | f = open(file_path, 'r') 27 | f.seek(0) 28 | channels_with_rate_limit = channel_name in [ 29 | 'leveling_debug', 'role_commands_debug', 'process_lurkers', "update_outdated_profile_pics" 30 | ] 31 | incident_report_chanel_name = config.get_config_value('channel_names', 'INCIDENT_REPORT_CHANNEL') 32 | while not bot.is_closed(): 33 | f.flush() 34 | line = f.readline() 35 | if line and line.strip() != "": 36 | # this was done so that no one gets accidentally pinged from the bot log channel 37 | if channel.name != incident_report_chanel_name: 38 | line = line.replace("@", "[at]") 39 | if line[0] == ' ': 40 | line = f".{line}" 41 | output = line 42 | # done because discord has a character limit of 2000 for each message 43 | # so what basically happens is it first tries to send the full message, then if it cant, it 44 | # breaks it down into 2000 sizes messages and send them individually 45 | message_sent = False 46 | try: 47 | await channel.send(output) 48 | message_sent = True 49 | except (aiohttp.ClientError, discord.errors.HTTPException): 50 | finished = False 51 | first_index, last_index = 0, 2000 52 | while not finished: 53 | await channel.send(output[first_index:last_index]) 54 | message_sent = True 55 | first_index = last_index 56 | last_index += 2000 57 | if len(output[first_index:last_index]) == 0: 58 | finished = True 59 | except RuntimeError: 60 | logger.debug( 61 | "[log_channel.py write_to_bot_log_channel()] encountered RuntimeError, " 62 | " will assume that the user is attempting to exit" 63 | ) 64 | break 65 | except Exception as exc: 66 | exc_str = f'{type(exc).__name__}: {exc}' 67 | raise Exception( 68 | f'[log_channel.py write_to_bot_log_channel()] write to channel failed\n{exc_str}' 69 | ) 70 | if message_sent and channels_with_rate_limit: 71 | # adding a sleep cause the amount of debug logs that I print due to the wall_e_models module can 72 | # trigger a Rate Limit exception if done too fast 73 | await asyncio.sleep(3) 74 | await asyncio.sleep(1) 75 | -------------------------------------------------------------------------------- /wall_e/utilities/send.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import discord 3 | 4 | 5 | def get_last_index(logger, content, index, reserved_space): 6 | """ 7 | This when the the size of contents is too big for a single discord message, this means 8 | that the message has to be split. and in order to make the output most visually appealing 9 | when splitting it is to see if there is a newline on which the message can be split instead. 10 | if there is no suitable newline, it will instead just cut down an existing line 11 | :param logger: the calling service's logger object 12 | :param content: the string that need to be cut down on 13 | :param index: the index to start of on when determining what is the max string that can be sent 14 | :param reserved_space: any prefixes that may need to be sent in all the messages that contain 15 | the split up string 16 | :return: the latest index that may need to be used in the next call to this function 17 | """ 18 | 19 | logger.debug(f"[send.py get_last_index()] index =[{index}] reserved_space =[{reserved_space}]") 20 | if len(content) - index < 2000 - reserved_space: 21 | logger.debug(f"[send.py get_last_index()] returning length of content =[{len(content)}]") 22 | return len(content) 23 | else: 24 | index_of_new_line = content.rfind('\n', index, index + (2000 - reserved_space)) 25 | if index_of_new_line != 0: 26 | last_index = index_of_new_line 27 | else: 28 | last_index = 2000 - reserved_space 29 | logger.debug(f"[send.py get_last_index()] index_of_new_line =[{index_of_new_line}]") 30 | return last_index 31 | 32 | 33 | async def helper_send(logger, ctx, content=None, tts=False, embed=None, file=None, files=None, 34 | delete_after=None, nonce=None, prefix=None, suffix=None, reference=None): 35 | """ 36 | send helper function that helps when dealing with a message that has too many characters 37 | :param logger: the calling service's logger object 38 | :param ctx: the ctx object that is in a command's parameter if it is a dot command 39 | :param content: the message that may need to be cut down 40 | :param tts: the tts flag to send to the discord send message 41 | :param embed: the embed that will need to be included in all the messages sent that contain 42 | the cut down content 43 | :param file: the file that will need to be included in all the messages sent that contain 44 | the cut down content 45 | :param files: the files that will need to be included in all the messages sent that contain 46 | the cut down content 47 | :param delete_after: how long to wait before deleting all the messages that were sent that contain 48 | the cut down content 49 | :param nonce: the nonce flag to send to the discord send message 50 | :param prefix: the prefix to surround all the messages that contain the cut down content 51 | :param suffix: the suffix to surround all the messages that contain the cut down content 52 | :param reference: the original message this message being sent is a reference to 53 | :return: 54 | """ 55 | # adds the requested prefix and suffic to the contents 56 | formatted_content = content 57 | if prefix is not None: 58 | formatted_content = prefix + content 59 | if suffix is not None: 60 | formatted_content = formatted_content + suffix 61 | 62 | # so what basically happens is it first tries to send the full message, then if it cant, it breaks it 63 | # down into 2000 sizes messages and send them individually 64 | try: 65 | await ctx.send(formatted_content, reference=reference) 66 | except (aiohttp.ClientError, discord.errors.HTTPException): 67 | # used for determing how much space for each of the messages need to be reserved for the requested suffix 68 | # and prefix 69 | reserved_space = 0 70 | if prefix is not None: 71 | reserved_space += len(prefix) 72 | if suffix is not None: 73 | reserved_space += len(suffix) 74 | logger.debug(f"[send.py send()] reserved_space = [{reserved_space}]") 75 | last_index = get_last_index(logger, content, 0, reserved_space) 76 | first = True # this is only necessary because it wouldnt make sense to have any potential embeds or file[s] 77 | # with each message 78 | first_index = 0 79 | finished = False 80 | while not finished: 81 | if first: 82 | first = False 83 | formatted_content = content[first_index:last_index] 84 | if prefix is not None: 85 | formatted_content = prefix + formatted_content 86 | if suffix is not None: 87 | formatted_content = formatted_content + suffix 88 | logger.debug( 89 | f"[send.py send()] messaage sent off with first_index = [{first_index}] and last_index =" 90 | f" [{last_index}]" 91 | ) 92 | await ctx.send(formatted_content, tts=tts, embed=embed, file=file, files=files, 93 | delete_after=delete_after, nonce=nonce, reference=reference) 94 | else: 95 | formatted_content = content[first_index:last_index] 96 | if prefix is not None: 97 | formatted_content = prefix + formatted_content 98 | if suffix is not None: 99 | formatted_content = formatted_content + suffix 100 | logger.debug( 101 | f"[send.py send()] messaage sent off with first_index = [{first_index}] and last_index = " 102 | f"[{last_index}]" 103 | ) 104 | await ctx.send( 105 | formatted_content, tts=tts, delete_after=delete_after, nonce=nonce, reference=reference 106 | ) 107 | first_index = last_index 108 | last_index = get_last_index(logger, content, first_index + 1, reserved_space) 109 | logger.debug(f"[send.py send()] last_index updated to {last_index}") 110 | if len(content[first_index:last_index]) == 0: 111 | finished = True 112 | except Exception as exc: 113 | exc_str = f'{type(exc).__name__}: {exc}' 114 | logger.error(f'[send.py send()] write to channel failed\n{exc_str}') 115 | -------------------------------------------------------------------------------- /wall_e/utilities/setup_logger.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import os 4 | import sys 5 | from traceback import TracebackException 6 | 7 | import pytz 8 | 9 | WALL_E_LOG_HANDLER_NAME = "wall_e" 10 | SYS_LOG_HANDLER_NAME = "sys" 11 | 12 | date_timezone = pytz.timezone('US/Pacific') 13 | 14 | error_logging_level = logging.ERROR 15 | 16 | warn_logging_level = logging.WARNING 17 | 18 | 19 | class WalleWarnStreamHandler(logging.StreamHandler): 20 | def emit(self, record): 21 | if record.levelno < error_logging_level: 22 | super().emit(record) 23 | 24 | 25 | class WalleDebugStreamHandler(logging.StreamHandler): 26 | def emit(self, record): 27 | if record.levelno < warn_logging_level: 28 | super().emit(record) 29 | 30 | 31 | class PSTFormatter(logging.Formatter): 32 | def __init__(self, fmt=None, datefmt=None, tz=None): 33 | super(PSTFormatter, self).__init__(fmt, datefmt) 34 | self.tz = tz 35 | 36 | def formatTime(self, record, datefmt=None): # noqa: N802 37 | """ 38 | 39 | :param record: 40 | :param datefmt: 41 | :return: 42 | """ 43 | dt = datetime.datetime.fromtimestamp(record.created, self.tz) 44 | if datefmt: 45 | return dt.strftime(datefmt) 46 | else: 47 | return str(dt) 48 | 49 | 50 | REDIRECT_STD_STREAMS = True 51 | date_formatting_in_log = '%Y-%m-%d %H:%M:%S' 52 | date_formatting_in_filename = "%Y_%m_%d_%H_%M_%S" 53 | sys_stream_formatting = PSTFormatter( 54 | '%(asctime)s = %(levelname)s = %(name)s = %(message)s', date_formatting_in_log, tz=date_timezone 55 | ) 56 | # creates an easy way for create_github_issue to tell when an issue that does not have a stacktrace still needs 57 | # to have github issue created 58 | reportable_error_formatting = PSTFormatter( 59 | '%(asctime)s = %(levelname)s = REPORTABLE = %(name)s = %(message)s', date_formatting_in_log, tz=date_timezone 60 | ) 61 | 62 | 63 | class Loggers: 64 | loggers = [] 65 | logger_list_indices = {} 66 | 67 | @classmethod 68 | def get_logger(cls, logger_name): 69 | """ 70 | Initiates and returns a logger for the specific logger_name 71 | :param logger_name: the name to assign to the returned logic 72 | :return:the logger 73 | """ 74 | if logger_name == SYS_LOG_HANDLER_NAME: 75 | return cls._setup_sys_logger() 76 | else: 77 | return cls._setup_logger(logger_name) 78 | 79 | @classmethod 80 | def _setup_sys_logger(cls): 81 | """ 82 | Creates a sys logger that directs anything going to sys.stdout/err to a log file 83 | and the stream as well 84 | :return: the sys logger 85 | """ 86 | date = datetime.datetime.now(date_timezone).strftime(date_formatting_in_filename) 87 | if not os.path.exists(f"logs/{SYS_LOG_HANDLER_NAME}"): 88 | os.makedirs(f"logs/{SYS_LOG_HANDLER_NAME}") 89 | if not os.path.exists(f"logs/{SYS_LOG_HANDLER_NAME}"): 90 | os.makedirs(f"logs/{SYS_LOG_HANDLER_NAME}") 91 | 92 | sys_logger = logging.getLogger(SYS_LOG_HANDLER_NAME) 93 | sys_logger.setLevel(logging.DEBUG) 94 | 95 | debug_log_file_absolute_path = ( 96 | f"logs/{SYS_LOG_HANDLER_NAME}/{date}_debug.log" 97 | ) 98 | sys_stream_warn_log_file_absolute_path = ( 99 | f"logs/{SYS_LOG_HANDLER_NAME}/{date}_warn.log" 100 | ) 101 | sys_stream_error_log_file_absolute_path = ( 102 | f"logs/{SYS_LOG_HANDLER_NAME}/{date}_error.log" 103 | ) 104 | 105 | # ensures that anything printed to this logger at level DEBUG or above goes to the specified file 106 | debug_filehandler = logging.FileHandler(debug_log_file_absolute_path) 107 | debug_filehandler.setLevel(logging.DEBUG) 108 | sys_logger.addHandler(debug_filehandler) 109 | 110 | # ensures that anything printed to this logger at level WARN or above goes to the specified file 111 | warn_filehandler = logging.FileHandler(sys_stream_warn_log_file_absolute_path) 112 | warn_filehandler.setLevel(warn_logging_level) 113 | sys_logger.addHandler(warn_filehandler) 114 | 115 | # ensures that anything printed to this logger at level ERROR or above goes to the specified file 116 | error_filehandler = logging.FileHandler(sys_stream_error_log_file_absolute_path) 117 | error_filehandler.setLevel(error_logging_level) 118 | sys_logger.addHandler(error_filehandler) 119 | 120 | # ensures that anything from the log goes to the stdout 121 | if REDIRECT_STD_STREAMS: 122 | sys.stdout = sys.__stdout__ 123 | sys_stdout_stream_handler = WalleDebugStreamHandler(sys.stdout) 124 | sys_stdout_stream_handler.setLevel(logging.DEBUG) 125 | sys_logger.addHandler(sys_stdout_stream_handler) 126 | if REDIRECT_STD_STREAMS: 127 | sys.stdout = LoggerWriter(sys_logger.info) 128 | 129 | sys_std_warn_stream_handler = WalleWarnStreamHandler(sys.stdout) 130 | sys_std_warn_stream_handler.setLevel(warn_logging_level) 131 | sys_logger.addHandler(sys_std_warn_stream_handler) 132 | 133 | if REDIRECT_STD_STREAMS: 134 | sys.stderr = sys.__stderr__ 135 | sys_stderr_stream_handler = logging.StreamHandler(sys.stderr) 136 | sys_stderr_stream_handler.setLevel(error_logging_level) 137 | sys_logger.addHandler(sys_stderr_stream_handler) 138 | if REDIRECT_STD_STREAMS: 139 | sys.stderr = LoggerWriter(sys_logger.error) 140 | 141 | return ( 142 | sys_logger, debug_log_file_absolute_path, sys_stream_warn_log_file_absolute_path, 143 | sys_stream_error_log_file_absolute_path 144 | ) 145 | 146 | @classmethod 147 | def _setup_logger(cls, service_name): 148 | """ 149 | Creates a logger for the specified service that prints to a file and the sys.stdout 150 | and sys.stderr 151 | :param service_name: the name of the service that is initializing the logger 152 | :return: the logger 153 | """ 154 | date = datetime.datetime.now(date_timezone).strftime(date_formatting_in_filename) 155 | if not os.path.exists(f"logs/{service_name}"): 156 | os.makedirs(f"logs/{service_name}") 157 | debug_log_file_absolute_path = f"logs/{service_name}/{date}_debug.log" 158 | warn_log_file_absolute_path = f"logs/{service_name}/{date}_warn.log" 159 | error_log_file_absolute_path = f"logs/{service_name}/{date}_error.log" 160 | 161 | logger = logging.getLogger(service_name) 162 | logger.setLevel(logging.DEBUG) 163 | 164 | debug_filehandler = logging.FileHandler(debug_log_file_absolute_path) 165 | debug_filehandler.setLevel(logging.DEBUG) 166 | debug_filehandler.setFormatter(sys_stream_formatting) 167 | logger.addHandler(debug_filehandler) 168 | 169 | warn_filehandler = logging.FileHandler(warn_log_file_absolute_path) 170 | warn_filehandler.setFormatter(sys_stream_formatting) 171 | warn_filehandler.setLevel(warn_logging_level) 172 | logger.addHandler(warn_filehandler) 173 | 174 | error_filehandler = logging.FileHandler(error_log_file_absolute_path) 175 | error_filehandler.setFormatter(reportable_error_formatting) 176 | error_filehandler.setLevel(error_logging_level) 177 | logger.addHandler(error_filehandler) 178 | 179 | sys_stdout_stream_handler = WalleDebugStreamHandler(sys.stdout) 180 | sys_stdout_stream_handler.setFormatter(sys_stream_formatting) 181 | sys_stdout_stream_handler.setLevel(logging.DEBUG) 182 | logger.addHandler(sys_stdout_stream_handler) 183 | 184 | sys_std_warn_stream_handler = WalleWarnStreamHandler(sys.stdout) 185 | sys_std_warn_stream_handler.setFormatter(sys_stream_formatting) 186 | sys_std_warn_stream_handler.setLevel(warn_logging_level) 187 | logger.addHandler(sys_std_warn_stream_handler) 188 | 189 | sys_sterr_stream_handler = logging.StreamHandler() 190 | sys_sterr_stream_handler.setFormatter(reportable_error_formatting) 191 | sys_sterr_stream_handler.setLevel(error_logging_level) 192 | logger.addHandler(sys_sterr_stream_handler) 193 | 194 | return ( 195 | logger, debug_log_file_absolute_path, warn_log_file_absolute_path, error_log_file_absolute_path 196 | ) 197 | 198 | 199 | class LoggerWriter: 200 | def __init__(self, level): 201 | """ 202 | User to direct the sys.stdout/err to the specified log level 203 | :param level: 204 | """ 205 | self.level = level 206 | 207 | def write(self, message): 208 | """ 209 | writes from the sys.stdout/err to the logger object for sys_logger 210 | :param message: the message to write to the log 211 | :return: 212 | """ 213 | if message != '\n': 214 | # removing newline that is created [I believe] when stdout automatically adds a newline to the string 215 | # before passing it to this method, and self.level itself also adds a newline 216 | message = message[:-1] if message[-1:] == "\n" else message 217 | self.level(message) 218 | 219 | def flush(self): 220 | pass 221 | 222 | 223 | def print_wall_e_exception(value, tb, error_logger, limit=None, chain=True): 224 | """ 225 | Used to print the stack trace to a specific logger if there is an error 226 | duplicates traceback.print_exception() for a logger object instead 227 | :param value: the error that was encountered 228 | :param tb: the traceback 229 | :param error_logger: the logger to direct the error to 230 | :param limit: if there is a limit the user wants to specify for prnting 231 | :param chain: i dont actually know and dont care to look into 232 | :return: 233 | """ 234 | for line in TracebackException(type(value), value, tb, limit=limit).format(chain=chain): 235 | error_logger(line) 236 | -------------------------------------------------------------------------------- /wall_e/utilities/slash_command_examples.json: -------------------------------------------------------------------------------- 1 | { 2 | "tex option": { 3 | "header": "/Tex Examples", 4 | "description": "* `/tex e^{i\\theta} = \\cos x + i \\sin x`\n* `/tex x = 2*\\pi*n_{1} + Re(\\theta) + iIm(\\theta)`" 5 | } 6 | } -------------------------------------------------------------------------------- /wall_e/utilities/wall_e_bot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from typing import Optional, Sequence 4 | 5 | import discord 6 | from discord import Intents, Message 7 | from discord.abc import Snowflake 8 | from discord.ext import commands 9 | from discord.ext.commands import Cog 10 | from discord.utils import MISSING 11 | 12 | from utilities.global_vars import wall_e_config, logger, sys_debug_log_file_absolute_path, \ 13 | sys_error_log_file_absolute_path, wall_e_debug_log_file_absolute_path, wall_e_error_log_file_absolute_path, \ 14 | discordpy_debug_log_file_absolute_path, discordpy_error_log_file_absolute_path, \ 15 | incident_report_debug_log_file_absolute_path, sys_warn_log_file_absolute_path, \ 16 | wall_e_warn_log_file_absolute_path, discordpy_warn_log_file_absolute_path 17 | 18 | from extensions.help_commands import EmbedHelpCommand 19 | from overriden_coroutines.delete_help_messages import delete_help_command_messages 20 | from overriden_coroutines.detect_reactions import reaction_detected 21 | from overriden_coroutines.error_handlers import report_text_command_error, report_slash_command_error 22 | from utilities.bot_channel_manager import BotChannelManager 23 | from utilities.discordpy_stream_handler import DiscordPyDebugStreamHandler 24 | from utilities.embed import embed as imported_embed 25 | from utilities.file_uploading import start_file_uploading 26 | 27 | intents = Intents.all() 28 | 29 | extension_location_python_path = "extensions." 30 | 31 | 32 | class WalleBot(commands.Bot): 33 | def __init__(self): 34 | self.bot_channel_manager = BotChannelManager(wall_e_config, self) 35 | self.listeners = [] 36 | super().__init__(command_prefix='.', intents=intents, help_command=EmbedHelpCommand()) 37 | self.uploading = False 38 | 39 | def run( 40 | self, token: str = None, *, reconnect: bool = True, log_handler: Optional[logging.Handler] = MISSING, 41 | log_formatter: logging.Formatter = MISSING, log_level: int = MISSING, root_logger: bool = False) -> None: 42 | logger.info("[wall_e_bot.py] Wall-E is starting up") 43 | super(WalleBot, self).run( 44 | token, 45 | log_handler=DiscordPyDebugStreamHandler() 46 | ) 47 | 48 | async def setup_hook(self) -> None: 49 | self.add_listener(report_text_command_error, "on_command_error") 50 | self.tree.on_error = report_slash_command_error 51 | self.add_listener(reaction_detected, "on_raw_reaction_add") 52 | delete_help_command_messages.start() 53 | 54 | await self.add_custom_extension() 55 | logger.debug("[wall_e_bot.py] extensions loaded") 56 | await super().setup_hook() 57 | 58 | async def add_custom_extension(self, module_path_and_name: str = None): 59 | adding_all_extensions = module_path_and_name is None 60 | extension_unloaded = False 61 | for extension in wall_e_config.get_extensions(): 62 | if extension_unloaded: 63 | break 64 | try: 65 | logger.debug(f"[wall_e_bot.py] attempting to load extension {extension} ") 66 | # the below piece of logic will not work well in the test guild if there 67 | # are multiple PRs being worked on at the same time that have different 68 | # slash command as there is no way to avoid a conflict. I tried to fix this 69 | # by having the bot in multiple guilds, one for each PR, but if an extension is loaded to wall_e 70 | # after the on_ready signal has already been received, any on_ready functions in the extension 71 | # that is loaded will not be run, which is a pre-req for almost all the extensions 72 | await self.load_extension(extension) 73 | logger.debug(f"[wall_e_bot.py] {extension} successfully loaded") 74 | if not adding_all_extensions: 75 | extension_unloaded = True 76 | break 77 | except Exception as err: 78 | exception = f'{type(err).__name__}: {err}' 79 | logger.error(f'[wall_e_bot.py] Failed to load extension {extension}\n{exception}') 80 | if adding_all_extensions: 81 | time.sleep(20) 82 | exit(1) 83 | 84 | async def load_extension(self, name: str, *, package: Optional[str] = None) -> None: 85 | extension_name = name if extension_location_python_path in name else f"{extension_location_python_path}{name}" 86 | await super(WalleBot, self).load_extension(extension_name, package=package) 87 | 88 | async def unload_extension(self, name: str, *, package: Optional[str] = None) -> None: 89 | extension_name = name if extension_location_python_path in name else f"{extension_location_python_path}{name}" 90 | await super(WalleBot, self).unload_extension(extension_name, package=package) 91 | 92 | async def reload_extension(self, name: str, *, package: Optional[str] = None) -> None: 93 | await super(WalleBot, self).reload_extension(f"{extension_location_python_path}{name}", package=package) 94 | 95 | async def add_cog( 96 | self, 97 | cog: Cog, 98 | /, 99 | *, 100 | override: bool = False, 101 | guild: Optional[Snowflake] = MISSING, 102 | guilds: Sequence[Snowflake] = MISSING, 103 | ) -> None: 104 | guild = discord.Object(id=int(wall_e_config.get_config_value("basic_config", "GUILD_ID"))) 105 | await super(WalleBot, self).add_cog(cog, override=override, guild=guild, guilds=guilds) 106 | 107 | async def on_message(self, message: Message, /) -> None: 108 | """ 109 | Function that gets called any input or output from the script 110 | :param message: 111 | :return: 112 | """ 113 | if message.guild is None and message.author != self.user: 114 | em = await imported_embed( 115 | logger, 116 | ctx=message.author, 117 | description="[welcome to the machine](https://platform.openai.com/login?launch)" 118 | ) 119 | if em is not None: 120 | await message.author.send(embed=em) 121 | else: 122 | await self.process_commands(message) 123 | 124 | async def on_ready(self): 125 | """ 126 | indicator that all functions that use "wait_until_ready" will start running soon 127 | :return: 128 | """ 129 | bot_guild = self.guilds[0] 130 | # tries to open log file in prep for write_to_bot_log_channel function 131 | if self.uploading is False: 132 | try: 133 | await start_file_uploading( 134 | logger, bot_guild, self, wall_e_config, sys_debug_log_file_absolute_path, "sys_debug" 135 | ) 136 | await start_file_uploading( 137 | logger, bot_guild, self, wall_e_config, sys_warn_log_file_absolute_path, "sys_warn" 138 | ) 139 | await start_file_uploading( 140 | logger, bot_guild, self, wall_e_config, sys_error_log_file_absolute_path, "sys_error" 141 | ) 142 | await start_file_uploading( 143 | logger, bot_guild, self, wall_e_config, wall_e_debug_log_file_absolute_path, "wall_e_debug" 144 | ) 145 | await start_file_uploading( 146 | logger, bot_guild, self, wall_e_config, wall_e_warn_log_file_absolute_path, "wall_e_warn" 147 | ) 148 | await start_file_uploading( 149 | logger, bot_guild, self, wall_e_config, wall_e_error_log_file_absolute_path, "wall_e_error" 150 | ) 151 | await start_file_uploading( 152 | logger, bot_guild, self, wall_e_config, discordpy_debug_log_file_absolute_path, "discordpy_debug" 153 | ) 154 | await start_file_uploading( 155 | logger, bot_guild, self, wall_e_config, discordpy_warn_log_file_absolute_path, "discordpy_warn" 156 | ) 157 | await start_file_uploading( 158 | logger, bot_guild, self, wall_e_config, discordpy_error_log_file_absolute_path, "discordpy_error" 159 | ) 160 | await start_file_uploading( 161 | logger, bot_guild, self, wall_e_config, incident_report_debug_log_file_absolute_path, 162 | wall_e_config.get_config_value('channel_names', 'INCIDENT_REPORT_CHANNEL'), 163 | categorized_channel=False 164 | ) 165 | await self.bot_channel_manager.create_or_get_channel_id( 166 | logger, bot_guild, wall_e_config.get_config_value('basic_config', 'ENVIRONMENT'), 167 | wall_e_config.get_config_value('channel_names', 'EMBED_AVATAR_CHANNEL'), 168 | ) 169 | await self.bot_channel_manager.create_or_get_channel_id_for_service_logs( 170 | logger, bot_guild, wall_e_config, "member_update_listener_debug" 171 | ) 172 | await self.bot_channel_manager.create_or_get_channel_id_for_service_logs( 173 | logger, bot_guild, wall_e_config, "member_update_listener_warn" 174 | ) 175 | await self.bot_channel_manager.create_or_get_channel_id_for_service_logs( 176 | logger, bot_guild, wall_e_config, "member_update_listener_error" 177 | ) 178 | await self.bot_channel_manager.create_or_get_channel_id_for_service_logs( 179 | logger, bot_guild, wall_e_config, "member_update_listener_discordpy_debug" 180 | ) 181 | await self.bot_channel_manager.create_or_get_channel_id_for_service_logs( 182 | logger, bot_guild, wall_e_config, "member_update_listener_discordpy_warn" 183 | ) 184 | await self.bot_channel_manager.create_or_get_channel_id_for_service_logs( 185 | logger, bot_guild, wall_e_config, "member_update_listener_discordpy_error" 186 | ) 187 | await self.bot_channel_manager.fix_text_channel_positioning(logger, guild=bot_guild) 188 | self.uploading = True 189 | except Exception as e: 190 | raise Exception( 191 | "[wall_e_bot.py] Could not open log file to read from and sent entries to bot_log channel due to " 192 | f"following error {e}") 193 | logger.info('[wall_e_bot.py on_ready()] Logged in as') 194 | logger.info(f'[wall_e_bot.py on_ready()] {self.user.name}({self.user.id})') 195 | logger.info('[wall_e_bot.py on_ready()] ------') 196 | -------------------------------------------------------------------------------- /wall_e/wait-for-postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # needed for CI/user_scripts/setup-dev-env.sh 3 | 4 | # aquired from https://docs.docker.com/compose/startup-order/ 5 | set -e 6 | 7 | host="$1" 8 | shift 9 | cmd="$@" 10 | 11 | until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$host" -U "postgres" -c '\q'; do 12 | >&2 echo "Postgres is unavailable - sleeping" 13 | sleep 1 14 | done 15 | 16 | >&2 echo "Postgres is up - executing command" 17 | 18 | PGPASSWORD=$POSTGRES_PASSWORD psql --set=WALL_E_DB_USER="${database_config__WALL_E_DB_USER}" \ 19 | --set=WALL_E_DB_PASSWORD="${database_config__WALL_E_DB_PASSWORD}" \ 20 | --set=WALL_E_DB_DBNAME="${database_config__WALL_E_DB_DBNAME}" \ 21 | -h "$host" -U "postgres" -f WalleModels/create-database.ddl 22 | 23 | python3 django_manage.py makemigrations 24 | python3 django_manage.py migrate 25 | 26 | exec $cmd 27 | 28 | -------------------------------------------------------------------------------- /wall_e/wall_e_models: -------------------------------------------------------------------------------- 1 | ../.wall_e_models/wall_e_models -------------------------------------------------------------------------------- /wall_e_pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CSSS/wall_e/839a6468219ef25ea0592d30ea5f91aa42d13a80/wall_e_pic.jpg --------------------------------------------------------------------------------