Skip to content

Latest commit

 

History

History
522 lines (370 loc) · 24.1 KB

File metadata and controls

522 lines (370 loc) · 24.1 KB

Contributing

A big welcome and thank you for considering contributing to CSS' open-source projects! We welcome anybody who wants to contribute, and we actively encourage everyone to do so, especially if you have never contributed before.

Quick Links

Getting Started

If you have never used Git before, we would recommend that you read GitHub's Getting Started guide. Additionally, linked below are some helpful resources:

If you are new to contributing to open-source projects on GitHub, the general workflow is as follows:

  1. Fork this repository and clone it
  2. Create a branch off main
  3. Make your changes and commit them
  4. Push your local branch to your remote fork
  5. Open a new pull request on GitHub

We recommend also reading the following if you're unsure or not confident:

TeX-Bot is written in Python using Pycord and uses Discord's slash-commands & user-commands. We would recommend being somewhat familiar with the Pycord library, Python language & project terminology before contributing.

Using the Issue Tracker

We use GitHub issues to track bugs and feature requests. If you find an issue with TeX-Bot, the best place to report it is through the issue tracker. If you are looking for issues to contribute code to, it's a good idea to look at the issues labelled "good-first-issue"!

When submitting an issue, please be as descriptive as possible. If you are submitting a bug report, please include the steps to reproduce the bug, and the environment it is in. If you are submitting a feature request, please include the steps to implement the feature.

Repository Structure

Top level files

Other significant directories

  • cogs/: contains all the cogs within this project, see below for more information
  • utils/: contains common utility classes and functions used by the top-level modules and cogs
  • db/core/models/: contains all the database ORM models to interact with storing information longer-term (between individual command events)
  • tests/: contains the complete test suite for this project, based on the Pytest framework

Cogs

Cogs are attachable modules that are loaded onto the Bot instance. They combine related listeners and commands (each as individual methods) into one class. There are separate cog files for each activity, and one __init__.py file which instantiates them all:

For more information about the purpose of each cog, please look at the documentation within the files themselves

Making Your First Contribution

After you have found an issue which needs solving, it's time to start working on a fix! However, there are a few guidelines we would like you to follow first.

Running tests

To ensure your changes adhere to the required functionality of this project, a test suite has been provided in the tests directory. The test suite uses Pytest, and can be run with the following command:

uv run pytest

Pycharm & VS Code also provide GUI interfaces to run the Pytest test suite.

Code Style

In general, follow the formatting in the file you are editing. You should also run the static analysis linting/type checking tools to validate your code.

ruff

Ruff is a static analysis code linter, which will alert you to possible formatting mistakes in your Python code. It can be run with the following command:

uv run ruff check

There are many additional flags to provide more advanced linting help (E.g. --fix). See ruff's documentation for additional configuration options.

mypy

Mypy is a static type checker, which will alert you to possible typing errors in your Python code. It can be run with the following command:

uv run mypy .

Although there is a PyCharm plugin to provide GUI control and inline warnings for mypy, it has been rather temperamental recently. So it is suggested to avoid using it, and run mypy from the command-line instead.

PyMarkdown

PyMarkdown is a static analysis Markdown linter, which will alert you to possible formatting mistakes in your MarkDown files. It can be run with the following command:

uv run ccft-pymarkdown scan-all --with-git

This command includes the removal of custom-formatted tables. See the CCFT-PyMarkdown tool for more information on linting Markdown files that contain custom-formatted tables.

Git Commit Messages

Commit messages should be written in the imperative present tense. For example, "Fix bug #1".

Commit subjects should start with a capital letter and not end in a full-stop.

Additionally, we request that you keep the commit subject under 80 characters for a comfortable viewing experience on GitHub and other git tools. If you need more, please use the body of the commit. (See Robert Painsi's Commit Message Guidelines for how to write good commit messages.)

For example:

Fix TeX becoming sentient

<more detailed description here>

What Happens Next?

Once you have made your changes, please describe them in your pull request in full. We will then review them and communicate with you on GitHub. We may ask you to change a few things, so please do check GitHub or your emails frequently.

After that, that's it! You've made your first contribution. 🎉

License

Please note that any contributions you make will be made under the terms of the Apache Licence 2.0.

Guidance

We aim to get more people involved with our projects, and help build members' confidence in using git and contributing to open-source. If you see an error, we encourage you to be bold and fix it yourself, rather than just raising an issue. If you are stuck, need help, or have a question, the best place to ask is on our Discord.

Happy contributing!

Guides

Creating a New Cog

Cogs are modular components of TeX-Bot that group related commands and listeners into a single class. To create a new cog, follow these steps:

  1. Create the Cog File

    • Navigate to the cogs/ directory.
    • Create a new Python file with a name that reflects the purpose of the cog (e.g., example_cog.py).
  2. Define the Cog Class

    • Import the necessary modules, including TeXBotBaseCog from utils.
    • Define a class that inherits from TeXBotBaseCog.
    • Add a docstring to describe the purpose of the cog.

    Example:

    from utils import TeXBotBaseCog
    
    class ExampleCog(TeXBotBaseCog):
        """A cog for demonstrating functionality."""
    
        def do_something(arguments):
         print("do something")
  3. Add Commands and Listeners

    • Define methods within the class for commands and event listeners.
    • Use decorators like @discord.slash_command() or @TeXBotBaseCog.listener() to register the callback method to their interaction type.
    • Include any necessary checks using CommandChecks decorators.

    Example:

    import discord
    from utils import CommandChecks
    
     __all__: "Sequence[str]" = (
         "ExampleCog",
     )
    
    class ExampleCog(TeXBotBaseCog):
        """A cog for demonstrating functionality."""
  4. Register the Cog

    • Edit cogs/__init__.py to add your new cog class to the list of cogs in the setup function.
    • Also, include the cog class in the __all__ sequence to ensure it is properly exported.

    Example:

    from .example_cog import ExampleCog
    
    __all__: "Sequence[str]" = (
        ...existing cogs...
        "ExampleCog",
    )
    
    def setup(bot: "TeXBot") -> None:
        """Add all the cogs to the bot, at bot startup."""
        cogs: Iterable[type[TeXBotBaseCog]] = (
            ...existing cogs...
            ExampleCog,
        )
        Cog: type[TeXBotBaseCog]
        for Cog in cogs:
            bot.add_cog(Cog(bot))
  5. Test the Cog

    • Run the bot with your changes and ensure the new cog is loaded without errors.
    • Test the commands and listeners to verify they work as expected.
  6. Document the Cog

    • Add comments and docstrings to explain the functionality of the cog.
    • Update the CONTRIBUTING.md file or other relevant documentation if necessary.

Creating a New Environment Variable

To add a new environment variable to the project, follow these steps:

  1. Define the Variable in development .env

    • Open the .env file in the project root directory (or create one if it doesn't exist).
    • Add the new variable in the format VARIABLE_NAME=value.
    • Ensure the variable name is descriptive and uses uppercase letters with underscores.
  2. Update config.py

    • Open the config.py file.
    • Add a new private setup method in the Settings class to validate and load the variable. For example:
      @classmethod
      def _setup_new_variable(cls) -> None:
          raw_value: str | None = os.getenv("NEW_VARIABLE")
      
          if not raw_value or not re.fullmatch(r"<validation_regex>", raw_value):
              raise ImproperlyConfiguredError("NEW_VARIABLE is invalid or missing.")
      
          cls._settings["NEW_VARIABLE"] = raw_value
    • Replace <validation_regex> with a regular expression to validate the variable's format, if applicable.
  3. Call the Setup Method

    • Add the new setup method to the _setup_env_variables method in config.py:
      @classmethod
      def _setup_env_variables(cls) -> None:
          if cls._is_env_variables_setup:
              logger.warning("Environment variables have already been set up.")
              return
      
          cls._settings = {}
      
          cls._setup_new_variable()
          # Add other setup methods here
      
          cls._is_env_variables_setup = True
  4. Document the Variable

    • Update the README.md file under the "Setting Environment Variables" section to include the new variable, its purpose and any valid values.
  5. Test the Variable

    • Run the bot with your changes and ensure the new variable is loaded correctly.
    • Test edge cases, such as missing, blank or invalid values in the .env file, to confirm that error handling functions correctly.

Creating a Response Button

Response buttons are interactive UI components that allow users to respond to bot messages with predefined actions. To create a response button, follow these steps:

  1. Define the Button Class

    • Create a new class in your cog file that inherits from discord.ui.View.
    • Add button callback response methods using the @discord.ui.button decorator.
    • Each button method should define the button's label, a custom response ID and style.

    Example:

    from discord.ui import View
    
    class ConfirmActionView(View):
        """A discord.View containing buttons to confirm or cancel an action."""
    
        @discord.ui.button(
            label="Yes",
            style=discord.ButtonStyle.green,
            custom_id="confirm_yes",
        )
        async def confirm_yes(self, button: discord.Button, interaction: discord.Interaction) -> None:
            # Handle the 'Yes' button click
            await interaction.response.send_message("Action confirmed.", ephemeral=True)
    
        @discord.ui.button(
            label="No",
            style=discord.ButtonStyle.red,
            custom_id="confirm_no",
        )
        async def confirm_no(self, button: discord.Button, interaction: discord.Interaction) -> None:
            # Handle the 'No' button click
            await interaction.response.send_message("Action cancelled.", ephemeral=True)
  2. Send the View with a Message

    • Use the view parameter of the send or respond method to attach the button view to a message. This could be sent in response to a command, event handler or scheduled task.

    Example:

    await ctx.send(
        content="Do you want to proceed?",
        view=ConfirmActionView(),
    )
  3. Handle Button Interactions

    • Define logic within each button method to handle user interactions.
    • Use interaction.response to send feedback or perform actions based on the button clicked.
  4. Test the Button

    • Run the bot with your changes and ensure the buttons appear and function as expected.
    • Test edge cases, such as multiple users interacting with the buttons simultaneously.
  5. Document the Button

    • Add comments and docstrings to explain the purpose and functionality of the button.
    • Update relevant documentation if necessary.

Creating and Interacting with Django Models

Data Protection Consideration

When making changes to the database model, it is essential to consider the data protection implications of these changes. If personal data is being collected, stored or processed, it is essential that this is in compliance with the law. In the UK, the relevant law is the Data Protection Act 2018. As a general rule, any changes that have data protection implications should be checked and approved by the organisation responsible for running the application. Django models are used to interact with the database in this project. They allow you to define the structure of your data and provide an API to query and manipulate it. To create and interact with Django models, follow these steps:

  1. Define a Model

    • Navigate to the db/core/models/ directory.
    • Create a new Python file with a name that reflects the purpose of the model (e.g., example_model.py).
    • If your model is a new property related to each Discord member (E.g. the number of smiley faces of each Discord member, then define a class that inherits from BaseDiscordMemberWrapper (found within the .utils module within the db/core/models/ directory).
    • If your model is unrelated to Discord members, then define a class that inherits from AsyncBaseModel (also found within the .utils module within the db/core/models/ directory).
    • Add your Django fields to the class to represent the model's data structure.
    • If your model inherits from BaseDiscordMemberWrapper then you must declare a field called discord_member which must be either a Django ForeignKey field or a OneToOneField, depending upon the relationship between your model and each Discord member.
    • Define the static class string holding the display name for multiple instances of your class (E.g., INSTANCES_NAME_PLURAL: str = "Members' Smiley Faces")

    Example:

    from django.db import models
    
    from .utils import AsyncBaseModel, BaseDiscordMemberWrapper
    
    class ExampleModel(AsyncBaseModel):
        """A model for demonstrating functionality."""
    
        INSTANCES_NAME_PLURAL: str = "Example Model objects"
    
        name = models.CharField(max_length=255)
        created_at = models.DateTimeField(auto_now_add=True)
    class MemberSmileyFaces(BaseDiscordMemberWrapper):
        """Model to represent the number of smiley faces of each Discord member."""
    
        INSTANCES_NAME_PLURAL: str = "Discord Members' Smiley Faces"
    
         discord_member = models.OneToOneField(
             DiscordMember,
             on_delete=models.CASCADE,
             related_name="smiley_faces",
             verbose_name="Discord Member",
             blank=False,
             null=False,
             primary_key=True,
         )
         count = models.IntegerField(
             "Number of smiley faces",
             null=False,
             blank=True,
             default=0,
         )
  2. Apply Migrations

    • Run the following commands to create and apply migrations for your new model:
      uv run manage.py makemigrations
      uv run manage.py migrate
  3. Query the Model

    • Use Django's ORM to interact with the model. For example:
      from db.core.models.example_model import ExampleModel
      
      class ExampleCog(TeXBotBaseCog):
          """A cog for demonstrating model access."""
      
          async def create_example(self, name: str) -> None:
              """Create a new instance of ExampleModel."""
              await ExampleModel.objects.acreate(name=name)
      
          async def retrieve_examples(self) -> list[ExampleModel]:
              """Retrieve all instances of ExampleModel."""
              return await ExampleModel.objects.all()
      
          async def filter_examples(self, name: str) -> list[ExampleModel]:
              """Filter instances of ExampleModel by name."""
              return await ExampleModel.objects.filter(name=name)
      
          async def update_example(self, example: ExampleModel, new_name: str) -> None:
              """Update the name of an ExampleModel instance."""
              example.name = new_name
              await example.asave()
      
          async def delete_example(self, example: ExampleModel) -> None:
              """Delete an ExampleModel instance."""
              await example.adelete()
  4. Document the Model

    • Add comments and docstrings to explain the purpose and functionality of your new model.

Member Retrieval DB Queries via Hashed Discord ID

To retrieve members from the database using their hashed Discord ID, follow these steps:

  1. Hash the Discord ID

    • Use a consistent hashing algorithm to hash the Discord ID before storing or querying it in the database.

    Example:

    import hashlib
    
    def hash_discord_id(discord_id: str) -> str:
        return hashlib.sha256(discord_id.encode()).hexdigest()
  2. Query the Database

    • A custom filter field is implemented for models that inherit from BaseDiscordMemberWrapper, this allows you to filter using the unhashed Discord ID, despite only the hashed Discord ID being stored in the database.

    Example:

    from db.core.models.member_smiley_faces import MemberSmileyFaces
    
    class MyExampleCommandCog(TeXBotBaseCog):
        @tasks.loop(minutes=5)
        async def check_member_smiley_faces(self) -> None:
            member_smiley_faces = await MemberSmileyFaces.objects.filter(discord_id=1234567).afirst()
    
            if member_smiley_faces:
                print(f"Member's smiley faces found: {member_smiley_faces.discord_member.name}")
            else:
                print("Member's smiley facesnot found.")

    It is unlikely that you will need to query the DiscordMember model directly. Instead, the attributes of the member can be accessed by the relationship between each new Django model to the DiscordMember model.

  3. Test the Query

    • Ensure the query works as expected by testing it with valid and invalid Discord IDs.
  4. Document the Query

    • Add comments and docstrings to explain the purpose and functionality of the query.