Edit on GitHub

tickets_plus

Tickets+ - A Discord bot for extending Tickets.

Welcome to Tickets+

CodeQL Pylint DeepSource

A Discord bot that adds extensions to the Tickets discord bot.1 Made by the Tickets+ team, a group of volunteers2 who want to add features to the Tickets bot. But didn't know how to code in rust.

With this bot, you can add staff notes, staff responses, and community support to your server. This bot acts as a supplementary, separate bot3 to the main Tickets bot.

Feature 1 - Staff Notes

Due to the implementation of staff notes in the main bot this is now legacy, but still available servers registered before the v0.2 update.

Private threads are _(now)_ free!

This creates a private thread in a channel and adds the staff roles to it4. You can now disable pings, just use /settings staffpings to toggle. The 'open message' is adjustable via /settings openmsg. $channels is replaced with the channel mention.

Feature 2 - Staff Response

You can now respond as the staff team5 using /respond. The command is limited to users with roles added to staff6. The 'name' of the team7 is adjustable via /settings staffname.

Feature 3 - Community Support

Automatically adds roles8 to the channel created by Tickets bots9. Additionally, facilitates correct ping behavior with said community roles.

Minor Feature 1 - Message Discovery

The bot will display previews of discord message links. Requires message content intent.

Minor Feature 2 - Strip Buttons

Due to the main Tickets' bot not facilitating permissions check with the 'Close' and 'Close with Reason' buttons, this bot will strip the buttons, enabling the use of / command perms to limit access. Toggle with /setting stripbuttons.

Setup

You can add the public instance of the bot using the links here:

Here are the steps to host your copy of this bot.:

  1. Open the directory you want the project to be placed in.
  2. Use git clone https://github.com/Tech-TTGames/Tickets-Plus.git or download and unpack the repo.
  3. Ensure that python3.12 is installed and available, same for pip.
  4. Run curl -sSL https://install.python-poetry.org | python3 -. And follow instructions provided there.
    • Change some poetry settings as-needed. You can add the --local flag to set those settings only to the current directory
      • poetry config virtualenvs.in-project true installs the virtual environment in the project, not in a poetry-specific location (recommended).
  5. Run poetry install
    • Depending on the DB used, add -E pgsql or -E sqlite
    • If you want development packages, add --with dev
  6. Install PostgreSQL. Guide Here
    1. Set up automatic PostgreSQL startup on Linux and for windows just start it via services.msc
    2. Set up user and database for the bot. <FIELD> are required and to be replaced with your stuff. [FIELD] are optional and can be ignored.
      • Linux:
        1. sudo -u postgres -i
        2. createuser --pwprompt <dbuser> The prompt will ask you for a password for new user - <dbpass>.
        3. createdb -E UTF8 -O <dbuser> <dbname> [comment] <dbuser> being the same as in the previous step.
      • Windows:
        1. If the installed PostgreSQL bin isn't in PATH, use cd to go to the installation bin.
        2. createuser --pwprompt -U postgres <dbuser> The prompt will ask you for the password of the postgres user and the password for the new user - <dbpass>.
        3. createdb -E UTF8 -O <dbuser> -U postgres <dbname> [comment] <dbuser> being the same as in the previous step.
    3. Fill out config.json based on the database config environment. (refer to example_config.json)
      • Don't change “dbtype” unless you're using SQLite.
      • Unless you are using a remote server/changed config, don't touch “dbhost” and “dbport”.
      • Otherwise, all parameters are named _here_ and in example_config.json the same
  7. Create the bot on Discord Developers.
    1. Create application however you want.
    2. Create a bot.
    3. Turn on the 'Message Content' and 'Server Members' privileged intents. Probably disable 'Public Bot'.
    4. Input your bot token to secret.json. (Refer to example_secret.json)
    5. Invite the bot to your server! Replace the in the below invites with numbers from https://discord.com/developers/applications/<CLIENT_ID>/
      • The _easy link_ - https://discord.com/api/oauth2/authorize?client_id=<CLIENT_ID>&permissions=8&scope=bot%20applications.commands
      • The _safer link_ - https://discord.com/api/oauth2/authorize?client_id=<CLIENT_ID>&permissions=535059492048&scope=bot%20applications.commands
  8. Set up API (optional): This is an advanced step that will not be described in detail. (If certificate not set API skipped) If you want to use it here's some guidelines:
    1. Create an SSL certificate. Either self-signed or from a CA (like Let's Encrypt).
    2. Enter the path to the certificate in config.json under "ssl_cert", an in secret.json under "ssl_key" the path to the key.
    3. Create a safe token for the API and enter it in secret.json under "auth_token".
    4. And you're done with the basic setup. You can now use the API. If you want to use more bells and whistles, do it yourself.
    5. Connect the API to the main bot. Go to the https://panel.ticketsbot.net/manage/<YOUR_SERVER_ID>/integrations/create and create a new integration. Follow instructions there.
      • Use 'POST' requests.
      • The header for the token is ticketsplus-api-auth, the value is the token you set in secret.json.
      • Do not set any placeholders.
      • The URL is the URL of the domain/IP you are hosting the API on, with the port if you are using one.
      • The protocol _must_ be HTTPS.
      • The path is just /.
  9. Copy your _main_ guild ID and paste it into config.json under "dev_guild_id". This will enable the dev commands in your server. (Required)
  10. Start your bot! Use poetry run start or after activating venv (if present) python3 /tickets_plus/
  • Probably add a background service that will restart the bot on boot. _I use systemd for my bots._
  1. Finally send <bot ping> sync in the bot's DMs to sync / commands.

API DOCUMENTATION FOLLOWS:

This file is the main file for the bot. It contains the startup code for the bot. Other code is in the various submodules. This file is to be used as a module, not as a script.

Typical usage example:

For a standard startup, use start_bot. For a custom startup, use the code in the example below.

#!/usr/bin/env python3
import asyncio
import tickets_plus
loop = asyncio.get_event_loop()
loop.run_until_complete(tickets_plus.start_bot(config))

  1. This bot is not affiliated with the Tickets Bot team. When a feature is added to the main bot, any feature that is no longer needed will be discontinued here. 

  2. Tech-TTGames, I'm the only one here right now. I had some help in general, but I'm the only one who has done any coding. More people are welcome to join. I had help with organization and checking my code from kk5dire and Ben Foster with hosting and some documentation changes. 

  3. This setup assumes you have the main bot added to the server and configured. Support will not be provided to those who do not assume a likewise configuration. 

  4. Staff role defined in per-server settings. 

  5. Staff role defined in per-server settings. 

  6. Staff role defined in per-server settings. 

  7. Staff role defined in per-server settings. 

  8. Community roles defined in per-server settings. 

  9. Tickets bot defined in per-server settings. 

  1"""Tickets+ - A Discord bot for extending Tickets.
  2
  3.. include:: ../README.md
  4
  5**API DOCUMENTATION FOLLOWS:**
  6
  7This file is the main file for the bot.
  8It contains the startup code for the bot.
  9Other code is in the various submodules.
 10This file is to be used as a module, not as a script.
 11
 12Typical usage example:
 13    For a standard startup, use start_bot.
 14    For a custom startup, use the code in the example below.
 15    ```py
 16    #!/usr/bin/env python3
 17    import asyncio
 18    import tickets_plus
 19    loop = asyncio.get_event_loop()
 20    loop.run_until_complete(tickets_plus.start_bot(config))
 21    ```
 22"""
 23# License: EPL-2.0
 24# SPDX-License-Identifier: EPL-2.0
 25# Copyright (c) 2021-present The Tickets+ Contributors
 26# This Source Code may also be made available under the following
 27# Secondary Licenses when the conditions for such availability set forth
 28# in the Eclipse Public License, v. 2.0 are satisfied: GPL-3.0-only OR
 29# If later approved by the Initial Contributor, GPL-3.0-or-later.
 30
 31import logging
 32import os
 33import signal
 34import ssl
 35import sys
 36
 37import discord
 38import sqlalchemy
 39from discord.ext import commands
 40from sqlalchemy.ext import asyncio as sa_asyncio
 41
 42from tickets_plus import bot
 43from tickets_plus.api import routes
 44from tickets_plus.database import config, const, models
 45
 46
 47# pylint: disable=unused-argument
 48def sigint_handler(sign, frame):
 49    """Handles SIGINT (Ctrl+C)"""
 50    logging.info("SIGINT received. Exiting.")
 51    sys.exit(0)
 52
 53
 54signal.signal(signal.SIGINT, sigint_handler)
 55
 56
 57async def start_bot(stat_data: config.MiniConfig = config.MiniConfig()) -> None:  # shush deepsource # skipcq: FLK-E124
 58    """Sets up the bot and starts it. Coroutine.
 59
 60    This function uses the existing .json files to set up the bot.
 61    It also sets up logging, and starts the bot.
 62
 63    Args:
 64        stat_data: The config to use.
 65            Must implement the getitem method.
 66            And the get_url method.
 67            If None, a new one will be created.
 68    """
 69    print("Setting up bot...")
 70    try:
 71        # Set up logging
 72        dt_fmr = "%Y-%m-%d %H:%M:%S"
 73        const.HANDLER.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s", dt_fmr))
 74
 75        # Set up bot logging
 76        logging.root.setLevel(logging.INFO)
 77        logging.root.addHandler(const.HANDLER)
 78
 79        # Set up discord.py logging
 80        dscrd_logger = logging.getLogger("discord")
 81        dscrd_logger.setLevel(logging.INFO)
 82        dscrd_logger.addHandler(const.HANDLER)
 83
 84        # Set up sqlalchemy logging
 85        sql_logger = logging.getLogger("sqlalchemy.engine")
 86        sql_logger.setLevel(logging.WARNING)
 87        sql_logger.addHandler(const.HANDLER)
 88
 89        sql_pool_logger = logging.getLogger("sqlalchemy.pool")
 90        sql_pool_logger.setLevel(logging.WARNING)
 91        sql_pool_logger.addHandler(const.HANDLER)
 92
 93        if os.environ.get("TICKETS_PLUS_VERBOSE", "false").lower() == "true":
 94            logging.info("Enabling verbose logging.")
 95            handler2 = logging.StreamHandler()
 96            logging.root.addHandler(handler2)
 97            dscrd_logger.addHandler(handler2)
 98            sql_logger.addHandler(handler2)
 99            sql_pool_logger.addHandler(handler2)
100        logging.info("Logging set up.")
101    # pylint: disable=broad-exception-caught # skipcq: PYL-W0718
102    except Exception as exc:
103        logging.exception("Logging setup failed. Aborting startup.")
104        print("Logging: FAILED")
105        print(f"Error: {exc}")
106        print("Aborting...")
107        return
108    print("Logging: OK")
109
110    # Set up database
111    try:
112        logging.info("Creating engine...")
113        if "asyncpg" in stat_data.getitem("dbtype"):
114            engine = sa_asyncio.create_async_engine(stat_data.get_url(),
115                                                    pool_size=10,
116                                                    max_overflow=-1,
117                                                    pool_recycle=600,
118                                                    connect_args={"server_settings": {
119                                                        "jit": "off"
120                                                    }})
121        else:
122            engine = sa_asyncio.create_async_engine(
123                stat_data.get_url(),
124                pool_size=10,
125                max_overflow=-1,
126                pool_recycle=600,
127            )
128        logging.info("Engine created. Ensuring tables...")
129        async with engine.begin() as conn:
130            await conn.execute(sqlalchemy.schema.CreateSchema("tickets_plus", if_not_exists=True))
131            await conn.run_sync(models.Base.metadata.create_all)
132            await conn.commit()
133        logging.info("Tables ensured. Starting bot...")
134    # pylint: disable=broad-exception-caught # skipcq: PYL-W0718
135    except Exception as exc:
136        logging.exception("Database setup failed. Aborting startup.")
137        print("Database: FAILED")
138        print(f"Error: {exc}")
139        print("Aborting...")
140        return
141    print("Database: OK")
142    try:
143        bot_instance = bot.TicketsPlusBot(
144            db_engine=engine,
145            intents=const.INTENTS,
146            command_prefix=commands.when_mentioned,
147            status=discord.Status.online,
148            activity=discord.Activity(type=discord.ActivityType.playing, name="with tickets"),
149        )
150    # pylint: disable=broad-exception-caught # skipcq: PYL-W0718
151    except Exception as exc:
152        logging.exception("Bot setup failed. Exiting.")
153        print("Bot: FAILED")
154        print(f"Error: {exc}")
155        print("Exiting...")
156        return
157    print("Bot: OK")
158
159    # Tornado API setup
160    try:
161        api_routes = routes.make_app(bot_instance)
162        tls_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
163        tls_ctx.options |= ssl.OP_NO_TLSv1_2
164        tls_ctx.options |= ssl.OP_CIPHER_SERVER_PREFERENCE
165        tls_ctx.options |= ssl.OP_NO_RENEGOTIATION
166        try:
167            tls_ctx.load_cert_chain(
168                stat_data.getitem("ssl_cert"),
169                config.Secret().ssl_key,
170            )
171        # pylint: disable=broad-exception-caught # skipcq: PYL-W0718
172        except Exception as e:
173            _ = e
174            logging.info("SSL cert not found. Starting without API...")
175            print("API: SKIPPED (SSL cert not found)")
176        else:
177            tkn = stat_data.getitem("auth_token")
178            frbddn = ["thequickbrownfoxjumpedoverthelazydog", ""]
179            if tkn is None or tkn in frbddn:
180                raise ValueError("API Auth token not set.")
181            logging.info("SSL cert and key loaded. Starting API...")
182            api_routes.listen(stat_data.getitem("https_port"), protocol="https", ssl_options=tls_ctx)
183    # pylint: disable=broad-exception-caught # skipcq: PYL-W0718
184    except Exception as exc:
185        logging.exception("API setup failed. Aborting startup.")
186        print("API: FAILED")
187        print(f"Error: {exc}")
188        print("Aborting...")
189        return
190    print("API: OK")
191
192    try:
193        print("ALL OK. Starting bot...")
194        await bot_instance.start(config.Secret().token)
195    except KeyboardInterrupt:
196        logging.info("Keyboard interrupt detected. Shutting down...")
197        print("Keyboard interrupt detected. Shutting down...")
198        await bot_instance.close()
199    except SystemExit as exc:
200        logging.info("System exit code: %s detected. Closing bot...", exc.code)
201        print(f"System exit code: {exc.code} detected. Closing bot...")
202        await bot_instance.close()
203    else:
204        print("Internal bot shutdown. (/close was used.)")
205        logging.info("Bot shutdown gracefully.")
206    logging.info("Bot shutdown complete.")
207    print("Thanks for using Tickets+!")
def sigint_handler(sign, frame):
49def sigint_handler(sign, frame):
50    """Handles SIGINT (Ctrl+C)"""
51    logging.info("SIGINT received. Exiting.")
52    sys.exit(0)

Handles SIGINT (Ctrl+C)

async def start_bot( stat_data: tickets_plus.database.config.MiniConfig = <tickets_plus.database.config.MiniConfig object>) -> None:
 58async def start_bot(stat_data: config.MiniConfig = config.MiniConfig()) -> None:  # shush deepsource # skipcq: FLK-E124
 59    """Sets up the bot and starts it. Coroutine.
 60
 61    This function uses the existing .json files to set up the bot.
 62    It also sets up logging, and starts the bot.
 63
 64    Args:
 65        stat_data: The config to use.
 66            Must implement the getitem method.
 67            And the get_url method.
 68            If None, a new one will be created.
 69    """
 70    print("Setting up bot...")
 71    try:
 72        # Set up logging
 73        dt_fmr = "%Y-%m-%d %H:%M:%S"
 74        const.HANDLER.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s", dt_fmr))
 75
 76        # Set up bot logging
 77        logging.root.setLevel(logging.INFO)
 78        logging.root.addHandler(const.HANDLER)
 79
 80        # Set up discord.py logging
 81        dscrd_logger = logging.getLogger("discord")
 82        dscrd_logger.setLevel(logging.INFO)
 83        dscrd_logger.addHandler(const.HANDLER)
 84
 85        # Set up sqlalchemy logging
 86        sql_logger = logging.getLogger("sqlalchemy.engine")
 87        sql_logger.setLevel(logging.WARNING)
 88        sql_logger.addHandler(const.HANDLER)
 89
 90        sql_pool_logger = logging.getLogger("sqlalchemy.pool")
 91        sql_pool_logger.setLevel(logging.WARNING)
 92        sql_pool_logger.addHandler(const.HANDLER)
 93
 94        if os.environ.get("TICKETS_PLUS_VERBOSE", "false").lower() == "true":
 95            logging.info("Enabling verbose logging.")
 96            handler2 = logging.StreamHandler()
 97            logging.root.addHandler(handler2)
 98            dscrd_logger.addHandler(handler2)
 99            sql_logger.addHandler(handler2)
100            sql_pool_logger.addHandler(handler2)
101        logging.info("Logging set up.")
102    # pylint: disable=broad-exception-caught # skipcq: PYL-W0718
103    except Exception as exc:
104        logging.exception("Logging setup failed. Aborting startup.")
105        print("Logging: FAILED")
106        print(f"Error: {exc}")
107        print("Aborting...")
108        return
109    print("Logging: OK")
110
111    # Set up database
112    try:
113        logging.info("Creating engine...")
114        if "asyncpg" in stat_data.getitem("dbtype"):
115            engine = sa_asyncio.create_async_engine(stat_data.get_url(),
116                                                    pool_size=10,
117                                                    max_overflow=-1,
118                                                    pool_recycle=600,
119                                                    connect_args={"server_settings": {
120                                                        "jit": "off"
121                                                    }})
122        else:
123            engine = sa_asyncio.create_async_engine(
124                stat_data.get_url(),
125                pool_size=10,
126                max_overflow=-1,
127                pool_recycle=600,
128            )
129        logging.info("Engine created. Ensuring tables...")
130        async with engine.begin() as conn:
131            await conn.execute(sqlalchemy.schema.CreateSchema("tickets_plus", if_not_exists=True))
132            await conn.run_sync(models.Base.metadata.create_all)
133            await conn.commit()
134        logging.info("Tables ensured. Starting bot...")
135    # pylint: disable=broad-exception-caught # skipcq: PYL-W0718
136    except Exception as exc:
137        logging.exception("Database setup failed. Aborting startup.")
138        print("Database: FAILED")
139        print(f"Error: {exc}")
140        print("Aborting...")
141        return
142    print("Database: OK")
143    try:
144        bot_instance = bot.TicketsPlusBot(
145            db_engine=engine,
146            intents=const.INTENTS,
147            command_prefix=commands.when_mentioned,
148            status=discord.Status.online,
149            activity=discord.Activity(type=discord.ActivityType.playing, name="with tickets"),
150        )
151    # pylint: disable=broad-exception-caught # skipcq: PYL-W0718
152    except Exception as exc:
153        logging.exception("Bot setup failed. Exiting.")
154        print("Bot: FAILED")
155        print(f"Error: {exc}")
156        print("Exiting...")
157        return
158    print("Bot: OK")
159
160    # Tornado API setup
161    try:
162        api_routes = routes.make_app(bot_instance)
163        tls_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
164        tls_ctx.options |= ssl.OP_NO_TLSv1_2
165        tls_ctx.options |= ssl.OP_CIPHER_SERVER_PREFERENCE
166        tls_ctx.options |= ssl.OP_NO_RENEGOTIATION
167        try:
168            tls_ctx.load_cert_chain(
169                stat_data.getitem("ssl_cert"),
170                config.Secret().ssl_key,
171            )
172        # pylint: disable=broad-exception-caught # skipcq: PYL-W0718
173        except Exception as e:
174            _ = e
175            logging.info("SSL cert not found. Starting without API...")
176            print("API: SKIPPED (SSL cert not found)")
177        else:
178            tkn = stat_data.getitem("auth_token")
179            frbddn = ["thequickbrownfoxjumpedoverthelazydog", ""]
180            if tkn is None or tkn in frbddn:
181                raise ValueError("API Auth token not set.")
182            logging.info("SSL cert and key loaded. Starting API...")
183            api_routes.listen(stat_data.getitem("https_port"), protocol="https", ssl_options=tls_ctx)
184    # pylint: disable=broad-exception-caught # skipcq: PYL-W0718
185    except Exception as exc:
186        logging.exception("API setup failed. Aborting startup.")
187        print("API: FAILED")
188        print(f"Error: {exc}")
189        print("Aborting...")
190        return
191    print("API: OK")
192
193    try:
194        print("ALL OK. Starting bot...")
195        await bot_instance.start(config.Secret().token)
196    except KeyboardInterrupt:
197        logging.info("Keyboard interrupt detected. Shutting down...")
198        print("Keyboard interrupt detected. Shutting down...")
199        await bot_instance.close()
200    except SystemExit as exc:
201        logging.info("System exit code: %s detected. Closing bot...", exc.code)
202        print(f"System exit code: {exc.code} detected. Closing bot...")
203        await bot_instance.close()
204    else:
205        print("Internal bot shutdown. (/close was used.)")
206        logging.info("Bot shutdown gracefully.")
207    logging.info("Bot shutdown complete.")
208    print("Thanks for using Tickets+!")

Sets up the bot and starts it. Coroutine.

This function uses the existing .json files to set up the bot. It also sets up logging, and starts the bot.

Arguments:
  • stat_data: The config to use. Must implement the getitem method. And the get_url method. If None, a new one will be created.