Initial Commit importing code from old repo and making changes so it is standalone
This commit is contained in:
parent
953e048d44
commit
0caeba5cbd
336
.gitignore
vendored
Normal file
336
.gitignore
vendored
Normal 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
65
Dockerfile
Normal 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
7
geeksbot_web/__init__.py
Normal 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(".")
|
||||||
|
]
|
||||||
|
)
|
||||||
0
geeksbot_web/channels/__init__.py
Normal file
0
geeksbot_web/channels/__init__.py
Normal file
3
geeksbot_web/channels/admin.py
Normal file
3
geeksbot_web/channels/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
10
geeksbot_web/channels/api_urls.py
Normal file
10
geeksbot_web/channels/api_urls.py
Normal 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')
|
||||||
|
]
|
||||||
7
geeksbot_web/channels/apps.py
Normal file
7
geeksbot_web/channels/apps.py
Normal 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")
|
||||||
23
geeksbot_web/channels/migrations/0001_initial.py
Normal file
23
geeksbot_web/channels/migrations/0001_initial.py
Normal 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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
28
geeksbot_web/channels/migrations/0002_auto_20190921_0250.py
Normal file
28
geeksbot_web/channels/migrations/0002_auto_20190921_0250.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
geeksbot_web/channels/migrations/__init__.py
Normal file
0
geeksbot_web/channels/migrations/__init__.py
Normal file
85
geeksbot_web/channels/models.py
Normal file
85
geeksbot_web/channels/models.py
Normal 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)
|
||||||
9
geeksbot_web/channels/serializers.py
Normal file
9
geeksbot_web/channels/serializers.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .models import Channel
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Channel
|
||||||
|
fields = "__all__"
|
||||||
3
geeksbot_web/channels/tests.py
Normal file
3
geeksbot_web/channels/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
14
geeksbot_web/channels/utils.py
Normal file
14
geeksbot_web/channels/utils.py
Normal 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)
|
||||||
97
geeksbot_web/channels/views.py
Normal file
97
geeksbot_web/channels/views.py
Normal 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)
|
||||||
0
geeksbot_web/config/__init__.py
Normal file
0
geeksbot_web/config/__init__.py
Normal file
0
geeksbot_web/config/settings/__init__.py
Normal file
0
geeksbot_web/config/settings/__init__.py
Normal file
305
geeksbot_web/config/settings/base.py
Normal file
305
geeksbot_web/config/settings/base.py
Normal 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"]
|
||||||
62
geeksbot_web/config/settings/local.py
Normal file
62
geeksbot_web/config/settings/local.py
Normal 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...
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
155
geeksbot_web/config/settings/production.py
Normal file
155
geeksbot_web/config/settings/production.py
Normal 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...
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
53
geeksbot_web/config/settings/test.py
Normal file
53
geeksbot_web/config/settings/test.py
Normal 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...
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
50
geeksbot_web/config/urls.py
Normal file
50
geeksbot_web/config/urls.py
Normal 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
|
||||||
39
geeksbot_web/config/wsgi.py
Normal file
39
geeksbot_web/config/wsgi.py
Normal 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
20
geeksbot_web/conftest.py
Normal 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()
|
||||||
5
geeksbot_web/contrib/__init__.py
Normal file
5
geeksbot_web/contrib/__init__.py
Normal 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
|
||||||
|
"""
|
||||||
5
geeksbot_web/contrib/sites/__init__.py
Normal file
5
geeksbot_web/contrib/sites/__init__.py
Normal 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
|
||||||
|
"""
|
||||||
42
geeksbot_web/contrib/sites/migrations/0001_initial.py
Normal file
42
geeksbot_web/contrib/sites/migrations/0001_initial.py
Normal 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())],
|
||||||
|
)
|
||||||
|
]
|
||||||
@ -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",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
@ -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)]
|
||||||
5
geeksbot_web/contrib/sites/migrations/__init__.py
Normal file
5
geeksbot_web/contrib/sites/migrations/__init__.py
Normal 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
|
||||||
|
"""
|
||||||
0
geeksbot_web/dmessages/__init__.py
Normal file
0
geeksbot_web/dmessages/__init__.py
Normal file
10
geeksbot_web/dmessages/admin.py
Normal file
10
geeksbot_web/dmessages/admin.py
Normal 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)
|
||||||
21
geeksbot_web/dmessages/api_urls.py
Normal file
21
geeksbot_web/dmessages/api_urls.py
Normal 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'),
|
||||||
|
]
|
||||||
7
geeksbot_web/dmessages/apps.py
Normal file
7
geeksbot_web/dmessages/apps.py
Normal 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")
|
||||||
59
geeksbot_web/dmessages/migrations/0001_initial.py
Normal file
59
geeksbot_web/dmessages/migrations/0001_initial.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
95
geeksbot_web/dmessages/migrations/0002_auto_20190920_2139.py
Normal file
95
geeksbot_web/dmessages/migrations/0002_auto_20190920_2139.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
geeksbot_web/dmessages/migrations/0003_auto_20190921_0721.py
Normal file
18
geeksbot_web/dmessages/migrations/0003_auto_20190921_0721.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
geeksbot_web/dmessages/migrations/__init__.py
Normal file
0
geeksbot_web/dmessages/migrations/__init__.py
Normal file
291
geeksbot_web/dmessages/models.py
Normal file
291
geeksbot_web/dmessages/models.py
Normal 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')
|
||||||
30
geeksbot_web/dmessages/serializers.py
Normal file
30
geeksbot_web/dmessages/serializers.py
Normal 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__"
|
||||||
3
geeksbot_web/dmessages/tests.py
Normal file
3
geeksbot_web/dmessages/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
28
geeksbot_web/dmessages/utils.py
Normal file
28
geeksbot_web/dmessages/utils.py
Normal 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)
|
||||||
180
geeksbot_web/dmessages/views.py
Normal file
180
geeksbot_web/dmessages/views.py
Normal 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
46
geeksbot_web/entrypoint
Executable 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
|
||||||
0
geeksbot_web/guilds/__init__.py
Normal file
0
geeksbot_web/guilds/__init__.py
Normal file
8
geeksbot_web/guilds/admin.py
Normal file
8
geeksbot_web/guilds/admin.py
Normal 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)
|
||||||
14
geeksbot_web/guilds/api_urls.py
Normal file
14
geeksbot_web/guilds/api_urls.py
Normal 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'),
|
||||||
|
]
|
||||||
7
geeksbot_web/guilds/apps.py
Normal file
7
geeksbot_web/guilds/apps.py
Normal 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")
|
||||||
35
geeksbot_web/guilds/migrations/0001_initial.py
Normal file
35
geeksbot_web/guilds/migrations/0001_initial.py
Normal 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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
25
geeksbot_web/guilds/migrations/0002_auto_20190921_0250.py
Normal file
25
geeksbot_web/guilds/migrations/0002_auto_20190921_0250.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
||||||
0
geeksbot_web/guilds/migrations/__init__.py
Normal file
0
geeksbot_web/guilds/migrations/__init__.py
Normal file
133
geeksbot_web/guilds/models.py
Normal file
133
geeksbot_web/guilds/models.py
Normal 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}"
|
||||||
8
geeksbot_web/guilds/permissions.py
Normal file
8
geeksbot_web/guilds/permissions.py
Normal 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)
|
||||||
16
geeksbot_web/guilds/serializers.py
Normal file
16
geeksbot_web/guilds/serializers.py
Normal 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"]
|
||||||
3
geeksbot_web/guilds/tests.py
Normal file
3
geeksbot_web/guilds/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
0
geeksbot_web/guilds/urls.py
Normal file
0
geeksbot_web/guilds/urls.py
Normal file
21
geeksbot_web/guilds/utils.py
Normal file
21
geeksbot_web/guilds/utils.py
Normal 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)
|
||||||
133
geeksbot_web/guilds/views.py
Normal file
133
geeksbot_web/guilds/views.py
Normal 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
30
geeksbot_web/manage.py
Executable 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)
|
||||||
0
geeksbot_web/patreon/__init__.py
Normal file
0
geeksbot_web/patreon/__init__.py
Normal file
9
geeksbot_web/patreon/admin.py
Normal file
9
geeksbot_web/patreon/admin.py
Normal 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)
|
||||||
7
geeksbot_web/patreon/apps.py
Normal file
7
geeksbot_web/patreon/apps.py
Normal 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")
|
||||||
37
geeksbot_web/patreon/migrations/0001_initial.py
Normal file
37
geeksbot_web/patreon/migrations/0001_initial.py
Normal 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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
geeksbot_web/patreon/migrations/__init__.py
Normal file
0
geeksbot_web/patreon/migrations/__init__.py
Normal file
164
geeksbot_web/patreon/models.py
Normal file
164
geeksbot_web/patreon/models.py
Normal 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}"
|
||||||
77
geeksbot_web/patreon/patron.py
Normal file
77
geeksbot_web/patreon/patron.py
Normal 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)
|
||||||
16
geeksbot_web/patreon/serializers.py
Normal file
16
geeksbot_web/patreon/serializers.py
Normal 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__"
|
||||||
3
geeksbot_web/patreon/tests.py
Normal file
3
geeksbot_web/patreon/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
21
geeksbot_web/patreon/utils.py
Normal file
21
geeksbot_web/patreon/utils.py
Normal 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)
|
||||||
3
geeksbot_web/patreon/views.py
Normal file
3
geeksbot_web/patreon/views.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
0
geeksbot_web/rcon/__init__.py
Normal file
0
geeksbot_web/rcon/__init__.py
Normal file
6
geeksbot_web/rcon/admin.py
Normal file
6
geeksbot_web/rcon/admin.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import RconServer
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
admin.site.register(RconServer)
|
||||||
10
geeksbot_web/rcon/api_urls.py
Normal file
10
geeksbot_web/rcon/api_urls.py
Normal 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'),
|
||||||
|
]
|
||||||
7
geeksbot_web/rcon/apps.py
Normal file
7
geeksbot_web/rcon/apps.py
Normal 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")
|
||||||
35
geeksbot_web/rcon/migrations/0001_initial.py
Normal file
35
geeksbot_web/rcon/migrations/0001_initial.py
Normal 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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
22
geeksbot_web/rcon/migrations/0002_rconserver_whitelist.py
Normal file
22
geeksbot_web/rcon/migrations/0002_rconserver_whitelist.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
geeksbot_web/rcon/migrations/__init__.py
Normal file
0
geeksbot_web/rcon/migrations/__init__.py
Normal file
128
geeksbot_web/rcon/models.py
Normal file
128
geeksbot_web/rcon/models.py
Normal 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}"
|
||||||
0
geeksbot_web/rcon/rcon_lib/__init__.py
Normal file
0
geeksbot_web/rcon/rcon_lib/__init__.py
Normal file
118
geeksbot_web/rcon/rcon_lib/arcon.py
Normal file
118
geeksbot_web/rcon/rcon_lib/arcon.py
Normal 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
|
||||||
183
geeksbot_web/rcon/rcon_lib/rcon.py
Normal file
183
geeksbot_web/rcon/rcon_lib/rcon.py
Normal 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'))
|
||||||
9
geeksbot_web/rcon/serializers.py
Normal file
9
geeksbot_web/rcon/serializers.py
Normal 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__"
|
||||||
3
geeksbot_web/rcon/tests.py
Normal file
3
geeksbot_web/rcon/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
19
geeksbot_web/rcon/utils.py
Normal file
19
geeksbot_web/rcon/utils.py
Normal 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)
|
||||||
78
geeksbot_web/rcon/views.py
Normal file
78
geeksbot_web/rcon/views.py
Normal 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)
|
||||||
0
geeksbot_web/shared_libs/TicTacToe/__init__.py
Normal file
0
geeksbot_web/shared_libs/TicTacToe/__init__.py
Normal file
74
geeksbot_web/shared_libs/TicTacToe/board.py
Normal file
74
geeksbot_web/shared_libs/TicTacToe/board.py
Normal 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
|
||||||
|
|
||||||
195
geeksbot_web/shared_libs/TicTacToe/player.py
Normal file
195
geeksbot_web/shared_libs/TicTacToe/player.py
Normal 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
|
||||||
0
geeksbot_web/shared_libs/__init__.py
Normal file
0
geeksbot_web/shared_libs/__init__.py
Normal file
13
geeksbot_web/static/css/project.css
Normal file
13
geeksbot_web/static/css/project.css
Normal 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;
|
||||||
|
}
|
||||||
0
geeksbot_web/static/fonts/.gitkeep
Normal file
0
geeksbot_web/static/fonts/.gitkeep
Normal file
BIN
geeksbot_web/static/images/favicons/favicon.ico
Normal file
BIN
geeksbot_web/static/images/favicons/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
1
geeksbot_web/static/js/project.js
Normal file
1
geeksbot_web/static/js/project.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
/* Project specific Javascript goes here. */
|
||||||
0
geeksbot_web/static/sass/custom_bootstrap_vars.scss
Normal file
0
geeksbot_web/static/sass/custom_bootstrap_vars.scss
Normal file
37
geeksbot_web/static/sass/project.scss
Normal file
37
geeksbot_web/static/sass/project.scss
Normal 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;
|
||||||
|
}
|
||||||
9
geeksbot_web/templates/403.html
Normal file
9
geeksbot_web/templates/403.html
Normal 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 %}
|
||||||
9
geeksbot_web/templates/404.html
Normal file
9
geeksbot_web/templates/404.html
Normal 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 %}
|
||||||
13
geeksbot_web/templates/500.html
Normal file
13
geeksbot_web/templates/500.html
Normal 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 %}
|
||||||
|
|
||||||
|
|
||||||
12
geeksbot_web/templates/account/account_inactive.html
Normal file
12
geeksbot_web/templates/account/account_inactive.html
Normal 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 %}
|
||||||
|
|
||||||
10
geeksbot_web/templates/account/base.html
Normal file
10
geeksbot_web/templates/account/base.html
Normal 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
Loading…
x
Reference in New Issue
Block a user