A containerized code execution webserver and discord bot.
"""
ASGI config for DiscoCode project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""

import os
import django
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
from django.core.management import call_command
import atexit

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DiscoCode.settings')
django.setup()

from DiscoCodeClient.routing import websocket_urlpatterns as client_websocket


# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
django_asgi_app = get_asgi_application()

application = ProtocolTypeRouter({
    "http": django_asgi_app,
    "websocket": AllowedHostsOriginValidator(
        AuthMiddlewareStack(URLRouter([
          *client_websocket
        ]))
    ),
})

# AT EXIT CMD
atexit.register(call_command, 'sigint')

# AT STARTUP CMD
call_command('initializedb')
call_command('update_languages')
call_command('startloop')



DiscoCode/asgi.py
nickheyer/DiscoCode
"""
Django settings for DiscoCode project.

Generated by 'django-admin startproject' using Django 4.2.4.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
BACKUPS_DIR = BASE_DIR.parent

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-(g73e%4r+5t5&c)z1p-1a+!vu1fdbymtjmxw2e@=o)5ghj$!b6'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']

CORS_ALLOW_ALL_ORIGINS = True

# Application definition

INSTALLED_APPS = [
    'daphne',
    'channels',
    'DiscoCodeClient',
    'DiscoCodeBot',
    'corsheaders',
    'rest_framework',
    'rest_framework.authtoken',
    'jazzmin',
    'drf_spectacular',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'DiscoCode.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'DiscoCode.wsgi.application'
ASGI_APPLICATION = 'DiscoCode.asgi.application'

# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'data/db.sqlite3',
    }
}


CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels.layers.InMemoryChannelLayer',
    },
}


AUTH_USER_MODEL = 'DiscoCodeClient.User'

# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema'
    
}

# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'root': {
        'handlers': ['console'],
        'level': 'DEBUG',
    },
}

JAZZMIN_SETTINGS = {
    # title of the window (Will default to current_admin_site.site_title if absent or None)
    "site_title": "DiscoCode Admin Panel",

    # Title on the login screen (19 chars max) (defaults to current_admin_site.site_header if absent or None)
    "site_header": "DiscoCode",

    # Title on the brand (19 chars max) (defaults to current_admin_site.site_header if absent or None)
    "site_brand": "DiscoCode",

    # Logo to use for your site, must be present in static files, used for brand on top left
    "site_logo": "DiscoCodeClient/images/favicon.png",

    # Logo to use for your site, must be present in static files, used for login form logo (defaults to site_logo)
    "login_logo": "DiscoCodeClient/images/favicon.png",

    # Logo to use for login form in dark themes (defaults to login_logo)
    "login_logo_dark": "DiscoCodeClient/images/favicon.png",

    # CSS classes that are applied to the logo above
    "site_logo_classes": "img-circle",

    # Relative path to a favicon for your site, will default to site_logo if absent (ideally 32x32 px)
    "site_icon": "DiscoCodeClient/images/favicon.png",

    # Welcome text on the login screen
    "welcome_sign": "Welcome to DiscoCode",

    # Copyright on the footer
    "copyright": "Heyer.app Ltd",

    # List of model admins to search from the search bar, search bar omitted if excluded
    # If you want to use a single search field you dont need to use a list, you can use a simple string 
    "search_model": ["auth.User", "auth.Group"],

    # Field name on user model that contains avatar ImageField/URLField/Charfield or a callable that receives the user
    "user_avatar": None,

    ############
    # Top Menu #
    ############

    # Links to put along the top menu
    "topmenu_links": [

        # Url that gets reversed (Permissions can be added)
        {"name": "Home",  "url": "/", "permissions": ["auth.view_user"]},

        # external url that opens in a new window (Permissions can be added)
        {"name": "Support", "url": "https://github.com/nickheyer/DiscoCode/issues", "new_window": True},

        # model admin to link to (Permissions checked against model)
        {"model": "auth.User"},
    ],

    #############
    # User Menu #
    #############

    # Additional links to include in the user menu on the top right ("app" url type is not allowed)
    "usermenu_links": [
        {"name": "Support", "url": "https://github.com/nickheyer/DiscoCode/issues", "new_window": True},
        {"model": "auth.user"},
        {"name": "GitHub", "url": "https://github.com/nickheyer/DiscoCode/", "new_window": True},
    ],

    #############
    # Side Menu #
    #############

    # Whether to display the side menu
    "show_sidebar": True,

    # Whether to aut expand the menu
    "navigation_expanded": True,
    "default_icon_parents": "fas fa-chevron-circle-right",
    "default_icon_children": "fas fa-circle",
    "custom_css": None,
    "custom_js": 'DiscoCodeClient/javascript/misc_common.js',
    "use_google_fonts_cdn": True,
    "show_ui_builder": False,
    "changeform_format": "horizontal_tabs",
    "changeform_format_overrides": {"auth.user": "collapsible", "auth.group": "vertical_tabs"},
    "language_chooser": False,
}


DiscoCode/settings.py
nickheyer/DiscoCode
"""
URL configuration for DiscoCode project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView


urlpatterns = [
    path("", include("DiscoCodeClient.urls")),
    path('db/', admin.site.urls),
    path('docs/schema/', SpectacularAPIView.as_view(), name='schema'),
    path('docs/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]

DiscoCode/urls.py
nickheyer/DiscoCode
"""
WSGI config for DiscoCode project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DiscoCode.settings')

application = get_wsgi_application()

DiscoCode/wsgi.py
nickheyer/DiscoCode
class Command:
    """
    --- CLASS FIELD DESCRIPTIONS ---  [ SEE BELOW ]
    name: Command name.
    permissions: Currently allowed options are [ unregistered, user, admin, developer, owner ].
    slash_enabled: Allow slash command usage.
    aliases: Parsed commands allowed, ie: !df movie == !df film if ['movie', 'film'].
    description: Description to be given in help menu.
    requires_input: If command requires input following the command.
    conditions: Can take 3 different data-types as input...
      - tuple: (function, arguments-for-function)
      - function: function that returns truthy/falsey value
      - string: refers to settings/configuration value in DB, ie: 'is_radarr_enabled'
    """
        
    name = None
    permissions = []
    slash_enabled = True
    aliases = []
    description = ''
    requires_input = False
    conditions = []
    alt_prefix = False


    """
    --- CLASS METHOD ARGUMENTS DESCRIPTIONS --- [ SEE BELOW ]

    message: The original Discord Message instance.
      - .channel: The Discord channel that the message is in.
        - .reply: Reply to the message in that channel.
        - .send: Send to the channel that the message is in.
      - .

    ctx: The parsed message object returned from parser.py
      - .primary: The text following the command, ie: "!df echo <this is the primary argument>"
      - .bot = The Discord bot client instance.
      - .message = The Discord message object (same as first argument)
      - .message_content = The text content of the Discord message
      - .server_id = The ID of the Discord server: message.guild.id
      - .config = The Configuration object. It includes the merged user-specific settings.
      - .is_slash = Is the incoming message a slash command.
      - .valid = Is this a valid message.
    """

    async def execute(self, message, ctx):
        pass

    @classmethod
    def from_dict(cls, data):
        return cls(**data)
DiscoCodeBot/base_command.py
nickheyer/DiscoCode
import json
import os
import sys
import importlib
import discord
import asyncio

from DiscoCodeClient.utils import (
    get_verbose_dict,
    get_dict,
    get_logs,
    get_users_dict,
    add_refresh_log,
    update_state,
    update_servers,
    change_bot_state,
)
from django.core.serializers.json import DjangoJSONEncoder
from DiscoCodeBot import events
from DiscoCodeBot.registry import CommandRegistry
from DiscoCodeBot.base_command import Command
from DiscoCodeBot.dynamic import generate_commands


class DiscordBot:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not isinstance(cls._instance, cls) or cls._instance.client.is_closed():
            cls._instance = super(DiscordBot, cls).__new__(cls, *args, **kwargs)
            cls._instance._initialize_bot()
        return cls._instance

    def _initialize_bot(self):
        # DECLARING INTENTS (SEE README FOR CONTRIBUTORS)
        self.intents = discord.Intents.all()
        self.intents.members = True

        # INSTANTIATING DISCORD CLIENT
        self.client = discord.Client(intents=self.intents)

        # INSTANTIATING DISCORD COMMAND TREE
        self.tree = discord.app_commands.CommandTree(self.client)

        # REGISTERING COMMANDS
        self.registry = CommandRegistry(self.tree)
        self.registry.register(self.import_commands())

        # ATTACHING EVENT LISTENERS TO CLIENT
        self._attach_events()

    def _attach_events(self):
        @self.client.event
        async def on_guild_join(guild):
            await events.on_guild_join(self, guild)

        @self.client.event
        async def on_ready():
            self.registry.register(await generate_commands(), alt=True)
            await events.on_ready(self)

        @self.client.event
        async def on_message(message):
            await events.on_message(self, message)

        @self.client.event
        async def on_interaction(interaction):
            await events.on_interaction(self, interaction)

        @self.client.event
        async def on_error(_event, *args, **_kwargs):
            print('RAISING AN ERROR, HERE ARE ARGS: ')
            print(args)
            await events.on_error(self, *args)

    def get_client(self):
        return self.client

    async def start(self, token):
        async def runner(bot_token):
            async with self.client:
                await self.client.start(bot_token)

        bot_coro = runner(token)
        asyncio.create_task(bot_coro)

    async def close(self):
        await self.client.close()
        await self.emit(
            {
                "event": "bot_off_finished",
                "data": {"success": True, "bot_name": "discord"},
            }
        )

    async def emit(self, data):
        # GETTING OUR CLIENT WS CONNECTION TO SEND MESSAGES FROM BOT TO CLIENT
        from DiscoCodeClient.consumers import ClientConsumer

        websockets = ClientConsumer.active_consumers
        for consumer in websockets:
            # SENDING MSG TO ALL CURRENTLY CONNECTED CLIENTS
            asyncio.create_task(
                consumer.send(text_data=json.dumps(data, cls=DjangoJSONEncoder))
            )

    async def update_client(self):
        response_data = {
            "event": "client_info",
            "data": {
                "config": await get_verbose_dict("Configuration"),
                "state": await get_dict("State"),
                "log": await get_logs(100),
                "users": await get_users_dict(),
            },
        }
        await self.emit(response_data)

    async def send_log(self, entry, num=100):
        logs = await add_refresh_log(f"[BOT] {entry}", num)
        await self.emit({"event": "bot_log_added", "data": {"log": logs}})

    async def get_unadded_users(self, users):
        unadded = list(
            {
                str(m)
                for g in self.client.guilds
                for m in g.members
                if not m.bot and str(m) not in users
            }
        )

        return unadded

    async def get_servers(self):
        if not self.client:
            return []
        servers = [
            {"server_name": str(guild), "server_id": guild.id}
            for guild in self.client.guilds
        ]

        await update_servers(servers)
        return servers

    async def change_presence(self, presence):
        await self.client.change_presence(
            activity=discord.Activity(
                type=discord.ActivityType.listening, name=presence
            )
        )
        await update_state({"current_activity": f"Bot: 'Listening to {presence}'"})
        await self.update_client()

    def import_commands(self, directory="commands"):
        command_classes = []
        full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), directory)
        sys.path.insert(0, full_path)
        command_files = [f[:-3] for f in os.listdir(full_path) if f.endswith('.py')]
        for file_name in command_files:
            module_name = f"{file_name}"
            module = importlib.import_module(module_name, 'DiscoCodeBot.commands')
            for attr_name in dir(module):
                
                attr = getattr(module, attr_name)
                if isinstance(attr, type) and issubclass(attr, Command) and attr is not Command:
                    command_classes.append(attr())
        
        #sys.path.pop(0)
        return command_classes
DiscoCodeBot/bot.py
nickheyer/DiscoCode
from DiscoCodeBot.base_command import Command
from DiscoCodeClient.utils import get_user, add_user, edit_user
from discord import Color, Embed

class AddAdminCommand(Command):
    name = "add-admin"
    permissions = ["owner"]
    description = "Add/modify admins"
    aliases = ["add-admin", "admin", "mod", "op"]
    requires_input = True

    async def execute(self, message, ctx):
        users_to_op = []
        users_to_add = []
        if len(message.mentions) == 0:
            existing = await get_user(username=ctx.primary)
            if not existing:
                users_to_add.append(ctx.primary)
            else:
                users_to_op.append(existing)
        else:
            for user in message.mentions:
                existing = await get_user(username=str(user))
                if not existing:
                  users_to_add.append(str(user))
                else:
                  users_to_op.append(existing)

        if not users_to_add and not users_to_op:
            final_color = Color.brand_red()
            final_title = "No Users To Mod/Op"
            description = "Note: User(s) not found. Try again."

        else:
            final_color = Color.brand_green()
            final_title = "Authorization Granted"
            description = ''
            user_mentions = []
            for username in users_to_add:
                user_dict = {
                    "username": username,
                    "is_server_restricted": True,
                    "is_admin": True,
                    "servers": [
                        {
                            "server_name": str(message.guild),
                            "server_id": message.guild.id,
                        }
                    ],
                }
                await add_user(user_dict)
                await ctx.bot.send_log(f"ADDED ADMIN: {username}")
                user_mention = message.guild.get_member_named(username)
                if user_mention:
                    user_mentions.append(user_mention.mention)
                else: # User not found by bot - Check permissions
                    user_mentions.append(username)
                    if ctx.config.is_debug:
                        await ctx.bot.send_log(f"USER {username} NOT FOUND IN SERVER - CHECK PERMISSIONS TO BOT")

            for existing in users_to_op:
                user_dict = {
                    'id': existing.id,
                    'is_admin': True
                }
                await edit_user(user_dict)
                await ctx.bot.send_log(f"UPGRADED TO ADMIN: {existing.username}")
                user_mention = message.guild.get_member_named(existing.username)
                if user_mention:
                    user_mentions.append(user_mention.mention)
                else: # User not found by bot - Check permissions
                    user_mentions.append(existing.username)
                    if ctx.config.is_debug:
                        await ctx.bot.send_log(f"USER {existing.username} NOT FOUND IN SERVER - CHECK PERMISSIONS TO BOT")

            description += f"{message.author.mention} Added/Upgraded Admins: {', '.join(user_mentions)}\n"
  
        embed = Embed(title=final_title, color=final_color, description=description)
        await message.reply(embed=embed)
        await ctx.bot.update_client()
DiscoCodeBot/commands/add_admin.py
nickheyer/DiscoCode
from DiscoCodeBot.base_command import Command
from DiscoCodeClient.utils import get_user, add_user
from discord import Color, Embed

class AddUserCommand(Command):
    name = "add-user"
    permissions = ["admin", "owner"]
    description = "Add/modify users"
    aliases = ["add-user", "user", "add"]
    requires_input = True

    async def execute(self, message, ctx):
        users_to_add = []
        if len(message.mentions) == 0:
            existing = await get_user(username=ctx.primary)
            if not existing:
                users_to_add.append(ctx.primary)
        else:
            for user in message.mentions:
                existing = await get_user(username=str(user))
                if not existing:
                    users_to_add.append(str(user))

        if not users_to_add:
            final_color = Color.brand_red()
            final_title = "No Users To Add"
            description = "Note: Users cannot be added a second time!"

        else:
            final_color = Color.brand_green()
            final_title = "Authorization Granted"
            user_mentions = []
            for username in users_to_add:
                user_dict = {
                    "username": username,
                    "is_server_restricted": True,
                    "is_admin": False,
                    "servers": [
                        {
                            "server_name": str(message.guild),
                            "server_id": message.guild.id,
                        }
                    ],
                }
                await add_user(user_dict)
                await ctx.bot.send_log(f"ADDED USER: {username}")
                user_mention = message.guild.get_member_named(username)
                if user_mention:
                    user_mentions.append(user_mention.mention)
                else: # User not found by bot - Check permissions
                    user_mentions.append(username)
                    if ctx.config.is_debug:
                        await ctx.bot.send_log(f"USER {username} NOT FOUND IN SERVER - CHECK PERMISSIONS TO BOT")

            description = f"{message.author.mention} Added: {', '.join(user_mentions)}"
  
        embed = Embed(title=final_title, color=final_color, description=description)
        await message.reply(embed=embed)
        await ctx.bot.update_client()
DiscoCodeBot/commands/add_user.py
nickheyer/DiscoCode
from DiscoCodeBot.base_command import Command
from DiscoCodeClient.utils import get_user, delete_user
from discord import Color, Embed


class DeleteUserCommand(Command):
    name = "delete-user"
    permissions = ["admin", "owner"]
    description = "Remove/modify users"
    aliases = ["delete-user", "remove", "delete", "rm", "del"]
    requires_input = True

    async def execute(self, message, ctx):
        users_to_rm = []
        rm_admins = ctx.user.is_superuser
        if len(message.mentions) == 0:
            existing = await get_user(username=ctx.primary, is_admin=rm_admins, is_superuser=False)
            if existing:
                users_to_rm.append(ctx.primary)
        else:
            for user in message.mentions:
                existing = await get_user(username=str(user), is_admin=rm_admins, is_superuser=False)
                if existing:
                    users_to_rm.append(str(user))

        if not users_to_rm:
            final_color = Color.brand_red()
            final_title = "No Users To Delete"
            description = "Note: User(s) were not found in database. Please confirm user exists!"

        else:
            final_color = Color.brand_green()
            final_title = "Authorization Granted"
            user_mentions = []
            for username in users_to_rm:
                await delete_user(username=username)
                await ctx.bot.send_log(f"DELETED USER: {username}")
                user_mention = message.guild.get_member_named(username)
                if user_mention:
                    user_mentions.append(user_mention.mention)
                else:  # User not found by bot - Check permissions
                    user_mentions.append(username)
                    if ctx.config.is_debug:
                        await ctx.bot.send_log(
                            f"USER {username} NOT FOUND IN SERVER - CHECK PERMISSIONS TO BOT"
                        )

            description = f"{message.author.mention} Deleted: {', '.join(user_mentions)}"

        embed = Embed(title=final_title, color=final_color, description=description)
        await message.reply(embed=embed)
        await ctx.bot.update_client()

DiscoCodeBot/commands/delete_user.py
nickheyer/DiscoCode
from DiscoCodeBot.base_command import Command
from DiscoCodeBot.managers.ui_manager import ListCommands
from DiscoCodeBot.registry import CommandRegistry

class HelpCommand(Command):
    name = "help"
    aliases = ["help"]
    permissions = ["user", "developer", "unregistered"]
    description = "Display all authorized commands."

    async def execute(self, message, ctx):
        registry = CommandRegistry()
        all_commands = registry.all()
        commands = ListCommands(ctx, all_commands)
        embed = await commands.generate_embed()
        await message.reply(embed=embed)
DiscoCodeBot/commands/help.py
nickheyer/DiscoCode
from DiscoCodeBot.base_command import Command
from DiscoCodeBot.managers.ui_manager import ListLanguages, PaginatedView
from DiscoCodeBot.registry import CommandRegistry

class LangCommand(Command):
    name = "lang"
    aliases = ["lang", "langs", "language", "languages"]
    permissions = ["user", "developer"]
    description = "Display all executable languages."

    async def execute(self, message, ctx):
        registry = CommandRegistry()
        alts = registry.alts()
        commands = ListLanguages(ctx, alts)
        embeds = await commands.generate_embeds()
        view = PaginatedView(ctx, embeds)
        await message.reply(embed=embeds[0], view=view)
DiscoCodeBot/commands/languages.py
nickheyer/DiscoCode
from DiscoCodeBot.base_command import Command
from DiscoCodeBot.managers.ui_manager import ListCommands
from DiscoCodeBot.registry import CommandRegistry

class TestCommand(Command):
    name = "test"
    aliases = ["test"]
    permissions = ["admin"]
    description = "Test that bot is recognizing commands."

    async def execute(self, message, ctx):
        raise Exception('TEST ERROR')
DiscoCodeBot/commands/test.py
nickheyer/DiscoCode
from DiscoCodeClient.utils import get_config
from DiscoCodeBot.bot import DiscordBot


async def main():
    print("[CONTROLLER] BOT STARTING")

    config = await get_config()
    token = (
        config.discord_token
    )

    bot_instance = DiscordBot()
    await bot_instance.start(token)


async def kill():
    bot_instance = DiscordBot()
    await bot_instance.close()

    print("[CONTROLLER] BOT KILLED")

DiscoCodeBot/controller.py
nickheyer/DiscoCode
from DiscoCodeBot.base_command import Command
from DiscoCodeClient.models import Language
from DiscoCodeBot.managers.ui_manager import ReadOutput, OutputBtnView
from channels.db import database_sync_to_async
import requests
import json


@database_sync_to_async
def generate_commands():
    commands = []

    class LangCommand(Command):
        def __init__(self, lang) -> None:
            super().__init__()
            self.lang = lang
            self.name = lang.language
            self.aliases = lang.aliases
            self.requires_input = True
            self.slash_enabled = False
            self.permissions = ["user", "developer"]
            self.description = f"Execute {lang.language} code"
            self.conditions = []
            self.alt_prefix = True

        async def execute(self, message, ctx):
            lang = self.lang

            is_enabled = lang.is_enabled
            if not is_enabled:
                return

            code = ctx.primary

            url = ctx.config.exec_endpoint

            async with message.channel.typing():
              payload = json.dumps(
                  {
                      "language": self.name,
                      "version": lang.version,
                      "files": [{"content": code}],
                  }
              )

              headers = {"Content-Type": "application/json"}
              response = requests.request("POST", url, headers=headers, data=payload)
              resp_json = response.json()

              btn_view = OutputBtnView(ctx, resp_json)
              out_cls = ReadOutput(ctx, resp_json)
              embed = await out_cls.generate_embed()
              await message.reply(embed=embed, view=btn_view)

    for lang in Language.objects.all():
        commands.append(LangCommand(lang))

    return commands

DiscoCodeBot/dynamic.py
nickheyer/DiscoCode
import discord
import traceback
from io import StringIO

from DiscoCodeClient.utils import (
    get_config as config,
    add_err_log
)

from DiscoCodeBot.parser import (
    MessageHandler,
    FromInteraction
)

# DISCORD BOT EVENTS - LOADED ON INSTANTIATION BY BOT.PY


# WHEN BOT JOINS SERVER
async def on_guild_join(bot, guild):
    await bot.get_servers()
    await bot.send_log(f"{bot.client.user} JOINED {guild.name}")

# WHEN BOT SIGNS IN / STARTS UP
async def on_ready(bot):
    status = f"CONNECTED WITH {bot.client.user}"
    await bot.get_servers()
    await bot.send_log(status)
    config_cls = await config()
    await bot.change_presence(f"/help & {config_cls.prefix_keyword} help")
    await bot.tree.sync(guild=None)
    await bot.emit(
        {"event": "bot_on_finished", "data": {"success": True, "bot_name": "discord"}}
    )

# WHEN BOT SEES A NEW MESSAGE IN CHAT
async def on_message(bot, message):
    if message.author == bot.client.user:
        return
    config_cls = await config()
    ctx = MessageHandler(bot, message, config_cls)
    fn = await ctx.get_fn()
    await fn()

# WHEN BOT GETS SLASH COMMAND
async def on_interaction(bot, interaction) -> None:
    if interaction.type != discord.InteractionType.application_command:
      return
    config_cls = await config()
    await interaction.response.send_message(content='*...*', silent=True, ephemeral=True, delete_after=0.5)
    message = FromInteraction(interaction, config_cls)
    ctx = MessageHandler(bot, message, config_cls)
    fn = await ctx.get_fn()
    await fn()

# WHEN BOT ENCOUNTERS ERROR
async def on_error(bot, *args):
    err = traceback.format_exc()
    channel = args[0].channel
    report_content = StringIO(str(err))
    await add_err_log(str(err))
    config_cls = await config()
    if config_cls.is_debug:
        embeded = discord.Embed(
            title="Error Occured :(",
            url="https://github.com/nickheyer/DiscoCode/issues/new",
            description="\nError occured. "
            + "Consider copy and pasting the below "
            + "log file into a bug report "
            + "using the below link.\n\nThank you!",
            color=0x966FD6,
        )
        embeded.set_author(name=str(bot.client.user), icon_url=bot.client.user.display_avatar)
        embeded.add_field(
            name="Submit Bug Report:",
            value="https://github.com/nickheyer/DiscoCode/issues/new",
            inline=False,
        )
        embeded.add_field(
            name="Error Preview:", value=f"```{str(err)[:512]}...```", inline=False
        )
        await channel.send(embed=embeded)
        report_content.seek(0) # Reset the file pointer to the beginning of the file
        await channel.send(file=discord.File(report_content, filename="error_log.txt"))
DiscoCodeBot/events.py
nickheyer/DiscoCode
from DiscoCodeClient.utils import get_users_in_server, add_user
from DiscoCodeBot.managers.ui_manager import ApproveNewUser


async def handle_unregistered(message, ctx):
    admins = await get_users_in_server(ctx.server_id, ["admin"])
    admin_mentions = ""

    for user in admins:
        discord_user = message.guild.get_member_named(user)
        admin_mentions += f"{discord_user.mention}\n"

    response_message = f"User requires registration before use.\nAdmin approval needed.\n{admin_mentions}"
    view = ApproveNewUser(ctx, message, admins, response_message)
    timeout = await view.send_response()
    if not timeout:
        if view.result in ["DENIED", False, None]:
            return
        user_dict = {
                "username": str(message.author),
                "is_server_restricted": True,
                "is_admin": False,
                "servers": [
                    {
                        "server_name": str(message.guild),
                        "server_id": message.guild.id,
                    }
                ],
            }
        await add_user(user_dict)
        await ctx.bot.send_log(f"Added User: {message.author} ({view.result})")
    else:
        print("Timed-Out")
DiscoCodeBot/lib/utils/utils.py
nickheyer/DiscoCode

async def requires_admin(cls):
  if not cls.user_config.is_admin:
    return True
  return False
DiscoCodeBot/managers/permission_manager.py
nickheyer/DiscoCode
import discord
import io
import json


from DiscoCodeClient.utils import (
    eval_user_roles,
    get_users_in_server,
    get_user,
    get_lang
)

from DiscoCodeBot.managers.permission_manager import requires_admin


class ApproveNewUser(discord.ui.View):
    def __init__(self, context, response_object, auth_users, reason):
        self.context = context
        self.config = context.config
        super().__init__(timeout=self.config.session_timeout)
        self.response = response_object
        self.authorized_users = auth_users
        self.prompt = reason
        self.embed = None

    async def on_timeout(self) -> None:
        self.result = "TIMED_OUT"
        await self.generate_embed(timed_out=True)
        await self.reply.edit(embed=self.embed, view=None)
        return await super().on_timeout()

    @discord.ui.button(
        label="Register User",
        style=discord.ButtonStyle.blurple,
        custom_id="register_user",
        row=0,
    )
    async def register_user_button(
        self, interaction: discord.Interaction, button: discord.ui.Button
    ):
        if interaction.user not in self.authorized_users:
            return
        self.result = "REGISTER_USER"
        await self.generate_embed(interaction)
        self.stop()

    @discord.ui.button(
        label="Deny", style=discord.ButtonStyle.gray, custom_id="deny", row=0
    )
    async def select_button(
        self, interaction: discord.Interaction, button: discord.ui.Button
    ):
        if interaction.user not in self.authorized_users:
            return
        self.result = "DENIED"
        await self.generate_embed(interaction)
        self.stop()

    async def generate_embed(self, interaction=False, timed_out=False):
        if not self.authorized_users:
            embed = discord.Embed(
                title="Authorization Required", color=discord.Color.brand_red()
            )
            embed.description = (
                "Authorization is required for this action, but no admin users exist in this server.\n\n"
                + "Please add/designate an admin user in the web-ui (where you started this bot).\n\n"
                + "If you continue to encounter errors like this, consider submitting a bug-report:\n\n"
                + "*Here*: https://discord.gg/wXs6z922VG\n*or*\n*Here*: https://github.com/nickheyer/DiscoCode/issues/new"
            )
            embed.url = "https://discord.gg/wXs6z922VG"
            self.clear_items()
            self.embed = embed
            self.result = False
            self.stop()
            return embed
        elif timed_out:
            embed = discord.Embed(
                title="Authorization Timed-Out", color=discord.Color.brand_red()
            )
            embed.description = "No responses provided, moving on."
            self.embed = embed
            return embed
        elif not interaction:
            embed = discord.Embed(
                title="Authorization Required", color=discord.Color.dark_embed()
            )
            embed.description = self.prompt
            self.embed = embed
            return embed
        final_color = None
        final_title = None
        if self.result in ["DENIED"]:
            final_color = discord.Color.brand_red()
            final_title = "Authorization Denied"
        else:
            final_color = discord.Color.brand_green()
            final_title = "Authorization Granted"
        embed = discord.Embed(title=final_title, color=final_color)
        embed.description = (
            f"Decided By: {interaction.user.mention}\nOutcome: {self.result}"
        )
        await interaction.response.edit_message(embed=embed, view=None)

    async def send_response(self):
        await self.generate_embed()
        if self.authorized_users:
            self.reply = await self.response.channel.send(embed=self.embed, view=self)
            return await self.wait()
        self.reply = await self.response.channel.send(embed=self.embed)


class ApproveRequest(discord.ui.View):
    def __init__(self, context, response_object, reason, original_embed):
        self.context = context
        self.config = context.config
        super().__init__(timeout=self.config.session_timeout)
        self.response = response_object
        self.prompt = reason
        self.embed = None
        self.result = None
        self.original_embed = original_embed

    async def async_init(self):
        self.authorized_users = await self.get_admins(self.response)

    async def on_timeout(self) -> None:
        self.result = False
        timeout_embed = await self.generate_embed(timed_out=True)
        await self.response.edit(content=None, embed=timeout_embed, view=None)
        return await super().on_timeout()

    async def get_admins(self, message_object):
        users = await get_users_in_server(message_object.guild.id, ["admin"])
        discord_users = []
        for user in users:
            user = message_object.guild.get_member_named(user)
            if user:
                discord_users.append(user)
        return discord_users

    def get_admin_mentions(self):
        message_template = ""
        for user in self.authorized_users:
            message_template += f"{user.mention}\n"
        return message_template

    @discord.ui.button(
        label="Approve", style=discord.ButtonStyle.green, custom_id="approve", row=0
    )
    async def register_admin_button(
        self, interaction: discord.Interaction, button: discord.ui.Button
    ):
        if interaction.user not in self.authorized_users:
            return
        self.result = True
        await interaction.response.edit_message(
            content=None, view=None, embed=self.original_embed
        )
        self.stop()

    @discord.ui.button(
        label="Deny", style=discord.ButtonStyle.red, custom_id="deny", row=0
    )
    async def select_button(
        self, interaction: discord.Interaction, button: discord.ui.Button
    ):
        if interaction.user not in self.authorized_users:
            return
        self.result = False
        embed = discord.Embed(title="Request Denied", color=discord.Color.brand_red())
        embed.description = f"Decided By: {interaction.user.mention}\nOutcome: Denied"
        await interaction.response.edit_message(content=None, view=None, embed=embed)
        self.stop()

    async def generate_embed(self, interaction=False, timed_out=False):
        if timed_out:
            embed = discord.Embed(
                title="Authorization Timed-Out", color=discord.Color.brand_red()
            )
            embed.description = "No responses provided, moving on."
            self.embed = embed
            return embed
        elif not self.authorized_users:
            embed = discord.Embed(
                title="Authorization Required", color=discord.Color.brand_red()
            )
            embed.description = (
                "Authorization is required for this action, but no admin users exist in this server.\n\n"
                + "Please add/designate an admin user in the web-ui (where you started this bot).\n\n"
                + "If you continue to encounter errors like this, consider submitting a bug-report:\n\n"
                + "*Here*: https://discord.gg/wXs6z922VG\n*or*\n*Here*: https://github.com/nickheyer/DiscoCode/issues/new"
            )
            embed.url = "https://discord.gg/wXs6z922VG"
            self.clear_items()
            self.embed = embed
            self.result = False
            self.stop()
            return embed
        elif not interaction:
            embed = discord.Embed(
                title="Authorization Required", color=discord.Color.dark_purple()
            )
            embed.description = self.prompt
            self.embed = embed
            return embed


class ListCommands:
    def __init__(self, context, command_classes):
        self.context = context
        self.config = context.config
        self.prefix = self.config.prefix_keyword
        self.response = context.message
        self.commands = command_classes
        self.user = context.message.author
        self.user_str = str(self.user)
        self.embed = None

    async def generate_embed(self):
        embed = discord.Embed(
            title="Help",
            description="List of available commands:",
            color=self.user.color,
        )
        user_roles = set(await eval_user_roles(self.user_str))
        _debug = self.config.is_debug
        if _debug:
          print(f"THE CURRENT ROLES FOR THIS USER: {user_roles}")
          print(f"CURRENT COMANDS FROM CONTEXT: {self.commands}")
        for command_cls in self.commands:
            required_roles = set(command_cls.permissions)
            authorized_roles = required_roles.intersection(user_roles)
            
            if not required_roles or authorized_roles or _debug:
                name = command_cls.name
                aliases = "*, *".join(command_cls.aliases)
                usage = f"{command_cls.aliases[0]}" + (
                    " <input>" if command_cls.requires_input else ""
                )
                description = command_cls.description
                field_text = f"Description: `{description}`\n" if description else ""
                field_text += f"Usage: `{self.prefix} {usage}`"
                if command_cls.slash_enabled:
                    field_text += f"\nSlash: `/{usage}`"
                if _debug and (required_roles and not authorized_roles):
                    field_text += "\nAuthorization: `Not authorized`"
                elif _debug:
                    field_text += "\nAuthorization: `Authorized`"
                embed.add_field(
                    name=f"{name} [*{aliases}*]", value=field_text, inline=False
                )

        if _debug:
            information_field = f'Username: `{self.user_str}`\nUser Roles: `{"`, `".join(user_roles)}`\nCommands Registered: `{len(self.commands)}`'
            embed.add_field(name="User Debug Information", value=information_field, inline=False)

        self.embed = embed
        return embed

class ListLanguages:
    def __init__(self, context, command_classes, commands_per_embed=10):
        self.context = context
        self.config = context.config
        self.prefix = self.config.alt_prefix
        self.response = context.message
        self.commands = command_classes
        self.user = context.message.author
        self.user_str = str(self.user)
        self.commands_per_embed = commands_per_embed

    async def generate_embeds(self):
        embeds = []
        for i in range(0, len(self.commands), self.commands_per_embed):
            embed = discord.Embed(
                title="Executable Languages",
                description=f"\n__Example Usage__:\n\n{self.prefix}\```<name of language or alias>\n<your code>\n\```\n\n__List of available languages__:\n\n",
                color=self.user.color,
            )

            cmd_group = self.commands[i:i + self.commands_per_embed]
            for command_cls in cmd_group:
                lang = await get_lang(command_cls.name)
                name = command_cls.name
                aliases = "*, *".join(command_cls.aliases)
                description = command_cls.description
                field_text = f"Description: `{description}`\n" if description else ""
                field_text += f"Aliases: *{aliases}*\n"
                embed.add_field(
                    name=f"{name} {'✖' if not lang or not lang.is_enabled else '✔'}", value=field_text, inline=False
                )

            embed.set_footer(text=f"Displaying {i + 1}-{min(len(self.commands), i + self.commands_per_embed)} of {len(self.commands)} languages")

            embeds.append(embed)
        
        return embeds


class OutputBtnView(discord.ui.View):
    def __init__(self, ctx, output):
        super().__init__()
        self.output = output
        self.ctx = ctx

    async def interaction_check(self, interaction: discord.Interaction):
        return True

    @discord.ui.button(label="Generate JSON", custom_id="json_generator", style=discord.ButtonStyle.green)
    async def json_button_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
        file = discord.File(fp=io.BytesIO(json.dumps(self.output, indent=4).encode()), filename="output.json")
        await interaction.response.send_message("Here is your JSON file:", file=file, ephemeral=True)
    
    @discord.ui.button(label="See More Languages...", custom_id="see_more", style=discord.ButtonStyle.blurple)
    async def more_button_callback(self, interaction: discord.Interaction, button: discord.ui.Button):
        lang_cmd = self.ctx.bot.registry.get('lang')
        await lang_cmd.execute(self.ctx.message, self.ctx)
        await interaction.response.edit_message(content=interaction.message.content, embeds=interaction.message.embeds, view=None)


class PaginatedView(discord.ui.View):
    def __init__(self, ctx, embeds):
        super().__init__()
        self.embeds = embeds
        self.ctx = ctx
        self.user = ctx.message.author
        self.current_page = 0

    async def interaction_check(self, interaction: discord.Interaction) -> bool:
        return interaction.user == self.user

    @discord.ui.button(label="Previous", custom_id="paginate_next", style=discord.ButtonStyle.blurple)
    async def previous_page(self, interaction: discord.Interaction, button: discord.ui.Button):
        if self.current_page > 0:
            self.current_page -= 1
            await interaction.response.edit_message(embed=self.embeds[self.current_page])

    @discord.ui.button(label="Next", custom_id="paginate_prev", style=discord.ButtonStyle.blurple)
    async def next_page(self, interaction: discord.Interaction, button: discord.ui.Button):
        if self.current_page < len(self.embeds) - 1:
            self.current_page += 1
            await interaction.response.edit_message(embed=self.embeds[self.current_page])

class ReadOutput:
    def __init__(self, context, output):
        self.context = context
        self.config = context.config
        self.response = context.message
        self.user = context.message.author
        self.output = output
        self.results = output.get('run')
        self.compile_results = output.get('compile', {})
        self.return_code = self.results.get('code', 0)
        self.embed = None

    def trunc(self, out):
      max_len = self.config.max_stdout_len
      length = len(out)
      if length > max_len:
          return f"{out[:max_len - 3]}..."
      else:
          return out
      
    async def generate_embed(self):
        if self.results.get("stderr"):
            embed_color = discord.Color.red()
        else:
            embed_color = discord.Color.green()

        embed = discord.Embed(
            title="Code Execution",
            description="Results of your code execution",
            color=embed_color,
        )

        embed.add_field(name='Language', value=f'```{self.output.get("language", "N/A")}```', inline=True)
        embed.add_field(name='Version', value=f'```{self.output.get("version", "N/A")}```', inline=True)
        
        if self.compile_results:
            compile_stderr = self.trunc(self.compile_results.get('stderr', '').strip())
            if compile_stderr:
                embed.add_field(name='Compile Errors/Warnings', value=f'```diff\n-{self.trunc(compile_stderr)}\n```', inline=False)
                
        if self.results.get("stderr"):
            embed.add_field(name='Run-time Error', value=f'```diff\n-{self.trunc(self.results.get("stderr", "Error Occurred"))}\n```', inline=False)
        if self.results.get("stdout"):
            embed.add_field(name='Output', value=f'```{self.trunc(self.results.get("stdout", "No results"))}```', inline=False)

        embed.set_footer(text=f'Requested by {self.user.name}', icon_url=self.user.display_avatar)

        self.embed = embed
        return embed
DiscoCodeBot/managers/ui_manager.py
nickheyer/DiscoCode
import re
import asyncio

from DiscoCodeClient.utils import get_users_for_auth, get_user_settings, get_user
from DiscoCodeBot.registry import CommandRegistry

class MessageHandler:
    def __init__(self, bot, message, config) -> None:
        
        self.bot = bot
        self.message = message
        self.message_content = message.content
        self.server_id = message.guild.id

        self.alt_prefix_used = False
        
        self.config = config

        self.command = None
        self.primary = None
        self.is_slash = hasattr(message, 'is_slash')

        self.verifier = self.__parse_message()
        self.rejection = None
        
        
    async def __parse_message(self):
        
        req_parsing_steps = [ # WILL IGNORE MESSAGE ON FAILURE
          self.__parse_sender,
          self.__parse_prefix,
          self.__parse_command
        ]

        addtional_steps = [ # WILL TRIGGER COMMAND.REJECT IF EXISTS
          (self.__parse_permissions, 'Failed to validate user request based on current permissions or state.'),
          (self.__parse_conditions, 'Conditions missing for command usage.'),
          (self.__parse_slash, 'Slash command not allowed for this command usage.'),
          (self.__parse_args, 'Failed to parse command arguments. Arguments invalid.'),
        ]

        for step in req_parsing_steps:
            result = await step() if asyncio.iscoroutinefunction(step) else step()
            if not result:
                return False

        for step, rej in addtional_steps:
            result = await step() if asyncio.iscoroutinefunction(step) else step()
            if not result:
                self.rejection = rej
                return False         
        return True
            
    async def __parse_sender(self):
        self.sender = self.message.author
        self.username = str(self.message.author)
        self.is_bot = self.sender.bot
        self.user = await get_user(username=self.username)
        self.user_settings = await get_user_settings(self.user)
        for key, val in self.user_settings.items():
            if hasattr(self.config, key):
                setattr(self.config, key, val)
        return not self.is_bot

    def __parse_prefix(self):
        prefix = self.config.prefix_keyword
        alt_prefix = self.config.alt_prefix
        message = self.message_content.strip()

        if message.startswith(alt_prefix) and message.endswith('```'):
            self.alt_prefix_used = True
            self.split_message = [alt_prefix]
            self.split_message.extend(
                message[len(alt_prefix):].strip(' `\n').split('\n')
            )
        elif message.startswith(prefix):
            self.split_message = [prefix]
            self.split_message.extend(
                message[len(prefix):].strip().split(' ')
            )
        else:
            return False
        return len(self.split_message) > 1

    def __parse_command(self):
        if len(self.split_message) < 2:
            return False
        else:
            registry = CommandRegistry()
            cmd_str = self.split_message[1].lower().strip()
            cmd = registry.get(cmd_str)
            if cmd and (not self.alt_prefix_used or cmd.alt_prefix):
                self.cmd_str = cmd_str
                self.command = cmd
                return True
            return False

    def __parse_slash(self):
        if self.is_slash:
          return self.command.slash_enabled
        else:
          return True
        
    async def __parse_permissions(self):
        permissions = self.command.permissions
        if not permissions:
            return True
        return await get_users_for_auth(self.server_id, permissions, self.username)
    
    def __parse_conditions(self):
        conditions = self.command.conditions
        for requirement in conditions:
            if isinstance(requirement, tuple):
                fn = requirement[0]
                result = fn(*requirement[1])
                if not result:
                    return False
            elif hasattr(requirement, "__call__"):
                fn = requirement
                result = fn()
                if not result:
                    return False
            elif isinstance(requirement, str):
                config_setting = getattr(self.config, requirement, False)
                if not config_setting:
                    return False
            else:
                if not requirement:
                    return False
        return True

    def __parse_args(self):
        if not self.command.requires_input:
            return True
        if len(self.split_message) < 3:
            return False
        else:
            if self.alt_prefix_used:
              self.primary = '\n'.join(self.split_message[2:])
            else:
              self.primary = ' '.join(self.split_message[2:])
            return True
        
    async def _default(*args):
        return None

    async def get_fn(self):
        print('IN GET FN')
        if not (await self.verifier):
            if self.rejection and hasattr(self.command, 'reject'):
                print('IN REJECT!')
                fn = self.command.reject
            else:
                print('IN DEFAULT :(')
                fn = self._default
        else:
            print('IN EXECUTE!')
            fn = self.command.execute
        self.fn = lambda: fn(self.message, self)
        return self.fn

class FromInteraction:
    def __init__(self, interaction, config) -> None:
        self.config = config
        self.interaction = interaction
        self.id = interaction.id
        self.channel = interaction.channel
        self.created_at = interaction.created_at
        self.author = interaction.user
        self.guild = interaction.guild
        self.command = interaction.data.get('name', '')
        self.content = f'{self.config.prefix_keyword} {self.command} '
        for text in interaction.data.get('options', []):
            self.content+= text.get('value', '')
        self.type = interaction.type
        self.application_id = interaction.application_id
        self.is_slash = True
        self.components = []
        self.attachments = []
        self.embeds = []
        self.mention_everyone = False
        self.mentions = [self.guild.get_member(int(match)) for match in re.findall(r"<@!?([0-9]{15,20})>", self.content)]
        self.nonce = None
        self.pinned = False
        self.reactions = []
    
    async def reply(self, *args, **kwargs):
        return await self.channel.send( *args, **kwargs)
    
    async def edit(self):
        return True
    
    async def delete(self):
        return True
DiscoCodeBot/parser.py
nickheyer/DiscoCode
import discord

class CommandRegistry:
    _instance = None

    def __new__(cls, tree=None):
        if not isinstance(cls._instance, cls):
            cls._instance = super(CommandRegistry, cls).__new__(cls)
            cls._instance.init(tree)
        return cls._instance

    def init(self, tree):
        self.commands = {}
        self.alt_commands = {}
        self.tree = tree

    def register(self, command_cls_list=[], alt=False):
        for command_cls in command_cls_list:
            command = command_cls
            print(f'REGISTERING COMMAND: {command.name}')

            for alias in command.aliases:
                print(f'REGISTERING ALIAS: {alias} / {command}')
                if not alt:
                    self.commands[alias] = command
                else:
                    self.alt_commands[alias] = command
              
            if command.slash_enabled:
                self._update_slash_command(command)

    def _update_slash_command(self, cmd):
        command = discord.app_commands.Command(
            name=cmd.name,
            description=cmd.description,
            callback=self.cb_factory(cmd.requires_input),
            guild_ids=None,
        )
        self.tree.add_command(command, override=True)

    def cb_factory(self, requires_input: bool):
        if requires_input:
            async def stub_callback_with_arg(interaction: discord.Interaction, input: str):
                pass
            return stub_callback_with_arg
        else:
            async def stub_callback(interaction: discord.Interaction):
                pass
            return stub_callback

    def get(self, name):
        command = self.commands.get(name)
        if not command:
            command = self.alt_commands.get(name)
        return command

    def all(self):
        if not self.commands:
            return []
        commands_ret = list(set(self.commands.values()))
        sorted_commands = sorted(commands_ret, key=lambda x: x.name)
        return sorted_commands
    
    def alts(self):
        if not self.alt_commands:
            return []
        commands_ret = list(set(self.alt_commands.values()))
        sorted_commands = sorted(commands_ret, key=lambda x: x.name)
        return sorted_commands
DiscoCodeBot/registry.py
nickheyer/DiscoCode
from django.contrib import admin
from DiscoCodeClient.models import (
    Configuration,
    State,
    ErrLog,
    EventLog,
    User,
    DiscordServer,
    Language
)

models_to_register = [
    Configuration,
    State,
    ErrLog,
    EventLog,
    User,
    DiscordServer,
    Language
]

for model in models_to_register:
    admin.site.register(model)
DiscoCodeClient/admin.py
nickheyer/DiscoCode
from django.apps import AppConfig

class DiscoCodeClientConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'DiscoCodeClient'
DiscoCodeClient/apps.py
nickheyer/DiscoCode
from .client import ClientConsumer

DiscoCodeClient/consumers/__init__.py
nickheyer/DiscoCode
import json
import logging
from functools import wraps
import os
import sys

from django.core.serializers.json import DjangoJSONEncoder
from channels.generic.websocket import AsyncWebsocketConsumer
from django.core.management.color import color_style

from DiscoCodeBot.controller import (
    main,
    kill,
)

from DiscoCodeBot.bot import DiscordBot
from DiscoCodeBot.dynamic import generate_commands

from DiscoCodeClient.utils import (
    get_verbose_dict,
    get_dict,
    get_logs,
    add_log,
    get_users_dict,
    add_refresh_log,
    update_config,
    update_langs,
    get_config,
    add_user,
    edit_user,
    delete_user,
    get_user_dict,
    reset_entire_db,
    export_data,
    import_data,
    update_state,
    get_users_list,
    get_all_user_servers,
    change_bot_state,
    validate_discord_token,
    get_dicts
)


style = color_style()
logger = logging.getLogger(__name__)


def event_handler(event_name):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            return await func(*args, **kwargs)

        wrapper.event_name = event_name
        return wrapper

    return decorator


class ClientConsumer(AsyncWebsocketConsumer):
    event_handlers = {}
    active_consumers = set()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        for name, value in ClientConsumer.__dict__.items():
            if hasattr(value, "event_name"):
                self.event_handlers[value.event_name] = value

    async def connect(self):
        logger.debug("CONNECTED TO CLIENT WEBSOCKET!")
        ClientConsumer.active_consumers.add(self)
        await self.accept()

    async def disconnect(self, close_code):
        ClientConsumer.active_consumers.remove(self)

    async def receive(self, text_data):
        text_data_json = json.loads(text_data)

        event = text_data_json.get("event")
        data = text_data_json.get("data")
        callback_id = data.get("callbackId")

        # logger.debug(text_data_json)

        handler = self.event_handlers.get(event)
        if handler:
            await handler(self, data, callback_id)

    async def emit(self, data):
        return await self.send(text_data=json.dumps(data, cls=DjangoJSONEncoder))

    async def send_log(self, entry, num=100):
        logs = await add_refresh_log(f"[CLIENT] {entry}", num)
        return await self.emit({"event": "bot_log_added", "data": {"log": logs}})

    async def shutdown_server(self, *args, **kwargs):
        await update_state({"discord_state": False, "current_activity": "Offline"})
        if sys.platform in ["linux", "linux2"]:
            os.system("pkill -f daphne")
        else:
            os.system("pkill -f django")
        sys.exit()

    async def update_client(self, callback_id=None):
        response_data = {
            "event": "client_info",
            "data": {
                "config": await get_verbose_dict("Configuration"),
                "state": await get_dict("State"),
                "log": await get_logs(100),
                "users": await get_users_dict(),
                "languages": await get_dicts("Language")
            },
        }

        if callback_id:
            response_data["callbackId"] = callback_id

        await self.emit(response_data)

    async def validate_startup(self):
        config = await get_config()

        await self.send_log("VALIDATING DISCORD TOKEN/CONNECTION...")
        if not validate_discord_token(config.discord_token):
            await self.send_log("DISCORD VALIDATION FAILED ✖ CANCELLING STARTUP.")
            return False
        await self.send_log("DISCORD VALIDATION PASSED ✔")
        return True

    # CLIENT COMMANDS/ROUTES

    @event_handler("client_connect")
    async def client_connect(self, _data=None, callback_id=None):
        await self.update_client(callback_id)

    @event_handler("get_config")
    async def get_config(self, _data=None, callback_id=None):
        response_data = await get_dict("Configuration")
        response_data["callbackId"] = callback_id
        await self.emit({"event": "config", "data": response_data})

    @event_handler("update_config")
    async def update_config(self, data=None, callback_id=None):
        response_data = await update_config(data)
        
        possible_err = response_data.get('error')
        if possible_err:
          await self.emit({"event": "config_updated", "data": {"error": str(possible_err)}})
          await self.update_client()
          return

        if len(response_data.get("changed", [])) > 0:
            verbose_config = await get_verbose_dict("Configuration")
            verbose_names = verbose_config.get("verbose")

            updated_configs = ", ".join(
                [
                    verbose_names[verb] if verbose_names.get(verb) else verb
                    for verb in response_data["changed"]
                ]
            )
            await self.send_log(f"Configuration updated: {updated_configs}")
        if callback_id:
            response_data["callbackId"] = callback_id
            await self.emit({"event": "config_updated", "data": response_data})

    @event_handler("update_langs")
    async def update_langs(self, data=None, callback_id=None):
        response_data = await update_langs(data.get('langConfig', []))
        possible_err = response_data.get('error')
        if callback_id:
          prepared_response = {'callbackId': callback_id}
          if possible_err:
            prepared_response['data'] = {"error": str(possible_err)}
            await self.emit(prepared_response)

        config = await get_config()
        if config.is_debug:
            changes = response_data.get('changes', [])
            for change in changes:
                await add_log(f"[CLIENT] {change}")

        await self.update_client()
        DiscordBot().registry.register(await generate_commands(), alt=True)

    @event_handler("add_user_from_client")
    async def add_user_from_client(self, data=None, callback_id=None):
        user_info = data.get("user_info")
        added = await add_user(user_info)
        if not added:
            await self.emit(
                {
                    "callbackId": callback_id,
                    "data": {
                        "username": user_info.get("username"),
                        "error": "User already exists!",
                    },
                }
            )
        else:
            await self.emit(
                {
                    "callbackId": callback_id,
                    "data": {
                        "username": user_info.get("username"),
                        "success": "User added.",
                    },
                }
            )
            response_data = {"users": await get_users_dict()}
            await self.send_log(f"User added: {user_info.get('username')}")
            await self.emit({"event": "users_updated", "data": response_data})

    @event_handler("edit_user_from_client")
    async def edit_user_from_client(self, data=None, callback_id=None):
        user_info = data.get("user_info")
        added = await edit_user(user_info)
        if not added:
            await self.emit(
                {
                    "callbackId": callback_id,
                    "data": {
                        "username": user_info.get("username"),
                        "error": "Nothing changed!",
                    },
                }
            )
        else:
            await self.emit(
                {
                    "callbackId": callback_id,
                    "data": {
                        "username": user_info.get("username"),
                        "success": "User edited.",
                    },
                }
            )
            response_data = {"users": await get_users_dict()}
            await self.send_log(f"User edited: {user_info.get('username')}")
            await self.emit({"event": "users_updated", "data": response_data})

    @event_handler("get_user_info_from_id")
    async def get_user_info_from_id(self, data=None, callback_id=None):
        response_data = await get_user_dict(id=data.get("id"))
        await self.emit({"callbackId": callback_id, "data": response_data})

    @event_handler("delete_user")
    async def delete_user_with_dict(self, data=None, callback_id=None):
        response_data = await delete_user(id=data.get("user_id"))

        if not response_data:
            response_data = {"error": "Unable to delete user"}

        await self.emit({"callbackId": callback_id, "data": response_data})

        updated_users = {"users": await get_users_dict()}
        await self.emit({"event": "users_updated", "data": updated_users})

    @event_handler("reset_db_from_client")
    async def reset_db(self, _data=None, callback_id=None):
        try:
            await reset_entire_db()
            response = {"success": True}
        except Exception as e:
            response = {"error": e}
        await self.emit({"callbackId": callback_id, "data": response})

    @event_handler("import_export_from_client")
    async def import_export_db(self, data=None, callback_id=None):
        action = data.get("action")
        if not action:
            return

        if action == "export":
            data_choices = data.get("choices", {})
            choices = [choice for choice in data_choices.keys() if data_choices[choice]]
            export_data_content = await export_data(choices)
            await self.emit(
                {
                    "callbackId": callback_id,
                    "data": {"content": export_data_content, "success": True},
                }
            )
        elif action == "import":
            content = data.get("data")
            import_data_result = await import_data(content)
            response = {"callbackId": callback_id, "data": {}}

            if import_data_result:
                response["data"]["success"] = True
            else:
                response["data"][
                    "error"
                ] = "Unable to import file. Potentially invalid operation."

            await self.emit(response)

    @event_handler("server_off")
    async def turn_server_off(self, _data=None, _callback_id=None):
        await self.shutdown_server()

    @event_handler("change_client_status")
    async def change_presence(self, data=None, callback_id=None):
        status = {"current_activity": data.get("status", "Waiting for input...")}
        await update_state(status)
        await self.emit({"callbackId": callback_id, "data": status})

    @event_handler("get_unadded_users")
    async def get_unadded_users_from_bot(self, _data=None, _callback_id=None):
        users = await get_users_list()
        unadded = await DiscordBot().get_unadded_users(users)
        await self.emit({"event": "unadded_users_sent", "data": {"unadded": unadded}})

    @event_handler("request_servers_from_client")
    async def request_servers_from_client(self, data=None, callback_id=None):
        await DiscordBot().get_servers()
        server_list = await get_all_user_servers(data.get("username"))
        await self.emit(
            {"callbackId": callback_id, "data": {"servers": server_list}}
        )

    # BOT COMMANDS/ROUTES

    @event_handler("bot_on")
    async def turn_bot_on(self, _data=None, _callback_id=None):
        is_valid = await self.validate_startup()
        if not is_valid:
            await self.send_log("STARTUP FAILED ✖")
            await self.emit(
                {
                    "event": "bot_on_finished",
                    "data": {
                        "success": False,
                        "bot_name": "discord",
                        "error": "Startup failure, see console.",
                    },
                }
            )
            return

        await self.send_log("STARTUP COMMENCING ✔")
        await change_bot_state(True)
        print(style.NOTICE("[Bot] - PROCESS IS BEING STARTED..."))
        await main()
        await self.update_client()
        print(style.SUCCESS("[Bot] - BOT IS FINSIHED STARTING!..."))

    @event_handler("bot_off")
    async def turn_bot_off(self, _data=None, _callback_id=None):
        await change_bot_state(False)
        print(style.NOTICE("[Bot] - PROCESS IS BEING KILLED..."))
        await kill()
        await self.send_log("USER: BOT SHUTTING DOWN")
        await self.update_client()

        print(style.SUCCESS("[Bot] - BOT IS KILLED!..."))

    # VALIDATORS
    @event_handler("test-connection")
    async def test_connection(self, data=None, callback_id=None):
        validation_type = data.get("connection")
        config = data.get("config")
        errors = []

        if validation_type == "discord":
            if not validate_discord_token(config.get("discord_token")):
                errors.append("Discord token is invalid")

        if errors:
            await self.emit(
                {"callbackId": callback_id, "data": {"error": ". ".join(errors)}}
            )
        else:
            try:
                await self.update_config(config)
                await self.emit({"callbackId": callback_id, "data": {"success": True}})
            except Exception as e:
                await self.emit(
                    {"callbackId": callback_id, "data": {"error": str(e)}}
                )
DiscoCodeClient/consumers/client.py
nickheyer/DiscoCode
from django.core.management import BaseCommand
from django.apps import apps
from django.core.serializers import base, python, deserialize
import sys



def load_fixture(json_data):
    old_get_model = python._get_model

    def _get_model(model_identifier):
        try:
            return apps.get_model(model_identifier)
        except (LookupError, TypeError):
            raise base.DeserializationError("Invalid model identifier: '%s'" % model_identifier)

    python._get_model = _get_model

    try:
        for obj in deserialize('json', json_data):
            obj.save()
    finally:
        # Restore old _get_model() function
        python._get_model = old_get_model

class Command(BaseCommand):
    help = 'Load data from a JSON string'

    def handle(self, *args, **options):
      data = sys.stdin.read()
      load_fixture(data)
DiscoCodeClient/management/commands/importdata.py
nickheyer/DiscoCode
from django.core.management.base import BaseCommand
from django.db.utils import OperationalError
from DiscoCodeClient.models import (
    Configuration,
    State
)

class Command(BaseCommand):
    help = 'Create a Default instances if they do not exist'

    def handle(self, *args, **kwargs):
        try:
            _config, _config_created = Configuration.objects.get_or_create()
            _state, _state_created = State.objects.get_or_create()
            self.stdout.write(self.style.SUCCESS('Successfully initialized DB'))
        except OperationalError as e:
            self.stderr.write(self.style.ERROR(f'Database Operational Error: {e}'))
        except Exception as e:
            self.stderr.write(self.style.ERROR(f'Unexpected Error: {e}'))
DiscoCodeClient/management/commands/initializedb.py
nickheyer/DiscoCode
from django.core.management.base import BaseCommand
from DiscoCodeClient.utils import (
    update_state_sync,
    add_log_sync,
    get_state_sync
    
)

class Command(BaseCommand):
    help = 'To run when application is sent a signal interrupt'

    def handle(self, *args, **kwargs):
        state = get_state_sync()
        if state.discord_state:
            add_log_sync('[BOT] SIGINT: BOT SHUTTING DOWN')
        add_log_sync('[CLIENT] SIGINT: SERVER SHUTTING DOWN')
        status = {"current_activity": 'Offline'}
        update_state_sync(status)
        
        self.stdout.write(self.style.SUCCESS('SUCCESFUL SIGINT SHUTDOWN'))

DiscoCodeClient/management/commands/sigint.py
nickheyer/DiscoCode
from django.core.management.base import BaseCommand
import asyncio
import websockets
import json
from multiprocessing import Process
from DiscoCodeClient.utils import get_state_sync, add_log_sync

# A BS HACK TO SPIN UP AN EVENT LOOP FOR PRE-REQUEST BOT STARTUP
class Command(BaseCommand):
    help = 'To run when application is launched'

    def handle(self, *args, **kwargs):
        state = get_state_sync()
        discord_state = state.discord_state
        add_log_sync('[CLIENT] STARTUP: SERVER STARTING UP')

        if discord_state: # IF WE LEFT THE BOT RUNNING AT SIGTERM, ETC.
          add_log_sync('[BOT] STARTUP: BOT STARTING UP')
          host = state.host_url
          uri = f"ws://{host}/ws/client/"
          self.stdout.write(f"Connecting to: {uri}")
          p = Process(target=self.ws_client, args=(uri,))
          p.start()

    @staticmethod
    def ws_client(uri):
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)

        async def bot_starter_ws_client():
            async with websockets.connect(uri) as websocket:
                bot_on_data = {
                    "event": "bot_on",
                    "data": {}
                }
                await websocket.send(json.dumps(bot_on_data))
                await websocket.recv()

        loop.run_until_complete(bot_starter_ws_client())

DiscoCodeClient/management/commands/startloop.py
nickheyer/DiscoCode
import requests
from django.core.management.base import BaseCommand
from DiscoCodeClient.models import Language, Configuration

class Command(BaseCommand):
    help = 'Update Language models with Piston API runtimes'

    def handle(self, *args, **kwargs):
        
        config = Configuration.objects.first()
        Language.objects.update(is_enabled=False)

        response = requests.get(config.runtime_endpoint)
        if response.status_code == 200:
            language_list = response.json()

            if not isinstance(language_list, list):
                self.stdout.write(self.style.ERROR('Unexpected API response format'))
                return

            running_aliases = set()
            for lang_data in language_list:
                if not isinstance(lang_data, dict) or 'language' not in lang_data:
                    self.stdout.write(self.style.ERROR('Unexpected language data format'))
                    continue

                language_name = lang_data.get('language')
                language, created = Language.objects.get_or_create(language=language_name)
                
                aliases = lang_data.get('aliases', [])
                aliases.append(language_name)
                for alias in aliases:
                    if alias not in language.aliases and alias not in running_aliases and ' ' not in alias:
                        language.aliases.append(alias)
                        running_aliases.add(alias)

                language.version = lang_data.get('version')
                language.is_enabled = True
                
                language.save()

                if created:
                    self.stdout.write(self.style.SUCCESS(f'Added new language: {language_name}'))
                else:
                    self.stdout.write(self.style.SUCCESS(f'Updated language: {language_name}'))
            
            self.stdout.write(self.style.SUCCESS('Successfully updated languages'))
DiscoCodeClient/management/commands/update_languages.py
nickheyer/DiscoCode
# Generated by Django 4.2.4 on 2023-09-28 04:21

import django.core.validators
from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('auth', '0012_alter_user_first_name_max_length'),
    ]

    operations = [
        migrations.CreateModel(
            name='Configuration',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('prefix_keyword', models.CharField(default='!dc', max_length=255, null=True, verbose_name='Chat-Prefix For Commands')),
                ('alt_prefix', models.CharField(default='$', max_length=255, null=True, verbose_name='Prefix For Code Execution Commands')),
                ('discord_token', models.CharField(default='', max_length=255, null=True, verbose_name='Discord Token')),
                ('session_timeout', models.IntegerField(default=60, null=True, verbose_name='Session Timeout')),
                ('is_verbose_logging', models.BooleanField(default=False, verbose_name='Verbose logging in console')),
                ('is_debug', models.BooleanField(default=False, verbose_name='Send Debug Message On Error')),
                ('runtime_endpoint', models.CharField(default='https://emkc.org/api/v2/piston/runtimes', max_length=255, null=True, verbose_name='Query Runtime Endpoint')),
                ('exec_endpoint', models.CharField(default='https://emkc.org/api/v2/piston/execute', max_length=255, null=True, verbose_name='Code Execution Endpoint')),
                ('max_stdout_len', models.IntegerField(default=512, validators=[django.core.validators.MaxValueValidator(900, 'Discord message character limit exceeded - (Max set to 900).')], verbose_name='Maximum Length of Execution Response')),
            ],
        ),
        migrations.CreateModel(
            name='DiscordServer',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('server_name', models.CharField(max_length=255, null=True)),
                ('server_id', models.CharField(max_length=255, null=True)),
            ],
        ),
        migrations.CreateModel(
            name='ErrLog',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('created', models.DateTimeField(auto_now_add=True)),
                ('entry', models.TextField(default='Error Occured')),
            ],
        ),
        migrations.CreateModel(
            name='EventLog',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('created', models.DateTimeField(auto_now_add=True)),
                ('entry', models.CharField(default='Event Occured', max_length=2048)),
            ],
        ),
        migrations.CreateModel(
            name='State',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('discord_state', models.BooleanField(default=False)),
                ('app_state', models.BooleanField(default=True)),
                ('current_activity', models.CharField(default='Offline', max_length=255)),
                ('host_url', models.CharField(default='0.0.0.0:6565', max_length=128)),
            ],
        ),
        migrations.CreateModel(
            name='User',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('password', models.CharField(max_length=128, verbose_name='password')),
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
                ('added', models.DateTimeField(default=django.utils.timezone.now)),
                ('is_admin', models.BooleanField(default=False)),
                ('is_server_restricted', models.BooleanField(default=False)),
                ('username', models.CharField(max_length=255, unique=True)),
                ('is_additional_settings', models.BooleanField(default=False)),
                ('is_superuser', models.BooleanField(default=False)),
                ('is_staff', models.BooleanField(default=False)),
                ('is_active', models.BooleanField(default=True)),
                ('session_timeout', models.IntegerField(default=60)),
                ('discord_servers', models.ManyToManyField(blank=True, related_name='users', to='DiscoCodeClient.discordserver')),
                ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
                ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
            ],
            options={
                'abstract': False,
            },
        ),
        migrations.CreateModel(
            name='Language',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('language', models.CharField(max_length=100, unique=True)),
                ('version', models.CharField(blank=True, max_length=32)),
                ('is_enabled', models.BooleanField(default=True)),
                ('aliases', models.JSONField(default=list)),
                ('last_updated', models.DateTimeField(auto_now=True)),
            ],
            options={
                'indexes': [models.Index(fields=['language', 'version'], name='DiscoCodeCl_languag_83f49e_idx')],
            },
        ),
    ]

DiscoCodeClient/migrations/0001_initial.py
nickheyer/DiscoCode
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.core.validators import MaxValueValidator
from django.utils.timezone import now

import os

class Configuration(models.Model):
    prefix_keyword = models.CharField("Chat-Prefix For Commands", max_length=255, null=True, default="!dc")
    alt_prefix = models.CharField("Prefix For Code Execution Commands", max_length=255, null=True, default="$")
    discord_token = models.CharField("Discord Token", max_length=255, blank=True, default="")

    session_timeout = models.IntegerField("Session Timeout", null=True, default=60)

    is_verbose_logging = models.BooleanField("Verbose logging in console", default=False)
    is_debug = models.BooleanField("Send Debug Message On Error", default=False)
    runtime_endpoint = models.CharField("Query Runtime Endpoint", max_length=255, null=True, default="https://emkc.org/api/v2/piston/runtimes")
    exec_endpoint = models.CharField("Code Execution Endpoint", max_length=255, null=True, default="https://emkc.org/api/v2/piston/execute")
    max_stdout_len = models.IntegerField("Maximum Length of Execution Response", default=512, validators=[MaxValueValidator(900, 'Discord message character limit exceeded - (Max set to 900).')])

    def save(self, *args, **kwargs):
        self.full_clean()
        super().save(*args, **kwargs)

class State(models.Model):
    discord_state = models.BooleanField(default=False)
    app_state = models.BooleanField(default=True)
    current_activity = models.CharField(max_length=255, default="Offline")
    host_url = models.CharField(max_length=128, default='0.0.0.0:6565')


class ErrLog(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    entry = models.TextField(default="Error Occured")


class EventLog(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    entry = models.CharField(max_length=2048, default="Event Occured")


class DiscordServer(models.Model):
    server_name = models.CharField(max_length=255, null=True)
    server_id = models.CharField(max_length=255, null=True)

class UserManager(BaseUserManager):
    def create_user(self, username, password=None, **extra_fields):
        if not username:
            raise ValueError('The Username field is required')
        user = self.model(username=username, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

class User(AbstractBaseUser, PermissionsMixin):
    added = models.DateTimeField(default=now, editable=True, )
    is_admin = models.BooleanField(default=False)
    is_server_restricted = models.BooleanField(default=False)
    username = models.CharField(max_length=255, unique=True, null=False)
    discord_servers = models.ManyToManyField(DiscordServer, related_name="users", blank=True)
    is_additional_settings = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    session_timeout = models.IntegerField(default=60)

    objects = UserManager()

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = []

    def has_perm(self, perm, obj=None):
        """Does the user have a specific permission?"""
        return True

    def has_module_perms(self, app_label):
        """Does the user have permissions to view the app"""
        return True

    def __str__(self):
        return self.username


class Language(models.Model):
    language = models.CharField(max_length=100, unique=True)
    version = models.CharField(max_length=32, blank=True)
    is_enabled = models.BooleanField(default=True)
    aliases = models.JSONField(default=list)
    last_updated = models.DateTimeField(auto_now=True)

    class Meta:
        indexes = [models.Index(fields=['language', 'version'])]
DiscoCodeClient/models.py
nickheyer/DiscoCode
from rest_framework import permissions

class AllowGETUnauthenticated(permissions.BasePermission):

    def has_permission(self, request, view):
        if request.method == 'GET':
            return True
        return request.user and request.user.is_authenticated

DiscoCodeClient/permissions.py
nickheyer/DiscoCode
from django.urls import re_path

from DiscoCodeClient.consumers import client

websocket_urlpatterns = [
    re_path(r'ws/client/$', client.ClientConsumer.as_asgi())
]
DiscoCodeClient/routing.py
nickheyer/DiscoCode
from rest_framework import serializers
from .models import Configuration, State, ErrLog, EventLog, DiscordServer, User, Language

class ConfigurationSerializer(serializers.ModelSerializer):
    class Meta:
        model = Configuration
        fields = '__all__'

class StateSerializer(serializers.ModelSerializer):
    class Meta:
        model = State
        fields = '__all__'

class ErrLogSerializer(serializers.ModelSerializer):
    class Meta:
        model = ErrLog
        fields = '__all__'

class EventLogSerializer(serializers.ModelSerializer):
    class Meta:
        model = EventLog
        fields = '__all__'

class DiscordServerSerializer(serializers.ModelSerializer):
    class Meta:
        model = DiscordServer
        fields = '__all__'

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = '__all__'

class LangSerializer(serializers.ModelSerializer):
    class Meta:
        model = Language
        fields = '__all__'

DiscoCodeClient/serializers.py
nickheyer/DiscoCode
from django.urls import path, include
from rest_framework.routers import DefaultRouter

from DiscoCodeClient import views

router = DefaultRouter()
router.register(r'configuration', views.ConfigurationViewSet, basename='configuration')
router.register(r'state', views.StateViewSet, basename='state')
router.register(r'errlogs', views.ErrLogViewSet)
router.register(r'eventlogs', views.EventLogViewSet)
router.register(r'discord-servers', views.DiscordServerViewSet)
router.register(r'users', views.UserViewSet)
router.register(r'languages', views.LangViewSet)

urlpatterns = [
    path("", views.index, name="index"),
    path("api/", include(router.urls)),
    path('runtimes', views.forward_runtime, name='forward_runtimes'),
    path('execute', views.forward_execute, name='forward_execute'),
]

DiscoCodeClient/urls.py
nickheyer/DiscoCode
from datetime import datetime, timedelta

from django.db import models
from django.core.management import call_command
from django.forms.models import model_to_dict
from django.apps import apps
from channels.db import database_sync_to_async
from django.core.exceptions import ObjectDoesNotExist
from asgiref.sync import async_to_sync
from django.core.management.color import color_style
from rest_framework.authtoken.models import Token

import requests
import io
import subprocess

from DiscoCodeClient.models import (
    Configuration,
    State,
    ErrLog,
    EventLog,
    User,
    DiscordServer,
    Language
)

style = color_style()

@database_sync_to_async
def save_instance(instance):
    instance.save()
    

def get_config_sync():
    try:
        return Configuration.objects.first()
    except ObjectDoesNotExist:
        return None


@database_sync_to_async
def get_config():
    return get_config_sync()


@database_sync_to_async
def get_config_dict():
    config = get_config_sync()
    return model_to_dict(config)


@database_sync_to_async
def update_config(data):
    config = get_config_sync()
    changed = []
    for k, v in data.items():
        if hasattr(config, k):
            attribute = getattr(config, k)
            if str(attribute) != str(v) and (attribute or v):
                changed.append(k)
            setattr(config, k, v)
    try:
        config.save()
    except Exception as e:
        return {'error': e}
    config_dict = model_to_dict(config)
    config_dict["changed"] = changed
    return config_dict

def get_langs_sync():
    try:
        return Language.objects.all()
    except ObjectDoesNotExist:
        return []

@database_sync_to_async
def get_langs():
  return get_langs_sync()

@database_sync_to_async
def get_lang(name):
    try:
        return Language.objects.filter(language=name).first()
    except ObjectDoesNotExist:
        return None

@database_sync_to_async
def update_langs(data):
    changes = []
    for lang in data:

        lang_id = lang.get('id')
        if not lang_id:
            continue
        
        lang_inst = Language.objects.filter(id=lang_id).first()
        if not lang_inst:
            continue

        lang_aliases = lang.get('aliases')
        if lang_aliases and isinstance(lang_aliases, list):
          inst_aliases_set = set(lang_inst.aliases)
          inc_aliases_set = set(lang_aliases)

          for inc in inc_aliases_set:
              if ' ' in inc:
                  inc_aliases_set.remove(inc)
          if inst_aliases_set.symmetric_difference(inc_aliases_set):
            changes.append(f'{lang_inst.language}: {lang_inst.aliases} -> {lang_aliases}')
            lang_inst.aliases = list(inc_aliases_set)
            

        
        lang_enabled = lang.get('is_enabled')
        if lang_enabled is not None and isinstance(lang_enabled, bool):
            if lang_inst.is_enabled != lang_enabled:
                changes.append(f'{lang_inst.language}: {lang_inst.is_enabled} -> {lang_enabled}')
                lang_inst.is_enabled = lang_enabled
                
        try:
            lang_inst.save()
        except Exception as e:
            print(e)
            return {'error': e}
    return { 'success': True, 'changes': changes }

def get_state_sync():
    try:
        return State.objects.first()
    except ObjectDoesNotExist:
        return None


@database_sync_to_async
def get_state():
    return get_state_sync()


@database_sync_to_async
def get_state_dict():
    state = get_state_sync()
    return model_to_dict(state)


def update_state_sync(data):
    state = get_state_sync()
    changed = []
    for k, v in data.items():
        if hasattr(state, k):
            attribute = getattr(state, k)
            if str(attribute) != str(v):
                changed.append(k)
            setattr(state, k, v)
    state.save()
    state_dict = model_to_dict(state)
    state_dict["changed"] = changed
    return state_dict


@database_sync_to_async
def update_state(data):
    update_state_sync(data)


@database_sync_to_async
def get_logs(num):
    try:
        logs = EventLog.objects.all().order_by("-created")[:num]
        if logs.exists():
            return [
                f"{log.created.strftime('%Y-%m-%d %H:%M:%S')}: {log.entry}"
                for log in logs
            ][::-1]
        else:
            print("No logs found")
            return []
    except Exception as e:
        print(f"Error fetching logs: {e}")
        return []


def add_log_sync(entry):
    try:
        EventLog.objects.create(entry=entry)
    except Exception as e:
        print(f"Error adding log: {e}")


@database_sync_to_async
def add_log(entry):
    add_log_sync(entry)


@database_sync_to_async
def add_refresh_log(entry, num=100):
    try:
        EventLog.objects.create(entry=entry)
    except Exception as e:
        print(f"Error adding log: {e}")
    try:
        logs = EventLog.objects.all().order_by("-created")[:num]
        if logs.exists():
            return [
                f"{log.created.strftime('%Y-%m-%d %H:%M:%S')}: {log.entry}"
                for log in logs
            ][::-1]
        else:
            print("No logs found")
            return []
    except Exception as e:
        print(f"Error fetching logs: {e}")
        return []


@database_sync_to_async
def get_dict(model_str):
    try:
        found_model = apps.get_model("DiscoCodeClient", model_str)
    except LookupError:
        return {}
    try:
        instance = found_model.objects.first()
        if instance is None:
            return {}
        return model_to_dict(instance)
    except Exception as e:
        print(f"An error occurred: {e}")
        return {}

@database_sync_to_async
def get_dicts(model_str):
    try:
        found_model = apps.get_model("DiscoCodeClient", model_str)
    except LookupError:
        return {}
    try:
        serialized = []
        instances = found_model.objects.all()
        for inst in instances:
            serialized.append(model_to_dict(inst))
        
        return serialized
  
    except Exception as e:
        print(f"An error occurred: {e}")
        return []

def get_verbose_dict_sync(model_str):
    try:
        found_model = apps.get_model("DiscoCodeClient", model_str)
    except LookupError:
        print(f"Model {model_str} not found.")
        return {}

    print(f"Checking model: {found_model}")
    try:
        instance = found_model.objects.first()
        if instance is None:
            return {}

        fields = model_to_dict(instance)
        meta_fields = found_model._meta.get_fields()
        verbose_names = {}
        field_types = {}

        for field in meta_fields:
            if hasattr(field, "name"):
                verbose_names[field.name] = field.verbose_name
                field_types[field.name] = type(field).__name__

        return {"fields": fields, "verbose": verbose_names, "types": field_types}
    except Exception as e:
        print(f"An error occurred: {e}")
        return {}


@database_sync_to_async
def get_verbose_dict(model_str):
    return get_verbose_dict_sync(model_str)

def get_user_token_sync(user_cls = None, username = None, id = None):
    if not user_cls:
       user_cls = get_user_sync(username, id)
       if not user_cls:
           return None 
    token, _created = Token.objects.get_or_create(user=user_cls)
    return token.key

@database_sync_to_async
def get_user_token(user_cls = None, username = None, id = None):
    return get_user_token_sync(user_cls, username, id)

@database_sync_to_async
def get_users_dict():
    users = User.objects.all()
    users_dict = []
    for user in users:
        user_dict = model_to_dict(user, exclude=["discord_servers"])
        user_dict['token'] = get_user_token_sync(user)
        users_dict.append(user_dict)
        
    return users_dict


@database_sync_to_async
def get_users_list():
    users = User.objects.values_list("username", flat=True)
    return list(users)


@database_sync_to_async
def add_user(info):
    username = info.get("username", None)
    if not username:
        return False

    existing = User.objects.filter(username=username).first()
    if existing:
      return False
    
    new_user = User.objects.create_user(username=username)

    for k, v in info.items():
        if k in ["id", "discord_servers", "added", "username", 'password']:
            continue
        if hasattr(new_user, k):
            field = User._meta.get_field(k)
            if isinstance(field, models.BooleanField):
                v = bool(v)
            elif isinstance(field, models.IntegerField):
                v = int(v)
            setattr(new_user, k, v)

    password = info.get('password')
    new_user.set_password(password)
    new_user.save()

    for server in info.get("servers", []):
        new_server, _ = DiscordServer.objects.get_or_create(
            server_name=server["server_name"], server_id=server["server_id"]
        )
        new_server.users.add(new_user)

    return True


@database_sync_to_async
def edit_user(info):
    user = User.objects.filter(id=info.get("id", "-1")).first()
    if not user:
        return False

    for k, v in info.items():
        if k in ["id", "servers", "added"]:
            continue
        if hasattr(user, k):
            field = User._meta.get_field(k)
            if str(getattr(user, k)) != str(v):
                if k == 'password':
                    user.set_password(v)
                else:
                    if isinstance(field, models.BooleanField):
                        v = bool(v) if v else False
                    elif isinstance(field, models.IntegerField):
                        v = int(v) if v else 0
                    setattr(user, k, v)
                    

    user.save()

    for server in info.get("servers", []):
        new_server, _ = DiscordServer.objects.get_or_create(
            server_name=server["server_name"], server_id=server["server_id"]
        )

        if not new_server.users.filter(id=user.id).exists():
            new_server.users.add(user)

    return True


@database_sync_to_async
def delete_user(**kwargs):
    if not kwargs.get("username") and not kwargs.get("id"):
        return None
    user = User.objects.filter(**kwargs).first()
    if not user:
        return None
    username = user.username
    user.delete()
    return username


def get_user_sync(**kwargs):
    if not kwargs.get("username") and not kwargs.get("id"):
      return None
    user = User.objects.filter(**kwargs).first()
    if user:
        return user
    return None


@database_sync_to_async
def get_user_dict(**kwargs):
    user = get_user_sync(**kwargs)
    if user:
        user_dict = model_to_dict(user, exclude=["discord_servers"])
        user_dict['token'] = get_user_token_sync(user)
        return user_dict
    return None


@database_sync_to_async
def get_user(**kwargs):
    return get_user_sync(**kwargs)

@database_sync_to_async
def get_user_settings(user):
    if not user:
        return {}
    user_settings = model_to_dict(
        user,
        exclude=[
            "id",
            "added",
            "is_admin",
            "is_server_restricted",
            "username",
            "discord_servers",
            "is_additional_settings",
        ],
    )
    return user_settings


@database_sync_to_async
def edit_user_from_dict(data):
    user_info = data.get("user_info")
    username = user_info.get("username", None)
    user_id = user_info.get("id", None)
    user = None

    if user_id:
        user = User.objects.filter(id=user_id).first()
    elif username:
        user = User.objects.filter(username=username).first()

    if not user:
        return None

    user.username = username
    changes_made = []

    for k, v in data.items():
        if hasattr(user, k):
            data_in_model = getattr(user, k)
            if data_in_model != type(data_in_model)(v):
                changes_made.append(k)
            if k not in ["id", "discord_servers", "added", "username"]:
                setattr(user, k, v)

    user.save()

    server_list = data.get("servers", [])
    existing_server_ids = [server.server_id for server in user.discord_servers.all()]
    new_server_ids = [server["server_id"] for server in server_list]

    # Remove user from servers not in the new list
    for server_id in existing_server_ids:
        if server_id not in new_server_ids:
            server = DiscordServer.objects.get(server_id=server_id)
            server.users.remove(user)

    # Add or update user to servers in the new list
    for server_data in server_list:
        server, _created = DiscordServer.objects.get_or_create(
            server_id=server_data["server_id"]
        )
        server.server_name = server_data["server_name"]
        server.save()
        server.users.add(user)

    return changes_made


@database_sync_to_async
def get_all_user_servers(username):
    servers = DiscordServer.objects.all()
    user = User.objects.filter(username=username).first()

    user_servers = []

    for server in servers:
        server_dict = model_to_dict(server)
        server_dict["active"] = user and user in server.users.all()
        user_servers.append(server_dict)

    return user_servers


def eval_user_roles_sync(user):
    roles = []
    if isinstance(user, str):
        user, _username = User.objects.filter(username=user).first(), user
        if not user:
            return ["unregistered"]
    if user.is_admin:
        roles.extend(["user", "admin"])
    else:
        roles.append("user")
    if user.is_server_restricted:
        roles.append("restricted")
    else:
        roles.append("unrestricted")
    if user.is_superuser:
        roles.append("owner")
    return roles


@database_sync_to_async
def eval_user_roles(user):
    return eval_user_roles_sync(user)


@database_sync_to_async
def get_users_in_server(server_id, permissions=[]):
    server = DiscordServer.objects.filter(server_id=server_id).first()

    if not server:
        return []

    # Fetch users associated with the server
    users_in_server = server.users.all()

    # Fetch users who aren't server restricted
    unrestricted_users = User.objects.filter(is_server_restricted=False)

    # Combine both lists of users (deduplicate with distinct)
    relev_users = (users_in_server | unrestricted_users).distinct()

    # If no permissions were provided, just return all relevant users
    if not permissions:
        return list(relev_users.values_list("username", flat=True))

    approved_users = []
    for user in relev_users:
        user_roles = eval_user_roles_sync(user)
        is_approved = all(perm in user_roles for perm in permissions)
        if is_approved:
            approved_users.append(user.username)

    return approved_users


@database_sync_to_async
def get_discord_server_from_id(server_id):
    return DiscordServer.objects.filter(server_id=server_id).first()


def get_users_for_auth_sync(server_id, permissions, username):
    if (
        "unregistered" in permissions
        and not User.objects.filter(username=username).exists()
    ):
        return True
    server = DiscordServer.objects.filter(server_id=server_id).first()
    users_in_server = User.objects.filter(discord_servers=server)
    unrestricted_users = User.objects.filter(is_server_restricted=False)
    result = users_in_server | unrestricted_users

    approved_users = []
    for user in result:
        user_roles = eval_user_roles_sync(user)
        is_approved = any(perm in user_roles for perm in permissions)

        if is_approved or not permissions:
            approved_users.append(user.username)

    return username in approved_users


@database_sync_to_async
def get_users_for_auth(server_id, permissions, username):
    return get_users_for_auth_sync(server_id, permissions, username)


@database_sync_to_async
def update_servers(servers, delete = False):
    
    if delete:
        server_ids = [s["server_id"] for s in servers]
        DiscordServer.objects.exclude(server_id__in=server_ids).delete()

    for server in servers:
        _get, _created = DiscordServer.objects.get_or_create(
            server_id=server["server_id"],
            defaults={"server_name": server["server_name"]},
        )


@database_sync_to_async
def get_required_fields():
    fields = [
        "prefix_keyword",
        "discord_token",
    ]

    return fields


@database_sync_to_async
def reset_database(choices):
    if choices.get("State", False):
        State.objects.all().delete()
    if choices.get("Configuration", False):
        Configuration.objects.all().delete()
    if choices.get("EventLog", False):
        EventLog.objects.all().delete()
        ErrLog.objects.all().delete()
    if choices.get("User", False):
        # Deleting users will likely also delete related models due to cascading
        User.objects.all().delete()
        DiscordServer.objects.all().delete()
    if choices.get("Language", False):
        # Deleting users will likely also delete related models due to cascading
        Language.objects.all().delete()


@database_sync_to_async
def reset_entire_db():
    call_command("flush", "--noinput")

    # Create default State and Configuration if needed
    if not State.objects.exists():
        State.objects.create()
    if not Configuration.objects.exists():
        Configuration.objects.create()

@database_sync_to_async
def export_data(choices):
    """
    Exports data from specified models to a JSON file (buffer till it reaches client).
    """
    models_to_dump = []
    if "Configuration" in choices:
        models_to_dump.append("DiscoCodeClient.Configuration")
        models_to_dump.append("DiscoCodeClient.Language")
    if "EventLog" in choices:
        models_to_dump.append("DiscoCodeClient.EventLog")
        models_to_dump.append("DiscoCodeClient.ErrLog")
    if "User" in choices:
        models_to_dump.append("DiscoCodeClient.DiscordServer")
        models_to_dump.append("DiscoCodeClient.User")

    buffer = io.StringIO()
    call_command("dumpdata", *models_to_dump, stdout=buffer, format="json")

    dumped_data = buffer.getvalue()
    buffer.close()
    
    return dumped_data


@database_sync_to_async
def import_data(data_obj):
    """
    Imports data from a JSON string into specified models.
    """

    if not data_obj:
        return False
    
    data_str = data_obj

    process = subprocess.Popen(
        ['python', 'manage.py', 'importdata'], 
        stdin=subprocess.PIPE, 
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )
    stdout, stderr = process.communicate(input=data_str)
    
    if process.returncode != 0:
        print("STDOUT:", stdout)
        print("STDERR:", stderr)
        return False

    if not State.objects.exists():
        State.objects.create()
    if not Configuration.objects.exists():
        Configuration.objects.create()
    
    return True


def handle_stdout(pipe, logger=None):
    verbose_logging = get_config_sync().is_verbose_logging
    for line in pipe:
        raw = line.strip()
        log = f"[BOT OUT]: {raw}"
        if logger and verbose_logging:
            _logs = async_to_sync(logger)(log, 100)
        else:
            event = EventLog(entry=log)
            event.save()


def handle_stderr(pipe, logger=None):
    verbose_logging = get_config_sync().is_verbose_logging
    for line in pipe:
        raw = line.strip()
        log = f"[BOT ERR]: {raw}"
        if logger and verbose_logging:
            _logs = async_to_sync(logger)(log, 100)
        else:
            error = ErrLog(entry=log)
            error.save()

@database_sync_to_async
def add_err_log(entry):
    error = ErrLog(entry=entry)
    error.save()

@database_sync_to_async
def change_bot_state(current_state):
    state = get_state_sync()
    state.discord_state = current_state
    state.save()

def validate_discord_token(token):
    print(style.SUCCESS(f"VALIDATING DISCORD TOKEN: {token}"))
    if not token:
        return False
    response = requests.get(
        "https://discord.com/api/v9/users/@me",
        headers = {"Authorization": f"Bot {token}"}
    )
    print(style.NOTICE(f"DISCORD VALIDATION RESPONSE:\n{response}"))
    return response.status_code == 200

DiscoCodeClient/utils.py
nickheyer/DiscoCode
from rest_framework import viewsets, permissions, mixins
from django.shortcuts import render
from django.http import JsonResponse, HttpRequest, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt

import requests
import json

from DiscoCodeClient.utils import (
    update_state_sync
)


# ---------------- INDEX, ETC. ----------------

def index(request):
    update_state_sync({ 'host_url': request.get_host() })
    return render(request, "DiscoCodeClient/index.html")

def forward_runtime(request: HttpRequest):
    if request.method == 'GET':
        response = requests.get('https://emkc.org/api/v2/piston/runtimes')
        return JsonResponse(response.json(), safe=False)

    return JsonResponse({'error': 'Method not allowed'}, status=405)


@csrf_exempt
def forward_execute(request):
    if request.method == 'POST':
        try:
            request_data = json.loads(request.body)
        except json.JSONDecodeError:
            return HttpResponseBadRequest("Invalid JSON format")
        
        response = requests.post('https://emkc.org/api/v2/piston/execute', json=request_data)
        return JsonResponse(response.json(), safe=False)

    return JsonResponse({'error': 'Method not allowed'}, status=405)

# ---------------- REST API --------------------

from DiscoCodeClient import models, serializers


# Configuration / State limited to update (PUT)
class ConfigurationViewSet(mixins.RetrieveModelMixin,
                           mixins.UpdateModelMixin,
                           mixins.ListModelMixin,
                           viewsets.GenericViewSet):
    permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser]
    serializer_class = serializers.ConfigurationSerializer
     
    def get_queryset(self):
        return models.Configuration.objects.filter(id=models.Configuration.objects.first().id)


class StateViewSet(mixins.RetrieveModelMixin,
                  mixins.UpdateModelMixin,
                  mixins.ListModelMixin,
                  viewsets.GenericViewSet):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, ]
    serializer_class = serializers.StateSerializer

    def get_queryset(self):
        return models.State.objects.filter(id=models.State.objects.first().id)


class ErrLogViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, ]
    queryset = models.ErrLog.objects.all()
    serializer_class = serializers.ErrLogSerializer

class EventLogViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, ]
    queryset = models.EventLog.objects.all()
    serializer_class = serializers.EventLogSerializer

class DiscordServerViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, ]
    queryset = models.DiscordServer.objects.all()
    serializer_class = serializers.DiscordServerSerializer

class UserViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAuthenticated, permissions.IsAdminUser]
    queryset = models.User.objects.all()
    serializer_class = serializers.UserSerializer

class LangViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, ]
    queryset = models.Language.objects.all()
    serializer_class = serializers.LangSerializer
DiscoCodeClient/views.py
nickheyer/DiscoCode
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DiscoCode.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()

manage.py
nickheyer/DiscoCode