diff --git a/.gitignore b/.gitignore index 70dffab..4a2e9dd 100644 --- a/.gitignore +++ b/.gitignore @@ -157,6 +157,3 @@ cython_debug/ # Debugging files debug.py - -# Schedule .ics files -/files/schedules/ diff --git a/alembic/versions/08d21b2d1a0a_deadlines.py b/alembic/versions/08d21b2d1a0a_deadlines.py new file mode 100644 index 0000000..25147cf --- /dev/null +++ b/alembic/versions/08d21b2d1a0a_deadlines.py @@ -0,0 +1,39 @@ +"""Deadlines + +Revision ID: 08d21b2d1a0a +Revises: 3962636f3a3d +Create Date: 2022-08-12 23:44:13.947011 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "08d21b2d1a0a" +down_revision = "3962636f3a3d" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "deadlines", + sa.Column("deadline_id", sa.Integer(), nullable=False), + sa.Column("course_id", sa.Integer(), nullable=True), + sa.Column("name", sa.Text(), nullable=False), + sa.Column("deadline", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint( + ["course_id"], + ["ufora_courses.course_id"], + ), + sa.PrimaryKeyConstraint("deadline_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("deadlines") + # ### end Alembic commands ### diff --git a/alembic/versions/0d03c226d881_initial_currency_models.py b/alembic/versions/0d03c226d881_initial_currency_models.py new file mode 100644 index 0000000..feec2c1 --- /dev/null +++ b/alembic/versions/0d03c226d881_initial_currency_models.py @@ -0,0 +1,56 @@ +"""Initial currency models + +Revision ID: 0d03c226d881 +Revises: b2d511552a1f +Create Date: 2022-06-30 20:02:27.284759 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "0d03c226d881" +down_revision = "b2d511552a1f" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table("users", sa.Column("user_id", sa.BigInteger(), nullable=False), sa.PrimaryKeyConstraint("user_id")) + op.create_table( + "bank", + sa.Column("bank_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=True), + sa.Column("dinks", sa.BigInteger(), server_default="0", nullable=False), + sa.Column("interest_level", sa.Integer(), server_default="1", nullable=False), + sa.Column("capacity_level", sa.Integer(), server_default="1", nullable=False), + sa.Column("rob_level", sa.Integer(), server_default="1", nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.user_id"], + ), + sa.PrimaryKeyConstraint("bank_id"), + ) + op.create_table( + "nightly_data", + sa.Column("nightly_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=True), + sa.Column("last_nightly", sa.Date, nullable=True), + sa.Column("count", sa.Integer(), server_default="0", nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.user_id"], + ), + sa.PrimaryKeyConstraint("nightly_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("nightly_data") + op.drop_table("bank") + op.drop_table("users") + # ### end Alembic commands ### diff --git a/alembic/versions/1716bfecf684_add_birthdays.py b/alembic/versions/1716bfecf684_add_birthdays.py new file mode 100644 index 0000000..5f01615 --- /dev/null +++ b/alembic/versions/1716bfecf684_add_birthdays.py @@ -0,0 +1,38 @@ +"""Add birthdays + +Revision ID: 1716bfecf684 +Revises: 581ae6511b98 +Create Date: 2022-07-19 21:46:42.796349 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "1716bfecf684" +down_revision = "581ae6511b98" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "birthdays", + sa.Column("birthday_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=True), + sa.Column("birthday", sa.Date, nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.user_id"], + ), + sa.PrimaryKeyConstraint("birthday_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("birthdays") + # ### end Alembic commands ### diff --git a/alembic/versions/346b408c362a_create_tasks.py b/alembic/versions/346b408c362a_create_tasks.py new file mode 100644 index 0000000..f6efeeb --- /dev/null +++ b/alembic/versions/346b408c362a_create_tasks.py @@ -0,0 +1,36 @@ +"""Create tasks + +Revision ID: 346b408c362a +Revises: 1716bfecf684 +Create Date: 2022-07-23 19:41:07.029482 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "346b408c362a" +down_revision = "1716bfecf684" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "tasks", + sa.Column("task_id", sa.Integer(), nullable=False), + sa.Column("task", sa.Enum("BIRTHDAYS", "UFORA_ANNOUNCEMENTS", name="tasktype"), nullable=False), + sa.Column("previous_run", sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint("task_id"), + sa.UniqueConstraint("task"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("tasks") + sa.Enum("BIRTHDAYS", "UFORA_ANNOUNCEMENTS", name="tasktype").drop(op.get_bind()) + # ### end Alembic commands ### diff --git a/alembic/versions/36300b558ef1_meme_templates.py b/alembic/versions/36300b558ef1_meme_templates.py new file mode 100644 index 0000000..275133a --- /dev/null +++ b/alembic/versions/36300b558ef1_meme_templates.py @@ -0,0 +1,37 @@ +"""Meme templates + +Revision ID: 36300b558ef1 +Revises: 08d21b2d1a0a +Create Date: 2022-08-25 01:34:22.845955 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "36300b558ef1" +down_revision = "08d21b2d1a0a" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "meme", + sa.Column("meme_id", sa.Integer(), nullable=False), + sa.Column("name", sa.Text(), nullable=False), + sa.Column("template_id", sa.Integer(), nullable=False), + sa.Column("field_count", sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint("meme_id"), + sa.UniqueConstraint("name"), + sa.UniqueConstraint("template_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("meme") + # ### end Alembic commands ### diff --git a/alembic/versions/38b7c29f10ee_wordle.py b/alembic/versions/38b7c29f10ee_wordle.py new file mode 100644 index 0000000..8fe53b2 --- /dev/null +++ b/alembic/versions/38b7c29f10ee_wordle.py @@ -0,0 +1,63 @@ +"""Wordle + +Revision ID: 38b7c29f10ee +Revises: 36300b558ef1 +Create Date: 2022-08-29 20:21:02.413631 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "38b7c29f10ee" +down_revision = "36300b558ef1" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "wordle_word", + sa.Column("word_id", sa.Integer(), nullable=False), + sa.Column("word", sa.Text(), nullable=False), + sa.Column("day", sa.Date(), nullable=False), + sa.PrimaryKeyConstraint("word_id"), + sa.UniqueConstraint("day"), + ) + op.create_table( + "wordle_guesses", + sa.Column("wordle_guess_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=True), + sa.Column("guess", sa.Text(), nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.user_id"], + ), + sa.PrimaryKeyConstraint("wordle_guess_id"), + ) + op.create_table( + "wordle_stats", + sa.Column("wordle_stats_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=True), + sa.Column("last_win", sa.Date(), nullable=True), + sa.Column("games", sa.Integer(), server_default="0", nullable=False), + sa.Column("wins", sa.Integer(), server_default="0", nullable=False), + sa.Column("current_streak", sa.Integer(), server_default="0", nullable=False), + sa.Column("highest_streak", sa.Integer(), server_default="0", nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.user_id"], + ), + sa.PrimaryKeyConstraint("wordle_stats_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("wordle_stats") + op.drop_table("wordle_guesses") + op.drop_table("wordle_word") + # ### end Alembic commands ### diff --git a/alembic/versions/3962636f3a3d_add_custom_links.py b/alembic/versions/3962636f3a3d_add_custom_links.py new file mode 100644 index 0000000..ef4f13e --- /dev/null +++ b/alembic/versions/3962636f3a3d_add_custom_links.py @@ -0,0 +1,35 @@ +"""Add custom links + +Revision ID: 3962636f3a3d +Revises: 346b408c362a +Create Date: 2022-08-10 00:54:05.668255 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "3962636f3a3d" +down_revision = "346b408c362a" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "links", + sa.Column("link_id", sa.Integer(), nullable=False), + sa.Column("name", sa.Text(), nullable=False), + sa.Column("url", sa.Text(), nullable=False), + sa.PrimaryKeyConstraint("link_id"), + sa.UniqueConstraint("name"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("links") + # ### end Alembic commands ### diff --git a/alembic/versions/4ec79dd5b191_initial_migration.py b/alembic/versions/4ec79dd5b191_initial_migration.py new file mode 100644 index 0000000..2bf8362 --- /dev/null +++ b/alembic/versions/4ec79dd5b191_initial_migration.py @@ -0,0 +1,63 @@ +"""Initial migration + +Revision ID: 4ec79dd5b191 +Revises: +Create Date: 2022-06-19 00:31:58.384360 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "4ec79dd5b191" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "ufora_courses", + sa.Column("course_id", sa.Integer(), nullable=False), + sa.Column("name", sa.Text(), nullable=False), + sa.Column("code", sa.Text(), nullable=False), + sa.Column("year", sa.Integer(), nullable=False), + sa.Column("log_announcements", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("course_id"), + sa.UniqueConstraint("code"), + sa.UniqueConstraint("name"), + ) + op.create_table( + "ufora_announcements", + sa.Column("announcement_id", sa.Integer(), nullable=False), + sa.Column("course_id", sa.Integer(), nullable=True), + sa.Column("publication_date", sa.Date, nullable=True), + sa.ForeignKeyConstraint( + ["course_id"], + ["ufora_courses.course_id"], + ), + sa.PrimaryKeyConstraint("announcement_id"), + ) + op.create_table( + "ufora_course_aliases", + sa.Column("alias_id", sa.Integer(), nullable=False), + sa.Column("alias", sa.Text(), nullable=False), + sa.Column("course_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["course_id"], + ["ufora_courses.course_id"], + ), + sa.PrimaryKeyConstraint("alias_id"), + sa.UniqueConstraint("alias"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("ufora_course_aliases") + op.drop_table("ufora_announcements") + op.drop_table("ufora_courses") + # ### end Alembic commands ### diff --git a/alembic/versions/515dc3f52c6d_initial_migration.py b/alembic/versions/515dc3f52c6d_initial_migration.py deleted file mode 100644 index 947a008..0000000 --- a/alembic/versions/515dc3f52c6d_initial_migration.py +++ /dev/null @@ -1,246 +0,0 @@ -"""Initial migration - -Revision ID: 515dc3f52c6d -Revises: -Create Date: 2022-09-18 00:30:56.348634 - -""" -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision = "515dc3f52c6d" -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "custom_commands", - sa.Column("command_id", sa.Integer(), nullable=False), - sa.Column("name", sa.Text(), nullable=False), - sa.Column("indexed_name", sa.Text(), nullable=False), - sa.Column("response", sa.Text(), nullable=False), - sa.PrimaryKeyConstraint("command_id"), - sa.UniqueConstraint("name"), - ) - with op.batch_alter_table("custom_commands", schema=None) as batch_op: - batch_op.create_index(batch_op.f("ix_custom_commands_indexed_name"), ["indexed_name"], unique=False) - - op.create_table( - "dad_jokes", - sa.Column("dad_joke_id", sa.Integer(), nullable=False), - sa.Column("joke", sa.Text(), nullable=False), - sa.PrimaryKeyConstraint("dad_joke_id"), - ) - op.create_table( - "links", - sa.Column("link_id", sa.Integer(), nullable=False), - sa.Column("name", sa.Text(), nullable=False), - sa.Column("url", sa.Text(), nullable=False), - sa.PrimaryKeyConstraint("link_id"), - sa.UniqueConstraint("name"), - ) - op.create_table( - "meme", - sa.Column("meme_id", sa.Integer(), nullable=False), - sa.Column("name", sa.Text(), nullable=False), - sa.Column("template_id", sa.Integer(), nullable=False), - sa.Column("field_count", sa.Integer(), nullable=False), - sa.PrimaryKeyConstraint("meme_id"), - sa.UniqueConstraint("name"), - sa.UniqueConstraint("template_id"), - ) - op.create_table( - "tasks", - sa.Column("task_id", sa.Integer(), nullable=False), - sa.Column("task", sa.Enum("BIRTHDAYS", "SCHEDULES", "UFORA_ANNOUNCEMENTS", name="tasktype"), nullable=False), - sa.Column("previous_run", sa.DateTime(timezone=True), nullable=True), - sa.PrimaryKeyConstraint("task_id"), - sa.UniqueConstraint("task"), - ) - op.create_table( - "ufora_courses", - sa.Column("course_id", sa.Integer(), nullable=False), - sa.Column("name", sa.Text(), nullable=False), - sa.Column("code", sa.Text(), nullable=False), - sa.Column("year", sa.Integer(), nullable=False), - sa.Column("compulsory", sa.Boolean(), server_default="1", nullable=False), - sa.Column("role_id", sa.BigInteger(), nullable=True), - sa.Column("overarching_role_id", sa.BigInteger(), nullable=True), - sa.Column("log_announcements", sa.Boolean(), server_default="0", nullable=False), - sa.PrimaryKeyConstraint("course_id"), - sa.UniqueConstraint("code"), - sa.UniqueConstraint("name"), - ) - op.create_table("users", sa.Column("user_id", sa.BigInteger(), nullable=False), sa.PrimaryKeyConstraint("user_id")) - op.create_table( - "wordle_word", - sa.Column("word_id", sa.Integer(), nullable=False), - sa.Column("word", sa.Text(), nullable=False), - sa.Column("day", sa.Date(), nullable=False), - sa.PrimaryKeyConstraint("word_id"), - sa.UniqueConstraint("day"), - ) - op.create_table( - "bank", - sa.Column("bank_id", sa.Integer(), nullable=False), - sa.Column("user_id", sa.BigInteger(), nullable=True), - sa.Column("dinks", sa.BigInteger(), server_default="0", nullable=False), - sa.Column("invested", sa.BigInteger(), server_default="0", nullable=False), - sa.Column("interest_level", sa.Integer(), server_default="1", nullable=False), - sa.Column("capacity_level", sa.Integer(), server_default="1", nullable=False), - sa.Column("rob_level", sa.Integer(), server_default="1", nullable=False), - sa.ForeignKeyConstraint( - ["user_id"], - ["users.user_id"], - ), - sa.PrimaryKeyConstraint("bank_id"), - ) - op.create_table( - "birthdays", - sa.Column("birthday_id", sa.Integer(), nullable=False), - sa.Column("user_id", sa.BigInteger(), nullable=True), - sa.Column("birthday", sa.Date(), nullable=False), - sa.ForeignKeyConstraint( - ["user_id"], - ["users.user_id"], - ), - sa.PrimaryKeyConstraint("birthday_id"), - ) - op.create_table( - "bookmarks", - sa.Column("bookmark_id", sa.Integer(), nullable=False), - sa.Column("label", sa.Text(), nullable=False), - sa.Column("jump_url", sa.Text(), nullable=False), - sa.Column("user_id", sa.BigInteger(), nullable=True), - sa.ForeignKeyConstraint( - ["user_id"], - ["users.user_id"], - ), - sa.PrimaryKeyConstraint("bookmark_id"), - sa.UniqueConstraint("user_id", "label"), - ) - op.create_table( - "custom_command_aliases", - sa.Column("alias_id", sa.Integer(), nullable=False), - sa.Column("alias", sa.Text(), nullable=False), - sa.Column("indexed_alias", sa.Text(), nullable=False), - sa.Column("command_id", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint( - ["command_id"], - ["custom_commands.command_id"], - ), - sa.PrimaryKeyConstraint("alias_id"), - sa.UniqueConstraint("alias"), - ) - with op.batch_alter_table("custom_command_aliases", schema=None) as batch_op: - batch_op.create_index(batch_op.f("ix_custom_command_aliases_indexed_alias"), ["indexed_alias"], unique=False) - - op.create_table( - "deadlines", - sa.Column("deadline_id", sa.Integer(), nullable=False), - sa.Column("course_id", sa.Integer(), nullable=True), - sa.Column("name", sa.Text(), nullable=False), - sa.Column("deadline", sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["course_id"], - ["ufora_courses.course_id"], - ), - sa.PrimaryKeyConstraint("deadline_id"), - ) - op.create_table( - "nightly_data", - sa.Column("nightly_id", sa.Integer(), nullable=False), - sa.Column("user_id", sa.BigInteger(), nullable=True), - sa.Column("last_nightly", sa.Date(), nullable=True), - sa.Column("count", sa.Integer(), server_default="0", nullable=False), - sa.ForeignKeyConstraint( - ["user_id"], - ["users.user_id"], - ), - sa.PrimaryKeyConstraint("nightly_id"), - ) - op.create_table( - "ufora_announcements", - sa.Column("announcement_id", sa.Integer(), nullable=False), - sa.Column("course_id", sa.Integer(), nullable=True), - sa.Column("publication_date", sa.Date(), nullable=True), - sa.ForeignKeyConstraint( - ["course_id"], - ["ufora_courses.course_id"], - ), - sa.PrimaryKeyConstraint("announcement_id"), - ) - op.create_table( - "ufora_course_aliases", - sa.Column("alias_id", sa.Integer(), nullable=False), - sa.Column("alias", sa.Text(), nullable=False), - sa.Column("course_id", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint( - ["course_id"], - ["ufora_courses.course_id"], - ), - sa.PrimaryKeyConstraint("alias_id"), - sa.UniqueConstraint("alias"), - ) - op.create_table( - "wordle_guesses", - sa.Column("wordle_guess_id", sa.Integer(), nullable=False), - sa.Column("user_id", sa.BigInteger(), nullable=True), - sa.Column("guess", sa.Text(), nullable=False), - sa.ForeignKeyConstraint( - ["user_id"], - ["users.user_id"], - ), - sa.PrimaryKeyConstraint("wordle_guess_id"), - ) - op.create_table( - "wordle_stats", - sa.Column("wordle_stats_id", sa.Integer(), nullable=False), - sa.Column("user_id", sa.BigInteger(), nullable=True), - sa.Column("last_win", sa.Date(), nullable=True), - sa.Column("games", sa.Integer(), server_default="0", nullable=False), - sa.Column("wins", sa.Integer(), server_default="0", nullable=False), - sa.Column("current_streak", sa.Integer(), server_default="0", nullable=False), - sa.Column("highest_streak", sa.Integer(), server_default="0", nullable=False), - sa.ForeignKeyConstraint( - ["user_id"], - ["users.user_id"], - ), - sa.PrimaryKeyConstraint("wordle_stats_id"), - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("wordle_stats") - op.drop_table("wordle_guesses") - op.drop_table("ufora_course_aliases") - op.drop_table("ufora_announcements") - op.drop_table("nightly_data") - op.drop_table("deadlines") - with op.batch_alter_table("custom_command_aliases", schema=None) as batch_op: - batch_op.drop_index(batch_op.f("ix_custom_command_aliases_indexed_alias")) - - op.drop_table("custom_command_aliases") - op.drop_table("bookmarks") - op.drop_table("birthdays") - op.drop_table("bank") - op.drop_table("wordle_word") - op.drop_table("users") - op.drop_table("ufora_courses") - op.drop_table("tasks") - op.drop_table("meme") - op.drop_table("links") - op.drop_table("dad_jokes") - with op.batch_alter_table("custom_commands", schema=None) as batch_op: - batch_op.drop_index(batch_op.f("ix_custom_commands_indexed_name")) - - op.drop_table("custom_commands") - sa.Enum("BIRTHDAYS", "SCHEDULES", "UFORA_ANNOUNCEMENTS", name="tasktype").drop(op.get_bind()) - # ### end Alembic commands ### diff --git a/alembic/versions/581ae6511b98_add_dad_jokes.py b/alembic/versions/581ae6511b98_add_dad_jokes.py new file mode 100644 index 0000000..b3bed89 --- /dev/null +++ b/alembic/versions/581ae6511b98_add_dad_jokes.py @@ -0,0 +1,33 @@ +"""Add dad jokes + +Revision ID: 581ae6511b98 +Revises: 632b69cdadde +Create Date: 2022-07-15 23:37:08.147611 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "581ae6511b98" +down_revision = "632b69cdadde" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "dad_jokes", + sa.Column("dad_joke_id", sa.Integer(), nullable=False), + sa.Column("joke", sa.Text(), nullable=False), + sa.PrimaryKeyConstraint("dad_joke_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("dad_jokes") + # ### end Alembic commands ### diff --git a/alembic/versions/632b69cdadde_add_missing_defaults.py b/alembic/versions/632b69cdadde_add_missing_defaults.py new file mode 100644 index 0000000..0f326a7 --- /dev/null +++ b/alembic/versions/632b69cdadde_add_missing_defaults.py @@ -0,0 +1,28 @@ +"""Add missing defaults + +Revision ID: 632b69cdadde +Revises: 8c4ad0a1d699 +Create Date: 2022-07-03 16:29:07.387011 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '632b69cdadde' +down_revision = '8c4ad0a1d699' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/alembic/versions/8c4ad0a1d699_move_dinks_over_to_bank_add_invested_.py b/alembic/versions/8c4ad0a1d699_move_dinks_over_to_bank_add_invested_.py new file mode 100644 index 0000000..ad56f5e --- /dev/null +++ b/alembic/versions/8c4ad0a1d699_move_dinks_over_to_bank_add_invested_.py @@ -0,0 +1,32 @@ +"""Move dinks over to Bank & add invested amount + +Revision ID: 8c4ad0a1d699 +Revises: 0d03c226d881 +Create Date: 2022-07-03 16:27:11.330746 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8c4ad0a1d699' +down_revision = '0d03c226d881' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('bank', schema=None) as batch_op: + batch_op.add_column(sa.Column('invested', sa.BigInteger(), server_default='0', nullable=False)) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('bank', schema=None) as batch_op: + batch_op.drop_column('invested') + + # ### end Alembic commands ### diff --git a/alembic/versions/b2d511552a1f_add_custom_commands.py b/alembic/versions/b2d511552a1f_add_custom_commands.py new file mode 100644 index 0000000..83b004a --- /dev/null +++ b/alembic/versions/b2d511552a1f_add_custom_commands.py @@ -0,0 +1,57 @@ +"""Add custom commands + +Revision ID: b2d511552a1f +Revises: 4ec79dd5b191 +Create Date: 2022-06-21 22:10:05.590846 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b2d511552a1f' +down_revision = '4ec79dd5b191' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('custom_commands', + sa.Column('command_id', sa.Integer(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('indexed_name', sa.Text(), nullable=False), + sa.Column('response', sa.Text(), nullable=False), + sa.PrimaryKeyConstraint('command_id'), + sa.UniqueConstraint('name') + ) + with op.batch_alter_table('custom_commands', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_custom_commands_indexed_name'), ['indexed_name'], unique=False) + + op.create_table('custom_command_aliases', + sa.Column('alias_id', sa.Integer(), nullable=False), + sa.Column('alias', sa.Text(), nullable=False), + sa.Column('indexed_alias', sa.Text(), nullable=False), + sa.Column('command_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['command_id'], ['custom_commands.command_id'], ), + sa.PrimaryKeyConstraint('alias_id'), + sa.UniqueConstraint('alias') + ) + with op.batch_alter_table('custom_command_aliases', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_custom_command_aliases_indexed_alias'), ['indexed_alias'], unique=False) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('custom_command_aliases', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_custom_command_aliases_indexed_alias')) + + op.drop_table('custom_command_aliases') + with op.batch_alter_table('custom_commands', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_custom_commands_indexed_name')) + + op.drop_table('custom_commands') + # ### end Alembic commands ### diff --git a/alembic/versions/f5da771a155d_bookmarks.py b/alembic/versions/f5da771a155d_bookmarks.py new file mode 100644 index 0000000..154b907 --- /dev/null +++ b/alembic/versions/f5da771a155d_bookmarks.py @@ -0,0 +1,40 @@ +"""Bookmarks + +Revision ID: f5da771a155d +Revises: 38b7c29f10ee +Create Date: 2022-08-30 01:08:54.323883 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "f5da771a155d" +down_revision = "38b7c29f10ee" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "bookmarks", + sa.Column("bookmark_id", sa.Integer(), nullable=False), + sa.Column("label", sa.Text(), nullable=False), + sa.Column("jump_url", sa.Text(), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=True), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.user_id"], + ), + sa.PrimaryKeyConstraint("bookmark_id"), + sa.UniqueConstraint("user_id", "label"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("bookmarks") + # ### end Alembic commands ### diff --git a/database/crud/ufora_courses.py b/database/crud/ufora_courses.py index 5374c07..19369c1 100644 --- a/database/crud/ufora_courses.py +++ b/database/crud/ufora_courses.py @@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from database.schemas import UforaCourse, UforaCourseAlias -__all__ = ["get_all_courses", "get_course_by_code", "get_course_by_name"] +__all__ = ["get_all_courses", "get_course_by_name"] async def get_all_courses(session: AsyncSession) -> list[UforaCourse]: @@ -14,12 +14,6 @@ async def get_all_courses(session: AsyncSession) -> list[UforaCourse]: return list((await session.execute(statement)).scalars().all()) -async def get_course_by_code(session: AsyncSession, code: str) -> Optional[UforaCourse]: - """Try to find a course by its code""" - statement = select(UforaCourse).where(UforaCourse.code == code) - return (await session.execute(statement)).scalar_one_or_none() - - async def get_course_by_name(session: AsyncSession, query: str) -> Optional[UforaCourse]: """Try to find a course by its name diff --git a/database/enums.py b/database/enums.py index e2cf565..3f75130 100644 --- a/database/enums.py +++ b/database/enums.py @@ -10,5 +10,4 @@ class TaskType(enum.IntEnum): """Enum for the different types of tasks""" BIRTHDAYS = enum.auto() - SCHEDULES = enum.auto() UFORA_ANNOUNCEMENTS = enum.auto() diff --git a/database/schemas.py b/database/schemas.py index e18ffe7..f8fa018 100644 --- a/database/schemas.py +++ b/database/schemas.py @@ -197,9 +197,6 @@ class UforaCourse(Base): name: str = Column(Text, nullable=False, unique=True) code: str = Column(Text, nullable=False, unique=True) year: int = Column(Integer, nullable=False) - compulsory: bool = Column(Boolean, server_default="1", nullable=False) - role_id: Optional[int] = Column(BigInteger, nullable=True, unique=False) - overarching_role_id: Optional[int] = Column(BigInteger, nullable=True, unique=False) log_announcements: bool = Column(Boolean, server_default="0", nullable=False) announcements: list[UforaAnnouncement] = relationship( diff --git a/database/scripts/__init__.py b/database/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/database/scripts/db00_example.py b/database/scripts/db00_example.py deleted file mode 100644 index 6970829..0000000 --- a/database/scripts/db00_example.py +++ /dev/null @@ -1,23 +0,0 @@ -from sqlalchemy.ext.asyncio import AsyncSession - -from database.engine import DBSession -from database.schemas import UforaCourse - -__all__ = ["main"] - - -async def main(): - """Example script: add a Ufora course""" - session: AsyncSession - async with DBSession() as session: - modsim = UforaCourse( - course_id=439235, - code="C003786", - name="Modelleren en Simuleren", - year=3, - compulsory=False, - role_id=785577582561067028, - ) - - session.add_all([modsim]) - await session.commit() diff --git a/didier/cogs/owner.py b/didier/cogs/owner.py index 974896e..2fa4b4e 100644 --- a/didier/cogs/owner.py +++ b/didier/cogs/owner.py @@ -53,15 +53,6 @@ class Owner(commands.Cog): """Raise an exception for debugging purposes""" raise Exception(message) - @commands.command(name="Reload") - async def reload(self, ctx: commands.Context, *cogs: str): - """Reload the cogs passed as an argument""" - for cog in cogs: - await self.client.reload_extension(f"didier.cogs.{cog}") - - await self.client.confirm_message(ctx.message) - return await ctx.reply(f"Successfully reloaded {', '.join(cogs)}.", mention_author=False) - @commands.command(name="Sync") async def sync( self, diff --git a/didier/cogs/school.py b/didier/cogs/school.py index 6b2d835..5aca554 100644 --- a/didier/cogs/school.py +++ b/didier/cogs/school.py @@ -11,12 +11,9 @@ from didier import Didier from didier.data.apis.hydra import fetch_menu from didier.data.embeds.deadlines import Deadlines from didier.data.embeds.hydra import no_menu_found -from didier.data.embeds.schedules import Schedule, get_schedule_for_user -from didier.exceptions import HTTPException, NotInMainGuildException +from didier.exceptions import HTTPException from didier.utils.discord.converters.time import DateTransformer from didier.utils.discord.flags.school import StudyGuideFlags -from didier.utils.discord.users import to_main_guild_member -from didier.utils.types.datetime import skip_weekends class School(commands.Cog): @@ -36,30 +33,6 @@ class School(commands.Cog): embed = Deadlines(deadlines).to_embed() await ctx.reply(embed=embed, mention_author=False, ephemeral=False) - @commands.hybrid_command( - name="les", description="Show your personalized schedule for a given day.", aliases=["Sched", "Schedule"] - ) - @app_commands.rename(day_dt="date") - async def les(self, ctx: commands.Context, day_dt: Optional[app_commands.Transform[date, DateTransformer]] = None): - """Show your personalized schedule for a given day.""" - if day_dt is None: - day_dt = date.today() - - day_dt = skip_weekends(day_dt) - - async with ctx.typing(): - try: - member_instance = to_main_guild_member(self.client, ctx.author) - - # Always make sure there is at least one schedule in case it returns None - # this allows proper error messages - schedule = get_schedule_for_user(self.client, member_instance, day_dt) or Schedule() - - return await ctx.reply(embed=schedule.to_embed(day=day_dt), mention_author=False) - - except NotInMainGuildException: - return await ctx.reply(f"You are not a member of {self.client.main_guild.name}.", mention_author=False) - @commands.hybrid_command( name="menu", description="Show the menu in the Ghent University restaurants.", diff --git a/didier/cogs/tasks.py b/didier/cogs/tasks.py index 518f3f5..64f5501 100644 --- a/didier/cogs/tasks.py +++ b/didier/cogs/tasks.py @@ -11,7 +11,6 @@ from database.crud.birthdays import get_birthdays_on_day from database.crud.ufora_announcements import remove_old_announcements from database.crud.wordle import set_daily_word from didier import Didier -from didier.data.embeds.schedules import Schedule, parse_schedule_from_content from didier.data.embeds.ufora.announcements import fetch_ufora_announcements from didier.decorators.tasks import timed_task from didier.utils.discord.checks import is_owner @@ -42,7 +41,6 @@ class Tasks(commands.Cog): self._tasks = { "birthdays": self.check_birthdays, - "schedules": self.pull_schedules, "ufora": self.pull_ufora_announcements, "remove_ufora": self.remove_old_ufora_announcements, "wordle": self.reset_wordle_word, @@ -61,7 +59,6 @@ class Tasks(commands.Cog): # Start other tasks self.reset_wordle_word.start() - self.pull_schedules.start() @overrides def cog_unload(self) -> None: @@ -113,44 +110,6 @@ class Tasks(commands.Cog): async def _before_check_birthdays(self): await self.client.wait_until_ready() - @tasks.loop(time=DAILY_RESET_TIME) - @timed_task(enums.TaskType.SCHEDULES) - async def pull_schedules(self, **kwargs): - """Task that pulls the schedules & saves the files locally - - Schedules are then parsed & cached in memory - """ - _ = kwargs - - new_schedules: dict[settings.ScheduleType, Schedule] = {} - - async with self.client.postgres_session as session: - for data in settings.SCHEDULE_DATA: - if data.schedule_url is None: - return - - async with self.client.http_session.get(data.schedule_url) as response: - # If a schedule couldn't be fetched, log it and move on - if response.status != 200: - await self.client.log_warning( - f"Unable to fetch schedule {data.name} (status {response.status}).", log_to_discord=False - ) - continue - - # Write the content to a file - content = await response.text() - with open(f"files/schedules/{data.name}.ics", "w+") as fp: - fp.write(content) - - schedule = await parse_schedule_from_content(content, database_session=session) - if schedule is None: - continue - - new_schedules[data.name] = schedule - - # Only replace cached version if all schedules succeeded - self.client.schedules = new_schedules - @tasks.loop(minutes=10) @timed_task(enums.TaskType.UFORA_ANNOUNCEMENTS) async def pull_ufora_announcements(self, **kwargs): diff --git a/didier/data/constants.py b/didier/data/constants.py index 1387822..cea951c 100644 --- a/didier/data/constants.py +++ b/didier/data/constants.py @@ -1,6 +1,6 @@ +# The year in which we were in 1Ba import settings -# The year in which we were in 1Ba FIRST_YEAR = 2019 # Year to use when adding the current year of our education # to find the academic year diff --git a/didier/data/embeds/schedules.py b/didier/data/embeds/schedules.py deleted file mode 100644 index 9eefd61..0000000 --- a/didier/data/embeds/schedules.py +++ /dev/null @@ -1,220 +0,0 @@ -from __future__ import annotations - -import pathlib -import re -from dataclasses import dataclass, field -from datetime import date, datetime -from typing import TYPE_CHECKING, Optional - -if TYPE_CHECKING: - from didier import Didier - -import discord -from ics import Calendar -from overrides import overrides -from sqlalchemy.ext.asyncio import AsyncSession - -from database.crud.ufora_courses import get_course_by_code -from database.schemas import UforaCourse -from didier.data.embeds.base import EmbedBaseModel -from didier.utils.discord import colours -from didier.utils.types.datetime import LOCAL_TIMEZONE, int_to_weekday, time_string -from didier.utils.types.string import leading -from settings import ScheduleType - -__all__ = ["Schedule", "get_schedule_for_user", "parse_schedule_from_content", "parse_schedule"] - - -@dataclass -class Schedule(EmbedBaseModel): - """An entire schedule""" - - slots: set[ScheduleSlot] = field(default_factory=set) - - def __add__(self, other) -> Schedule: - """Combine schedules using the + operator""" - if not isinstance(other, Schedule): - raise TypeError("Argument to __add__ must be a Schedule") - - return Schedule(slots=self.slots.union(other.slots)) - - def __bool__(self) -> bool: - """Make empty schedules falsy""" - return bool(self.slots) - - def on_day(self, day: date) -> Schedule: - """Only show courses on a given day""" - return Schedule(set(filter(lambda slot: slot.start_time.date() == day, self.slots))) - - def personalize(self, roles: set[int]) -> Schedule: - """Personalize a schedule for a user, only adding courses they follow""" - personal_slots = set() - for slot in self.slots: - role_found = slot.role_id is not None and slot.role_id in roles - overarching_role_found = slot.overarching_role_id is not None and slot.overarching_role_id in roles - if role_found or overarching_role_found: - personal_slots.add(slot) - - return Schedule(personal_slots) - - @overrides - def to_embed(self, **kwargs) -> discord.Embed: - day: date = kwargs.get("day", date.today()) - day_str = f"{leading('0', str(day.day))}/{leading('0', str(day.month))}/{leading('0', str(day.year))}" - - embed = discord.Embed(title=f"Schedule - {int_to_weekday(day.weekday())} {day_str}") - - if self: - embed.colour = colours.ghent_university_blue() - else: - embed.colour = colours.error_red() - embed.description = ( - "No planned classes found.\n\n" - "In case this doesn't seem right, " - "make sure that you've got the roles of all of courses that you're taking on.\n\n" - "In case it does, enjoy your day off!" - ) - - return embed - - slots_sorted = sorted(list(self.slots), key=lambda k: k.start_time) - description_data = [] - - for slot in slots_sorted: - description_data.append( - f"{time_string(slot.start_time)} - {time_string(slot.end_time)}: {slot.course.name} " - f"in **{slot.location}**" - ) - - embed.description = "\n".join(description_data) - - return embed - - -@dataclass -class ScheduleSlot: - """A slot in the schedule""" - - course: UforaCourse - start_time: datetime - end_time: datetime - location: str - _hash: int = field(init=False) - - def __post_init__(self): - """Fix some properties to display more nicely""" - # Re-format the location data - room, building, campus = re.search(r"(.*)\. Gebouw (.*)\. Campus (.*)\. ", self.location).groups() - room = room.replace("PC / laptoplokaal ", "PC-lokaal") - self.location = f"{campus} {building} {room}" - - self._hash = hash(f"{self.course.course_id} {str(self.start_time)}") - - @property - def overarching_role_id(self) -> Optional[int]: - """Shortcut to getting the overarching role id for this slot""" - return self.course.overarching_role_id - - @property - def role_id(self) -> Optional[int]: - """Shortcut to getting the role id for this slot""" - return self.course.role_id - - @overrides - def __hash__(self) -> int: - return self._hash - - @overrides - def __eq__(self, other): - if not isinstance(other, ScheduleSlot): - return False - - return self._hash == other._hash - - -def get_schedule_for_user(client: Didier, member: discord.Member, day_dt: date) -> Optional[Schedule]: - """Get a user's schedule""" - roles: set[int] = {role.id for role in member.roles} - - main_schedule: Optional[Schedule] = None - - for schedule in client.schedules.values(): - personalized_schedule = schedule.on_day(day_dt).personalize(roles) - - if not personalized_schedule: - continue - - # Add the personalized one to the current main schedule - if main_schedule is None: - main_schedule = personalized_schedule - else: - main_schedule = main_schedule + personalized_schedule - - return main_schedule - - -def parse_course_code(summary: str) -> str: - """Parse a course's code out of the summary""" - code = re.search(r"^([^ ]+)\. ", summary) - - if code is None: - return summary - - code_group = code.groups()[0] - - # Strip off last character as it's not relevant - if code_group[-1].isalpha(): - return code_group[:-1] - - return code_group - - -def parse_time_string(string: str) -> datetime: - """Parse an ISO string to a timezone-aware datetime instance""" - return datetime.fromisoformat(string).astimezone(LOCAL_TIMEZONE) - - -async def parse_schedule_from_content(content: str, *, database_session: AsyncSession) -> Schedule: - """Parse a schedule file, taking the file content as an argument - - This can be used to avoid unnecessarily opening the file again if you already have its contents - """ - calendar = Calendar(content) - events = list(calendar.events) - course_codes: dict[str, UforaCourse] = {} - slots: set[ScheduleSlot] = set() - - for event in events: - code = parse_course_code(event.name) - - if code not in course_codes: - course = await get_course_by_code(database_session, code) - if course is None: - # raise ValueError(f"Unable to find course with code {code} (event {event.name})") # noqa: E800 - continue # TODO uncomment the line above after all courses have been added - - course_codes[code] = course - - # Overwrite the name to be the sanitized value - event.name = code - - slot = ScheduleSlot( - course=course_codes[code], - start_time=parse_time_string(str(event.begin)), - end_time=parse_time_string(str(event.end)), - location=event.location, - ) - - slots.add(slot) - - return Schedule(slots=slots) - - -async def parse_schedule(name: ScheduleType, *, database_session: AsyncSession) -> Optional[Schedule]: - """Read and then parse a schedule file""" - schedule_path = pathlib.Path(f"files/schedules/{name}.ics") - if not schedule_path.exists(): - return None - - with open(schedule_path, "r", encoding="utf-8") as fp: - return await parse_schedule_from_content(fp.read(), database_session=database_session) diff --git a/didier/didier.py b/didier/didier.py index f7bf388..12bd5b4 100644 --- a/didier/didier.py +++ b/didier/didier.py @@ -1,7 +1,5 @@ import logging import os -import pathlib -from functools import cached_property import discord from aiohttp import ClientSession @@ -14,7 +12,6 @@ from database.crud import custom_commands from database.engine import DBSession from database.utils.caches import CacheManager from didier.data.embeds.error_embed import create_error_embed -from didier.data.embeds.schedules import Schedule, parse_schedule from didier.exceptions import HTTPException, NoMatch from didier.utils.discord.prefix import get_prefix @@ -31,7 +28,6 @@ class Didier(commands.Bot): error_channel: discord.abc.Messageable initial_extensions: tuple[str, ...] = () http_session: ClientSession - schedules: dict[settings.ScheduleType, Schedule] = {} wordle_words: set[str] = set() def __init__(self): @@ -53,11 +49,6 @@ class Didier(commands.Bot): self.tree.on_error = self.on_app_command_error - @cached_property - def main_guild(self) -> discord.Guild: - """Obtain a reference to the main guild""" - return self.get_guild(settings.DISCORD_MAIN_GUILD) - @property def postgres_session(self) -> AsyncSession: """Obtain a session for the PostgreSQL database""" @@ -68,12 +59,6 @@ class Didier(commands.Bot): This hook is called once the bot is initialised """ - # Create directories that are ignored on GitHub - self._create_ignored_directories() - - # Load schedules - await self.load_schedules() - # Load the Wordle dictionary self._load_wordle_words() @@ -82,26 +67,19 @@ class Didier(commands.Bot): async with self.postgres_session as session: await self.database_caches.initialize_caches(session) - # Create aiohttp session - self.http_session = ClientSession() - # Load extensions await self._load_initial_extensions() await self._load_directory_extensions("didier/cogs") + # Create aiohttp session + self.http_session = ClientSession() + # Configure channel to send errors to if settings.ERRORS_CHANNEL is not None: self.error_channel = self.get_channel(settings.ERRORS_CHANNEL) else: self.error_channel = self.get_user(self.owner_id) - def _create_ignored_directories(self): - """Create directories that store ignored data""" - ignored = ["files/schedules"] - - for directory in ignored: - pathlib.Path(directory).mkdir(exist_ok=True, parents=True) - async def _load_initial_extensions(self): """Load all extensions that should be loaded before the others""" for extension in self.initial_extensions: @@ -131,18 +109,6 @@ class Didier(commands.Bot): for line in fp: self.wordle_words.add(line.strip()) - async def load_schedules(self): - """Parse & load all schedules into memory""" - self.schedules = {} - - async with self.postgres_session as session: - for schedule_data in settings.SCHEDULE_DATA: - schedule = await parse_schedule(schedule_data.name, database_session=session) - if schedule is None: - continue - - self.schedules[schedule_data.name] = schedule - async def get_reply_target(self, ctx: commands.Context) -> discord.Message: """Get the target message that should be replied to @@ -172,27 +138,13 @@ class Didier(commands.Bot): """Add an X to a message""" await message.add_reaction("❌") - async def _log(self, level: int, message: str, log_to_discord: bool = True): - """Log a message to the logging file, and optionally to the configured channel""" - methods = { - logging.ERROR: logger.error, - logging.WARNING: logger.warning, - } - - methods.get(level, logger.error)(message) + async def log_error(self, message: str, log_to_discord: bool = True): + """Send an error message to the logs, and optionally the configured channel""" + logger.error(message) if log_to_discord: # TODO pretty embed - # different colours per level? await self.error_channel.send(message) - async def log_error(self, message: str, log_to_discord: bool = True): - """Log an error message""" - await self._log(logging.ERROR, message, log_to_discord) - - async def log_warning(self, message: str, log_to_discord: bool = True): - """Log a warning message""" - await self._log(logging.WARNING, message, log_to_discord) - async def on_ready(self): """Event triggered when the bot is ready""" print(settings.DISCORD_READY_MESSAGE) diff --git a/didier/exceptions/__init__.py b/didier/exceptions/__init__.py index 1335dd4..4321cae 100644 --- a/didier/exceptions/__init__.py +++ b/didier/exceptions/__init__.py @@ -1,6 +1,5 @@ from .http_exception import HTTPException from .missing_env import MissingEnvironmentVariable from .no_match import NoMatch, expect -from .not_in_main_guild_exception import NotInMainGuildException -__all__ = ["HTTPException", "MissingEnvironmentVariable", "NoMatch", "expect", "NotInMainGuildException"] +__all__ = ["HTTPException", "MissingEnvironmentVariable", "NoMatch", "expect"] diff --git a/didier/exceptions/not_in_main_guild_exception.py b/didier/exceptions/not_in_main_guild_exception.py deleted file mode 100644 index 5572c44..0000000 --- a/didier/exceptions/not_in_main_guild_exception.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Union - -import discord - -import settings - -__all__ = ["NotInMainGuildException"] - - -class NotInMainGuildException(ValueError): - """Exception raised when a user is not a member of the main guild""" - - def __init__(self, user: Union[discord.User, discord.Member]): - super().__init__( - f"User {user.display_name} (id {user.id}) " - f"is not a member of the configured main guild (id {settings.DISCORD_MAIN_GUILD})." - ) diff --git a/didier/utils/discord/colours.py b/didier/utils/discord/colours.py index c8a55e6..5e69d3e 100644 --- a/didier/utils/discord/colours.py +++ b/didier/utils/discord/colours.py @@ -1,10 +1,6 @@ import discord -__all__ = ["error_red", "ghent_university_blue", "ghent_university_yellow", "google_blue", "urban_dictionary_green"] - - -def error_red() -> discord.Colour: - return discord.Colour.red() +__all__ = ["ghent_university_blue", "ghent_university_yellow", "google_blue", "urban_dictionary_green"] def ghent_university_blue() -> discord.Colour: diff --git a/didier/utils/discord/converters/time.py b/didier/utils/discord/converters/time.py index c53c2c6..2a328f9 100644 --- a/didier/utils/discord/converters/time.py +++ b/didier/utils/discord/converters/time.py @@ -53,7 +53,7 @@ def date_converter(argument: Optional[str]) -> date: raise commands.ArgumentParsingError(f"Unable to interpret `{original_argument}` as a date.") -class DateTransformer(commands.Converter, app_commands.Transformer): +class DateTransformer(app_commands.Transformer): """Application commands transformer for dates""" @overrides @@ -62,10 +62,6 @@ class DateTransformer(commands.Converter, app_commands.Transformer): ) -> list[app_commands.Choice[Union[int, float, str]]]: return autocomplete_day(str(value)) - @overrides - async def convert(self, ctx: commands.Context, argument: str) -> datetime.date: - return date_converter(argument) - @overrides async def transform(self, interaction: discord.Interaction, value: str) -> datetime.date: return date_converter(value) diff --git a/didier/utils/discord/users.py b/didier/utils/discord/users.py deleted file mode 100644 index 27feaa3..0000000 --- a/didier/utils/discord/users.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Union - -import discord - -from didier import Didier -from didier.exceptions import NotInMainGuildException - -__all__ = ["to_main_guild_member"] - - -def to_main_guild_member(client: Didier, user: Union[discord.User, discord.Member]) -> discord.Member: - """Turn a discord.User into a discord.Member instance - - This assumes the user is in CoC. If not, it raises a NotInMainGuildException - """ - main_guild = client.main_guild - - # Already a discord.Member instance - if isinstance(user, discord.Member) and user.guild == main_guild: - return user - - member = main_guild.get_member(user.id) - if member is None: - raise NotInMainGuildException(user) - - return member diff --git a/didier/utils/types/datetime.py b/didier/utils/types/datetime.py index 038362d..7372118 100644 --- a/didier/utils/types/datetime.py +++ b/didier/utils/types/datetime.py @@ -8,11 +8,9 @@ __all__ = [ "forward_to_next_weekday", "int_to_weekday", "parse_dm_string", - "skip_weekends", "str_to_date", "str_to_month", "str_to_weekday", - "time_string", "tz_aware_now", ] @@ -88,12 +86,6 @@ def parse_dm_string(argument: str) -> datetime.date: raise ValueError -def skip_weekends(dt_instance: datetime.date) -> datetime.date: - """Fast-forward a date instance until its weekday is no longer a weekend""" - to_skip = (7 - dt_instance.weekday()) if dt_instance.weekday() > 4 else 0 - return dt_instance + datetime.timedelta(days=to_skip) - - def str_to_date(date_str: str, formats: Union[list[str], str] = "%d/%m/%Y") -> datetime.date: """Turn a string into a DD/MM/YYYY date""" # Allow passing multiple formats in a list @@ -179,11 +171,6 @@ def str_to_weekday(argument: str) -> int: raise ValueError -def time_string(dt_instance: datetime.datetime) -> str: - """Get an HH:MM representation of a datetime instance""" - return dt_instance.strftime("%H:%M") - - def tz_aware_now() -> datetime.datetime: """Get the current date & time, but timezone-aware""" return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone(LOCAL_TIMEZONE) diff --git a/main.py b/main.py index f9abc1c..f791621 100644 --- a/main.py +++ b/main.py @@ -11,10 +11,7 @@ from didier import Didier async def run_bot(): """Run Didier""" didier = Didier() - try: - await didier.start(settings.DISCORD_TOKEN) - finally: - await didier.http_session.close() + await didier.start(settings.DISCORD_TOKEN) def setup_logging(): diff --git a/pyproject.toml b/pyproject.toml index acd06c2..2b40c14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ plugins = [ "sqlalchemy.ext.mypy.plugin" ] [[tool.mypy.overrides]] -module = ["discord.*", "feedparser.*", "ics.*", "markdownify.*"] +module = ["discord.*", "feedparser.*", "markdownify.*", "motor.*"] ignore_missing_imports = true [tool.pytest.ini_options] @@ -50,8 +50,7 @@ env = [ "POSTGRES_PASS = pytest", "POSTGRES_HOST = localhost", "POSTGRES_PORT = 5433", - "DISCORD_TOKEN = token", - "DISCORD_MAIN_GUILD = 123456789101112131415" + "DISCORD_TOKEN = token" ] markers = [ "postgres: tests that use PostgreSQL" diff --git a/requirements.txt b/requirements.txt index f30a3c1..759cb54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,6 @@ discord.py==2.0.1 git+https://github.com/Rapptz/discord-ext-menus@8686b5d environs==9.5.0 feedparser==6.0.10 -ics==0.7.2 markdownify==0.11.2 overrides==6.1.0 pydantic==1.9.1 diff --git a/run_db_scripts.py b/run_db_scripts.py deleted file mode 100644 index 91f4af2..0000000 --- a/run_db_scripts.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Script to run database-related scripts - -This is slightly ugly, but running the scripts directly isn't possible because of imports -This could be cleaned up a bit using importlib but this is safer -""" -import asyncio -import sys -from typing import Callable - -from database.scripts.db00_example import main as debug_add_courses - -script_mapping: dict[str, Callable] = {"debug_add_courses.py": debug_add_courses} - - -if __name__ == "__main__": - scripts = sys.argv[1:] - if not scripts: - print("No scripts provided.", file=sys.stderr) - exit(1) - - for script in scripts: - script_main = script_mapping.get(script.removeprefix("database/scripts/"), None) - if script_main is None: - print(f'Script "{script}" not found.', file=sys.stderr) - exit(1) - - asyncio.run(script_main()) - print(f"Successfully ran {script}") diff --git a/settings.py b/settings.py index 79efc78..7698cc9 100644 --- a/settings.py +++ b/settings.py @@ -1,5 +1,3 @@ -from dataclasses import dataclass -from enum import Enum from typing import Optional from environs import Env @@ -24,15 +22,10 @@ __all__ = [ "DISCORD_BOOS_REACT", "DISCORD_CUSTOM_COMMAND_PREFIX", "UFORA_ANNOUNCEMENTS_CHANNEL", - "BA3_ROLE", "UFORA_RSS_TOKEN", "URBAN_DICTIONARY_TOKEN", "IMGFLIP_NAME", "IMGFLIP_PASSWORD", - "BA3_SCHEDULE_URL", - "ScheduleType", - "ScheduleInfo", - "SCHEDULE_DATA", ] @@ -42,7 +35,6 @@ TESTING: bool = env.bool("TESTING", False) LOGFILE: str = env.str("LOGFILE", "didier.log") SEMESTER: int = env.int("SEMESTER", 2) YEAR: int = env.int("YEAR", 3) -MENU_TIMEOUT: int = env.int("MENU_TIMEOUT", 30) """Database""" # PostgreSQL @@ -56,7 +48,6 @@ POSTGRES_PORT: int = env.int("POSTGRES_PORT", "5432") DISCORD_TOKEN: str = env.str("DISCORD_TOKEN") DISCORD_READY_MESSAGE: str = env.str("DISCORD_READY_MESSAGE", "I'M READY I'M READY I'M READY") DISCORD_STATUS_MESSAGE: str = env.str("DISCORD_STATUS_MESSAGE", "with your Didier Dinks.") -DISCORD_MAIN_GUILD: int = env.int("DISCORD_MAIN_GUILD") DISCORD_TEST_GUILDS: list[int] = env.list("DISCORD_TEST_GUILDS", [], subcast=int) DISCORD_OWNER_GUILDS: Optional[list[int]] = env.list("DISCORD_OWNER_GUILDS", [], subcast=int) or None DISCORD_BOOS_REACT: str = env.str("DISCORD_BOOS_REACT", "<:boos:629603785840263179>") @@ -65,45 +56,11 @@ BIRTHDAY_ANNOUNCEMENT_CHANNEL: Optional[int] = env.int("BIRTHDAY_ANNOUNCEMENT_CH ERRORS_CHANNEL: Optional[int] = env.int("ERRORS_CHANNEL", None) UFORA_ANNOUNCEMENTS_CHANNEL: Optional[int] = env.int("UFORA_ANNOUNCEMENTS_CHANNEL", None) -"""Discord Role ID's""" -BA3_ROLE: Optional[int] = env.int("BA3_ROLE", 891743208248324196) -MA_CS_ROLE: Optional[int] = env.int("MA_CS_ROLE", None) -MA_CS_ENG_ROLE: Optional[int] = env.int("MA_CS_ENG_ROLE", None) +""""General config""" +MENU_TIMEOUT: int = env.int("MENU_TIMEOUT", 30) """API Keys""" UFORA_RSS_TOKEN: Optional[str] = env.str("UFORA_RSS_TOKEN", None) URBAN_DICTIONARY_TOKEN: Optional[str] = env.str("URBAN_DICTIONARY_TOKEN", None) IMGFLIP_NAME: Optional[str] = env.str("IMGFLIP_NAME", None) IMGFLIP_PASSWORD: Optional[str] = env.str("IMGFLIP_PASSWORD", None) - -"""Schedule URLs""" -BA3_SCHEDULE_URL: Optional[str] = env.str("BA3_SCHEDULE_URL", None) -MA_CS_SCHEDULE_URL: Optional[str] = env.str("MA_CS_SCHEDULE_URL", None) -MA_CS_ENG_SCHEDULE_URL: Optional[str] = env.str("MA_CS_ENG_SCHEDULE_URL", None) - - -"""Computed properties""" - - -class ScheduleType(str, Enum): - """Enum to differentiate schedules""" - - BA3 = "ba3" - MA_CS = "ma_cs" - MA_CS_ENG = "ma_cs_eng" - - -@dataclass -class ScheduleInfo: - """Dataclass to hold and combine some information about schedule-related settings""" - - role_id: Optional[int] - schedule_url: Optional[str] - name: Optional[str] = None - - -SCHEDULE_DATA = [ - ScheduleInfo(name=ScheduleType.BA3, role_id=BA3_ROLE, schedule_url=BA3_SCHEDULE_URL), - ScheduleInfo(name=ScheduleType.MA_CS, role_id=MA_CS_ROLE, schedule_url=MA_CS_SCHEDULE_URL), - ScheduleInfo(name=ScheduleType.MA_CS_ENG, role_id=MA_CS_ENG_ROLE, schedule_url=MA_CS_ENG_SCHEDULE_URL), -] diff --git a/tests/test_database/test_crud/test_tasks.py b/tests/test_database/test_crud/test_tasks.py index 5aee4d5..f3adede 100644 --- a/tests/test_database/test_crud/test_tasks.py +++ b/tests/test_database/test_crud/test_tasks.py @@ -46,7 +46,7 @@ async def test_set_execution_time_exists(postgres: AsyncSession, task: Task, tas await crud.set_last_task_execution_time(postgres, task_type) await postgres.refresh(task) - assert task.previous_run == datetime.datetime(year=2022, month=7, day=24, tzinfo=datetime.timezone.utc) + assert task.previous_run == datetime.datetime(year=2022, month=7, day=24) @freeze_time("2022/07/24") @@ -60,4 +60,4 @@ async def test_set_execution_time_doesnt_exist(postgres: AsyncSession, task_type results = list((await postgres.execute(statement)).scalars().all()) assert len(results) == 1 task = results[0] - assert task.previous_run == datetime.datetime(year=2022, month=7, day=24, tzinfo=datetime.timezone.utc) + assert task.previous_run == datetime.datetime(year=2022, month=7, day=24)