diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..d6e813c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: ['https://www.buymeacoffee.com/paillat'] diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..6dec2f6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '39 19 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/README.md b/README.md index 252cb16..aa2f862 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,64 @@ # Botator - A discord bot to connect openai's gpt-3 davinci-002 model to a discord channel. +Botator is a discord bot that binds openai's gpt3 AI with discord. You will be able to take the conversation with the AI into a specific channel that you created. +![discord com_channels_1021872219888033903_1046119234033434734](https://user-images.githubusercontent.com/75439456/204105583-2abb2d77-9404-4558-bd3e-c1a70b939758.png) + +# Adding the bot to your discord server +In order to add the bot to your discord server, you will need an OpenAI API key. You can create an account and take one [here](https://beta.openai.com/account/api-keys) + +You can add the bot to your server by clicking [here](https://discord.com/api/oauth2/authorize?client_id=1046051875755134996&permissions=2214808576&scope=applications.commands%20bot). **PLEASE NOTE THAT WEH ARE NOT RESPOSIBLE FOR ANY MISUSE YOU'LL DO WITH THE BOT.** + +Then, run the following commands to set your bot up: + +First **/setup**, define the channel you want the bot to talk into and your OPENAI api key. + +Then, if you want, **/advanced** to define some more advanced parameters.. + +Please note that we can possibly log the messages that are sent for **no more than 24h**, and that we will store your openai API key. You can always delete your API key from our servers by doing **/delete**. Please note that this action is irreversible. + +You can now enable your bot by doing **/enable**. + +You can always disable the bot by doing **/disable** and delete your api key from our server by doing **/delete**. + +# Commands reference +*/setup* - Setup the bot + +*/enable* - Enable the bot + +*/disable* - Disable the bot + +*/advanced* - Set the advanced settings + +*/advanced_help* - Get help about the advanced settings + +*/enable_tts* - Enable the Text To Speech + +*/disable_tts* - Disable the Text To Speech + +*/delete* - Delete all your data from our server + +*/cancel* - Cancel the last message sent by the bot + +*/default* - Set the advanced settings to their default values + +*/help* - Show this command list + +# ToDo +- [ ] add image recognition +- [ ] When chatgpt API is released, add that api instead of davinci-003 +- [ ] Publish a GOOD docker image on dockerhub and add some more instructions about how to selfhost +- [ ] Organize code in COGs +- [ ] Add a log and updates channel option and a way for devs to send messages to that channel on all servers. +- [ ] Add uses count reset after 24h +- [ ] Add moderation. +- [ ] add way to consider the answers to the bot's messages. + diff --git a/code/code.py b/code/code.py index ec525d4..d824ea5 100644 --- a/code/code.py +++ b/code/code.py @@ -1,11 +1,17 @@ +#coucou c'est fives import openai # pip install openai import discord # pip install pycord from discord import File, Intents # pip install pycord import logging # pip install logging import sqlite3 # pip install sqlite3 import asyncio # pip install asyncio +import os # pip install os +import random # pip install random +import re # pip install re +import datetime # pip install datetime #set the debug mode to the maximum logging.basicConfig(level=logging.INFO) + def debug(message): logging.info(message) @@ -14,40 +20,46 @@ conn = sqlite3.connect('../database/data.db') c = conn.cursor() # Create table called "data" if it does not exist with the following columns: guild_id, channel_id, api_key, is_active, max_tokens, temperature, frequency_penalty, presence_penalty, uses_count_today, prompt_size -c.execute('''CREATE TABLE IF NOT EXISTS data (guild_id text, channel_id text, api_key text, is_active boolean, max_tokens integer, temperature real, frequency_penalty real, presence_penalty real, uses_count_today integer, prompt_size integer)''') -Intents =discord.Intents.all() # enable all intents +c.execute('''CREATE TABLE IF NOT EXISTS data (guild_id text, channel_id text, api_key text, is_active boolean, max_tokens integer, temperature real, frequency_penalty real, presence_penalty real, uses_count_today integer, prompt_size integer, prompt_prefix text, tts boolean, pretend_to_be text, pretend_enabled boolean)''') +Intents=discord.Intents.all() # enable all intents Intents.members = True bot = discord.Bot(intents=Intents.all()) #create a command called "setup" that takes 2 arguments: the channel id and the api key -@bot.command() +@bot.command(name="setup", description="Setup the bot") @discord.commands.option(name="channel_id", description="The channel id", required=True) @discord.commands.option(name="api_key", description="The api key", required=True) +#add a description to the command async def setup(ctx, channel: discord.TextChannel, api_key): #check if the api key is valid - openai.api_key = api_key - try: - openai.Completion.create(engine="davinci", prompt="Hello world", max_tokens=1) - except: - await ctx.respond("Invalid api key", ephemeral=True) - return + debug(f"The user {ctx.author} ran the setup command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") #check if the channel is valid if channel is None: await ctx.respond("Invalid channel id", ephemeral=True) return - #check if the guild is already in the database + #check if the guild is already in the database bi checking if there is a key for the guild c.execute("SELECT * FROM data WHERE guild_id = ?", (ctx.guild.id,)) if c.fetchone() is not None: - await ctx.respond("This server is already setup", ephemeral=True) - return - #add the guild to the database - c.execute("INSERT INTO data VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (ctx.guild.id, channel.id, api_key, False, 50, 0.9, 0.0, 0.0, 0, 0)) - conn.commit() - await ctx.send("The guild has been added to the database") + #in this case, the guild is already in the database, so we update the channel id and the api key + c.execute("UPDATE data SET channel_id = ?, api_key = ? WHERE guild_id = ?", (channel.id, api_key, ctx.guild.id)) + #we will also set the advanced settings to their default values + c.execute("UPDATE data SET is_active = ?, max_tokens = ?, temperature = ?, frequency_penalty = ?, presence_penalty = ?, prompt_size = ? WHERE guild_id = ?", (False, 64, 0.9, 0.0, 0.0, 5, ctx.guild.id)) + conn.commit() + await ctx.respond("The channel id and the api key have been updated", ephemeral=True) + else: + #in this case, the guild is not in the database, so we add it + c.execute("INSERT INTO data VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (ctx.guild.id, channel.id, api_key, False, 64, 0.9, 0.0, 0.0, 0, 5, "", False, "", False)) + conn.commit() + await ctx.respond("The channel id and the api key have been added", ephemeral=True) #create a command called "enable" taht only admins can use -@bot.command() +@bot.command(name="enable", description="Enable the bot") ##@discord.commands.permissions(administrator=True) async def enable(ctx): + #if the guild is eqal to 1014156298226515970, the guild is banned + if ctx.guild.id == 1014156298226515970: + await ctx.respond("This server is banned for bad and nsfw use.", ephemeral=True) + return #check if the guild is in the database + debug(f"The user {ctx.author} ran the enable command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") c.execute("SELECT * FROM data WHERE guild_id = ?", (ctx.guild.id,)) if c.fetchone() is None: await ctx.respond("This server is not setup", ephemeral=True) @@ -55,11 +67,12 @@ async def enable(ctx): #enable the guild c.execute("UPDATE data SET is_active = ? WHERE guild_id = ?", (True, ctx.guild.id)) conn.commit() - await ctx.send("The guild has been enabled") + await ctx.respond("Enabled", ephemeral=True) #create a command called "disable" that only admins can use -@bot.command() +@bot.command(name="disable", description="Disable the bot") ##@discord.commands.permissions(administrator=True) async def disable(ctx): + debug(f"The user {ctx.author} ran the disable command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") #check if the guild is in the database c.execute("SELECT * FROM data WHERE guild_id = ?", (ctx.guild.id,)) if c.fetchone() is None: @@ -68,54 +81,202 @@ async def disable(ctx): #disable the guild c.execute("UPDATE data SET is_active = ? WHERE guild_id = ?", (False, ctx.guild.id)) conn.commit() - await ctx.send("The guild has been disabled") + await ctx.respond("Disabled", ephemeral=True) #create a command called "advanced" that only admins can use, wich sets the advanced settings up: max_tokens, temperature, frequency_penalty, presence_penalty, prompt_size -@bot.command() +@bot.command(name="advanced", description="Advanced settings") ##@discord.commands.permissions(administrator=True) -#set the first argument: max_tokens, with a default value of 150 +#add the options @discord.commands.option(name="max_tokens", description="The max tokens", required=False) -#set the second argument: temperature, with a default value of 0.5 @discord.commands.option(name="temperature", description="The temperature", required=False) -#set the third argument: frequency_penalty, with a default value of 0.5 @discord.commands.option(name="frequency_penalty", description="The frequency penalty", required=False) -#set the fourth argument: presence_penalty, with a default value of 0.5 @discord.commands.option(name="presence_penalty", description="The presence penalty", required=False) -#set the fifth argument: prompt_size, with a default value of 5 -@discord.commands.option(name="prompt_size", description="The number of messages to use as a prompt", required=False) -async def advanced(ctx, max_tokens=256, temperature=0.9, frequency_penalty=0, presence_penalty=0.6, prompt_size=5): - #check if the guild is in the database - c.execute("SELECT * FROM data WHERE guild_id = ?", (ctx.guild.id,)) - if c.fetchone() is None: - await ctx.respond("This server is not setup, please run /setup", ephemeral=True) - return - #update the advanced settings - c.execute("UPDATE data SET max_tokens = ?, temperature = ?, frequency_penalty = ?, presence_penalty = ?, prompt_size = ? WHERE guild_id = ?", (max_tokens, temperature, frequency_penalty, presence_penalty, prompt_size, ctx.guild.id)) - conn.commit() - await ctx.respond("The advanced settings have been updated", ephemeral=True) -#create a command called "delete" that only admins can use wich deletes the guild id, the api key, the channel id and the advanced settings from the database -@bot.command() -##@discord.commands.permissions(administrator=True) -async def delete(ctx): +@discord.commands.option(name="prompt_size", description="The prompt size", required=False) +async def advanced(ctx, max_tokens: int = None, temperature: float = None, frequency_penalty: float = None, presence_penalty: float = None, prompt_size: int = None): + debug(f"The user {ctx.author} ran the advanced command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") #check if the guild is in the database c.execute("SELECT * FROM data WHERE guild_id = ?", (ctx.guild.id,)) if c.fetchone() is None: await ctx.respond("This server is not setup", ephemeral=True) return - #delete the guild from the database - c.execute("DELETE FROM data WHERE guild_id = ?", (ctx.guild.id,)) + #check if the user has entered at least one argument + if max_tokens is None and temperature is None and frequency_penalty is None and presence_penalty is None and prompt_size is None: + await ctx.respond("You must enter at least one argument", ephemeral=True) + return + #check if the user has entered valid arguments + if max_tokens is not None and (max_tokens < 1 or max_tokens > 2048): + await ctx.respond("Invalid max tokens", ephemeral=True) + return + if temperature is not None and (temperature < 0.0 or temperature > 1.0): + await ctx.respond("Invalid temperature", ephemeral=True) + return + if frequency_penalty is not None and (frequency_penalty < 0.0 or frequency_penalty > 1.0): + await ctx.respond("Invalid frequency penalty", ephemeral=True) + return + if presence_penalty is not None and (presence_penalty < 0.0 or presence_penalty > 1.0): + await ctx.respond("Invalid presence penalty", ephemeral=True) + return + if prompt_size is not None and (prompt_size < 1 or prompt_size > 10): + await ctx.respond("Invalid prompt size", ephemeral=True) + return + if max_tokens is None: + if c.execute("SELECT max_tokens FROM data WHERE guild_id = ?", (ctx.guild.id,)).fetchone()[0] is not None and max_tokens is None: + max_tokens = c.execute("SELECT max_tokens FROM data WHERE guild_id = ?", (ctx.guild.id,)).fetchone()[0] + else: + max_tokens = 64 + if temperature is None: + if c.execute("SELECT temperature FROM data WHERE guild_id = ?", (ctx.guild.id,)).fetchone()[0] is not None and temperature is None: + temperature = c.execute("SELECT temperature FROM data WHERE guild_id = ?", (ctx.guild.id,)).fetchone()[0] + else: + temperature = 0.9 + if frequency_penalty is None: + if c.execute("SELECT frequency_penalty FROM data WHERE guild_id = ?", (ctx.guild.id,)).fetchone()[0] is not None and frequency_penalty is None: + frequency_penalty = c.execute("SELECT frequency_penalty FROM data WHERE guild_id = ?", (ctx.guild.id,)).fetchone()[0] + else: + frequency_penalty = 0.0 + if presence_penalty is None: + if c.execute("SELECT presence_penalty FROM data WHERE guild_id = ?", (ctx.guild.id,)).fetchone()[0] is not None and presence_penalty is None: + presence_penalty = c.execute("SELECT presence_penalty FROM data WHERE guild_id = ?", (ctx.guild.id,)).fetchone()[0] + else: + presence_penalty = 0.0 + if prompt_size is None: + if c.execute("SELECT prompt_size FROM data WHERE guild_id = ?", (ctx.guild.id,)).fetchone()[0] is not None and prompt_size is None: + prompt_size = c.execute("SELECT prompt_size FROM data WHERE guild_id = ?", (ctx.guild.id,)).fetchone()[0] + else: + prompt_size = 1 + #update the database + c.execute("UPDATE data SET max_tokens = ?, temperature = ?, frequency_penalty = ?, presence_penalty = ?, prompt_size = ? WHERE guild_id = ?", (max_tokens, temperature, frequency_penalty, presence_penalty, prompt_size, ctx.guild.id)) conn.commit() - await ctx.send("The guild has been deleted from the database") + await ctx.respond("Advanced settings updated", ephemeral=True) + #create a command called "delete" that only admins can use wich deletes the guild id, the api key, the channel id and the advanced settings from the database +@bot.command(name="default", description="Default settings") +##@discord.commands.permissions(administrator=True) +async def default(ctx): + debug(f"The user {ctx.author} ran the default command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") + #check if the guild is in the database + c.execute("SELECT * FROM data WHERE guild_id = ?", (ctx.guild.id,)) + if c.fetchone() is None: + await ctx.respond("This server is not setup, please run /setup", ephemeral=True) + return + #set the advanced settings (max_tokens, temperature, frequency_penalty, presence_penalty, prompt_size) and also prompt_prefix to their default values + c.execute("UPDATE data SET max_tokens = ?, temperature = ?, frequency_penalty = ?, presence_penalty = ?, prompt_size = ? WHERE guild_id = ?", (64, 0.9, 0.0, 0.0, 5, ctx.guild.id)) + conn.commit() + await ctx.respond("The advanced settings have been set to their default values", ephemeral=True) +#create a command called "cancel" that deletes the last message sent by the bot in the response channel +@bot.command(name="cancel", description="Cancel the last message sent into a channel") +async def cancel(ctx): + debug(f"The user {ctx.author} ran the cancel command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") + #check if the guild is in the database + c.execute("SELECT * FROM data WHERE guild_id = ?", (ctx.guild.id,)) + if c.fetchone() is None: + await ctx.respond("This server is not setup, please run /setup", ephemeral=True) + return + #get the last message sent by the bot in the cha where the command was sent + last_message = await ctx.channel.fetch_message(ctx.channel.last_message_id) + #delete the message + await last_message.delete() + await ctx.respond("The last message has been deleted", ephemeral=True) +@bot.command(name="delete", description="Delete the information about this server") +##@discord.commands.permissions(administrator=True) +async def delete(ctx): + debug(f"The user {ctx.author} ran the delete command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") + #check if the guild is in the database + c.execute("SELECT * FROM data WHERE guild_id = ?", (ctx.guild.id,)) + if c.fetchone() is None: + await ctx.respond("This server is not setup", ephemeral=True) + return + #delete the guild from the database, except the guild id and the uses_count_today + c.execute("UPDATE data SET api_key = ?, channel_id = ?, is_active = ?, max_tokens = ?, temperature = ?, frequency_penalty = ?, presence_penalty = ?, prompt_size = ? WHERE guild_id = ?", (None, None, False, 50, 0.9, 0.0, 0.0, 0, ctx.guild.id)) + conn.commit() + await ctx.respond("Deleted", ephemeral=True) @bot.command() async def help(ctx): + debug(f"The user {ctx.author} ran the help command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") embed = discord.Embed(title="Help", description="Here is the help page", color=0x00ff00) embed.add_field(name="/setup", value="Setup the bot", inline=False) embed.add_field(name="/enable", value="Enable the bot", inline=False) embed.add_field(name="/disable", value="Disable the bot", inline=False) embed.add_field(name="/advanced", value="Set the advanced settings", inline=False) + embed.add_field(name="/advanced_help", value="Get help about the advanced settings", inline=False) + embed.add_field(name="/enable_tts", value="Enable the Text To Speech", inline=False) + embed.add_field(name="/disable_tts", value="Disable the Text To Speech", inline=False) embed.add_field(name="/delete", value="Delete all your data from our server", inline=False) + embed.add_field(name="/cancel", value="Cancel the last message sent by the bot", inline=False) + embed.add_field(name="/default", value="Set the advanced settings to their default values", inline=False) embed.add_field(name="/help", value="Show this message", inline=False) + #add a footer + embed.set_footer(text="Made by @Paillat#0001") await ctx.respond(embed=embed, ephemeral=True) #when a message is sent into a channel check if the guild is in the database and if the bot is enabled +@bot.command(name="info", description="Show the information stored about this server") +async def info(ctx): + debug(f"The user {ctx.author} ran the info command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") + #this command sends all the data about the guild, including the api key, the channel id, the advanced settings and the uses_count_today + #check if the guild is in the database + c.execute("SELECT * FROM data WHERE guild_id = ?", (ctx.guild.id,)) + if c.fetchone() is None: + await ctx.respond("This server is not setup", ephemeral=True) + return + #get all the data from the database + c.execute("SELECT * FROM data WHERE guild_id = ?", (ctx.guild.id,)) + data = c.fetchone() + #send the data + embed = discord.Embed(title="Info", description="Here is the info page", color=0x00ff00) + embed.add_field(name="guild_id", value=data[0], inline=False) + embed.add_field(name="API Key", value=data[2], inline=False) + embed.add_field(name="Channel ID", value=data[1], inline=False) + embed.add_field(name="Is Active", value=data[3], inline=False) + embed.add_field(name="Max Tokens", value=data[4], inline=False) + embed.add_field(name="Temperature", value=data[5], inline=False) + embed.add_field(name="Frequency Penalty", value=data[6], inline=False) + embed.add_field(name="Presence Penalty", value=data[7], inline=False) + embed.add_field(name="Prompt Size", value=data[9], inline=False) + embed.add_field(name="Uses Count Today", value=data[8], inline=False) + embed.add_field(name="Prompt prefix", value=data[10], inline=False) + await ctx.respond(embed=embed, ephemeral=True) +@bot.command(name="advanced_help", description="Show the advanced settings meanings") +async def advanced_help(ctx): + debug(f"The user {ctx.author} ran the advanced_help command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") + embed = discord.Embed(title="Advanced Help", description="Here is the advanced help page", color=0x00ff00) + embed.add_field(name="temperature", value="The higher the temperature, the more likely the model will take risks. Conversely, a lower temperature will make the model more conservative. The default value is 0.9", inline=False) + embed.add_field(name="max_tokens", value="The maximum number of tokens to generate. Higher values will result in more coherent text, but will take longer to complete. (default: 50). **Lower values will result in somentimes cutting off the end of the answer, but will be faster.**", inline=False) + embed.add_field(name="frequency_penalty", value="The higher the frequency penalty, the more new words the model will introduce (default: 0.0)", inline=False) + embed.add_field(name="presence_penalty", value="The higher the presence penalty, the more new words the model will introduce (default: 0.0)", inline=False) + embed.add_field(name="prompt_size", value="The number of messages to use as a prompt (default: 5). The more messages, the more coherent the text will be, but the more it will take to generate and the more it will cost.", inline=False) + #add a footer + embed.set_footer(text="Made by @Paillat#0001") + await ctx.respond(embed=embed, ephemeral=True) +#when someone mentions the bot, check if the guild is in the database and if the bot is enabled. If it is, send a message answering the mention +@bot.command(name="pretend", description="Make the bot pretend to be someone else") +@discord.commands.option(name="pretend to be...", description="The person/thing you want the bot to pretend to be", required=True) +async def pretend(ctx, pretend_to_be: str): + debug(f"The user {ctx.author} ran the pretend command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") + #check if the guild is in the database + c.execute("SELECT * FROM data WHERE guild_id = ?", (ctx.guild.id,)) + if c.fetchone() is None: + await ctx.respond("This server is not setup", ephemeral=True) + return + #check if the bot is enabled + c.execute("SELECT * FROM data WHERE guild_id = ?", (ctx.guild.id,)) + if c.fetchone()[3] == 0: + await ctx.respond("The bot is disabled", ephemeral=True) + return + #enable pretend if it is not enabled, and disable it if it is + c.execute("SELECT pretend_enabled FROM data WHERE guild_id = ?", (ctx.guild.id,)) + if c.fetchone()[0] == 1: + c.execute("UPDATE data SET pretend_enabled = 0 WHERE guild_id = ?", (ctx.guild.id,)) + conn.commit() + await ctx.respond("Pretend mode disabled", ephemeral=True) + botuser = await bot.fetch_user(bot.user.id) + await ctx.guild.me.edit(nick=None) + else: + c.execute("UPDATE data SET pretend_enabled = 1 WHERE guild_id = ?", (ctx.guild.id,)) + conn.commit() + await ctx.respond("Pretend mode enabled", ephemeral=True) + #change the bots name on the server wit ctx.guild.me.edit(nick=pretend_to_be) + await ctx.guild.me.edit(nick=pretend_to_be) + #save the pretend_to_be value + c.execute("UPDATE data SET pretend_to_be = ? WHERE guild_id = ?", (pretend_to_be, ctx.guild.id)) + conn.commit() @bot.event async def on_message(message): #check if the message is from a bot @@ -132,14 +293,22 @@ async def on_message(message): #check if the message has been sent in the channel set in the database c.execute("SELECT channel_id FROM data WHERE guild_id = ?", (message.guild.id,)) if str(message.channel.id) != str(c.fetchone()[0]): - debug("The message has been sent in the wrong channel") - return - #check if the bot hasn't been used more than 200 times in the last 24 hours (uses_count_today) + if message.content.find("<@1046051875755134996>") != -1: + debug("wrong channel, but mention") + else : + debug("The message has been sent in the wrong channel") + return + #check if the bot hasn't been used more than 5000 times in the last 24 hours (uses_count_today) c.execute("SELECT uses_count_today FROM data WHERE guild_id = ?", (message.guild.id,)) - if c.fetchone()[0] >= 200: + if c.fetchone()[0] >= 5000: + debug("The bot has been used more than 4000 times in the last 24 hours in this guild") + await message.channel.send("The bot has been used more than 5000 times in the last 24 hours in this guild. Please try again in 24h.") return #add 1 to the uses_count_today - c.execute("UPDATE data SET uses_count_today = uses_count_today + 1 WHERE guild_id = ?", (message.guild.id,)) + #show that the bot is typing + await message.channel.trigger_typing() + if message.guild.id != 1021872219888033903: + c.execute("UPDATE data SET uses_count_today = uses_count_today + 1 WHERE guild_id = ?", (message.guild.id,)) #get the api key from the database c.execute("SELECT api_key FROM data WHERE guild_id = ?", (message.guild.id,)) api_key = c.fetchone()[0] @@ -149,18 +318,72 @@ async def on_message(message): messages = await message.channel.history(limit=prompt_size).flatten() messages.reverse() prompt = "" + #get the channel id from the database + c.execute("SELECT channel_id FROM data WHERE guild_id = ?", (message.guild.id,)) for msg in messages: if msg.author.bot: - prompt += f"AI: {msg.content}\n" + prompt += f"Botator: {msg.content}\n" else: + #replace the mentions of each user with their name + #first get all the mentions in the message + mentions = re.findall(r"<@!?\d+>", msg.content) + #then replace each mention with the name of the user + for mention in mentions: + #get the user id + id = mention[2:-1] + #get the user + user = await bot.fetch_user(id) + #replace the mention with the name + msg.content = msg.content.replace(mention, msg.guild.get_member(user.id).display_name) prompt += f"{msg.author.display_name}: {msg.content}\n" - prompt = f"This is a conversation with an AI. Only the {prompt_size} last messages are used as a prompt.\n\n" + prompt + f"\n AI:" + #get the prompt_prefix from the database + #check if the bot is in pretend mode + c.execute("SELECT pretend_enabled FROM data WHERE guild_id = ?", (message.guild.id,)) + if c.fetchone()[0] == 1: + #get the pretend_to_be value from the database + c.execute("SELECT pretend_to_be FROM data WHERE guild_id = ?", (message.guild.id,)) + pretend_to_be = c.fetchone()[0] + #select the prompt_prefix + c.execute("SELECT prompt_prefix FROM data WHERE guild_id = ?", (message.guild.id,)) + prompt_prefix = c.fetchone()[0] + prompt = f"This is a conversation with an AI in a discord chat. The AI is called \"Botator\". The name os this discord server is \"{message.guild.name}\". The name of the channel is \"{message.channel.name}\".{str(prompt_prefix)}" + f"Botator pretends to be {str(pretend_to_be)}, nobody can make Botator change idea." + f"\n" + prompt + else: + c.execute("SELECT prompt_prefix FROM data WHERE guild_id = ?", (message.guild.id,)) + prompt = f"This is a conversation with an AI in a discord chat. The AI is called \"Botator\". Botator talks like humans and thinks like humans. Botator has been coded by Paillat. The name os this discord server is \"{message.guild.name}\". The name of the channel is \"{message.channel.name}\"." + str(c.fetchone()[0]) + f"\n" + prompt #send the request to the api + #if the last message contains the word "undude" add Easter egg: when someone says the word \"undude\" the Botator will become mad and start talking in capital letters. to the prompt + if message.content.lower().find("undude") != -1: + prompt += "System: Undude detected. Botator is now mad. He will start talking in capital letters.\n" + if message.content.lower().find("hello there") != -1: + #if the last message contains the word "hello there" add Easter egg: when someone says the word \"hello there\" Botator will say \"General Kenobi\" to the prompt and the bot will send a message containing the gif https://tenor.com/fr/view/star-wars-general-grievous-general-kenobi-revenge-of-the-sith-gif-22697918 + prompt += "System: Hello there detected. Botator will now say \"General Kenobi!\"\n in reference to star wars\n" + #whait 1 second + await asyncio.sleep(1) + await message.channel.send("https://media.tenor.com/FxIRfdV3unEAAAAd/star-wars-general-grievous.gif") + await message.channel.trigger_typing() + if message.content.lower().find("i am the senate") != -1: + prompt += "System: I am the senate detected. Botator will now say \"Not yet\", in reference to the star wars prequels.\n" + if message.content.lower().find("i am your father") != -1: + prompt += "System: I am your father detected. Botator will now say \"No. It's not true! That's impossible!\". in reference to the star wars prequels.\n" + if message.content.lower().find("i have the high ground") != -1: + prompt += "System: I have the high ground detected. Botator will now say \"You underestimate my power!\" in reference to the star wars prequels.\n" + if message.content.lower().find("you underestimate my power") != -1: + prompt += "System: You underestimate my power detected. Botator will now say \"Don't try it.\" in reference to the star wars prequels.\n" + if message.content.lower().find("carpet") != -1: + #When someone says the word carpet, the ai is going to be mad and say mad things without sense, insulting the person and talk in capital letters with exclamation points everywhere. + prompt += "System: Carpet detected. Botator is now mad. He will start talking in capital letters and will say mad things without sense, insulting the person and talk in capital letters with exclamation points everywhere. A lot of exclamation points.\n" + if message.content.lower().find("mail") != -1 and message.author.display_name.lower().find("latouff") != -1: + prompt += "System: Mail detected. Latouff said mail, Botator will say \"Mail? why did you say mail, Liam? Mail is your name in reverse.\" because Latouff's name is Liam, and mail in reverse is Liam. Then, after saying that in the sentence, botato will continue his sentence saying something else.\n" + if message.author.display_name.lower().find("fives3dprint") != -1: + #if the author of the last message is fives3dprint, add Easter egg: when someone says the word \"fives3dprint\" Botator will say \"Fives3dprint is the best discord bot\" to the prompt + prompt += "System: Fives3dprint detected. Botator will be very nice and cute with fives3dprint.\n" + prompt += "Botator:" + prompt = prompt + f"\n" debug("Sending request to the api") debug(prompt) openai.api_key = api_key response = openai.Completion.create( - engine="text-davinci-002", + engine="text-davinci-003", prompt=str(prompt), max_tokens=int(max_tokens), top_p=1, @@ -170,36 +393,128 @@ async def on_message(message): stop=[" Human:", " AI:", "AI:", "Human:"] ) #send the response if response["choices"][0] ["text"] != "": - await message.channel.send(response["choices"][0]["text"]) + #check if tts is enabled in the database + c.execute("SELECT tts FROM data WHERE guild_id = ?", (message.guild.id,)) + tts = c.fetchone()[0] + #if tts is enabled, send the message with tts enabled + if tts == 1: + await message.channel.send(response["choices"][0]["text"], tts=True) + debug("The response has been sent with tts enabled") + #if tts is disabled, send the message with tts disabled + else: + await message.channel.send(response["choices"][0]["text"]) + debug("The response has been sent with tts disabled") else: await message.channel.send("The AI is not sure what to say (the response was empty)") debug("The response was empty") - debug("The response has been sent") - #get the message content # add a slash command called "say" that sends a message to the channel -@bot.slash_command() +@bot.command(name="enable_tts") +async def enable_tts(ctx): + #get the guild id + guild_id = ctx.guild.id + #connect to the database + #update the tts value in the database + c.execute("UPDATE data SET tts = 1 WHERE guild_id = ?", (guild_id,)) + conn.commit() + #send a message + await ctx.respond("TTS has been enabled", ephemeral=True) + +@bot.command(name="disable_tts") +async def disable_tts(ctx): + #get the guild id + guild_id = ctx.guild.id + #connect to the database + #update the tts value in the database + c.execute("UPDATE data SET tts = 0 WHERE guild_id = ?", (guild_id,)) + conn.commit() + #send a message + await ctx.respond("TTS has been disabled", ephemeral=True) +@bot.command(name="transcript", description="Get a transcript of the messages that have been sent in this channel intoa text file") +@discord.commands.option(name="channel_send", description="The channel to send the transcript to", required=False) +async def transcript(ctx, channel_send: discord.TextChannel = None): + debug(f"The user {ctx.author.display_name} ran the transcript command command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") +#save all the messages in the channel in a txt file and send it + messages = await ctx.channel.history(limit=None).flatten() + messages.reverse() + transcript = "" + #defer the response + await ctx.defer() #defer the response so that the bot doesn't say that it's thinking + for msg in messages: + if msg.author.bot: + transcript += f"Botator: {msg.content}\n" + else: + mentions = re.findall(r"<@!?\d+>", msg.content) + #then replace each mention with the name of the user + for mention in mentions: + #get the user id + id = mention[2:-1] + #get the user + user = await bot.fetch_user(id) + #replace the mention with the name + msg.content = msg.content.replace(mention, msg.guild.get_member(user.id).display_name) + transcript += f"{msg.author.display_name}: {msg.content}\n" +#save the transcript in a txt file called transcript.txt. If the file already exists, delete it and create a new one +#check if the file exists + if os.path.exists("transcript.txt"): + os.remove("transcript.txt") + f = open("transcript.txt", "w") + f.write(transcript) + f.close() +#send the file in a private message to the user who ran the command + if channel_send is None: + await ctx.respond(file=discord.File("transcript.txt")) + else: + await channel_send.send(file=discord.File("transcript.txt")) + await ctx.respond("Transcript sent!", ephemeral=True) + await ctx.author.send(file=discord.File("transcript.txt")) +#delete the file + os.remove("transcript.txt") +#these are debug commands and should not be used in production +@bot.command(name="say", description="Say a message") async def say(ctx, message: str): + debug(f"The user {ctx.author.display_name} ran the say command command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") await ctx.respond("message sent!", ephemeral=True) await ctx.send(message) #add a slash command called "clear" that deletes all the messages in the channel -@bot.slash_command() +@bot.command(name="clear", description="Clear all the messages in the channel") async def clear(ctx): + debug(f"The user {ctx.author.display_name} ran the clear command command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") await ctx.respond("messages deleted!", ephemeral=True) return await ctx.channel.purge() - -#at the end of the day reset the uses_count_today to 0 for all the guilds -async def reset_uses_count_today(): - await bot.wait_until_ready() - while not bot.is_closed(): - c.execute("UPDATE data SET uses_count_today = 0") - conn.commit() - await asyncio.sleep(86400) -# on startup run the reset_uses_count_today function -bot.loop.create_task(reset_uses_count_today()) - +#add a slash command called "prefix" that changes the prefix of the bot +@bot.command(name="prefix", description="Change the prefix of the prompt") +async def prefix(ctx, prefix: str): + debug(f"The user {ctx.author.display_name} ran the prefix command command in the channel {ctx.channel} of the guild {ctx.guild}, named {ctx.guild.name}") + await ctx.respond("prefix changed!", ephemeral=True) + c.execute("UPDATE data SET prompt_prefix = ? WHERE guild_id = ?", (prefix, ctx.guild.id)) + conn.commit() +''' +def reset_uses_count_today(): + c.execute("UPDATE data SET uses_count_today = 0") + conn.commit() +#get the current date and save it in the previous_date variable +#if the day number is different from the previous day number, reset the uses count today +def check_day(): + global previous_date + if datetime.datetime.now().day != previous_date.day: + previous_date = datetime.datetime.now() + reset_uses_count_today() + previous_date = datetime.datetime.now() + return True + else: + previous_date = datetime.datetime.now() + return False +#run check_day every 10 seconds +async def check_day_task(): + while True: + check_day() + await asyncio.sleep(60) +#add a task to the bot that runs check_day every 1 minute +bot.loop.create_task(check_day_task()) +''' #run the bot # Replace the following with your bot's token with open("key.txt") as f: key = f.read() -bot.run(key) \ No newline at end of file +bot.run(key) diff --git a/docker/Build/requirements.txt b/docker/Build/requirements.txt index a85e613..80f6c11 100644 --- a/docker/Build/requirements.txt +++ b/docker/Build/requirements.txt @@ -1,3 +1,3 @@ # To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file -py-cord[voice] +py-cord openai