Initial Commit importing code from old repo and making changes so it is standalone

This commit is contained in:
Dustin Pianalto 2019-12-11 10:34:25 -09:00
parent 953e048d44
commit 0caeba5cbd
152 changed files with 6340 additions and 0 deletions

336
.gitignore vendored Normal file
View File

@ -0,0 +1,336 @@
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
ssl_certs
.idea
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
staticfiles/
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# pyenv
.python-version
# Environments
.venv
venv/
ENV/
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
### Linux template
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Provided default Pycharm Run/Debug Configurations should be tracked by git
# In case of local modifications made by Pycharm, use update-index command
# for each changed file, like this:
# git update-index --assume-unchanged .idea/geeksbot_web.iml
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Windows template
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
### macOS template
# General
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### SublimeText template
# Cache files for Sublime Text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# Workspace files are user-specific
*.sublime-workspace
# Project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using Sublime Text
# *.sublime-project
# SFTP configuration file
sftp-config.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
Package Control.merged-ca-bundle
Package Control.user-ca-bundle
oscrypto-ca-bundle.crt
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
### Vim template
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
# Session
Session.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
### Project template
geeksbot_web/media/
.pytest_cache/
.ipython/
.env
.envs/*
!.envs/.local/

65
Dockerfile Normal file
View File

@ -0,0 +1,65 @@
FROM python:3.7-alpine AS geeksbot-web
ENV DEBIAN_FRONTEND noninteractive
ENV PYTHONUNBUFFERED 1
RUN adduser --disabled-password --home=/home/geeksbot --gecos "" geeksbot
RUN echo "geeksbot ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
RUN echo "geeksbot:docker" | chpasswd
RUN apk update && \
apk add --virtual build-deps gcc python3-dev musl-dev postgresql-dev \
# Pillow dependencies
&& apk add jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev tiff-dev tk-dev tcl-dev \
# CFFI dependencies
&& apk add libffi-dev py-cffi \
# Translations dependencies
&& apk add gettext \
# https://docs.djangoproject.com/en/dev/ref/django-admin/#dbshell
&& apk add postgresql-client
RUN mkdir /code
ENV LC_ALL C.UTF-8
ENV LANG C.UTF-8
RUN pip install --upgrade pip
RUN pip install virtualenv
WORKDIR /code
RUN apk update && apk add nginx && apk add supervisor
COPY requirements/base.txt .
COPY requirements/production.txt .
COPY requirements/web.txt .
RUN pip install -r production.txt
RUN pip install -r web.txt
RUN rm -f /etc/nginx/sites-enabled/default
RUN rm -f /etc/nginx/conf.d/default.conf
COPY ./services/nginx.conf /etc/nginx/nginx.conf
COPY ./services/geeksbot.conf /etc/nginx/sites-enabled/geeksbot
COPY ./services/gunicorn.conf /etc/gunicorn.conf
COPY ./services/supervisord.conf /etc/supervisor/supervisord.conf
COPY ./services/supervisor_geeksbot.conf /etc/supervisor/conf.d/geeksbot.conf
COPY ./ssl_certs/geeksbot_app/geeksbot_app_cert_chain.crt /etc/ssl/geeksbot_app_cert_chain.crt
COPY ./ssl_certs/geeksbot_app/geeksbot.app.key /etc/ssl/geeksbot.app.key
COPY ./.env /code/
RUN rm -rf /tmp/*
RUN mkdir -p /tmp/logs/nginx
RUN mkdir -p /tmp/logs/geeksbot
RUN mkdir -p /code/geeksbot_web
COPY geeksbot_web/* /code/geeksbot_web/
WORKDIR /code/geeksbot_web
# RUN sed -i 's/\r$//g' ./entrypoint
# RUN chmod +x ./entrypoint
EXPOSE 80 8000 443
ENTRYPOINT [ "./entrypoint" ]

7
geeksbot_web/__init__.py Normal file
View File

@ -0,0 +1,7 @@
__version__ = "2.0.0"
__version_info__ = tuple(
[
int(num) if num.isdigit() else num
for num in __version__.replace("-", ".", 1).split(".")
]
)

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,10 @@
from django.urls import path
from .views import ChannelsAPI, ChannelDetail, AdminChannelAPI
app_name = "channels_api"
urlpatterns = [
path("", view=ChannelsAPI.as_view(), name="list"),
path("<str:id>/", view=ChannelDetail.as_view(), name='detail'),
path("<str:guild_id>/admin/", view=AdminChannelAPI.as_view(), name='admin')
]

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class ChannelsConfig(AppConfig):
name = 'geeksbot_web.channels'
verbose_name = _("Channels")

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.4 on 2019-09-20 21:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('guilds', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Channel',
fields=[
('id', models.CharField(max_length=30, primary_key=True, serialize=False)),
('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')),
],
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.4 on 2019-09-21 02:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('channels', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='channel',
name='admin',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='channel',
name='default',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='channel',
name='new_patron',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,85 @@
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import status
from geeksbot_web.guilds.models import Guild
from .utils import create_error_response
from .utils import create_success_response
# Create your models here.
class Channel(models.Model):
id = models.CharField(max_length=30, primary_key=True)
guild = models.ForeignKey(Guild, on_delete=models.CASCADE)
default = models.BooleanField(default=False)
new_patron = models.BooleanField(default=False)
admin = models.BooleanField(default=False)
def update_channel(self, data):
if data.get('default'):
try:
existing_default = self.get_guild_channels(self.guild).get(default=True)
except ObjectDoesNotExist:
pass
else:
existing_default.default = False
existing_default.save()
finally:
self.default = data.get('default')
if data.get('new_patron'):
self.new_patron = data.get('new_patron')
if data.get('admin'):
self.admin = data.get('admin')
self.save()
return self
@classmethod
def add_new_channel(cls, data):
id = data.get('id')
if id and cls.get_channel_by_id(id):
return create_error_response('Channel Already Exists',
status=status.HTTP_409_CONFLICT)
guild_id = data.get('guild')
if not (id and guild_id):
return create_error_response('ID and Guild are required',
status=status.HTTP_400_BAD_REQUEST)
guild = Guild.get_guild_by_id(guild_id)
if not isinstance(guild, Guild):
return create_error_response('Guild Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
channel = cls(
id=id,
guild=guild,
default=data.get('default', False),
new_patron=data.get('new_patron', False),
admin=data.get('admin', False)
)
channel.save()
return create_success_response(channel, status.HTTP_201_CREATED, many=False)
@classmethod
def get_channel_by_id(cls, guild_id, channel_id):
try:
return cls.get_guild_channels(guild_id).get(id=channel_id)
except ObjectDoesNotExist:
return None
@classmethod
def get_guild_channels(cls, guild):
if isinstance(guild, Guild):
return cls.objects.filter(guild=guild)
elif isinstance(guild, (str, int)):
return cls.objects.filter(guild__id=guild)
@classmethod
def get_admin_channel(cls, guild_id):
try:
return cls.get_guild_channels(guild_id).get(admin=True)
except ObjectDoesNotExist:
return None
def __str__(self):
return str(id)

View File

@ -0,0 +1,9 @@
from rest_framework import serializers
from .models import Channel
class ChannelSerializer(serializers.ModelSerializer):
class Meta:
model = Channel
fields = "__all__"

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,14 @@
from rest_framework.response import Response
from rest_framework import status
def create_error_response(msg, status=status.HTTP_404_NOT_FOUND):
return Response({'details': msg},
status=status)
def create_success_response(channel_data, status, many: bool = False):
from .serializers import ChannelSerializer
return Response(ChannelSerializer(channel_data, many=many).data,
status=status)

View File

@ -0,0 +1,97 @@
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from django.core.exceptions import ObjectDoesNotExist
from geeksbot_web.utils.api_utils import PaginatedAPIView
from .models import Channel
from .utils import create_error_response
from .utils import create_success_response
# Create your views here.
# API Views
class ChannelsAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, format=None):
channels = Channel.get_guild_channels(guild_id)
page = self.paginate_queryset(channels)
if page is not None:
return create_success_response(page, status.HTTP_200_OK, many=True)
return create_success_response(channels, status.HTTP_200_OK, many=True)
def post(self, request, format=None):
data = dict(request.data)
return Channel.add_new_channel(data)
class AdminChannelAPI(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, format=None):
channel = Channel.get_admin_channel(guild_id)
if channel:
return create_success_response(channel, status=status.HTTP_200_OK)
return create_error_response('There is no admin channel configured for that guild',
status=status.HTTP_404_NOT_FOUND)
def put(self, request, guild_id, format=None):
data = dict(request.data)
channel = Channel.get_channel_by_id(guild_id, data['channel'])
if channel:
channel = channel.update_channel({'admin': True})
return create_success_response(channel, status=status.HTTP_202_ACCEPTED)
return create_error_response("That channel does not exist",
status=status.HTTP_404_NOT_FOUND)
def delete(self, request, guild_id, format=None):
channel = Channel.get_admin_channel(guild_id)
if channel:
channel = channel.update_channel({'admin': False})
return create_success_response(channel, status=status.HTTP_202_ACCEPTED)
return create_error_response("There is no admin channel configured",
status=status.HTTP_404_NOT_FOUND)
class ChannelDetail(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, channel_id, format=None):
try:
guild = Channel.get_channel_by_id(guild_id, channel_id)
except ObjectDoesNotExist:
return create_error_response("Channel Does not Exist",
status=status.HTTP_404_NOT_FOUND)
else:
return create_success_response(guild,
status=status.HTTP_200_OK)
def put(self, request, guild_id, channel_id, format=None):
channel = Channel.get_channel_by_id(guild_id, channel_id)
if channel:
data = dict(request.data)
channel = channel.update_channel(data)
return create_success_response(channel,
status=status.HTTP_202_ACCEPTED)
else:
return create_error_response('Channel Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
def delete(self, request, guild_id, channel_id, format=None):
channel = Channel.get_channel_by_id(guild_id, channel_id)
if channel:
# data = dict(request.data)
# TODO Add a check to verify user is allowed to delete...
# Possibly in object permissions...
channel.delete()
return create_success_response(guild,
status=status.HTTP_200_OK)
else:
return create_error_response('Channel Does Not Exist',
status=status.HTTP_404_NOT_FOUND)

View File

View File

View File

@ -0,0 +1,305 @@
"""
Base settings to build other settings files upon.
"""
import environ
import sys
ROOT_DIR = (
environ.Path(__file__) - 3
) # (geeksbot_web/config/settings/base.py - 3 = geeksbot_web/)
APPS_DIR = ROOT_DIR
CODE_DIR = ( environ.Path(__file__) - 4 )
sys.path.append(str(CODE_DIR))
print(sys.path)
env = environ.Env()
READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=True)
if READ_DOT_ENV_FILE:
# OS environment variables take precedence over variables from .env
env.read_env(str(CODE_DIR.path(".env")))
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = env.bool("DJANGO_DEBUG", False)
# Local time zone. Choices are
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# though not all of them may be available with every OS.
# In Windows, this must be set to your system time zone.
TIME_ZONE = "UTC"
# https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE = "en-us"
# https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1
# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
USE_I18N = True
# https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n
USE_L10N = True
# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
USE_TZ = True
# https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths
LOCALE_PATHS = [ROOT_DIR.path("locale")]
# DATABASES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {
"default": {
'ENGINE': 'django.db.backends.postgresql',
'NAME': env.str("POSTGRES_DB"),
'USER': env.str('POSTGRES_USER'),
'PASSWORD': env.str('POSTGRES_PASSWORD'),
'HOST': env.str('POSTGRES_HOST'),
'PORT': env.str('POSTGRES_PORT')
}
}
DATABASES["default"]["ATOMIC_REQUESTS"] = True
DATABASES['default']['CONN_MAX_AGE'] = 0
# URLS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf
ROOT_URLCONF = "config.urls"
# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
WSGI_APPLICATION = "config.wsgi.application"
# APPS
# ------------------------------------------------------------------------------
DJANGO_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.sites",
"django.contrib.messages",
"django.contrib.staticfiles",
# "django.contrib.humanize", # Handy template tags
"django.contrib.admin",
]
THIRD_PARTY_APPS = [
"crispy_forms",
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.discord",
"rest_framework",
"rest_framework.authtoken",
]
LOCAL_APPS = [
"geeksbot_web.users.apps.UsersConfig",
"geeksbot_web.guilds.apps.GuildsConfig",
"geeksbot_web.dmessages.apps.MessagesConfig",
"geeksbot_web.patreon.apps.PatreonConfig",
"geeksbot_web.rcon.apps.RconConfig",
"geeksbot_web.channels.apps.ChannelsConfig",
# Your stuff: custom apps go here
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
# MIGRATIONS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules
MIGRATION_MODULES = {"sites": "geeksbot_web.contrib.sites.migrations"}
# AUTHENTICATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model
AUTH_USER_MODEL = "users.User"
# https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url
LOGIN_REDIRECT_URL = "users:redirect"
# https://docs.djangoproject.com/en/dev/ref/settings/#login-url
LOGIN_URL = "account_login"
# PASSWORDS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
PASSWORD_HASHERS = [
# https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
]
# https://docs.djangoproject.com/en/dev/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"},
]
# MIDDLEWARE
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# STATIC
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#static-root
STATIC_ROOT = str(ROOT_DIR("staticfiles"))
# https://docs.djangoproject.com/en/dev/ref/settings/#static-url
STATIC_URL = "/static/"
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = [str(APPS_DIR.path("static"))]
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
# MEDIA
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = str(APPS_DIR("media"))
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
MEDIA_URL = "/media/"
# TEMPLATES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
TEMPLATES = [
{
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND
"BACKEND": "django.template.backends.django.DjangoTemplates",
# https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
"DIRS": [str(APPS_DIR.path("templates"))],
"OPTIONS": {
# https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
# https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types
"loaders": [
"django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader",
],
# https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
],
},
}
]
# http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs
CRISPY_TEMPLATE_PACK = "bootstrap4"
# FIXTURES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs
FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),)
# SECURITY
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly
SESSION_COOKIE_HTTPONLY = True
# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly
CSRF_COOKIE_HTTPONLY = True
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter
SECURE_BROWSER_XSS_FILTER = True
# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options
X_FRAME_OPTIONS = "DENY"
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
EMAIL_BACKEND = env(
"DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend"
)
# https://docs.djangoproject.com/en/2.2/ref/settings/#email-timeout
EMAIL_TIMEOUT = 5
# ADMIN
# ------------------------------------------------------------------------------
# Django Admin URL.
ADMIN_URL = "admin/"
# https://docs.djangoproject.com/en/dev/ref/settings/#admins
ADMINS = [("""Dustin Pianalto""", "dusty.p@geeksbot.app")]
# https://docs.djangoproject.com/en/dev/ref/settings/#managers
MANAGERS = ADMINS
# LOGGING
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#logging
# See https://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "%(levelname)s %(asctime)s %(module)s "
"%(process)d %(thread)d %(message)s"
}
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
}
},
"root": {"level": "INFO", "handlers": ["console"]},
}
# django-allauth
# ------------------------------------------------------------------------------
ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", False)
SOCIAL_ACCOUNT_ALLOW_REGISTRATION = env.bool('DJANGO_SOCIAL_ACCOUNT_ALLOW_REGISTRATION', True)
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_AUTHENTICATION_METHOD = "username"
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_EMAIL_REQUIRED = False
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_EMAIL_VERIFICATION = "optional"
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_ADAPTER = "geeksbot_web.users.adapters.AccountAdapter"
# https://django-allauth.readthedocs.io/en/latest/configuration.html
SOCIALACCOUNT_ADAPTER = "geeksbot_web.users.adapters.SocialAccountAdapter"
ACCOUNT_FORMS = {
'signup': 'geeksbot_web.users.forms.UserCreateForm',
}
# Your stuff...
# ------------------------------------------------------------------------------
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
],
"DEFAULT_PARSER_CLASSES": [
'rest_framework.parsers.JSONParser',
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
"PAGE_SIZE": 100,
"UNICODE_JSON": True
}
SILENCED_SYSTEM_CHECKS = ["auth.W004"]

View File

@ -0,0 +1,62 @@
from .base import * # noqa
from .base import env
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = True
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = env(
"DJANGO_SECRET_KEY",
default="j67xnBm5ZQ7SAQwhvaMnD4WAW1EeEFVQx0KxBOyHRMYCR4LV0rVkuslZJs1rtPUE",
)
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"]
# CACHES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#caches
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "",
}
}
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
EMAIL_BACKEND = env(
"DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend"
)
# https://docs.djangoproject.com/en/dev/ref/settings/#email-host
EMAIL_HOST = "localhost"
# https://docs.djangoproject.com/en/dev/ref/settings/#email-port
EMAIL_PORT = 1025
# django-debug-toolbar
# ------------------------------------------------------------------------------
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites
INSTALLED_APPS += ["debug_toolbar"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405
# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config
DEBUG_TOOLBAR_CONFIG = {
"DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
"SHOW_TEMPLATE_CONTEXT": True,
}
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips
INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"]
if env("USE_DOCKER") == "yes":
import socket
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS += [ip[:-1] + "1" for ip in ips]
# django-extensions
# ------------------------------------------------------------------------------
# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
INSTALLED_APPS += ["django_extensions"] # noqa F405
# Your stuff...
# ------------------------------------------------------------------------------

View File

@ -0,0 +1,155 @@
from .base import * # noqa
from .base import env
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = env("DJANGO_SECRET_KEY")
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["geeksbot.app"])
# DATABASES
# ------------------------------------------------------------------------------
DATABASES["default"]["ATOMIC_REQUESTS"] = True # noqa F405
DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405
# CACHES
# ------------------------------------------------------------------------------
REDIS_URL = f'redis://{env.str("REDIS_HOST")}:{env.str("REDIS_PORT")}/{env.str("REDIS_DB")}'
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
# Mimicing memcache behavior.
# http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior
"IGNORE_EXCEPTIONS": True,
},
}
}
# SECURITY
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect
SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True)
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure
SESSION_COOKIE_SECURE = True
# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-secure
CSRF_COOKIE_SECURE = True
# https://docs.djangoproject.com/en/dev/topics/security/#ssl-https
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-seconds
# TODO: set this to 60 seconds first and then to 518400 once you prove the former works
SECURE_HSTS_SECONDS = 60
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains
SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool(
"DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True
)
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload
SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True)
# https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff
SECURE_CONTENT_TYPE_NOSNIFF = env.bool(
"DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True
)
# MEDIA
# ------------------------------------------------------------------------------
# TEMPLATES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
TEMPLATES[0]["OPTIONS"]["loaders"] = [ # noqa F405
(
"django.template.loaders.cached.Loader",
[
"django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader",
],
)
]
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email
DEFAULT_FROM_EMAIL = env(
"DJANGO_DEFAULT_FROM_EMAIL", default="geeksbot <noreply@geeksbot.app>"
)
# https://docs.djangoproject.com/en/dev/ref/settings/#server-email
SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL)
# https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix
EMAIL_SUBJECT_PREFIX = env(
"DJANGO_EMAIL_SUBJECT_PREFIX", default="[geeksbot]"
)
# ADMIN
# ------------------------------------------------------------------------------
# Django Admin URL regex.
ADMIN_URL = env("DJANGO_ADMIN_URL")
# Anymail (Mailgun)
# ------------------------------------------------------------------------------
# https://anymail.readthedocs.io/en/stable/installation/#installing-anymail
INSTALLED_APPS += ["anymail"] # noqa F405
EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend"
# https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference
ANYMAIL = {
"MAILGUN_API_KEY": env("MAILGUN_API_KEY"),
"MAILGUN_SENDER_DOMAIN": env("MAILGUN_DOMAIN"),
"MAILGUN_API_URL": env("MAILGUN_API_URL", default="https://api.mailgun.net/v3"),
}
# Collectfast
# ------------------------------------------------------------------------------
# https://github.com/antonagestam/collectfast#installation
INSTALLED_APPS = ["collectfast"] + INSTALLED_APPS # noqa F405
AWS_PRELOAD_METADATA = True
# LOGGING
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#logging
# See https://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}},
"formatters": {
"verbose": {
"format": "%(levelname)s %(asctime)s %(module)s "
"%(process)d %(thread)d %(message)s"
}
},
"handlers": {
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
},
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
},
},
"root": {"level": "INFO", "handlers": ["console"]},
"loggers": {
"django.request": {
"handlers": ["mail_admins"],
"level": "ERROR",
"propagate": True,
},
"django.security.DisallowedHost": {
"level": "ERROR",
"handlers": ["console", "mail_admins"],
"propagate": True,
},
},
}
# Your stuff...
# ------------------------------------------------------------------------------

View File

@ -0,0 +1,53 @@
"""
With these settings, tests run faster.
"""
from .base import * # noqa
from .base import env
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = False
# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
SECRET_KEY = env(
"DJANGO_SECRET_KEY",
default="f2z33VJdMTuS8AHpyHr1p2AAR9daYVQFMLEqBGxEP2aZLmtBpRVudyOrhK1DL9Ov",
)
# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
TEST_RUNNER = "django.test.runner.DiscoverRunner"
# CACHES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#caches
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "",
}
}
# PASSWORDS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
# TEMPLATES
# ------------------------------------------------------------------------------
TEMPLATES[0]["OPTIONS"]["loaders"] = [ # noqa F405
(
"django.template.loaders.cached.Loader",
[
"django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader",
],
)
]
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
# Your stuff...
# ------------------------------------------------------------------------------

View File

@ -0,0 +1,50 @@
from django.conf import settings
from django.urls import include, path
from django.conf.urls.static import static
from django.contrib import admin
from django.views.generic import TemplateView
from django.views import defaults as default_views
urlpatterns = [
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
path(
"about/", TemplateView.as_view(template_name="pages/about.html"), name="about"
),
# Django Admin, use {% url 'admin:index' %}
path(settings.ADMIN_URL, admin.site.urls),
# User management
path("users/", include("geeksbot_web.users.urls", namespace="users")),
path("accounts/", include("allauth.urls")),
# Your stuff: custom urls includes go here
path("api/users/", include("geeksbot_web.users.api_urls", namespace="users_api")),
path("api/guilds/", include("geeksbot_web.guilds.api_urls", namespace="guilds_api")),
path("api/channels/", include("geeksbot_web.channels.api_urls", namespace="channels_api")),
path("api/messages/", include("geeksbot_web.dmessages.api_urls", namespace="messages_api")),
path("api/rcon/", include("geeksbot_web.rcon.api_urls", namespace="rcon_api")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
# This allows the error pages to be debugged during development, just visit
# these url in browser to see how these error pages look like.
urlpatterns += [
path(
"400/",
default_views.bad_request,
kwargs={"exception": Exception("Bad Request!")},
),
path(
"403/",
default_views.permission_denied,
kwargs={"exception": Exception("Permission Denied")},
),
path(
"404/",
default_views.page_not_found,
kwargs={"exception": Exception("Page not Found")},
),
path("500/", default_views.server_error),
]
if "debug_toolbar" in settings.INSTALLED_APPS:
import debug_toolbar
urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns

View File

@ -0,0 +1,39 @@
"""
WSGI config for geeksbot project.
This module contains the WSGI application used by Django's development server
and any production WSGI deployments. It should expose a module-level variable
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
this application via the ``WSGI_APPLICATION`` setting.
Usually you will have the standard Django WSGI application here, but it also
might make sense to replace the whole Django WSGI application with a custom one
that later delegates to the Django one. For example, you could introduce WSGI
middleware here, or combine a Django application with an application of another
framework.
"""
import os
import sys
from django.core.wsgi import get_wsgi_application
# This allows easy placement of apps within the interior
# geeksbot_web directory.
app_path = os.path.abspath(
os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)
)
sys.path.append(os.path.join(app_path, "geeksbot_web"))
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use
# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
application = get_wsgi_application()
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)

20
geeksbot_web/conftest.py Normal file
View File

@ -0,0 +1,20 @@
import pytest
from django.conf import settings
from django.test import RequestFactory
from geeksbot_web.users.tests.factories import UserFactory
@pytest.fixture(autouse=True)
def media_storage(settings, tmpdir):
settings.MEDIA_ROOT = tmpdir.strpath
@pytest.fixture
def user() -> settings.AUTH_USER_MODEL:
return UserFactory()
@pytest.fixture
def request_factory() -> RequestFactory:
return RequestFactory()

View File

@ -0,0 +1,5 @@
"""
To understand why this file is here, please read:
http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
"""

View File

@ -0,0 +1,5 @@
"""
To understand why this file is here, please read:
http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
"""

View File

@ -0,0 +1,42 @@
import django.contrib.sites.models
from django.contrib.sites.models import _simple_domain_name_validator
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = []
operations = [
migrations.CreateModel(
name="Site",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"domain",
models.CharField(
max_length=100,
verbose_name="domain name",
validators=[_simple_domain_name_validator],
),
),
("name", models.CharField(max_length=50, verbose_name="display name")),
],
options={
"ordering": ("domain",),
"db_table": "django_site",
"verbose_name": "site",
"verbose_name_plural": "sites",
},
bases=(models.Model,),
managers=[("objects", django.contrib.sites.models.SiteManager())],
)
]

View File

@ -0,0 +1,20 @@
import django.contrib.sites.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("sites", "0001_initial")]
operations = [
migrations.AlterField(
model_name="site",
name="domain",
field=models.CharField(
max_length=100,
unique=True,
validators=[django.contrib.sites.models._simple_domain_name_validator],
verbose_name="domain name",
),
)
]

View File

@ -0,0 +1,33 @@
"""
To understand why this file is here, please read:
http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
"""
from django.conf import settings
from django.db import migrations
def update_site_forward(apps, schema_editor):
"""Set site domain and name."""
Site = apps.get_model("sites", "Site")
Site.objects.update_or_create(
id=settings.SITE_ID,
defaults={
"domain": "geeksbot.app",
"name": "geeksbot",
},
)
def update_site_backward(apps, schema_editor):
"""Revert site domain and name to default."""
Site = apps.get_model("sites", "Site")
Site.objects.update_or_create(
id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"}
)
class Migration(migrations.Migration):
dependencies = [("sites", "0002_alter_domain_unique")]
operations = [migrations.RunPython(update_site_forward, update_site_backward)]

View File

@ -0,0 +1,5 @@
"""
To understand why this file is here, please read:
http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
"""

View File

View File

@ -0,0 +1,10 @@
from django.contrib import admin
from .models import Message
from .models import GuildInfo
from .models import AdminRequest
# Register your models here.
admin.site.register(Message)
admin.site.register(GuildInfo)
admin.site.register(AdminRequest)

View File

@ -0,0 +1,21 @@
from django.urls import path
from .views import MessageDetailAPI, MessagesAPI
from .views import RequestDetailAPI, RequestsAPI
from .views import CommentDetailAPI, CommentsAPI, CommentsCountAPI
from .views import WaitForMessageAPI
from .views import UserRequestsAPI
app_name = "messages_api"
urlpatterns = [
path("", view=MessagesAPI.as_view(), name="message_list"),
path("<str:id>/", view=MessageDetailAPI.as_view(), name='message_detail'),
path("<str:guild_id>/requests/", view=RequestsAPI.as_view(), name="requests_list"),
path("<str:guild_id>/requests/<str:request_id>/", view=RequestDetailAPI.as_view(), name='request_detail'),
path("<str:guild_id>/requests/<str:request_id>/comments/", view=CommentsAPI.as_view(), name="comments_list"),
path("<str:guild_id>/requests/<str:request_id>/comments/count/", view=CommentsCountAPI.as_view(), name="comments_count"),
path("<str:guild_id>/requests/<str:request_id>/comments/<str:comment_id>/", view=CommentDetailAPI.as_view(), name='comment_detail'),
path("<str:guild_id>/requests/user/<str:author_id>/", view=UserRequestsAPI.as_view(), name='user_requests_list'),
path("<str:id>/wait/", view=WaitForMessageAPI.as_view(), name='wait_for_message'),
path("<str:id>/wait/<int:timeout>/", view=WaitForMessageAPI.as_view(), name='wait_for_message_timeout'),
]

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class MessagesConfig(AppConfig):
name = 'geeksbot_web.dmessages'
verbose_name = _("DMessages")

View File

@ -0,0 +1,59 @@
# Generated by Django 2.2.4 on 2019-09-20 21:39
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AdminComment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.CharField(max_length=1000)),
('updated_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='AdminRequest',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('completed', models.BooleanField(default=False)),
('requested_at', models.DateTimeField(auto_now_add=True)),
('completed_at', models.DateTimeField(blank=True, default=None, null=True)),
('completed_message', models.CharField(blank=True, default=None, max_length=1000, null=True)),
('content', models.CharField(max_length=2000)),
],
),
migrations.CreateModel(
name='GuildInfo',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.PositiveSmallIntegerField()),
('text', models.TextField(max_length=1980)),
('format', models.PositiveSmallIntegerField()),
('channel', models.CharField(max_length=30)),
('message_number', models.PositiveSmallIntegerField()),
],
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.CharField(max_length=30, primary_key=True, serialize=False)),
('created_at', models.DateTimeField()),
('modified_at', models.DateTimeField(blank=True, null=True)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('content', models.CharField(max_length=2000)),
('previous_content', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=2000), default=list, size=None)),
('tagged_everyone', models.BooleanField()),
('embeds', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None)),
('previous_embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None), default=list, size=None)),
],
),
]

View File

@ -0,0 +1,95 @@
# Generated by Django 2.2.4 on 2019-09-20 21:39
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('guilds', '0001_initial'),
('dmessages', '0001_initial'),
('channels', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='message',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='message',
name='channel',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='channels.Channel'),
),
migrations.AddField(
model_name='message',
name='guild',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild'),
),
migrations.AddField(
model_name='message',
name='tagged_channels',
field=models.ManyToManyField(related_name='_message_tagged_channels_+', to='channels.Channel'),
),
migrations.AddField(
model_name='message',
name='tagged_roles',
field=models.ManyToManyField(to='guilds.Role'),
),
migrations.AddField(
model_name='message',
name='tagged_users',
field=models.ManyToManyField(related_name='_message_tagged_users_+', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='guildinfo',
name='guild',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild'),
),
migrations.AddField(
model_name='guildinfo',
name='message',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dmessages.Message'),
),
migrations.AddField(
model_name='adminrequest',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='adminrequest',
name='channel',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='channels.Channel'),
),
migrations.AddField(
model_name='adminrequest',
name='completed_by',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='adminrequest',
name='guild',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild'),
),
migrations.AddField(
model_name='adminrequest',
name='message',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='dmessages.Message'),
),
migrations.AddField(
model_name='admincomment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='admincomment',
name='request',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dmessages.AdminRequest'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-21 07:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dmessages', '0002_auto_20190920_2139'),
]
operations = [
migrations.AlterField(
model_name='message',
name='content',
field=models.CharField(blank=True, max_length=2000, null=True),
),
]

View File

@ -0,0 +1,291 @@
from datetime import datetime
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.postgres.fields import ArrayField
from rest_framework import status
from geeksbot_web.guilds.models import Guild
from geeksbot_web.guilds.models import Role
from geeksbot_web.users.models import User
from geeksbot_web.channels.models import Channel
from .utils import create_error_response
from .utils import create_success_response
from .utils import create_request_success_response
from .utils import create_comment_success_response
# Create your models here.
class Message(models.Model):
id = models.CharField(max_length=30, primary_key=True)
author = models.ForeignKey(User, related_name="+", on_delete=models.CASCADE)
guild = models.ForeignKey(Guild, on_delete=models.CASCADE)
channel = models.ForeignKey(Channel, related_name="+", on_delete=models.CASCADE)
created_at = models.DateTimeField()
modified_at = models.DateTimeField(null=True, blank=True)
deleted_at = models.DateTimeField(null=True, blank=True)
content = models.CharField(max_length=2000, null=True, blank=True)
previous_content = ArrayField(models.CharField(max_length=2000), default=list)
tagged_users = models.ManyToManyField(User, related_name="+")
tagged_channels = models.ManyToManyField(Channel, related_name="+")
tagged_roles = models.ManyToManyField(Role)
tagged_everyone = models.BooleanField()
embeds = ArrayField(models.TextField(), default=list)
previous_embeds = ArrayField(ArrayField(models.TextField()), default=list)
@classmethod
def add_new_message(cls, data):
id = data.get('id')
if id and cls.get_message_by_id(id):
return create_error_response("Message Already Exists",
status=status.HTTP_409_CONFLICT)
author_id = data.get('author')
guild_id = data.get('guild')
channel_id = data.get('channel')
created_at = data.get('created_at')
content = data.get('content')
tagged_everyone = data.get('tagged_everyone')
if not (id and author_id and guild_id and channel_id and created_at and (tagged_everyone is not None)):
return create_error_response("One or more required fields are missing.",
status=status.HTTP_400_BAD_REQUEST)
author = User.get_user_by_id(author_id)
if not isinstance(author, User):
return create_error_response("Author Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
guild = Guild.get_guild_by_id(guild_id)
if not isinstance(guild, Guild):
return create_error_response("Guild Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
channel = Channel.get_channel_by_id(guild_id, channel_id)
if not isinstance(channel, Channel):
return create_error_response("Channel Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
created_at = datetime.fromtimestamp(created_at)
message = cls(
id=id,
author=author,
guild=guild,
channel=channel,
created_at=created_at,
tagged_everyone=tagged_everyone or False,
content=content or '',
embeds=data.get('embeds') or []
)
message.save()
if data.get('tagged_users'):
tagged_users = data.get('tagged_users')
for user_id in tagged_users:
user = User.get_user_by_id(user_id)
if user:
message.tagged_users.add(user)
if data.get('tagged_roles'):
tagged_roles = data.get('tagged_roles')
for role_id in tagged_roles:
role = Role.get_role_by_id(role_id)
if role:
message.tagged_roles.add(role)
if data.get('tagged_channels'):
tagged_channels = data.get('tagged_channels')
for channel_id in tagged_channels:
channel = Channel.get_channel_by_id(guild_id, channel_id)
if channel:
message.tagged_channels.add(channel)
return create_success_response(message, status.HTTP_201_CREATED, many=False)
def update_message(self, data):
if data.get('modified_at'):
self.modified_at = datetime.fromtimestamp(int(data.get('modified_at')))
if data.get('deleted_at'):
self.deleted_at = datetime.fromtimestamp(int(data.get('deleted_at')))
if data.get('content'):
content = data.get('content')
if content != self.content:
self.previous_content.append(self.content)
self.content = content
if data.get('embeds'):
embeds = data.get('embeds')
if embeds != self.embeds:
self.previous_embeds.append(self.embeds)
self.embeds = embeds
if data.get('tagged_everyone'):
tagged_everyone = data.get('tagged_everyone')
if self.tagged_everyone or tagged_everyone:
self.tagged_everyone = True
if data.get('tagged_users'):
tagged_users = data.get('tagged_users')
for user in tagged_users:
if user not in self.tagged_users:
self.tagged_users.append(user)
if data.get('tagged_roles'):
tagged_roles = data.get('tagged_roles')
for role in tagged_roles:
if role not in self.tagged_roles:
self.tagged_roles.append(role)
if data.get('tagged_channels'):
tagged_channels = data.get('tagged_channels')
for channel in tagged_channels:
if channel not in self.tagged_channels:
self.tagged_channels.append(channel)
self.save()
return create_success_response(self, status.HTTP_202_ACCEPTED, many=False)
@classmethod
def get_message_by_id(cls, id):
try:
return cls.objects.get(id=id)
except ObjectDoesNotExist:
return None
def __str__(self):
return (f'{self.created_at} | '
f'{self.author.id}'
f'{" | Modified" if self.modified_at else ""}'
f'{" | Deleted" if self.deleted_at else ""}')
class GuildInfo(models.Model):
message = models.ForeignKey(
Message, on_delete=models.CASCADE, blank=True, null=True
)
guild = models.ForeignKey(Guild, on_delete=models.CASCADE)
type = models.PositiveSmallIntegerField()
text = models.TextField(max_length=1980)
format = models.PositiveSmallIntegerField()
channel = models.CharField(max_length=30)
message_number = models.PositiveSmallIntegerField()
def __str__(self):
return f"{self.guild.id} | {self.text[:25]}"
class AdminRequest(models.Model):
guild = models.ForeignKey(Guild, on_delete=models.CASCADE)
author = models.ForeignKey(User, related_name="+", on_delete=models.DO_NOTHING)
message = models.ForeignKey(Message, on_delete=models.DO_NOTHING)
channel = models.ForeignKey(Channel, on_delete=models.DO_NOTHING, null=True)
completed = models.BooleanField(default=False)
requested_at = models.DateTimeField(auto_now_add=True, blank=True)
completed_at = models.DateTimeField(null=True, blank=True, default=None)
completed_by = models.ForeignKey(
User, related_name="+", on_delete=models.DO_NOTHING, null=True, blank=True, default=None
)
completed_message = models.CharField(max_length=1000, null=True, blank=True, default=None)
content = models.CharField(max_length=2000)
def update_request(self, data):
completed = data.get('completed', False)
completed_by_id = data.get('completed_by')
completed_message = data.get('message', '')
if not self.completed and completed:
self.completed = completed
self.completed_at = datetime.utcnow()
self.completed_message = completed_message
user = User.get_user_by_id(completed_by_id)
if not isinstance(user, User):
return create_error_response('User Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
self.completed_by = user
self.save()
return create_request_success_response(self, status.HTTP_202_ACCEPTED)
@classmethod
def add_new_request(cls, guild_id, data):
author_id = data.get('author')
message_id = data.get('message')
channel_id = data.get('channel')
content = data.get('content')
if not (guild_id and author_id and message_id and channel_id and content):
return create_error_response("One or more of the required fields are missing.",
status=status.HTTP_400_BAD_REQUEST)
guild = Guild.get_guild_by_id(guild_id)
if not isinstance(guild, Guild):
return create_error_response('Guild Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
author = User.get_user_by_id(author_id)
if not isinstance(author, User):
return create_error_response('Author Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
message = Message.get_message_by_id(message_id)
if not isinstance(message, Message):
return create_error_response('Message Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
channel = Channel.get_channel_by_id(guild_id, channel_id)
if not isinstance(channel, Channel):
return create_error_response('Channel Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
print('test')
request = cls(
guild=guild,
author=author,
message=message,
channel=channel,
content=content
)
request.save()
return create_request_success_response(request, status.HTTP_201_CREATED, many=False)
@classmethod
def get_open_requests_by_guild(cls, guild_id):
return cls.objects.filter(guild__id=guild_id).filter(completed=False)
@classmethod
def get_open_request_by_id(cls, guild_id, request_id):
try:
return cls.get_open_requests_by_guild(guild_id).get(id=request_id)
except ObjectDoesNotExist:
return None
def __str__(self):
return f"{self.guild.id} | {self.requested_at} | By {self.author.id}"
@classmethod
def get_open_requests_by_guild_author(cls, guild_id, author_id):
return cls.get_open_requests_by_guild(guild_id).filter(author__id=author_id)
class AdminComment(models.Model):
request = models.ForeignKey(AdminRequest, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.DO_NOTHING)
content = models.CharField(max_length=1000)
updated_at = models.DateTimeField(auto_now_add=True, blank=True)
@classmethod
def add_new_comment(cls, data, guild_id, request_id):
author_id = data.get('author')
content = data.get('content')
if not (request_id and author_id and content):
return create_error_response('Request, Author, and Content are required fields',
status=status.HTTP_400_BAD_REQUEST)
request = AdminRequest.get_open_request_by_id(guild_id, request_id)
if not isinstance(request, AdminRequest):
return create_error_response("Admin Request Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
author = User.get_user_by_id(author_id)
if not isinstance(author, User):
return create_error_response("Author Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
comment = cls(
request=request,
author=author,
content=content
)
comment.save()
return create_comment_success_response(comment, status.HTTP_201_CREATED, many=False)
@classmethod
def get_comment_by_id(cls, comment_id):
try:
return cls.objects.get(id=comment_id)
except ObjectDoesNotExist:
return None
@classmethod
def get_comments_by_request(cls, request):
return cls.objects.filter(request=request).order_by('updated_at')

View File

@ -0,0 +1,30 @@
from rest_framework import serializers
from .models import Message
from .models import GuildInfo
from .models import AdminRequest
from .models import AdminComment
class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = "__all__"
class GuildInfoSerializer(serializers.ModelSerializer):
class Meta:
model = GuildInfo
fields = "__all__"
class AdminRequestSerializer(serializers.ModelSerializer):
class Meta:
model = AdminRequest
fields = "__all__"
class AdminCommentSerializer(serializers.ModelSerializer):
class Meta:
model = AdminComment
fields = "__all__"

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,28 @@
from rest_framework.response import Response
from rest_framework import status
def create_error_response(msg, status=status.HTTP_404_NOT_FOUND):
return Response({'details': msg},
status=status)
def create_success_response(message_data, status, many: bool = False):
from .serializers import MessageSerializer
return Response(MessageSerializer(message_data, many=many).data,
status=status)
def create_request_success_response(request_data, status, many: bool = False):
from .serializers import AdminRequestSerializer
return Response(AdminRequestSerializer(request_data, many=many).data,
status=status)
def create_comment_success_response(comment_data, status, many: bool = False):
from .serializers import AdminCommentSerializer
return Response(AdminCommentSerializer(comment_data, many=many).data,
status=status)

View File

@ -0,0 +1,180 @@
from time import sleep
from datetime import datetime
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.response import Response
from rest_framework import status
from .models import Message
from .models import AdminComment
from .models import AdminRequest
from .models import GuildInfo
from geeksbot_web.utils.api_utils import PaginatedAPIView
from .utils import create_error_response
from .utils import create_success_response
from .utils import create_request_success_response
from .utils import create_comment_success_response
from .serializers import AdminRequestSerializer
from .serializers import AdminCommentSerializer
# Create your views here.
# API Views
class MessagesAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, format=None):
messages = Message.objects.all()
page = self.paginate_queryset(messages)
if page:
return create_success_response(page, status.HTTP_200_OK, many=True)
return create_success_response(messages, status.HTTP_200_OK, many=True)
def post(self, request, format=None):
data = dict(request.data)
return Message.add_new_message(data)
class MessageDetailAPI(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, id, format=None):
message = Message.get_message_by_id(id)
if message:
return create_success_response(message, status.HTTP_200_OK, many=False)
else:
return create_error_response("Message Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
def put(self, request, id, format=None):
data = dict(request.data)
message = Message.get_message_by_id(id)
if message:
return message.update_message(data)
else:
return create_error_response('Message Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
class WaitForMessageAPI(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, id, timeout: int = 3, format=None):
message = Message.get_message_by_id(id)
try_count = 0
while not message:
sleep(0.1)
try_count += 1
if try_count > timeout * 10:
return create_error_response("Timeout reached before message is available.",
statu=status.HTTP_404_NOT_FOUND)
message = Message.get_message_by_id(id)
return create_success_response(message, status=status.HTTP_200_OK)
class RequestsAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, format=None):
requests = AdminRequest.get_open_requests_by_guild(guild_id)
page = self.paginate_queryset(requests)
if page is not None:
return create_request_success_response(page, status.HTTP_200_OK, many=True)
if requests:
return create_request_success_response(requests, status.HTTP_200_OK, many=True)
return create_error_response("No requests found")
def post(self, request, guild_id, format=None):
data = dict(request.data)
return AdminRequest.add_new_request(guild_id, data)
class UserRequestsAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, author_id, format=None):
requests = AdminRequest.get_open_requests_by_guild_author(guild_id, author_id)
page = self.paginate_queryset(requests)
if page is not None:
return create_request_success_response(page, status.HTTP_200_OK, many=True)
if requests:
return create_request_success_response(requests, status.HTTP_200_OK, many=True)
return create_error_response("No requests found")
class RequestDetailAPI(APIView):
permission_classes = [IsAuthenticated]
def get(self, req, guild_id, request_id, format=None):
req = AdminRequest.get_open_request_by_id(guild_id, request_id)
if req:
comments = AdminComment.get_comments_by_request(req)
if comments:
data = AdminRequestSerializer(req).data
data['comments'] = AdminCommentSerializer(comments, many=True).data
return Response(data, status.HTTP_200_OK)
else:
return create_request_success_response(req, status.HTTP_200_OK, many=False)
else:
return create_error_response("That Request Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
def put(self, request, guild_id, request_id, format=None):
req = AdminRequest.get_open_request_by_id(guild_id, request_id)
if req:
data = dict(request.data)
return req.update_request(data)
return create_error_response("That Request Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
def delete(self, request, guild_id, request_id, format=None):
data = dict(request.data)
request = AdminRequest.get_open_request_by_id(guild_id, request_id)
data['completed'] = True
data['completed_at'] = datetime.utcnow()
return request.update_request(data)
class CommentsAPI(PaginatedAPIView):
permissions_classes = [IsAuthenticated]
def get(self, request, guild_id, request_id, format=None):
comments = AdminComment.get_comments_by_request(request_id)
if comments:
return create_comment_success_response(comments, status=status.HTTP_200_OK, many=True)
return create_error_response("No Comments found")
def post(self, request, guild_id, request_id, format=None):
data = dict(request.data)
return AdminComment.add_new_comment(data, guild_id, request_id)
class CommentsCountAPI(PaginatedAPIView):
permissions_classes = [IsAuthenticated]
def get(self, request, guild_id, request_id, format=None):
comments = AdminComment.get_comments_by_request(request_id)
if comments:
return Response(len(comments), status=status.HTTP_200_OK)
return Response(0, status.HTTP_200_OK)
class CommentDetailAPI(APIView):
permissions_classes = [IsAuthenticated]
def get(self, request, request_id, comment_id, format=None):
comment = AdminComment.get_comment_by_id(comment_id)
if comment:
if comment.request.id != request_id:
return create_error_response("That comment is not for this request",
status=status.HTTP_400_BAD_REQUEST)
return create_comment_success_response(comment, status.HTTP_200_OK, many=False)
else:
return create_error_response("Comment Does Not Exist",
status=status.HTTP_404_NOT_FOUND)

46
geeksbot_web/entrypoint Executable file
View File

@ -0,0 +1,46 @@
#!/bin/sh
set -o errexit
set -o pipefail
set -o nounset
source ../.env
if [ -z "${POSTGRES_USER}" ]; then
base_postgres_image_default_user='postgres'
export POSTGRES_USER="${base_postgres_image_default_user}"
fi
echo "Checking on PostgreSQL"
postgres_ready() {
python << END
import sys
import psycopg2
try:
psycopg2.connect(
dbname="${POSTGRES_DB}",
user="${POSTGRES_USER}",
password="${POSTGRES_PASSWORD}",
host="${POSTGRES_HOST}",
port="${POSTGRES_PORT}",
)
except psycopg2.OperationalError:
sys.exit(-1)
sys.exit(0)
END
}
until postgres_ready; do
>&2 echo 'Waiting for PostgreSQL to become available...'
sleep 1
done
>&2 echo 'PostgreSQL is available'
python manage.py collectstatic --noinput
python manage.py makemigrations --noinput
python manage.py migrate
/usr/bin/supervisord -c /etc/supervisor/supervisord.conf

View File

View File

@ -0,0 +1,8 @@
from django.contrib import admin
from geeksbot_web.guilds.models import Guild
from geeksbot_web.guilds.models import Role
# Register your models here.
admin.site.register(Guild)
admin.site.register(Role)

View File

@ -0,0 +1,14 @@
from django.urls import path
from .views import GuildsAPI, GuildDetail
from .views import RolesAPI, RoleDetailAPI
from .views import AdminRolesAPI
app_name = "guilds_api"
urlpatterns = [
path("", view=GuildsAPI.as_view(), name="list"),
path("<str:id>/", view=GuildDetail.as_view(), name='detail'),
path("<str:guild_id>/roles/", view=RolesAPI.as_view(), name="list"),
path("<str:guild_id>/roles/admin/", view=AdminRolesAPI.as_view(), name='admin'),
path("<str:guild_id>/roles/<str:id>/", view=RoleDetailAPI.as_view(), name='detail'),
]

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class GuildsConfig(AppConfig):
name = 'geeksbot_web.guilds'
verbose_name = _("Guilds")

View File

@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-09-20 21:39
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Guild',
fields=[
('id', models.CharField(max_length=30, primary_key=True, serialize=False)),
('admin_chat', models.CharField(blank=True, max_length=30, null=True)),
('new_patron_message', models.TextField(blank=True, max_length=1000, null=True)),
('default_channel', models.CharField(max_length=30)),
('new_patron_channel', models.CharField(blank=True, max_length=30, null=True)),
('prefixes', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=10), size=None)),
],
),
migrations.CreateModel(
name='Role',
fields=[
('id', models.CharField(max_length=30, primary_key=True, serialize=False)),
('role_type', models.PositiveSmallIntegerField()),
('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')),
],
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 2.2.4 on 2019-09-21 02:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('guilds', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='guild',
name='admin_chat',
),
migrations.RemoveField(
model_name='guild',
name='default_channel',
),
migrations.RemoveField(
model_name='guild',
name='new_patron_channel',
),
]

View File

@ -0,0 +1,133 @@
import os
from django.db import models
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import status
from .utils import create_error_response
from .utils import create_success_response
from .utils import create_role_success_response
# Create your models here.
class Guild(models.Model):
id = models.CharField(max_length=30, primary_key=True)
new_patron_message = models.TextField(max_length=1000, blank=True, null=True)
prefixes = ArrayField(models.CharField(max_length=10))
def __str__(self):
return self.id
def update_guild(self, data):
if data.get('new_patron_message'):
self.new_patron_message = data.get('new_patron_message')
if data.get('add_prefix'):
if data.get('add_prefix') not in self.prefixes:
self.prefixes.append(data.get('add_prefix'))
if data.get('remove_prefix'):
if data.get('remove_prefix') in self.prefixes:
self.prefixes.remove(data.get('remove_prefix'))
if len(self.prefixes) <= 0:
self.prefixes = [os.environ['DISCORD_DEFAULT_PREFIX'], ]
self.save()
return self
@classmethod
def get_guild_by_id(cls, id):
try:
return cls.objects.get(id=id)
except ObjectDoesNotExist:
return None
@classmethod
def create_guild(cls, data):
id = data.get('id')
if not id:
return create_error_response('ID is required',
status=status.HTTP_400_BAD_REQUEST)
if cls.get_guild_by_id(id):
return create_error_response('That Guild already exists',
status.HTTP_409_CONFLICT)
guild = cls(
id=id,
prefixes=data.get('prefixes'),
new_patron_message=data.get('new_patron_message')
)
guild.save()
return create_success_response(guild, status.HTTP_201_CREATED, many=False)
class Role(models.Model):
id = models.CharField(max_length=30, primary_key=True)
guild = models.ForeignKey(Guild, on_delete=models.CASCADE, null=False)
role_type = models.PositiveSmallIntegerField()
def update_role(self, data):
if data.get('role_type'):
self.role_type = data.get('role_type')
self.save()
return self
@classmethod
def add_new_role(cls, guild_id, data):
id = data.get('id')
role_type = data.get('role_type')
if not (id and guild_id and (role_type is not None)):
return create_error_response("The Role ID, Guild, and Role Type are required",
status=status.HTTP_400_BAD_REQUEST)
if cls.get_role_by_id(id):
return create_error_response("That Role Already Exists",
status=status.HTTP_409_CONFLICT)
guild = Guild.get_guild_by_id(guild_id)
if not isinstance(guild, Guild):
return create_error_response("Guild Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
try:
role_type = int(role_type)
except ValueError:
return create_error_response("Role Type must be a positive number",
status=status.HTTP_400_BAD_REQUEST)
if role_type < 0:
return create_error_response("Role Type must be a positive number",
status=status.HTTP_400_BAD_REQUEST)
elif 1000 < role_type:
return create_error_response("Role Type must be less than 1000",
status=status.HTTP_400_BAD_REQUEST)
role = cls(
id=id,
guild=guild,
role_type=role_type
)
role.save()
return create_role_success_response(role, status.HTTP_201_CREATED, many=False)
@classmethod
def get_role_by_id(cls, guild_id, role_id):
try:
return cls.get_guild_roles(guild_id).get(id=role_id)
except ObjectDoesNotExist:
return None
@classmethod
def get_guild_roles(cls, guild):
return cls.objects.filter(guild__id=guild)
@classmethod
def get_admin_roles(cls, guild_id):
try:
return cls.get_guild_roles(guild_id).filter(role_type__gte=90)
except ObjectDoesNotExist:
return None
def __str__(self):
return f"{self.guild.id} | {self.id}"

View File

@ -0,0 +1,8 @@
from rest_framework.permissions import BasePermission
class GuildPermissions(BasePermission):
def has_permission(self, request, view):
return super().has_permission(request, view)
def has_object_permission(self, request, view, obj):
return super().has_object_permission(request, view, obj)

View File

@ -0,0 +1,16 @@
from rest_framework import serializers
from geeksbot_web.guilds.models import Guild
from geeksbot_web.guilds.models import Role
class GuildSerializer(serializers.ModelSerializer):
class Meta:
model = Guild
fields = "__all__"
class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = Role
fields = ["id", "guild", "role_type"]

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

View File

@ -0,0 +1,21 @@
from rest_framework.response import Response
from rest_framework import status
def create_error_response(msg, status=status.HTTP_404_NOT_FOUND):
return Response({'details': msg},
status=status)
def create_success_response(guild_data, status, many: bool = False):
from .serializers import GuildSerializer
return Response(GuildSerializer(guild_data, many=many).data,
status=status)
def create_role_success_response(role_data, status, many: bool = False):
from .serializers import RoleSerializer
return Response(RoleSerializer(role_data, many=many).data,
status=status)

View File

@ -0,0 +1,133 @@
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from django.core.exceptions import ObjectDoesNotExist
from geeksbot_web.utils.api_utils import PaginatedAPIView
from .models import Guild
from .models import Role
from .utils import create_error_response
from .utils import create_success_response
from .utils import create_role_success_response
# Create your views here.
# API Views
class GuildsAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, format=None):
guilds = Guild.objects.all()
page = self.paginate_queryset(guilds)
if page is not None:
return create_success_response(page, status.HTTP_200_OK, many=True)
return create_success_response(guilds, status.HTTP_200_OK, many=True)
def post(self, request, format=None):
data = dict(request.data)
return Guild.create_guild(data)
class GuildDetail(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, id, format=None):
try:
guild = Guild.objects.get(id=id)
except ObjectDoesNotExist:
return create_error_response("Guild Does not Exist",
status=status.HTTP_404_NOT_FOUND)
else:
return create_success_response(guild,
status=status.HTTP_200_OK)
def put(self, request, id, format=None):
guild = Guild.get_guild_by_id(id)
if guild:
data = dict(request.data)
guild = guild.update_guild(data)
return create_success_response(guild,
status=status.HTTP_202_ACCEPTED)
else:
return create_error_response('Guild Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
def delete(self, request, id, format=None):
guild = Guild.get_guild_by_id(id)
if guild:
# data = dict(request.data)
# TODO Add a check to verify user is allowed to delete...
# Possibly in object permissions...
guild.delete()
return create_success_response(guild,
status=status.HTTP_200_OK)
else:
return create_error_response('Guild Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
class RolesAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, format=None):
roles = Role.get_guild_roles(guild_id)
page = self.paginate_queryset(roles)
if page is not None:
return create_success_response(page, status.HTTP_200_OK, many=True)
return create_success_response(roles, status.HTTP_200_OK, many=True)
def post(self, request, guild_id, format=None):
data = dict(request.data)
return Role.add_new_role(guild_id, data)
class AdminRolesAPI(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, format=None):
roles = Role.get_admin_roles(guild_id)
if roles:
return create_role_success_response(roles, status=status.HTTP_200_OK, many=True)
return create_error_response('There are no admin roles configured',
status=status.HTTP_404_NOT_FOUND)
def put(self, request, guild_id, format=None):
data = dict(request.data)
role = Role.get_role_by_id(guild_id, data['role'])
if role:
role = role.update_role({'role_type': 100})
return create_role_success_response(role, status=status.HTTP_202_ACCEPTED)
return create_error_response("That role does not exist",
status=status.HTTP_404_NOT_FOUND)
class RoleDetailAPI(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, id, format=None):
try:
role = Role.objects.get(id=id)
except ObjectDoesNotExist:
return create_error_response("Guild Does not Exist",
status=status.HTTP_404_NOT_FOUND)
else:
return create_role_success_response(role,
status=status.HTTP_200_OK)
def put(self, request, guild_id, id, format=None):
role = Role.get_role_by_id(guild_id, id)
if role:
data = dict(request.data)
role = role.update_role(data)
return create_role_success_response(role,
status=status.HTTP_202_ACCEPTED)
else:
return create_error_response('Guild Does Not Exist',
status=status.HTTP_404_NOT_FOUND)

30
geeksbot_web/manage.py Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "geeksbot_web.config.settings.local")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django # noqa
except ImportError:
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?"
)
raise
# This allows easy placement of apps within the interior
# geeksbot_web directory.
current_path = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_path, "geeksbot_web"))
execute_from_command_line(sys.argv)

View File

View File

@ -0,0 +1,9 @@
from django.contrib import admin
from .models import PatreonCreator
from .models import PatreonTier
# Register your models here.
admin.site.register(PatreonCreator)
admin.site.register(PatreonTier)

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class PatreonConfig(AppConfig):
name = 'geeksbot_web.patreon'
verbose_name = _("Patreon")

View File

@ -0,0 +1,37 @@
# Generated by Django 2.2.4 on 2019-09-20 21:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('guilds', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='PatreonCreator',
fields=[
('creator', models.CharField(max_length=50, primary_key=True, serialize=False)),
('link', models.CharField(max_length=100, unique=True)),
('guilds', models.ManyToManyField(to='guilds.Guild')),
],
),
migrations.CreateModel(
name='PatreonTier',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('description', models.TextField()),
('amount', models.IntegerField(null=True)),
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='patreon.PatreonCreator')),
('guild', models.ManyToManyField(to='guilds.Guild')),
('next_lower_tier', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='patreon.PatreonTier')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Role')),
],
),
]

View File

@ -0,0 +1,164 @@
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import status
from geeksbot_web.guilds.models import Guild
from geeksbot_web.guilds.models import Role
from .utils import create_error_response
from .utils import create_success_creator_response
from .utils import create_success_tier_response
# Create your models here.
class PatreonCreator(models.Model):
guilds = models.ManyToManyField(Guild)
creator = models.CharField(max_length=50, null=False, primary_key=True)
link = models.CharField(max_length=100, null=False, unique=True)
def update_creator(self, data):
if data.get('guild'):
guild = Guild.get_guild_by_id(data.get('guild'))
if not isinstance(guild, Guild):
return create_error_response('Guild Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
self.guilds.add(guild)
if data.get('link'):
self.link = data.get('link')
self.save()
return create_success_creator_response(self, status.HTTP_202_ACCEPTED, many=False)
@classmethod
def add_new_creator(cls, data):
creator = data.get('creator')
if PatreonCreator.get_creator_by_name(creator):
return create_error_response('That Creator already exists',
status=status.HTTP_409_CONFLICT)
link = data.get('link')
if not (creator and link):
return create_error_response('Creator and Link are both required fields',
status=status.HTTP_400_BAD_REQUEST)
guild = Guild.get_guild_by_id(data.get('guild'))
if not guild:
return create_error_response('A Valid Guild is required',
status=status.HTTP_400_BAD_REQUEST)
new_creator = cls(
creator=creator,
link=link
)
new_creator.save()
new_creator.guilds.add(guild)
return create_success_creator_response(new_creator, status.HTTP_201_CREATED, many=False)
@classmethod
def get_creator_by_name(cls, name):
try:
return cls.objects.get(creator=name)
except ObjectDoesNotExist:
return None
def __str__(self):
return f"{self.guild.id} | {self.creator}"
class PatreonTier(models.Model):
creator = models.ForeignKey(PatreonCreator, on_delete=models.CASCADE)
guild = models.ManyToManyField(Guild)
name = models.CharField(max_length=50)
description = models.TextField()
role = models.ForeignKey(Role, on_delete=models.CASCADE)
amount = models.IntegerField(null=True)
next_lower_tier = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True)
def update_tier(self, data):
if data.get('guild'):
guild = Guild.get_guild_by_id(data.get('guild'))
if not isinstance(guild, Guild):
return create_error_response('Guild Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
self.guilds.add(guild)
if data.get('name'):
self.name = data.get('name')
if data.get('description'):
self.description = data.get('description')
if data.get('role'):
role = Role.get_role_by_id(data.get('role'))
if not isinstance(role, Role):
return create_error_response('Role Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
self.role = role
if data.get('amount'):
self.amount = data.get('amount')
if data.get('next_lower_tier'):
tier = self.get_tier_by_id(data.get('next_lower_tier'))
if not isinstance(tier, self.__class__):
return create_error_response('Next Lower Tier Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
self.next_lower_tier = tier
self.save()
return create_success_tier_response(tier, status.HTTP_202_ACCEPTED, many=False)
@classmethod
def get_tier_by_id(cls, id):
try:
return cls.objects.get(id=id)
except ObjectDoesNotExist:
return None
@classmethod
def add_new_tier(cls, data):
creator_str = data.get('creator')
guild_id = data.get('guild')
name = data.get('name')
description = data.get('description')
role_id = data.get('role')
next_lower_tier_id = data.get('next_lower_tier')
if not (creator_str and guild_id and name and description and role_id):
return create_error_response("The Creator's name, Guild, Tier name, Description, "
"and Discord Role are all required.",
status=status.HTTP_400_BAD_REQUEST)
creator = PatreonCreator.get_creator_by_name(creator_str)
if not isinstance(creator, PatreonCreator):
return create_error_response("Creator Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
guild = Guild.get_guild_by_id(guild_id)
if not isinstance(guild, Guild):
return create_error_response("Guild Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
role = Role.get_role_by_id(role_id)
if not isinstance(role, Role):
return create_error_response("Role Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
if next_lower_tier_id:
next_lower_tier = cls.get_tier_by_id(next_lower_tier_id)
if not isinstance(next_lower_tier, cls):
return create_error_response("Next Lower Tier Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
if next_lower_tier.guild != guild:
return create_error_response("The given next lower tier is not for the same guild.",
status=status.HTTP_400_BAD_REQUEST)
if next_lower_tier.creator != creator:
return create_error_response("The given next lower tier is not for the same creator.",
status=status.HTTP_400_BAD_REQUEST)
try:
PatreonTier.objects.filter(creator=creator, guilds__id=guild.id).get(name=name)
except ObjectDoesNotExist:
tier = cls(
creator=creator,
name=name,
description=description,
role=role,
amount=data.get('amount'),
next_lower_tier=next_lower_tier if next_lower_tier_id else None
)
tier.save()
return create_success_tier_response(tier, status.HTTP_201_CREATED, many=False)
else:
return create_error_response("A Tier with that name already exists for that creator in this guild.",
status=status.HTTP_409_CONFLICT)
def __str__(self):
return f"{self.guild.id} | {self.creator.creator} | {self.name}"

View File

@ -0,0 +1,77 @@
import discord
import gspread
from oauth2client.service_account import ServiceAccountCredentials
class Patron:
def __init__(self, *, discord_name: str=None, steam_id: int=None, patreon_tier: str=None, patron_of: str=None,
discord_discrim: int=None, discord_id: int=None, patreon_name: str=None, steam_name: str=None):
self.discord_name = discord_name
self.discord_discrim = discord_discrim
self.steam_id = steam_id
self.discord_id = discord_id
self.patreon_tier = patreon_tier
self.patron_of = patron_of
self.patreon_name = patreon_name
self.steam_name = steam_name
@classmethod
async def from_id(cls, bot, steam_id: int, *, discord_id: int=None):
scope = ['https://spreadsheets.google.com/feeds',
'https://www.googleapis.com/auth/drive']
credentials = ServiceAccountCredentials.from_json_keyfile_dict(bot.google_secret, scope)
gc = gspread.authorize(credentials)
sh = gc.open_by_key(bot.bot_secrets['sheet'])
ws = sh.worksheet('Current Whitelist')
try:
cell = ws.find(f'{steam_id}')
except gspread.CellNotFound:
return -1
else:
steam_name = None
if discord_id:
user_ref = bot.fs_db.document(f'users/{discord_id}')
user_info = (await bot.loop.run_in_executor(bot.tpe, user_ref.get)).to_dict()
if user_info:
steam_name = user_info.get('steam_name')
row = ws.row_values(cell.row)
return cls(patreon_name=row[1],
discord_name=row[2],
steam_id=row[5],
patreon_tier=row[4].split(' (')[1].strip(')') if len(row[4].split(' (')) > 1 else row[4],
patron_of=row[3].split(' (')[0],
discord_id=discord_id,
steam_name=steam_name)
@classmethod
async def from_name(cls, bot, discord_name: discord.Member, *, discord_id: int=None):
scope = ['https://spreadsheets.google.com/feeds',
'https://www.googleapis.com/auth/drive']
credentials = ServiceAccountCredentials.from_json_keyfile_dict(bot.google_secret, scope)
gc = gspread.authorize(credentials)
sh = gc.open_by_key(bot.bot_secrets['sheet'])
ws = sh.worksheet('Current Whitelist')
try:
cell = ws.find(f'{discord_name.name if isinstance(discord_name, discord.Member) else discord_name}')
except gspread.CellNotFound:
try:
cell = ws.find(f'{discord_name.nick if isinstance(discord_name, discord.Member) else discord_name}')
except gspread.CellNotFound:
return -1
steam_name = None
discord_id = discord_name.id if isinstance(discord_name, discord.Member) else discord_id
if discord_id:
user_ref = bot.fs_db.document(f'users/{discord_id}')
user_info = (await bot.loop.run_in_executor(bot.tpe, user_ref.get)).to_dict()
if user_info:
steam_name = user_info.get('steam_name')
row = ws.row_values(cell.row)
return cls(patreon_name=row[1],
discord_name=row[2],
discord_id=discord_id,
steam_id=row[5],
patreon_tier=row[4].split(' (')[1].strip(')') if len(row[4].split(' (')) > 1 else row[4],
patron_of=row[3].split(' (')[0],
steam_name=steam_name)

View File

@ -0,0 +1,16 @@
from rest_framework import serializers
from geeksbot_web.patreon.models import PatreonCreator
from geeksbot_web.patreon.models import PatreonTier
class PatreonCreatorSerializer(serializers.ModelSerializer):
class Meta:
model = PatreonCreator
fields = "__all__"
class PatreonTierSerializer(serializers.ModelSerializer):
class Meta:
model = PatreonTier
fields = "__all__"

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,21 @@
from rest_framework.response import Response
from rest_framework import status
def create_error_response(msg, status=status.HTTP_404_NOT_FOUND):
return Response({'details': msg},
status=status)
def create_success_creator_response(creator_data, status, many: bool = False):
from .serializers import PatreonCreatorSerializer
return Response(PatreonCreatorSerializer(creator_data, many=many).data,
status=status)
def create_success_tier_response(tier_data, status, many: bool = False):
from .serializers import PatreonTierSerializer
return Response(PatreonTierSerializer(tier_data, many=many).data,
status=status)

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

View File

@ -0,0 +1,6 @@
from django.contrib import admin
from .models import RconServer
# Register your models here.
admin.site.register(RconServer)

View File

@ -0,0 +1,10 @@
from django.urls import path
from .views import RCONServersAPI, RCONServerDetailAPI, ListPlayers
app_name = "rcon_api"
urlpatterns = [
path("<str:guild_id>/", view=RCONServersAPI.as_view(), name='guild_servers'),
path("<str:guild_id>/<str:name>/", view=RCONServerDetailAPI.as_view(), name="server_detail"),
path("<str:guild_id>/<str:name>/listplayers", view=ListPlayers.as_view(), name='listplayers'),
]

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class RconConfig(AppConfig):
name = 'geeksbot_web.rcon'
verbose_name = _("Rcon")

View File

@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-09-20 21:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('guilds', '0001_initial'),
('dmessages', '0001_initial'),
('channels', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='RconServer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('ip', models.GenericIPAddressField()),
('port', models.PositiveIntegerField()),
('password', models.CharField(max_length=50)),
('monitor_chat', models.BooleanField()),
('alerts_channel', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='channels.Channel')),
('guild', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='guilds.Guild')),
('info_channel', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='channels.Channel')),
('info_message', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='dmessages.Message')),
('monitor_chat_channel', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='channels.Channel')),
('settings_message', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='dmessages.Message')),
],
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.4 on 2019-09-20 21:39
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('rcon', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='rconserver',
name='whitelist',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
),
]

View File

128
geeksbot_web/rcon/models.py Normal file
View File

@ -0,0 +1,128 @@
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import status
from geeksbot_web.guilds.models import Guild
from geeksbot_web.dmessages.models import Message
from geeksbot_web.users.models import User
from geeksbot_web.channels.models import Channel
from .utils import create_error_response
from .utils import create_success_response
# Create your models here.
class RconServer(models.Model):
guild = models.ForeignKey(Guild, on_delete=models.CASCADE)
name = models.CharField(max_length=50)
ip = models.GenericIPAddressField()
port = models.PositiveIntegerField()
password = models.CharField(max_length=50)
monitor_chat = models.BooleanField()
monitor_chat_channel = models.ForeignKey(
Channel, on_delete=models.DO_NOTHING, related_name="+", null=True, blank=True, default=None
)
alerts_channel = models.ForeignKey(
Channel, on_delete=models.DO_NOTHING, related_name="+", null=True, blank=True, default=None
)
info_channel = models.ForeignKey(
Channel, on_delete=models.DO_NOTHING, related_name="+", null=True, blank=True, default=None
)
info_message = models.ForeignKey(
Message, on_delete=models.DO_NOTHING, related_name="+", null=True, blank=True, default=None
)
settings_message = models.ForeignKey(
Message, on_delete=models.DO_NOTHING, related_name="+", null=True, blank=True, default=None
)
whitelist = models.ManyToManyField(User, blank=True)
def update_server(self, data):
if data.get('name'):
self.name = data.get('name')
if data.get('ip'):
self.ip = data.get('ip')
if data.get('port'):
self.port = data.get('port')
if data.get('password'):
self.password = data.get('password')
if data.get('monitor_chat'):
self.monitor_chat = data.get('monitor_chat')
if 'monitor_chat_channel' in data.keys():
self.monitor_chat_channel = Channel.get_channel_by_id(data.get('monitor_chat_channel'))
if 'alerts_channel' in data.keys():
self.alerts_channel = Channel.get_channel_by_id(data.get('alerts_channel'))
if 'info_channel' in data.keys():
self.alerts_channel = Channel.get_channel_by_id(data.get('info_channel'))
if 'info_message' in data.keys():
self.info_message = Message.get_message_by_id(data.get('info_message'))
if 'settings_message' in data.keys():
self.settings_message = Message.get_message_by_id(data.get('settings_message'))
self.save()
return create_success_response(self, status.HTTP_202_ACCEPTED, many=False)
def add_whitelist(self, user_id):
user = User.get_user_by_id(user_id)
if not isinstance(user, User):
return create_error_response("User Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
if not user.steam_id:
return create_error_response("User does not have a Steam 64ID attached to their account",
status=status.HTTP_406_NOT_ACCEPTABLE)
self.whitelist.add(user)
return create_error_response("User has been added to the whitelist",
status=status.HTTP_200_OK)
def remove_from_whitelist(self, user_id):
user = User.get_user_by_id(user_id)
if not isinstance(user, User):
return create_error_response("User Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
self.whitelist.remove(user)
return create_error_response("User has been removed from the whitelist",
status=status.HTTP_200_OK)
@classmethod
def add_new_server(cls, data):
guild_id = data.get('guild')
name = data.get('name')
ip = data.get('ip')
port = data.get('port')
password = data.get('password')
if not (guild_id and name and ip and port and password):
return create_error_response("One or more of the required fields are missing",
status=status.HTTP_400_BAD_REQUEST)
guild = Guild.get_guild_by_id(guild_id)
if not isinstance(guild, Guild):
return create_error_response("Guild Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
server = cls(
guild=guild,
name=name,
ip=ip,
port=port,
password=password,
monitor_chat=data.get('monitor_chat', False)
)
server.save()
return create_success_response(server, status.HTTP_201_CREATED, many=False)
@classmethod
def get_server(cls, guild_id, name):
guild_servers = cls.get_guild_servers(guild_id)
if guild_servers:
try:
return guild_servers.get(name=name)
except ObjectDoesNotExist:
return None
return None
@classmethod
def get_guild_servers(cls, guild_id):
guild = Guild.get_guild_by_id(guild_id)
if not isinstance(guild, Guild):
return None
return cls.objects.filter(guild=guild)
def __str__(self):
return f"{self.guild.id} | {self.name}"

View File

View File

@ -0,0 +1,118 @@
from . import rcon
import asyncio
from typing import Union
import logging
arcon_log = logging.getLogger('arcon_lib')
class ARKServer(rcon.RCONConnection):
def __init__(self, *args, monitor_chat: bool=False, server_chat_channel: int=None,
server_messages_channel: int=None, **kwargs):
self.monitor_chat = monitor_chat
self.server_chat_channel = server_chat_channel
self.server_messages_channel = server_messages_channel
super().__init__(*args, **kwargs)
async def run_command(self, command: str, multi_packet: bool=False, reconnect_counter: int=0) \
-> Union[rcon.RCONPacket, str]:
arcon_log.debug(f'Command requested: {command}')
if self.authenticated:
packet = rcon.RCONPacket(next(self.packet_id), rcon.SERVERDATA_EXECCOMMAND, command)
with await self.lock:
try:
arcon_log.debug(f'Sending packet {packet.packet_id}')
await self.send_packet(packet)
arcon_log.debug(f'Packet Sent.')
except ConnectionResetError:
arcon_log.info(f'Connection to {self.host}:{self.port} lost, Reconnecting...')
self.lock.release()
await self._reconnect_and_resend(packet)
await self.lock.acquire()
finally:
arcon_log.debug(f'Waiting for response to packet {packet.packet_id}')
try:
response = await self.read(packet, multi_packet=multi_packet)
except asyncio.TimeoutError as e:
if reconnect_counter > 5:
return 'Reached max reconnects. Closing connection.'
arcon_log.warning(f'No response received: {e}\nAttempting to reconnect #{reconnect_counter}')
self.lock.release()
await self._reconnect()
await self.lock.acquire()
response = await self.run_command(command=command, multi_packet=multi_packet,
reconnect_counter=reconnect_counter + 1)
arcon_log.debug(f'Response Received:\n{response.packet_type}:{response.packet_id}:{response.body}')
response.body = response.body.strip('\x00\x00').strip()
return response
else:
return 'Server is not Authenticated. Please let the Admin know of this issue.'
async def getchat(self) -> str:
response = await self.run_command(command='getchat', multi_packet=True)
return response.body if isinstance(response, rcon.RCONPacket) else response
async def saveworld(self) -> str:
response = await self.run_command(command='saveworld')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def serverchat(self, message: str) -> str:
response = await self.run_command(command=f'serverchat {message}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def broadcast(self, message: str) -> str:
response = await self.run_command(command=f'broadcast {message}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def listplayers(self) -> str:
response = await self.run_command(command=f'listplayers')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def whitelist(self, steam_id: str) -> str:
response = await self.run_command(command=f'AllowPlayerToJoinNoCheck {steam_id}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def ban_player(self, steam_id: int) -> str:
response = await self.run_command(command=f'BanPlayer {steam_id}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def unban_player(self, steam_id: int) -> str:
response = await self.run_command(command=f'UnbanPlayer {steam_id}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def kick_player(self, steam_id: int) -> str:
response = await self.run_command(command=f'KickPlayer {steam_id}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def stop_server(self) -> int:
saved = await self.saveworld()
if saved == 'World Saved':
await self.serverchat(saved)
await asyncio.sleep(10)
response = await self.run_command(command='DoExit')
if response.body == 'Exiting...':
return 0
else:
return 2
else:
return 1
async def get_logs(self):
response = await self.run_command(command=f'GetGameLog', multi_packet=True)
return response.body if isinstance(response, rcon.RCONPacket) else response
async def server_chat_to_steam_id(self, steam_id: int, message: str) -> str:
response = await self.run_command(command=f'ServerChatTo {steam_id} {message}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def server_chat_to_player_name(self, player_name: str, message: str) -> str:
response = await self.run_command(command=f'ServerChatToPlayer "{player_name}" {message}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def set_time_of_day(self, hour: int, minute: int=00, seconds: int=00) -> str:
response = await self.run_command(command=f'SetTimeOfDay {hour}:{minute}:{seconds}')
return response.body if isinstance(response, rcon.RCONPacket) else response
async def destroy_wild_dinos(self):
response = await self.run_command(command='DestroyWildDinos')
return response.body if isinstance(response, rcon.RCONPacket) else response

View File

@ -0,0 +1,183 @@
import asyncio
import logging
import itertools
import struct
# Packet types
SERVERDATA_AUTH = 3
SERVERDATA_AUTH_RESPONSE = 2
SERVERDATA_EXECCOMMAND = 2
SERVERDATA_RESPONSE_VALUE = 0
__all__ = ['RCONPacket', 'RCONConnection']
rcon_log = logging.getLogger('rcon_lib')
class RCONPacket:
def __init__(self, packet_id: int=0, packet_type: int=-1, body: str=''):
self.packet_id = packet_id
self.packet_type = packet_type
self.body = body
def __str__(self):
"""Return the body of the packet"""
return self.body
def size(self):
"""Return the size of the packet"""
return len(self.body) + 10
def pack(self):
"""Return the packed packet"""
return struct.pack(f'<3i{len(self.body) + 2}s',
self.size(),
self.packet_id,
self.packet_type,
bytearray(self.body, 'utf-8'))
class RCONConnection:
"""Connection to an RCON server"""
def __init__(self, host: str, port: int, password: str='', single_packet: bool=False, loop=None):
"""Create a New RCON Connection
Parameters:
host (str): The hostname or IP address of the server to connect to
port (int): The port to connect to on the server
password (str): The password to authenticate with the server
single_packet (bool): True for servers who don't give 0 length SERVERDATA_RESPONSE_VALUE requests
"""
self.host = host
self.port = port
self.password = password
self.single_packet = single_packet
self.packet_id = itertools.count(1)
self.loop = loop or asyncio.get_event_loop()
self.reader = None
self.writer = None
self.lock = asyncio.Lock()
self.authenticated = False
async def connect(self):
"""Returns -1 if connection times out
Returns 1 if connection and auth are successful
Returns 0 if auth fails"""
try:
rcon_log.debug(f'Connecting to {self.host}:{self.port}...')
self.reader, self.writer = await asyncio.open_connection(self.host, self.port, loop=self.loop)
except TimeoutError as e:
rcon_log.error(f'Timeout error: {e}')
return -1
else:
rcon_log.debug('Connected. Attempting to Authenticate...')
auth_packet = RCONPacket(next(self.packet_id), SERVERDATA_AUTH, self.password)
with await self.lock:
await self.send_packet(auth_packet)
response = await self.read()
if response.packet_type == SERVERDATA_AUTH_RESPONSE and response.packet_id != -1:
rcon_log.debug(f'Authorized {response.packet_type}:{response.packet_id}:{response.body}')
self.authenticated = True
return 1
else:
rcon_log.debug(f'Not Authorized {response.packet_type}:{response.packet_id}:{response.body}')
self.authenticated = False
return 0
async def _reconnect(self):
self.writer = None
self.reader = None
connected = await self.connect()
rcon_log.info(f'Connection completed with a return of {connected}')
if connected != -1:
rcon_log.info('Connected')
else:
rcon_log.warning('Connection Failed')
return connected
async def _reconnect_and_resend(self, packet):
connected = await self._reconnect()
if connected != -1:
await asyncio.sleep(0.1)
rcon_log.info(f'Re-sending packet {packet.packet_id}')
await self.send_packet(packet)
rcon_log.info(f'Packet Sent.')
return connected
else:
return connected
async def keep_alive(self):
while True:
await asyncio.sleep(60)
ka_packet = RCONPacket(next(self.packet_id), SERVERDATA_EXECCOMMAND, '')
try:
with await self.lock:
await asyncio.wait_for(self.send_packet(ka_packet), 10, loop=self.loop)
await asyncio.wait_for(self.read(ka_packet), 10, loop=self.loop)
except asyncio.TimeoutError:
self.reader = None
self.writer = None
await self.connect()
async def send_packet(self, packet):
if packet.size() > 4096:
rcon_log.error('Packet Size is larger than 4096 bytes. Cannot send packet.')
raise RuntimeWarning('Packet Size is larger than 4096 bytes. Cannot send packet.')
if self.writer is None:
await self.connect()
rcon_log.debug(f'Sending Packet {packet.packet_id}: {packet.pack() if packet.packet_type is not SERVERDATA_AUTH else "Censored for Password Security."}')
self.writer.write(packet.pack())
await self.writer.drain()
rcon_log.debug(f'Packet {packet.packet_id} Sent.')
async def read(self, request: RCONPacket=None, multi_packet=False) -> RCONPacket:
rcon_log.debug(f'Waiting to receive response to packet {request.packet_id if request else None}')
response = RCONPacket()
try:
if request:
while response.packet_id != request.packet_id and response.packet_id < request.packet_id:
if multi_packet:
if request is None:
rcon_log.warning('A request packet is required to receive a multi packet response')
raise ValueError('A request packet is required to receive a multi packet response')
await asyncio.sleep(.01)
response = await self._receive_multi_packet()
rcon_log.debug(f'Received Multi-Packet response to packet {request.packet_id}:\n'
f'{response.packet_type}:{response.packet_id}:{response.body}')
else:
response = await self.receive_packet()
rcon_log.debug(f'Received Single-Packet response to packet {request.packet_id}:\n'
f'{response.packet_type}:{response.packet_id}:{response.body}')
else:
response = await self.receive_packet()
rcon_log.debug(f'Received Single-Packet response:\n'
f'{response.packet_type}:{response.packet_id}:{response.body}')
except struct.error as e:
rcon_log.error(f'Struct Error: {e}')
response = RCONPacket(body='Error receiving data from the server. Attempting to reconnect. '
'Please try again in a little bit.')
self.lock.release()
await self._reconnect()
await self.lock.acquire()
except AttributeError as e:
rcon_log.error(f'Attribute Error: {e}')
response = RCONPacket(body='Error receiving data from the server. Attempting to reconnect. '
'Please try again in a little bit.')
self.lock.release()
await self._reconnect()
await self.lock.acquire()
return response
async def receive_packet(self):
header = await self.reader.read(struct.calcsize('<3i'))
(packet_size, packet_id, packet_type) = struct.unpack('<3i', header)
body = await self.reader.read(packet_size - 8)
return RCONPacket(packet_id, packet_type, body.decode('ascii'))
async def _receive_multi_packet(self):
header = await self.reader.read(struct.calcsize('<3i'))
(packet_size, packet_id, packet_type) = struct.unpack('<3i', header)
body = await self.reader.readuntil(separator=b'\x00\x00')
return RCONPacket(packet_id, packet_type, body.decode('ascii'))

View File

@ -0,0 +1,9 @@
from rest_framework import serializers
from geeksbot_web.rcon.models import RconServer
class RconServerSerializer(serializers.ModelSerializer):
class Meta:
model = RconServer
fields = "__all__"

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,19 @@
from rest_framework.response import Response
from rest_framework import status
def create_error_response(msg, status=status.HTTP_404_NOT_FOUND):
return Response({'details': msg},
status=status)
def create_success_response(rcon_data, status, many: bool = False):
from .serializers import RconServerSerializer
return Response(RconServerSerializer(rcon_data, many=many).data,
status=status)
def create_rcon_response(message, status):
msg_list = message.split('\n')
return Response(msg_list, status=status)

View File

@ -0,0 +1,78 @@
import asyncio
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from .rcon_lib import arcon
from .models import RconServer
from .utils import create_error_response, create_success_response, create_rcon_response
from geeksbot_web.utils.api_utils import PaginatedAPIView
from .serializers import RconServerSerializer
# Create your views here.
# API Views
class RCONServersAPI(PaginatedAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, format=None):
servers = RconServer.get_guild_servers(guild_id)
page = self.paginate_queryset(servers)
if page:
return create_success_response(page, status.HTTP_200_OK, many=True)
return create_success_response(servers, status.HTTP_200_OK, many=True)
def post(self, request, guild_id, format=None):
data = dict(request.data)
data['guild'] = guild_id
return RconServer.add_new_server(data)
class RCONServerDetailAPI(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, name, format=None):
server = RconServer.get_server(guild_id, name)
if server:
return create_success_response(server, status.HTTP_200_OK, many=False)
else:
return create_error_response("RCON Server Does Not Exist",
status=status.HTTP_404_NOT_FOUND)
def put(self, request, guild_id, name, format=None):
data = dict(request.data)
server = RconServer.get_server(guild_id, name)
if server:
return server.update_server(data)
else:
return create_error_response('RCON Server Does Not Exist',
status=status.HTTP_404_NOT_FOUND)
class ListPlayers(PaginatedAPIView):
permission_classes = [IsAuthenticated]
def get(self, request, guild_id, name, format=None):
server: RconServer = RconServer.get_server(guild_id, name)
if server:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop = asyncio.get_event_loop()
ark = arcon.ARKServer(host=server.ip, port=server.port, password=server.password, loop=loop)
connected = loop.run_until_complete(ark.connect())
if connected == 1:
resp = loop.run_until_complete(ark.listplayers())
if resp == 'No Players Connected':
return create_rcon_response(resp, status=status.HTTP_204_NO_CONTENT)
else:
return create_rcon_response(resp, status=status.HTTP_200_OK)
else:
return create_error_response('Connection failure',
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return create_error_response('RCON Server Does Not Exist',
status=status.HTTP_404_NOT_FOUND)

View File

@ -0,0 +1,74 @@
from src.shared_libs.guid import Guid
from src.shared_libs.TicTacToe.player import Player
class Board:
def __init__(self):
self.id = Guid()
self.board = [[' ', ' ', ' '],
[' ', ' ', ' '],
[' ', ' ', ' ']]
self.history = []
self.winner = False
self.draw = False
self.play_count = 0
self.remaining_moves = [1, 2, 3, 4, 5, 6, 7, 8, 9]
self.winning_states = [[(0, 0), (0, 1), (0, 2)],
[(0, 0), (1, 1), (2, 2)],
[(1, 0), (1, 1), (1, 2)],
[(2, 0), (2, 1), (2, 2)],
[(0, 0), (1, 0), (2, 0)],
[(0, 1), (1, 1), (2, 1)],
[(0, 2), (1, 2), (2, 2)],
[(2, 0), (1, 1), (0, 2)]]
def __repr__(self):
return f'<TicTacToe Board id="{self.id}">'
def __str__(self):
return '┌───┬───┬───┐\n' \
'{0[0][0]}{0[0][1]}{0[0][2]}\n' \
'├───┼───┼───┤\n' \
'{0[1][0]}{0[1][1]}{0[1][2]}\n' \
'├───┼───┼───┤\n' \
'{0[2][0]}{0[2][1]}{0[2][2]}\n' \
'└───┴───┴───┘\n'.format(self.board)
def make_play(self, player: Player, position: int):
assert 1 <= position <= 9
assert isinstance(player, Player)
move = ((position - 1) // 3, (position - 1) % 3)
if not self.board[move[0]][move[1]] == ' ':
raise Warning("That cell is already taken. Please try again.")
self.history.append(self.board)
self.board[move[0]][move[1]] = player
self.play_count += 1
self.winner = self.check_winner()
self.draw = self.check_draw()
self.remaining_moves.remove(position)
def check_winner(self):
for state in self.winning_states:
if (self.board[state[0][0]][state[0][1]] ==
self.board[state[1][0]][state[1][1]] ==
self.board[state[2][0]][state[2][1]]) and \
self.board[state[0][0]][state[0][1]] != ' ':
return self.board[state[0][0]][state[0][1]]
return False
def check_draw(self):
for row in self.board:
for cell in row:
if cell == ' ':
return False
else:
return True
def clear(self):
self.board = [[' ', ' ', ' '],
[' ', ' ', ' '],
[' ', ' ', ' ']]
self.history = []
self.winner = False
self.play_count = 0

View File

@ -0,0 +1,195 @@
from src.shared_libs.guid import Guid
import random
from copy import deepcopy
__all__ = ['Player', 'AIPlayer']
class Player:
def __init__(self, token: str, *, name: str=None, id: str=None, discord_id: int=None):
if len(token) != 1:
raise Warning('Token must be exactly one character long.')
self.token = token
self.name = name or f'Player {self.token}'
self.id = id or Guid()
self.starting_player = False
self.discord_id = discord_id
def __repr__(self):
return f'<TicTacToe Player name="{self.name}" id="{self.id}">'
def __str__(self):
return self.token
def __eq__(self, other):
if isinstance(other, Player) and other.id == self.id:
return True
elif isinstance(other, str):
return self.token == other
class AIPlayer(Player):
def __init__(self, token: str=None, name: str=None, human: Player=None, *, id: str=None):
token = token or '🇽'
if human:
if human.token == token and human.token != '🇴':
token = '🇴'
elif human.token == '🇴':
token = '🇽'
super().__init__(token, name=name or f'Robot {token}', id=id)
self._corner_moves = [1, 3, 7, 9]
self._side_moves = [2, 4, 6, 8]
self._center_move = 5
self.remaining_corners = deepcopy(self._corner_moves)
self.remaining_sides = deepcopy(self._side_moves)
def make_selection(self, board, last_play: int=None) -> int:
if last_play in self.remaining_corners:
self.remaining_corners.remove(last_play)
elif last_play in self.remaining_sides:
self.remaining_sides.remove(last_play)
winning_move = self.check_winning_move(board)
if winning_move:
move = winning_move
else:
blocking_move = self.check_blocking_move(board)
if blocking_move:
move = blocking_move
else:
trap_move = self.attempt_trap(board)
if trap_move:
move = trap_move
else:
starting_move = self.starting_strategy(board)
if self.starting_player and starting_move:
move = starting_move
else:
if board.board[1][1] == ' ':
move = 5
else:
if self.check_corner_trap(board):
move = random.choice(self.remaining_sides)
else:
if self.remaining_corners:
move = random.choice(self.remaining_corners)
else:
move = random.choice(self.remaining_sides)
if move in self.remaining_corners:
self.remaining_corners.remove(move)
elif move in self.remaining_sides:
self.remaining_sides.remove(move)
print(move)
return move
def starting_strategy(self, board):
move = False
if board.play_count == 0:
move = random.choice(self.remaining_corners)
self.remaining_corners.remove(move)
elif board.play_count == 2:
if (board.board[0][0] == self and ' ' != board.board[2][2] != self) \
or (board.board[2][2] == self and ' ' != board.board[0][0] != self) \
or (board.board[2][0] == self and ' ' != board.board[0][2] != self) \
or (board.board[0][2] == self and ' ' != board.board[2][0] != self):
move = random.choice(self.remaining_corners)
else:
if board.board[0][0] == self:
move = 9
elif board.board[2][2] == self:
move = 1
elif board.board[0][2] == self:
move = 7
elif board.board[2][0] == self:
move = 3
self.remaining_corners.remove(move)
elif board.play_count == 4 and self.remaining_corners:
move = random.choice(self.remaining_corners)
self.remaining_corners.remove(move)
return move
def check_corner_trap(self, board):
if ' ' != board.board[0][0] == board.board[2][2] != self:
return True
elif ' ' != board.board[0][2] == board.board[2][0] != self:
return True
return False
def check_blocking_move(self, board):
for position in board.winning_states:
if ' ' != board.board[position[0][0]][position[0][1]] == \
board.board[position[1][0]][position[1][1]] != self \
and board.board[position[2][0]][position[2][1]] == ' ':
return ((position[2][0] * 3) + position[2][1]) + 1
elif ' ' != board.board[position[0][0]][position[0][1]] == \
board.board[position[2][0]][position[2][1]] != self \
and board.board[position[1][0]][position[1][1]] == ' ':
return ((position[1][0] * 3) + position[1][1]) + 1
elif ' ' != board.board[position[2][0]][position[2][1]] == \
board.board[position[1][0]][position[1][1]] != self \
and board.board[position[0][0]][position[0][1]] == ' ':
return ((position[0][0] * 3) + position[0][1]) + 1
return False
def check_winning_move(self, board):
for position in board.winning_states:
if board.board[position[0][0]][position[0][1]] == board.board[position[1][0]][position[1][1]] == self \
and board.board[position[2][0]][position[2][1]] == ' ':
return ((position[2][0] * 3) + position[2][1]) + 1
elif board.board[position[0][0]][position[0][1]] == board.board[position[2][0]][position[2][1]] == self \
and board.board[position[1][0]][position[1][1]] == ' ':
return ((position[1][0] * 3) + position[1][1]) + 1
elif board.board[position[2][0]][position[2][1]] == board.board[position[1][0]][position[1][1]] == self \
and board.board[position[0][0]][position[0][1]] == ' ':
return ((position[0][0] * 3) + position[0][1]) + 1
return False
def attempt_trap(self, board):
if board.board[1][1] == self:
if board.board[0][0] == self and \
board.board[0][1] == ' ' and \
board.board[0][2] == ' ' and \
board.board[2][0] == ' ':
return 3
elif board.board[0][0] == self and \
board.board[1][0] == ' ' and \
board.board[0][2] == ' ' and \
board.board[2][0] == ' ':
return 7
elif board.board[0][2] == self and \
board.board[0][1] == ' ' and \
board.board[0][0] == ' ' and \
board.board[2][2] == ' ':
return 1
elif board.board[0][2] == self and \
board.board[1][2] == ' ' and \
board.board[0][0] == ' ' and \
board.board[2][2] == ' ':
return 9
elif board.board[2][0] == self and \
board.board[0][0] == ' ' and \
board.board[0][1] == ' ' and \
board.board[2][2] == ' ':
return 1
elif board.board[2][0] == self and \
board.board[2][1] == ' ' and \
board.board[2][2] == ' ' and \
board.board[0][0] == ' ':
return 9
elif board.board[2][2] == self and \
board.board[2][1] == ' ' and \
board.board[2][0] == ' ' and \
board.board[0][2] == ' ':
return 7
elif board.board[2][2] == self and \
board.board[1][2] == ' ' and \
board.board[0][2] == ' ' and \
board.board[2][0] == ' ':
return 3
return False
def reset_game(self):
self.remaining_sides = deepcopy(self._side_moves)
self.remaining_corners = deepcopy(self._corner_moves)
self.starting_player = False

View File

View File

@ -0,0 +1,13 @@
/* These styles are generated from project.scss. */
.alert-debug {
color: black;
background-color: white;
border-color: #d6e9c6;
}
.alert-error {
color: #b94a48;
background-color: #f2dede;
border-color: #eed3d7;
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -0,0 +1 @@
/* Project specific Javascript goes here. */

View File

@ -0,0 +1,37 @@
// project specific CSS goes here
////////////////////////////////
//Variables//
////////////////////////////////
// Alert colors
$white: #fff;
$mint-green: #d6e9c6;
$black: #000;
$pink: #f2dede;
$dark-pink: #eed3d7;
$red: #b94a48;
////////////////////////////////
//Alerts//
////////////////////////////////
// bootstrap alert CSS, translated to the django-standard levels of
// debug, info, success, warning, error
.alert-debug {
background-color: $white;
border-color: $mint-green;
color: $black;
}
.alert-error {
background-color: $pink;
border-color: $dark-pink;
color: $red;
}

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Forbidden (403){% endblock %}
{% block content %}
<h1>Forbidden (403)</h1>
<p>CSRF verification failed. Request aborted.</p>
{% endblock content %}

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Page not found{% endblock %}
{% block content %}
<h1>Page not found</h1>
<p>This is not the page you were looking for.</p>
{% endblock content %}

View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block title %}Server Error{% endblock %}
{% block content %}
<h1>Ooops!!! 500</h1>
<h3>Looks like something went wrong!</h3>
<p>We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.</p>
{% endblock content %}

View File

@ -0,0 +1,12 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Account Inactive" %}{% endblock %}
{% block inner %}
<h1>{% trans "Account Inactive" %}</h1>
<p>{% trans "This account is inactive." %}</p>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %}
{% block content %}
<div class="row">
<div class="col-md-6 offset-md-3">
{% block inner %}{% endblock %}
</div>
</div>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More