Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Discord Bot Token
BOT_TOKEN=your_discord_bot_token_here

# Reddit API Configuration (Optional - if not provided, Reddit commands will be disabled)
PRAW_CLIENT_ID=your_reddit_client_id
PRAW_CLIENT_SECRET=your_reddit_client_secret

# Socket Server Configuration
SOCKET_AUTH_TOKEN=your_socket_auth_token
SOCKET_SERVER_PORT=8080

# Database/API Configuration (if applicable)
# Add any database URLs or API keys here

# Other Environment Variables
# Add any additional configuration here
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ log.txt

# VSCode
.vscode
*.code-workspace

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down Expand Up @@ -94,10 +95,11 @@ celerybeat-schedule
.env
.venv
env/
venv/
*venv/
ENV/
env.bak/
venv.bak/
poetry.lock

# Spyder project settings
.spyderproject
Expand Down
28 changes: 0 additions & 28 deletions Pipfile

This file was deleted.

757 changes: 0 additions & 757 deletions Pipfile.lock

This file was deleted.

19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,26 +67,23 @@ Python 3.8 required

```bash

# Your global Python installation needs to have pipenv
pip install pipenv
# Your global Python installation needs to have poetry
pip install poetry

# Clone the repo
git clone https://github.com/Tortoise-Community/Tortoise-BOT.git

# Change directories into the project
cd Tortoise-BOT

# [developer only] If you are developer you need to install dependencies for dev
pipenv install --dev
# Install dependencies (includes dev dependencies)
poetry install

# If you're not a developer just install required dependencies like this
pipenv install

# Activate the Pipenv shell (aka tell your terminal/whatever to use dependencies from the env in this project)
pipenv shell
# Activate the Poetry shell (aka tell your terminal/whatever to use dependencies from the env in this project)
poetry shell

# [developer only] Install pre-commit hook
pipenv run precommit
poetry run pre-commit install

# Before we run the bot we need to create .env file where all secret keys will be (tokens etc)

Expand Down Expand Up @@ -144,7 +141,7 @@ You can safely ignore these errors as they will not stop the bot from functionin
# Once you are in Tortoise-Bot/

# Run the bot
pipenv run start
poetry run start
```

# License
Expand Down
10 changes: 7 additions & 3 deletions bot/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
console.setFormatter(formatter)
console_logger.addHandler(console)

console_logger.info("Loading and starting the bot..")
load_dotenv()
Bot().run(os.getenv("BOT_TOKEN"))
def main():
console_logger.info("Loading and starting the bot..")
load_dotenv()
Bot().run(os.getenv("BOT_TOKEN"))

if __name__ == "__main__":
main()
27 changes: 13 additions & 14 deletions bot/api_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import asyncio
import json
import logging
from asyncio import AbstractEventLoop
from datetime import datetime, timezone
from typing import Optional, List, Union

Expand Down Expand Up @@ -35,9 +35,9 @@ def __str__(self):


class BaseAPIClient:
def __init__(self, base_api_url: str, *, loop: Optional[AbstractEventLoop], **kwargs):
def __init__(self, base_api_url: str, **kwargs):
self.base_api_url = base_api_url
self.session = aiohttp.ClientSession(loop=loop, **kwargs)
self.session = aiohttp.ClientSession(**kwargs)

def _url_for(self, endpoint: str) -> str:
return f"{self.base_api_url}{endpoint}"
Expand Down Expand Up @@ -83,8 +83,8 @@ async def delete(self, endpoint: str, **kwargs) -> Optional[dict]:


class GithubAPI(BaseAPIClient):
def __init__(self, *, loop: AbstractEventLoop):
super().__init__(github_repo_stats_endpoint, loop=loop)
def __init__(self):
super().__init__(github_repo_stats_endpoint)

async def get_project_commits(self, repository_name: str) -> int:
"""
Expand All @@ -109,12 +109,12 @@ async def get_project_commits(self, repository_name: str) -> int:


class TortoiseAPI(BaseAPIClient):
def __init__(self, *, loop: AbstractEventLoop):
def __init__(self):
auth_header = {
"Authorization": f"Token {os.getenv('API_ACCESS_TOKEN')}",
"Content-Type": "application/json"
}
super().__init__("https://api.tortoisecommunity.org/private/", loop=loop, headers=auth_header)
super().__init__("https://api.tortoisecommunity.org/private/", headers=auth_header)

async def get_suggestions_under_review(self) -> List[dict]:
# Gets all suggestion that are under-review
Expand All @@ -137,7 +137,7 @@ async def post_suggestion(self, author: User, message: Message, suggestion: str)
"author_id": author.id,
"author_name": author.display_name,
"brief": suggestion,
"avatar": str(author.avatar_url),
"avatar": str(author.avatar.url),
"link": message.jump_url,
"date": datetime.now(timezone.utc).isoformat()
}
Expand Down Expand Up @@ -284,8 +284,8 @@ class HataAPI(BaseAPIClient):
HATA_API_VERSION = "v1"
HATA_API_ENDPOINT = f"{HATA_API_URL}/{HATA_API_VERSION}/"

def __init__(self, *, loop: AbstractEventLoop):
super().__init__(self.HATA_API_ENDPOINT, loop=loop)
def __init__(self):
super().__init__(self.HATA_API_ENDPOINT)

async def search(self, search_for: str) -> List[dict]:
params = {"search_for": search_for}
Expand All @@ -297,10 +297,9 @@ class AdventOfCodeAPI(BaseAPIClient):
COOKIES = {"session": os.getenv("AOC_COOKIE")}
AOC_API_URL = "https://adventofcode.com/{year}/leaderboard/private/view/{leaderboard_id}"

def __init__(self, leaderboard_id: str, year: int = 2020, *, loop: AbstractEventLoop):
def __init__(self, leaderboard_id: str, year: int = 2020):
super().__init__(
self.AOC_API_URL.format(year=year, leaderboard_id=leaderboard_id),
loop=loop,
headers=self.AOC_REQUEST_HEADER,
cookies=self.COOKIES
)
Expand All @@ -313,8 +312,8 @@ class StackAPI(BaseAPIClient):
STACK_API_URL = "https://api.stackexchange.com"
STACK_API_VERSION = "2.2"

def __init__(self, *, loop: AbstractEventLoop):
super().__init__(f"{self.STACK_API_URL}/{self.STACK_API_VERSION}/", loop=loop)
def __init__(self):
super().__init__(f"{self.STACK_API_URL}/{self.STACK_API_VERSION}/")

async def search(
self,
Expand Down
89 changes: 62 additions & 27 deletions bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import discord
from discord.ext import commands
import aiohttp.client_exceptions

from bot.api_client import TortoiseAPI
from bot.constants import error_log_channel_id, bot_log_channel_id
Expand All @@ -24,14 +25,16 @@ class Bot(commands.Bot):
banned_extensions = ("advent_of_code",)

def __init__(self, prefix="t.", *args, **kwargs):
super(Bot, self).__init__(*args, command_prefix=prefix, intents=discord.Intents.all(), **kwargs)
self.api_client: TortoiseAPI = TortoiseAPI(loop=self.loop)
self._was_ready_once = False
kwargs.setdefault("activity", discord.Game(name="DM to Contact Staff"))
super(Bot, self).__init__(
*args, command_prefix=prefix, intents=discord.Intents.all(), **kwargs
)
self.api_client: TortoiseAPI = None
self.tortoise_meta_cache = {
"event_submission": False,
"mod_mail": False,
"bug_report": False,
"suggestions": False
"suggestions": False,
}

async def on_ready(self):
Expand All @@ -41,44 +44,70 @@ async def on_ready(self):
"Further logging output will go to log file.."
)

if not self._was_ready_once:
await self.on_first_ready()
self._was_ready_once = True

async def on_first_ready(self):
self.load_extensions()
await self.change_presence(activity=discord.Game(name="DM to Contact Staff"))
async def setup_hook(self):
self.api_client: TortoiseAPI = TortoiseAPI()
await self.load_extensions()
await self.reload_tortoise_meta_cache()
try:
version = subprocess.check_output(["git", "describe", "--always"]).strip().decode("utf-8")
version = (
subprocess.check_output(["git", "describe", "--always"])
.strip()
.decode("utf-8")
)
bot_log_channel = self.get_channel(bot_log_channel_id)
await bot_log_channel.send(embed=info(f"Bot restarted. Build version `{version}`", self.user, ""))
await bot_log_channel.send(
embed=info(f"Bot restarted. Build version `{version}`", self.user, "")
)
except Exception as e:
logger.info("Git image version not found", e)
logger.exception("Git image version not found", exc_info=True)
try:
version = (
subprocess.check_output(["git", "describe", "--always"])
.strip()
.decode("utf-8")
)
bot_log_channel = self.get_channel(bot_log_channel_id)
await bot_log_channel.send(
embed=info(f"Bot restarted. Build version `{version}`", self.user, "")
)
except Exception as e:
logger.exception("Git image version not found", exc_info=True)

async def reload_tortoise_meta_cache(self):
# For some reason it takes some time to propagate change in API database so if we fetch right away
# we will get old data.
await asyncio.sleep(3)
self.tortoise_meta_cache = await self.api_client.get_server_meta()
try:
# For some reason it takes some time to propagate change in API database so if we fetch right away
# we will get old data.
await asyncio.sleep(3)
self.tortoise_meta_cache = await self.api_client.get_server_meta()
except aiohttp.client_exceptions.ClientConnectorDNSError as e:
logging.error(f"DNS resolution failed for server meta: {e}")
self.tortoise_meta_cache = {} # Set empty cache as fallback
except Exception as e:
logging.error(f"Unexpected error loading server meta: {e}")
self.tortoise_meta_cache = {} # Set empty cache as fallback

def load_extensions(self):
async def load_extensions(self):
for extension_path in Path("bot/cogs").glob("*.py"):
extension_name = extension_path.stem

if extension_name in self.banned_extensions:
continue
elif self.allowed_extensions and extension_name not in self.allowed_extensions:
elif (
self.allowed_extensions
and extension_name not in self.allowed_extensions
):
continue

dotted_path = f"bot.cogs.{extension_name}"

try:
self.load_extension(dotted_path)
await self.load_extension(dotted_path)
console_logger.info(f"loaded {dotted_path}")
except Exception as e:
traceback_msg = traceback.format_exception(etype=type(e), value=e, tb=e.__traceback__)
console_logger.info(f"Failed to load cog {dotted_path} - traceback:{traceback_msg}")
traceback_msg = traceback.format_exception(type(e), e, e.__traceback__)
console_logger.info(
f"Failed to load cog {dotted_path} - traceback:{traceback_msg}"
)

@staticmethod
async def on_connect():
Expand Down Expand Up @@ -106,12 +135,18 @@ async def log_error(self, message: str):

for count, message in enumerate(split_messages):
if count < 5:
await error_log_channel.send(f"```Num {count+1}/{len(split_messages)}:\n{message}```")
await error_log_channel.send(
f"```Num {count+1}/{len(split_messages)}:\n{message}```"
)
else:
await error_log_channel.send("```Stopping spam, too many pages. See log for more info.```")
await error_log_channel.send(
"```Stopping spam, too many pages. See log for more info.```"
)
break

@staticmethod
def split_string_into_chunks(string: str, chunk_size: int) -> Generator[str, None, None]:
def split_string_into_chunks(
string: str, chunk_size: int
) -> Generator[str, None, None]:
for i in range(0, len(string), chunk_size):
yield string[i:i + chunk_size]
yield string[i : i + chunk_size]
6 changes: 3 additions & 3 deletions bot/cogs/advent_of_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class AdventOfCode(commands.Cog):

def __init__(self, bot):
self.bot = bot
self.aoc_api = AdventOfCodeAPI(self.TORTOISE_LEADERBOARD_ID, loop=self.bot.loop)
self.aoc_api = AdventOfCodeAPI(self.TORTOISE_LEADERBOARD_ID)
self._leaderboard_cache = None
self.update_leaderboard_cache.start()

Expand Down Expand Up @@ -87,5 +87,5 @@ async def aoc_countdown(self, ctx):
await ctx.send(embed=info(f"Day {current_day} ends in {ends_in}", title="Countdown", member=ctx.guild.me))


def setup(bot):
bot.add_cog(AdventOfCode(bot))
async def setup(bot):
await bot.add_cog(AdventOfCode(bot))
4 changes: 2 additions & 2 deletions bot/cogs/bot_owner_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ async def reload(self, ctx, extension_name):
await ctx.send(embed=success(f"{extension_name} reloaded.", ctx.me))


def setup(bot):
bot.add_cog(BotOwnerCommands(bot))
async def setup(bot):
await bot.add_cog(BotOwnerCommands(bot))
6 changes: 3 additions & 3 deletions bot/cogs/cmd_error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ async def on_command_error(self, ctx, error_):
else:
error_type = type(error)
feedback_message = f"Uncaught {error_type} exception in command '{ctx.command}'"
traceback_message = traceback.format_exception(etype=error_type, value=error, tb=error.__traceback__)
traceback_message = traceback.format_exception(error_type, error, error.__traceback__)
log_message = f"{feedback_message} {traceback_message}"
logger.critical(log_message)
await self.bot.log_error(log_message)
Expand All @@ -87,5 +87,5 @@ def _get_missing_permission(cls, error) -> str:
return message


def setup(bot):
bot.add_cog(CommandErrorHandler(bot))
async def setup(bot):
await bot.add_cog(CommandErrorHandler(bot))
Loading
Loading