diff --git a/.gitignore b/.gitignore index 68bc17f..9ff0a8f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.log + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/cord/accounts.py b/cord/accounts.py new file mode 100644 index 0000000..6ea03ea --- /dev/null +++ b/cord/accounts.py @@ -0,0 +1,47 @@ +"""Account system functionality.""" + +import os +import json +import requests + +import embedder + +from dotenv import load_dotenv + +load_dotenv() + +async def request_user_by_discord_id(discord_id): + return requests.get( + url=f'https://api.nova-oss.com/users?discord_id={discord_id}', + timeout=3, + headers={ + 'Content-Type': 'application/json', + 'Authorization': os.getenv('CORE_API_KEY') + } + ) + +async def get_account(interaction): + try: + get_response = await request_user_by_discord_id(interaction.user.id) + + except Exception as exc: + await embedder.error(interaction, """Sorry, +there was an error while checking if you have an account. +Please report this issue to the staff!""", ephemeral=True) + raise exc + + if get_response.status_code == 404: + return await embedder.error(interaction, """You +don't have an account yet!""", ephemeral=True) + + await embedder.info(interaction, f"""**Your account** +This is all we have stored about your API account in our database. +Feel free to request a removal of your account by contacting the staff. + +||```json +{json.dumps(get_response.json(), indent=4)} +```|| +(Click to reveal) + +Learn more about how to use our API at **https://nova-oss.com**. +""", ephemeral=True) diff --git a/cord/autochat.py b/cord/autochat.py index 55d1e7f..80d9b05 100644 --- a/cord/autochat.py +++ b/cord/autochat.py @@ -1,18 +1,31 @@ -import random import embedder +from nextcord import DMChannel async def process(message): - if message.content == '/key': - responses = [ - 'https://media.tenor.com/t9f91LQWsM4AAAAS/breaking-bad-funny.gif', - 'NPC detected, command rejected.', - 'https://images-ext-1.discordapp.net/external/DXc3r4PyRR3m_4AzevpxWhfFtavdcMeDpZqFj3Ig4hc/https/media.tenor.com/b7swbvaVKhUAAAPo/seriously-laugh.mp4', - 'there👏is👏no👏/key👏command👏', - 'https://i.imgflip.com/7tuhc6.jpg' - ] + text = message.content - await message.reply(random.choice(responses)) - await message.channel.send('Jokes aside - the project is still under development. There\'s no **`/key`** command.') + # IGNORE BOTS + if message.author.bot: + return - if message.content.startswith('/') and ('commands' not in message.channel.name) and (not message.author.guild_permissions.manage_messages): - await embedder.warn(message, 'Please only run commands in `/commands`.') + # IGNORE DM CHANNELS + if isinstance(message.channel, DMChannel): + return + + if 'N0V4x0SS' in text or 'T3BlbkFJ' in text: + await embedder.warn(message, f'{message.author.mention}, I think you sent an *OpenAI* or *NovaAI* key in here, which could lead to other users accessing your API account without your knowledge. Be very careful with API credentials!', delete_after=15) + await message.delete() + + # COMMANDS: WRONG CHANNEL + commands_allowed = ('commands' in message.channel.name) or (message.author.guild_permissions.manage_messages) + + if text.startswith('/') and not commands_allowed: + await embedder.error(message, f'{message.author.mention}, plesae __only__ run commands in <#1133103276871667722>.', delete_after=10) + await message.delete() + return + + # COMMANDS: NOT RAN CORRECTLY + if text.startswith('/') and len(text) > 2: + await embedder.warn(message, """Need help running commands? Check out +**https://nova-oss.com/novacord**!""", delete_after=10) + return diff --git a/cord/bot.py b/cord/bot.py index 5d36765..1c79022 100644 --- a/cord/bot.py +++ b/cord/bot.py @@ -1,13 +1,14 @@ -# This example requires the 'members' and 'message_content' privileged intents to function. +"""Bot base.""" import os import nextcord -import system import chatbot import embedder import autochat +import accounts import community +import credential_manager from dotenv import load_dotenv from nextcord.ext import commands @@ -15,9 +16,11 @@ from nextcord import SlashOption load_dotenv() +guild_ids = [int(guild_id) for guild_id in os.getenv('DISCORD_GUILD_IDS').split()] + bot = commands.Bot( intents=nextcord.Intents.all(), - default_guild_ids=[int(guild_id) for guild_id in os.getenv('DISCORD_GUILD_IDS').split()] # so slash commands work + default_guild_ids=guild_ids # so slash commands work ) @bot.event @@ -33,29 +36,34 @@ async def on_message(message): await autochat.process(message) await bot.process_commands(message) -@bot.slash_command(description='Chat with AI') -async def chat(interaction: nextcord.Interaction, - prompt: str = SlashOption(description='AI Prompt', required=True) -): - await chatbot.respond(interaction, prompt) +# @bot.slash_command(description='Chat with AI') +# async def chat(interaction: nextcord.Interaction, +# prompt: str = SlashOption(description='AI Prompt', required=True) +# ): +# await chatbot.respond(interaction, prompt) @bot.slash_command(description='Sets your DMs up, so you can write the bot.') -async def dm(interaction: nextcord.Interaction): +async def dm_setup(interaction: nextcord.Interaction): try: await interaction.user.create_dm() await embedder.info(interaction.user.dm_channel, 'Hello!') except nextcord.Forbidden: await embedder.error(interaction, text="""Please open this server\'s options, go to `Privacy Settings` and enable `Direct Messages` as well as `Message Requests`.""") - else: + + else: await embedder.ok(interaction, 'Great, DMs are set up successfully!') -@bot.slash_command(description='Get your secret NovaAI API credentials.') +@bot.slash_command(description='Create your account and get your API key.') async def credentials(interaction: nextcord.Interaction): - return await system.get_credentials(interaction) + return await credential_manager.get_credentials(interaction) @bot.slash_command(description='Leaderboard.') async def leaderboard(interaction: nextcord.Interaction): - await community.leaderboard(interaction) + return await community.leaderboard(interaction) + +@bot.slash_command(description='Get info and stats about your NovaAI API account.') +async def account(interaction: nextcord.Interaction): + return await accounts.get_account(interaction) bot.run(os.getenv('DISCORD_TOKEN')) diff --git a/cord/credential_manager.py b/cord/credential_manager.py new file mode 100644 index 0000000..f628d36 --- /dev/null +++ b/cord/credential_manager.py @@ -0,0 +1,73 @@ +"""Account system functionality.""" + +import os +import requests + +import embedder +import accounts +import tos_verification + +from dotenv import load_dotenv + +load_dotenv() + + +async def get_credentials(interaction): + for _ in range(2): + try: + get_response = await accounts.request_user_by_discord_id(interaction.user.id) + except Exception as exc: + await embedder.error(interaction, """Sorry, + there was an issue while checking if you already have an account. + Please report this issue to the staff!""", ephemeral=True) + raise exc + + if get_response.status_code == 200: # user exists + break + + # NEW USER + read_tos = await tos_verification.verify(interaction) + + if not read_tos: + await interaction.delete_original_message() + return + + # CREATE USER + get_response = requests.post( + url='https://api.nova-oss.com/users', + timeout=3, + headers={ + 'Content-Type': 'application/json', + 'Authorization': os.getenv('CORE_API_KEY') + }, + json={ + 'discord_id': interaction.user.id + } + ) + + try: + get_response.raise_for_status() + + except Exception as exc: + await embedder.error(interaction, """Sorry, + your account could not be created. Please report this issue to the staff!""", ephemeral=True) + + raise exc + + else: + await embedder.ok(interaction, f"""Welcome to NovaAI, {interaction.user.mention}! + Your account was created successfully.""", ephemeral=True) + + api_key = get_response.json()['api_key'] + + await embedder.info(interaction, f"""This is your **secret** API key. +Don't paste it on untrusted websites, apps or programs. +Store it securely using a `.env` file in the environment variables +or use a secure password manager like *KeePass*, *ProtonPass* or *Bitwarden*. +We reserve the right to __disable your API key at any time__ if you violate our terms of service. +If you accept the terms of service and privacy policy, feel free to use the following API key: + +## ||`{api_key}`|| + +Learn more about how to use our API at **https://nova-oss.com**. +""", ephemeral=True) diff --git a/cord/embedder.py b/cord/embedder.py index b48fa0a..916139d 100644 --- a/cord/embedder.py +++ b/cord/embedder.py @@ -9,7 +9,8 @@ async def send( text: str, content: str = '', ephemeral: bool = False, - color: nextcord.Color = nextcord.Color.blue() + color: nextcord.Color = nextcord.Color.blue(), + **kwargs ): edit = False @@ -28,7 +29,7 @@ async def send( end = '' - if milliseconds > 10000: # https://youtu.be/-5wpm-gesOY + if milliseconds > 5000: # https://youtu.be/-5wpm-gesOY end = f' in {milliseconds}ms' embed.set_footer(text=f'Powered by NovaAI{end}', icon_url='https://i.ibb.co/LDyFcSh/fav-blurple.png') @@ -36,19 +37,19 @@ async def send( interaction_type = Union[nextcord.Interaction, nextcord.InteractionResponse] - # these checks are done so this function is easy as fuck to use + # these checks are done so this function is easy to use if edit: - return await ctx.edit(embed=embed, content=content) + return await ctx.edit(embed=embed, content=content, **kwargs) if isinstance(ctx, nextcord.Message): - response = await ctx.reply(embed=embed, content=content) + response = await ctx.reply(embed=embed, content=content, **kwargs) elif isinstance(ctx, interaction_type): - response = await ctx.send(embed=embed, ephemeral=ephemeral, content=content) + response = await ctx.send(embed=embed, ephemeral=ephemeral, content=content, **kwargs) else: - response = await ctx.send(embed=embed, content=content) + response = await ctx.send(embed=embed, content=content, **kwargs) return response diff --git a/cord/keys.py b/cord/keys.py deleted file mode 100644 index b621453..0000000 --- a/cord/keys.py +++ /dev/null @@ -1,4 +0,0 @@ -import secrets - -async def create(user): - return 'nv-' + secrets.token_urlsafe(32) diff --git a/cord/system.py b/cord/system.py deleted file mode 100644 index 685ab6c..0000000 --- a/cord/system.py +++ /dev/null @@ -1,65 +0,0 @@ -import os -import asyncio -import requests - -import keys -import embedder - -from dotenv import load_dotenv - -load_dotenv() - -async def get_credentials(interaction): - try: - resp = requests.post( - url='https://nova-oss.com/api/tos-verification', - timeout=5, - headers={'Content-Type': 'application/json', 'Authorization': os.getenv('TOS_VERIFICATION_KEY')} - ).json() - except Exception as exc: - await embedder.error(interaction, """Sorry, the API server for the verification system is not functioning, -which means you can\'t create a new key right now. Please report this issue to the staff!""") - raise exc - - tos_code = resp['code'] - tos_emoji = resp['emoji'] - - tos_message = await embedder.warn(interaction, f"""# THIS IS JUST A DEMO/EXAMPLE! -# THE KEY WON'T WORK! -# THE SYSTEM ISN'T READY YET. -# DON'T SAVE THE KEY!!! -You have to read the privacy policy and terms of service first. -In the latter, there is a hidden emoji which you'll have to send (NOT react!) in here. - -https://nova-oss.com/legal/privacy -https://nova-oss.com/legal/terms?verify={tos_code} - -I know it's annoying, but it really helps combat spam bots and abuse. - -This message will be deleted and your code will run out **after about 10 minutes** -if you don't pass the verification, but **feel free to run this command again** at any time. -""", ephemeral=True) - - def check(message): return interaction.user.id == message.author.id and message.content == tos_emoji - - try: - answer = await interaction.client.wait_for('message', timeout=666, check=check) - except asyncio.TimeoutError: - await tos_message.delete() - requests.delete( - url=f'https://nova-oss.com/api/tos-verification/{tos_code}', - timeout=5, - headers={'Content-Type': 'application/json', 'Authorization': os.getenv('TOS_VERIFICATION_KEY')} - ) - - else: - await answer.delete() - api_key = await keys.create(interaction.user) - await embedder.ok(interaction, f"""This is your **secret** API key. Don't paste it on untrusted websites, apps or programs. -Store it securely using a `.env` file in the environment variables or use a secure password manager like *KeePass*, *ProtonPass* or *Bitwarden*. -We reserve the right to __disable your API key at any time__ if you violate our terms of service. -If you accept the terms of service and privacy policy, feel free to use the following API key: - -## ||`{api_key}`|| - -""", ephemeral=True) diff --git a/cord/tos_verification.py b/cord/tos_verification.py new file mode 100644 index 0000000..04f8406 --- /dev/null +++ b/cord/tos_verification.py @@ -0,0 +1,75 @@ +"""The module for the Terms of Service verification system.""" + +import os +import asyncio +import requests + +import embedder + +from dotenv import load_dotenv + +load_dotenv() + +async def verify(interaction) -> bool: + try: + resp = requests.post( + url='https://nova-oss.com/api/tos-verification', + timeout=5, + headers={ + 'Content-Type': 'application/json', + 'Authorization': os.getenv('TOS_VERIFICATION_KEY') + } + ).json() + except Exception as exc: + await embedder.error(interaction, """Sorry, +the API server for the verification system is not functioning, +which means you can\'t create a new key right now. Please report this issue to the staff!""") + raise exc + + success = False + tos_code = resp['code'] + tos_emoji = resp['emoji'] + + tos_message = await embedder.warn(interaction, f""" +You have to read the privacy policy and terms of service first. +In the latter, there is a hidden emoji which you'll have to __send__ (NOT react!) in here. + +https://nova-oss.com/legal/privacy +https://nova-oss.com/legal/terms?verify={tos_code} + +I know it's annoying, but it really helps combat spam bots and abuse. + +This message will be deleted and your code will run out **after 10 minutes** +if you don't pass the verification, but **feel free to run this command again** at any time. +""", ephemeral=True) + + def check(message): + correct_user = interaction.user.id == message.author.id + return correct_user and message.content == tos_emoji + + try: + while True: + received_answer = await interaction.client.wait_for('message', timeout=600, check=check) + await received_answer.delete() + + if received_answer.content == tos_emoji: + break + + except asyncio.TimeoutError: + await tos_message.delete() + + else: + success = True + + finally: + await tos_message.delete() + requests.delete( + url=f'https://nova-oss.com/api/tos-verification/{tos_code}', + timeout=5, + headers={ + 'Content-Type': 'application/json', + 'Authorization': os.getenv('TOS_VERIFICATION_KEY') + } + ) + + return success