tickets_plus
Tickets+ - A Discord bot for extending Tickets.
Welcome to Tickets+
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.:
- Open the directory you want the project to be placed in.
- Use
git clone https://github.com/Tech-TTGames/Tickets-Plus.gitor download and unpack the repo. - Ensure that python3.12 is installed and available, same for pip.
- Run
curl -sSL https://install.python-poetry.org | python3 -. And follow instructions provided there.- Change some poetry settings as-needed. You can add the
--localflag to set those settings only to the current directorypoetry config virtualenvs.in-project trueinstalls the virtual environment in the project, not in a poetry-specific location (recommended).
- Change some poetry settings as-needed. You can add the
- Run
poetry install- Depending on the DB used, add
-E pgsqlor-E sqlite - If you want development packages, add
--with dev
- Depending on the DB used, add
- Install PostgreSQL. Guide Here
- Set up automatic PostgreSQL startup on Linux and for windows just start it via
services.msc - 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:
sudo -u postgres -icreateuser --pwprompt <dbuser>The prompt will ask you for a password for new user -<dbpass>.createdb -E UTF8 -O <dbuser> <dbname> [comment]<dbuser>being the same as in the previous step.
- Windows:
- If the installed PostgreSQL bin isn't in PATH, use
cdto go to the installation bin. 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>.createdb -E UTF8 -O <dbuser> -U postgres <dbname> [comment]<dbuser>being the same as in the previous step.
- If the installed PostgreSQL bin isn't in PATH, use
- Linux:
- 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
- Set up automatic PostgreSQL startup on Linux and for windows just start it via
- Create the bot on Discord Developers.
- Create application however you want.
- Create a bot.
- Turn on the 'Message Content' and 'Server Members' privileged intents. Probably disable 'Public Bot'.
- Input your bot token to secret.json. (Refer to example_secret.json)
- 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
- The _easy link_ -
- 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:
- Create an SSL certificate. Either self-signed or from a CA (like Let's Encrypt).
- Enter the path to the certificate in config.json under "ssl_cert", an in secret.json under "ssl_key" the path to the key.
- Create a safe token for the API and enter it in secret.json under "auth_token".
- 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.
- Connect the API to the main bot. Go to the
https://panel.ticketsbot.net/manage/<YOUR_SERVER_ID>/integrations/createand 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
/.
- 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)
- Start your bot! Use
poetry run startor 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._
- Finally send
<bot ping> syncin 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))
-
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. ↩
-
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. ↩
-
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. ↩
-
Staff role defined in per-server settings. ↩
-
Staff role defined in per-server settings. ↩
-
Staff role defined in per-server settings. ↩
-
Staff role defined in per-server settings. ↩
-
Community roles defined in per-server settings. ↩
-
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+!")
49def sigint_handler(sign, frame): 50 """Handles SIGINT (Ctrl+C)""" 51 logging.info("SIGINT received. Exiting.") 52 sys.exit(0)
Handles SIGINT (Ctrl+C)
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.