From 250ed0e40f4cd0b381ed32202b7fd2010502101e Mon Sep 17 00:00:00 2001 From: Pierre Berthe Date: Sun, 4 Feb 2024 18:36:45 +0100 Subject: [PATCH] Initial commit --- .gitignore | 4 + bot.py | 253 +++++++++++++++++++++++++++++++++++++++++++++++ disco.service | 12 +++ helper.py | 7 ++ launch.sh | 5 + requirements.txt | 16 +++ 6 files changed, 297 insertions(+) create mode 100644 .gitignore create mode 100644 bot.py create mode 100644 disco.service create mode 100644 helper.py create mode 100755 launch.sh create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a6806f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +.venv +.vscode +__py* \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..d745a9b --- /dev/null +++ b/bot.py @@ -0,0 +1,253 @@ +from os import getenv +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo +from typing import Iterator, Optional +from dotenv import load_dotenv +from asyncache import cached +from cachetools import TTLCache +from requests import get, post + +import discord +from discord import app_commands +from discord.ext import tasks + +from helper import nth_element + +load_dotenv() + +GUILD_ID = int(getenv("GUILD_ID")) +TOKEN = getenv("DISCORD_TOKEN") +WEATHER_API_KEY = getenv("WEATHER_API_KEY") +GENERAL_CHANNEL_ID = int(getenv("GENERAL_CHANNEL_ID")) + +MY_GUILD = discord.Object(id=GUILD_ID) + + +tt = datetime.now(ZoneInfo("Europe/Paris")) +md = tt.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) +mid = md.timetz() + + + + +def embed_from_quote(my_quote: dict): + embed = discord.Embed(title="Quote") + embed.description = my_quote['content'] + embed.set_author(name=my_quote['author']) + return embed + + +class Dropdown(discord.ui.Select): + def __init__(self): + + # Set the options that will be presented inside the dropdown + options = [ + discord.SelectOption( + label='Red', description='Your favourite colour is red', emoji='🟥'), + discord.SelectOption( + label='Green', description='Your favourite colour is green', emoji='🟩'), + discord.SelectOption( + label='Blue', description='Your favourite colour is blue', emoji='🟦'), + ] + + # The placeholder is what will be shown when no option is chosen + # The min and max values indicate we can only pick one of the three options + # The options parameter defines the dropdown options. We defined this above + super().__init__(placeholder='Choose your favourite colour...', + min_values=1, max_values=1, options=options) + + async def callback(self, interaction: discord.Interaction): + # Use the interaction object to send a response message containing + # the user's favourite colour or choice. The self object refers to the + # Select object, and the values attribute gets a list of the user's + # selected options. We only want the first one. + await interaction.response.send_message(f'Your favourite colour is {self.values[0]}') + + +class DropdownView(discord.ui.View): + def __init__(self): + super().__init__() + + # Adds the dropdown to our view object. + self.add_item(Dropdown()) + + +class MyClient(discord.Client): + def __init__(self, *, client_intents: discord.Intents): + super().__init__(intents=client_intents) + self.tree = app_commands.CommandTree(self) + + async def setup_hook(self): + self.tree.copy_global_to(guild=MY_GUILD) + self.quote_task.start() + await self.tree.sync(guild=MY_GUILD) + + @tasks.loop(time=mid) + async def quote_task(self): + channel = self.get_channel(GENERAL_CHANNEL_ID) + response = get("https://api.quotable.io/random?maxLength=230") + my_quote = response.json() + await channel.send(embed=embed_from_quote(my_quote)) + + @quote_task.before_loop + async def before_quote_task(self): + await self.wait_until_ready() + + +intents = discord.Intents.default() +client = MyClient(client_intents=intents) + + +@client.event +async def on_ready(): + print(f'Logged in as {client.user} (ID: {client.user.id})') + print('------') + + +@client.tree.command() +async def hello(interaction: discord.Interaction): + """Says hello!""" + await interaction.response.send_message(f'Hi, {interaction.user.mention}') + + +@client.tree.command() +@app_commands.describe( + first_value='The first value you want to add something to', + second_value='The value you want to add to the first value', +) +async def add(interaction: discord.Interaction, first_value: int, second_value: int): + """Adds two numbers together.""" + await interaction.response.send_message(f'{first_value} + {second_value} = {first_value + second_value}') + + +@client.tree.command() +@app_commands.rename(text_to_send='text') +@app_commands.describe(text_to_send='Text to send in the current channel') +async def send(interaction: discord.Interaction, text_to_send: str): + """Sends the text into the current channel.""" + await interaction.response.send_message(text_to_send) + + +@client.tree.command() +@app_commands.describe(member='The member you want to get the joined date from; defaults to the user who uses the ' + 'command') +async def joined(interaction: discord.Interaction, member: Optional[discord.Member] = None): + """Says when a member joined.""" + member = member or interaction.user + await interaction.response.send_message(f'{member} joined {discord.utils.format_dt(member.joined_at)}', ephemeral=True) + + +@client.tree.context_menu(name='Show Join Date') +async def show_join_date(interaction: discord.Interaction, member: discord.Member): + await interaction.response.send_message(f'{member} joined at {discord.utils.format_dt(member.joined_at)}', ephemeral=True) + + +@client.tree.context_menu(name='Translate') +async def translate(interaction: discord.Interaction, message: discord.Message): + await interaction.response.defer(ephemeral=True) + """Translate text to another language.""" + try: + response = post('https://translate.argosopentech.com/translate', json={ + 'q': message.content, + 'source': 'fr', + 'target': 'en' + }, timeout=30) + response.raise_for_status() + except Exception as e: + await interaction.followup.send(f'An error occurred: {e}', ephemeral=True) + return + try: + json = response.json() + translated_text = json['translatedText'] + except Exception as e: + await interaction.followup.send(f'An error occurred: {e}', ephemeral=True) + return + await interaction.followup.send(f'Translated text: {translated_text}', ephemeral=True) + + +@client.tree.command() +async def quote(interaction: discord.Interaction): + """Sends a random quote.""" + response = get('https://api.quotable.io/random?maxLength=230') + my_quote = response.json() + await interaction.response.send_message(embed=embed_from_quote(my_quote)) + + +@cached(cache=TTLCache(maxsize=1024, ttl=600)) +async def get_weather_json(city: str): + try: + weather_response = get( + f'https://api.weatherapi.com/v1/current.json?key={WEATHER_API_KEY}&q={city}&aqi=no') + weather_response.raise_for_status() + forecast_response = get( + f'https://api.weatherapi.com/v1/forecast.json?key={WEATHER_API_KEY}&q={city}&days=1&aqi=no&alerts=no') + forecast_response.raise_for_status() + except Exception as e: + raise e + return weather_response.json(), forecast_response.json() + + +@client.tree.command() +@app_commands.describe(city='The city you want to get the weather for') +async def weather(interaction: discord.Interaction, city: str): + """Get the current weather""" + try: + weather_response, forecast_response = await get_weather_json(city) + except Exception as _e: + await interaction.response.send_message(f'Error: the city "{city}" was not found', ephemeral=True) + return + current_temp = weather_response['current']['temp_c'] + forecast = forecast_response['forecast']['forecastday'][0] + forecast_min = forecast['day']['mintemp_c'] + forecast_max = forecast['day']['maxtemp_c'] + embed = discord.Embed(title="Weather") + embed.description = f'Current temperature: {current_temp}°C\n' f'Forecast: Min: {forecast_min}°C, Max: {forecast_max}°C ' + await interaction.response.send_message(embed=embed) + +# A command tha tell the time until midnight +@client.tree.command() +async def midnight(interaction: discord.Interaction): + """Sends the time until midnight.""" + paris_dt = datetime.now(ZoneInfo("Europe/Paris")) + midnight = paris_dt.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1) + time_until_midnight = midnight - paris_dt + await interaction.response.send_message(f'Time until midnight: {time_until_midnight}') + + +# A command that say what the best programming language is +@client.tree.command() +async def best_language(interaction: discord.Interaction): + """Sends the best programming language.""" + await interaction.response.send_message('The best programming language is Rust') + +@client.tree.command() +async def color(interaction: discord.Interaction): + """Sends a random color.""" + await interaction.response.send_message(view=DropdownView()) + + +def fibo_gen() -> Iterator[int]: + a, b = 0, 1 + while True: + yield a + a, b = b, a + b + + +def fib(n: int) -> int: + return nth_element(fibo_gen(), n) + + +# @client.tree.command() +# async def fibonacci(interaction: discord.Interaction, n: int): +# """Sends the nth fibonacci number.""" +# await interaction.response.send_message(f'{n}th fibonacci number is {fib(n)}') + + +@client.event +async def on_member_join(member: discord.Member): + guild = member.guild + channel = client.get_channel(GENERAL_CHANNEL_ID) + await channel.send(f'Welcome {member.mention} to {guild.name}!') + +if __name__ == "__main__": + client.run(TOKEN) diff --git a/disco.service b/disco.service new file mode 100644 index 0000000..8d89103 --- /dev/null +++ b/disco.service @@ -0,0 +1,12 @@ +[Unit] +Description=disco +After=network.target + +[Service] +Type=simple +User=pierre +WorkingDirectory=/home/pierre/disco +ExecStart=/home/pierre/disco/launch.sh + +[Install] +WantedBy=multi-user.target diff --git a/helper.py b/helper.py new file mode 100644 index 0000000..f0d47f3 --- /dev/null +++ b/helper.py @@ -0,0 +1,7 @@ +from typing import Iterator + + +def nth_element(my_iter: Iterator, n: int) -> int: + for _ in range(n): + next(my_iter) + return next(my_iter) diff --git a/launch.sh b/launch.sh new file mode 100755 index 0000000..000f726 --- /dev/null +++ b/launch.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +source .venv/bin/activate +pip install -r requirements.txt +python bot.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4dbaa45 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +aiohttp==3.9.1 +aiosignal==1.3.1 +asyncache==0.3.1 +attrs==23.1.0 +cachetools==5.3.2 +certifi==2023.11.17 +charset-normalizer==3.3.2 +discord.py==2.3.2 +frozenlist==1.4.1 +idna==3.6 +multidict==6.0.4 +pipdeptree==2.13.1 +python-dotenv==1.0.0 +requests==2.31.0 +urllib3==2.1.0 +yarl==1.9.4