From 0caeba5cbda4f97c724dfa1e05258f4e17833b5a Mon Sep 17 00:00:00 2001 From: Dustin Pianalto Date: Wed, 11 Dec 2019 10:34:25 -0900 Subject: [PATCH] Initial Commit importing code from old repo and making changes so it is standalone --- .gitignore | 336 +++++++++ Dockerfile | 65 ++ geeksbot_web/__init__.py | 7 + geeksbot_web/channels/__init__.py | 0 geeksbot_web/channels/admin.py | 3 + geeksbot_web/channels/api_urls.py | 10 + geeksbot_web/channels/apps.py | 7 + .../channels/migrations/0001_initial.py | 23 + .../migrations/0002_auto_20190921_0250.py | 28 + geeksbot_web/channels/migrations/__init__.py | 0 geeksbot_web/channels/models.py | 85 +++ geeksbot_web/channels/serializers.py | 9 + geeksbot_web/channels/tests.py | 3 + geeksbot_web/channels/utils.py | 14 + geeksbot_web/channels/views.py | 97 +++ geeksbot_web/config/__init__.py | 0 geeksbot_web/config/settings/__init__.py | 0 geeksbot_web/config/settings/base.py | 305 ++++++++ geeksbot_web/config/settings/local.py | 62 ++ geeksbot_web/config/settings/production.py | 155 ++++ geeksbot_web/config/settings/test.py | 53 ++ geeksbot_web/config/urls.py | 50 ++ geeksbot_web/config/wsgi.py | 39 + geeksbot_web/conftest.py | 20 + geeksbot_web/contrib/__init__.py | 5 + geeksbot_web/contrib/sites/__init__.py | 5 + .../contrib/sites/migrations/0001_initial.py | 42 ++ .../migrations/0002_alter_domain_unique.py | 20 + .../0003_set_site_domain_and_name.py | 33 + .../contrib/sites/migrations/__init__.py | 5 + geeksbot_web/dmessages/__init__.py | 0 geeksbot_web/dmessages/admin.py | 10 + geeksbot_web/dmessages/api_urls.py | 21 + geeksbot_web/dmessages/apps.py | 7 + .../dmessages/migrations/0001_initial.py | 59 ++ .../migrations/0002_auto_20190920_2139.py | 95 +++ .../migrations/0003_auto_20190921_0721.py | 18 + geeksbot_web/dmessages/migrations/__init__.py | 0 geeksbot_web/dmessages/models.py | 291 ++++++++ geeksbot_web/dmessages/serializers.py | 30 + geeksbot_web/dmessages/tests.py | 3 + geeksbot_web/dmessages/utils.py | 28 + geeksbot_web/dmessages/views.py | 180 +++++ geeksbot_web/entrypoint | 46 ++ geeksbot_web/guilds/__init__.py | 0 geeksbot_web/guilds/admin.py | 8 + geeksbot_web/guilds/api_urls.py | 14 + geeksbot_web/guilds/apps.py | 7 + .../guilds/migrations/0001_initial.py | 35 + .../migrations/0002_auto_20190921_0250.py | 25 + geeksbot_web/guilds/migrations/__init__.py | 0 geeksbot_web/guilds/models.py | 133 ++++ geeksbot_web/guilds/permissions.py | 8 + geeksbot_web/guilds/serializers.py | 16 + geeksbot_web/guilds/tests.py | 3 + geeksbot_web/guilds/urls.py | 0 geeksbot_web/guilds/utils.py | 21 + geeksbot_web/guilds/views.py | 133 ++++ geeksbot_web/manage.py | 30 + geeksbot_web/patreon/__init__.py | 0 geeksbot_web/patreon/admin.py | 9 + geeksbot_web/patreon/apps.py | 7 + .../patreon/migrations/0001_initial.py | 37 + geeksbot_web/patreon/migrations/__init__.py | 0 geeksbot_web/patreon/models.py | 164 +++++ geeksbot_web/patreon/patron.py | 77 ++ geeksbot_web/patreon/serializers.py | 16 + geeksbot_web/patreon/tests.py | 3 + geeksbot_web/patreon/utils.py | 21 + geeksbot_web/patreon/views.py | 3 + geeksbot_web/rcon/__init__.py | 0 geeksbot_web/rcon/admin.py | 6 + geeksbot_web/rcon/api_urls.py | 10 + geeksbot_web/rcon/apps.py | 7 + geeksbot_web/rcon/migrations/0001_initial.py | 35 + .../migrations/0002_rconserver_whitelist.py | 22 + geeksbot_web/rcon/migrations/__init__.py | 0 geeksbot_web/rcon/models.py | 128 ++++ geeksbot_web/rcon/rcon_lib/__init__.py | 0 geeksbot_web/rcon/rcon_lib/arcon.py | 118 +++ geeksbot_web/rcon/rcon_lib/rcon.py | 183 +++++ geeksbot_web/rcon/serializers.py | 9 + geeksbot_web/rcon/tests.py | 3 + geeksbot_web/rcon/utils.py | 19 + geeksbot_web/rcon/views.py | 78 ++ .../shared_libs/TicTacToe/__init__.py | 0 geeksbot_web/shared_libs/TicTacToe/board.py | 74 ++ geeksbot_web/shared_libs/TicTacToe/player.py | 195 +++++ geeksbot_web/shared_libs/__init__.py | 0 geeksbot_web/static/css/project.css | 13 + geeksbot_web/static/fonts/.gitkeep | 0 .../static/images/favicons/favicon.ico | Bin 0 -> 8348 bytes geeksbot_web/static/js/project.js | 1 + .../static/sass/custom_bootstrap_vars.scss | 0 geeksbot_web/static/sass/project.scss | 37 + geeksbot_web/templates/403.html | 9 + geeksbot_web/templates/404.html | 9 + geeksbot_web/templates/500.html | 13 + .../templates/account/account_inactive.html | 12 + geeksbot_web/templates/account/base.html | 10 + geeksbot_web/templates/account/email.html | 80 ++ .../templates/account/email_confirm.html | 32 + geeksbot_web/templates/account/login.html | 48 ++ geeksbot_web/templates/account/logout.html | 22 + .../templates/account/password_change.html | 17 + .../templates/account/password_reset.html | 26 + .../account/password_reset_done.html | 17 + .../account/password_reset_from_key.html | 25 + .../account/password_reset_from_key_done.html | 10 + .../templates/account/password_set.html | 17 + geeksbot_web/templates/account/signup.html | 23 + .../templates/account/signup_closed.html | 12 + .../templates/account/verification_sent.html | 13 + .../account/verified_email_required.html | 24 + geeksbot_web/templates/base.html | 110 +++ geeksbot_web/templates/pages/about.html | 1 + geeksbot_web/templates/pages/home.html | 1 + geeksbot_web/templates/users/user_detail.html | 37 + geeksbot_web/templates/users/user_form.html | 17 + geeksbot_web/users/__init__.py | 0 geeksbot_web/users/adapters.py | 35 + geeksbot_web/users/admin.py | 17 + geeksbot_web/users/api_urls.py | 11 + geeksbot_web/users/apps.py | 13 + geeksbot_web/users/forms.py | 20 + geeksbot_web/users/migrations/0001_initial.py | 70 ++ geeksbot_web/users/migrations/__init__.py | 0 geeksbot_web/users/models.py | 207 ++++++ geeksbot_web/users/serializers.py | 57 ++ geeksbot_web/users/tests/__init__.py | 0 geeksbot_web/users/tests/factories.py | 27 + geeksbot_web/users/tests/test_forms.py | 40 + geeksbot_web/users/tests/test_models.py | 8 + geeksbot_web/users/tests/test_urls.py | 23 + geeksbot_web/users/tests/test_views.py | 52 ++ geeksbot_web/users/urls.py | 14 + geeksbot_web/users/utils.py | 36 + geeksbot_web/users/views.py | 163 +++++ geeksbot_web/utils/api_utils.py | 33 + geeksbot_web/utils/permissions.py | 27 + locale/README.rst | 6 + requirements/base.txt | 5 + requirements/local.txt | 30 + requirements/production.txt | 4 + requirements/web.txt | 22 + services/Dockerfile-base | 30 + services/geeksbot.conf | 48 ++ services/gunicorn.conf | 10 + services/nginx.conf | 44 ++ services/postgresql/postgres.conf | 690 ++++++++++++++++++ services/supervisor_geeksbot.conf | 15 + services/supervisord.conf | 21 + 152 files changed, 6340 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 geeksbot_web/__init__.py create mode 100644 geeksbot_web/channels/__init__.py create mode 100644 geeksbot_web/channels/admin.py create mode 100644 geeksbot_web/channels/api_urls.py create mode 100644 geeksbot_web/channels/apps.py create mode 100644 geeksbot_web/channels/migrations/0001_initial.py create mode 100644 geeksbot_web/channels/migrations/0002_auto_20190921_0250.py create mode 100644 geeksbot_web/channels/migrations/__init__.py create mode 100644 geeksbot_web/channels/models.py create mode 100644 geeksbot_web/channels/serializers.py create mode 100644 geeksbot_web/channels/tests.py create mode 100644 geeksbot_web/channels/utils.py create mode 100644 geeksbot_web/channels/views.py create mode 100644 geeksbot_web/config/__init__.py create mode 100644 geeksbot_web/config/settings/__init__.py create mode 100644 geeksbot_web/config/settings/base.py create mode 100644 geeksbot_web/config/settings/local.py create mode 100644 geeksbot_web/config/settings/production.py create mode 100644 geeksbot_web/config/settings/test.py create mode 100644 geeksbot_web/config/urls.py create mode 100644 geeksbot_web/config/wsgi.py create mode 100644 geeksbot_web/conftest.py create mode 100644 geeksbot_web/contrib/__init__.py create mode 100644 geeksbot_web/contrib/sites/__init__.py create mode 100644 geeksbot_web/contrib/sites/migrations/0001_initial.py create mode 100644 geeksbot_web/contrib/sites/migrations/0002_alter_domain_unique.py create mode 100644 geeksbot_web/contrib/sites/migrations/0003_set_site_domain_and_name.py create mode 100644 geeksbot_web/contrib/sites/migrations/__init__.py create mode 100644 geeksbot_web/dmessages/__init__.py create mode 100644 geeksbot_web/dmessages/admin.py create mode 100644 geeksbot_web/dmessages/api_urls.py create mode 100644 geeksbot_web/dmessages/apps.py create mode 100644 geeksbot_web/dmessages/migrations/0001_initial.py create mode 100644 geeksbot_web/dmessages/migrations/0002_auto_20190920_2139.py create mode 100644 geeksbot_web/dmessages/migrations/0003_auto_20190921_0721.py create mode 100644 geeksbot_web/dmessages/migrations/__init__.py create mode 100644 geeksbot_web/dmessages/models.py create mode 100644 geeksbot_web/dmessages/serializers.py create mode 100644 geeksbot_web/dmessages/tests.py create mode 100644 geeksbot_web/dmessages/utils.py create mode 100644 geeksbot_web/dmessages/views.py create mode 100755 geeksbot_web/entrypoint create mode 100644 geeksbot_web/guilds/__init__.py create mode 100644 geeksbot_web/guilds/admin.py create mode 100644 geeksbot_web/guilds/api_urls.py create mode 100644 geeksbot_web/guilds/apps.py create mode 100644 geeksbot_web/guilds/migrations/0001_initial.py create mode 100644 geeksbot_web/guilds/migrations/0002_auto_20190921_0250.py create mode 100644 geeksbot_web/guilds/migrations/__init__.py create mode 100644 geeksbot_web/guilds/models.py create mode 100644 geeksbot_web/guilds/permissions.py create mode 100644 geeksbot_web/guilds/serializers.py create mode 100644 geeksbot_web/guilds/tests.py create mode 100644 geeksbot_web/guilds/urls.py create mode 100644 geeksbot_web/guilds/utils.py create mode 100644 geeksbot_web/guilds/views.py create mode 100755 geeksbot_web/manage.py create mode 100644 geeksbot_web/patreon/__init__.py create mode 100644 geeksbot_web/patreon/admin.py create mode 100644 geeksbot_web/patreon/apps.py create mode 100644 geeksbot_web/patreon/migrations/0001_initial.py create mode 100644 geeksbot_web/patreon/migrations/__init__.py create mode 100644 geeksbot_web/patreon/models.py create mode 100644 geeksbot_web/patreon/patron.py create mode 100644 geeksbot_web/patreon/serializers.py create mode 100644 geeksbot_web/patreon/tests.py create mode 100644 geeksbot_web/patreon/utils.py create mode 100644 geeksbot_web/patreon/views.py create mode 100644 geeksbot_web/rcon/__init__.py create mode 100644 geeksbot_web/rcon/admin.py create mode 100644 geeksbot_web/rcon/api_urls.py create mode 100644 geeksbot_web/rcon/apps.py create mode 100644 geeksbot_web/rcon/migrations/0001_initial.py create mode 100644 geeksbot_web/rcon/migrations/0002_rconserver_whitelist.py create mode 100644 geeksbot_web/rcon/migrations/__init__.py create mode 100644 geeksbot_web/rcon/models.py create mode 100644 geeksbot_web/rcon/rcon_lib/__init__.py create mode 100644 geeksbot_web/rcon/rcon_lib/arcon.py create mode 100644 geeksbot_web/rcon/rcon_lib/rcon.py create mode 100644 geeksbot_web/rcon/serializers.py create mode 100644 geeksbot_web/rcon/tests.py create mode 100644 geeksbot_web/rcon/utils.py create mode 100644 geeksbot_web/rcon/views.py create mode 100644 geeksbot_web/shared_libs/TicTacToe/__init__.py create mode 100644 geeksbot_web/shared_libs/TicTacToe/board.py create mode 100644 geeksbot_web/shared_libs/TicTacToe/player.py create mode 100644 geeksbot_web/shared_libs/__init__.py create mode 100644 geeksbot_web/static/css/project.css create mode 100644 geeksbot_web/static/fonts/.gitkeep create mode 100644 geeksbot_web/static/images/favicons/favicon.ico create mode 100644 geeksbot_web/static/js/project.js create mode 100644 geeksbot_web/static/sass/custom_bootstrap_vars.scss create mode 100644 geeksbot_web/static/sass/project.scss create mode 100644 geeksbot_web/templates/403.html create mode 100644 geeksbot_web/templates/404.html create mode 100644 geeksbot_web/templates/500.html create mode 100644 geeksbot_web/templates/account/account_inactive.html create mode 100644 geeksbot_web/templates/account/base.html create mode 100644 geeksbot_web/templates/account/email.html create mode 100644 geeksbot_web/templates/account/email_confirm.html create mode 100644 geeksbot_web/templates/account/login.html create mode 100644 geeksbot_web/templates/account/logout.html create mode 100644 geeksbot_web/templates/account/password_change.html create mode 100644 geeksbot_web/templates/account/password_reset.html create mode 100644 geeksbot_web/templates/account/password_reset_done.html create mode 100644 geeksbot_web/templates/account/password_reset_from_key.html create mode 100644 geeksbot_web/templates/account/password_reset_from_key_done.html create mode 100644 geeksbot_web/templates/account/password_set.html create mode 100644 geeksbot_web/templates/account/signup.html create mode 100644 geeksbot_web/templates/account/signup_closed.html create mode 100644 geeksbot_web/templates/account/verification_sent.html create mode 100644 geeksbot_web/templates/account/verified_email_required.html create mode 100644 geeksbot_web/templates/base.html create mode 100644 geeksbot_web/templates/pages/about.html create mode 100644 geeksbot_web/templates/pages/home.html create mode 100644 geeksbot_web/templates/users/user_detail.html create mode 100644 geeksbot_web/templates/users/user_form.html create mode 100644 geeksbot_web/users/__init__.py create mode 100644 geeksbot_web/users/adapters.py create mode 100644 geeksbot_web/users/admin.py create mode 100644 geeksbot_web/users/api_urls.py create mode 100644 geeksbot_web/users/apps.py create mode 100644 geeksbot_web/users/forms.py create mode 100644 geeksbot_web/users/migrations/0001_initial.py create mode 100644 geeksbot_web/users/migrations/__init__.py create mode 100644 geeksbot_web/users/models.py create mode 100644 geeksbot_web/users/serializers.py create mode 100644 geeksbot_web/users/tests/__init__.py create mode 100644 geeksbot_web/users/tests/factories.py create mode 100644 geeksbot_web/users/tests/test_forms.py create mode 100644 geeksbot_web/users/tests/test_models.py create mode 100644 geeksbot_web/users/tests/test_urls.py create mode 100644 geeksbot_web/users/tests/test_views.py create mode 100644 geeksbot_web/users/urls.py create mode 100644 geeksbot_web/users/utils.py create mode 100644 geeksbot_web/users/views.py create mode 100644 geeksbot_web/utils/api_utils.py create mode 100644 geeksbot_web/utils/permissions.py create mode 100644 locale/README.rst create mode 100644 requirements/base.txt create mode 100644 requirements/local.txt create mode 100644 requirements/production.txt create mode 100644 requirements/web.txt create mode 100644 services/Dockerfile-base create mode 100644 services/geeksbot.conf create mode 100644 services/gunicorn.conf create mode 100644 services/nginx.conf create mode 100644 services/postgresql/postgres.conf create mode 100644 services/supervisor_geeksbot.conf create mode 100644 services/supervisord.conf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2221d8 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2aa06aa --- /dev/null +++ b/Dockerfile @@ -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" ] diff --git a/geeksbot_web/__init__.py b/geeksbot_web/__init__.py new file mode 100644 index 0000000..055908f --- /dev/null +++ b/geeksbot_web/__init__.py @@ -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(".") + ] +) diff --git a/geeksbot_web/channels/__init__.py b/geeksbot_web/channels/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/channels/admin.py b/geeksbot_web/channels/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/geeksbot_web/channels/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/geeksbot_web/channels/api_urls.py b/geeksbot_web/channels/api_urls.py new file mode 100644 index 0000000..63a1fdf --- /dev/null +++ b/geeksbot_web/channels/api_urls.py @@ -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("/", view=ChannelDetail.as_view(), name='detail'), + path("/admin/", view=AdminChannelAPI.as_view(), name='admin') +] diff --git a/geeksbot_web/channels/apps.py b/geeksbot_web/channels/apps.py new file mode 100644 index 0000000..fbd0dbc --- /dev/null +++ b/geeksbot_web/channels/apps.py @@ -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") diff --git a/geeksbot_web/channels/migrations/0001_initial.py b/geeksbot_web/channels/migrations/0001_initial.py new file mode 100644 index 0000000..e1a6387 --- /dev/null +++ b/geeksbot_web/channels/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/geeksbot_web/channels/migrations/0002_auto_20190921_0250.py b/geeksbot_web/channels/migrations/0002_auto_20190921_0250.py new file mode 100644 index 0000000..b2cef5a --- /dev/null +++ b/geeksbot_web/channels/migrations/0002_auto_20190921_0250.py @@ -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), + ), + ] diff --git a/geeksbot_web/channels/migrations/__init__.py b/geeksbot_web/channels/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/channels/models.py b/geeksbot_web/channels/models.py new file mode 100644 index 0000000..0d7b552 --- /dev/null +++ b/geeksbot_web/channels/models.py @@ -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) diff --git a/geeksbot_web/channels/serializers.py b/geeksbot_web/channels/serializers.py new file mode 100644 index 0000000..81af17b --- /dev/null +++ b/geeksbot_web/channels/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from .models import Channel + + +class ChannelSerializer(serializers.ModelSerializer): + class Meta: + model = Channel + fields = "__all__" diff --git a/geeksbot_web/channels/tests.py b/geeksbot_web/channels/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/geeksbot_web/channels/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/geeksbot_web/channels/utils.py b/geeksbot_web/channels/utils.py new file mode 100644 index 0000000..11fe383 --- /dev/null +++ b/geeksbot_web/channels/utils.py @@ -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) diff --git a/geeksbot_web/channels/views.py b/geeksbot_web/channels/views.py new file mode 100644 index 0000000..ccb236a --- /dev/null +++ b/geeksbot_web/channels/views.py @@ -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) diff --git a/geeksbot_web/config/__init__.py b/geeksbot_web/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/config/settings/__init__.py b/geeksbot_web/config/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/config/settings/base.py b/geeksbot_web/config/settings/base.py new file mode 100644 index 0000000..147702d --- /dev/null +++ b/geeksbot_web/config/settings/base.py @@ -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"] diff --git a/geeksbot_web/config/settings/local.py b/geeksbot_web/config/settings/local.py new file mode 100644 index 0000000..15c3aa5 --- /dev/null +++ b/geeksbot_web/config/settings/local.py @@ -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... +# ------------------------------------------------------------------------------ diff --git a/geeksbot_web/config/settings/production.py b/geeksbot_web/config/settings/production.py new file mode 100644 index 0000000..778939e --- /dev/null +++ b/geeksbot_web/config/settings/production.py @@ -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 " +) +# 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... +# ------------------------------------------------------------------------------ diff --git a/geeksbot_web/config/settings/test.py b/geeksbot_web/config/settings/test.py new file mode 100644 index 0000000..3a4c593 --- /dev/null +++ b/geeksbot_web/config/settings/test.py @@ -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... +# ------------------------------------------------------------------------------ diff --git a/geeksbot_web/config/urls.py b/geeksbot_web/config/urls.py new file mode 100644 index 0000000..3e8d306 --- /dev/null +++ b/geeksbot_web/config/urls.py @@ -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 diff --git a/geeksbot_web/config/wsgi.py b/geeksbot_web/config/wsgi.py new file mode 100644 index 0000000..765e845 --- /dev/null +++ b/geeksbot_web/config/wsgi.py @@ -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) diff --git a/geeksbot_web/conftest.py b/geeksbot_web/conftest.py new file mode 100644 index 0000000..b93c3cd --- /dev/null +++ b/geeksbot_web/conftest.py @@ -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() diff --git a/geeksbot_web/contrib/__init__.py b/geeksbot_web/contrib/__init__.py new file mode 100644 index 0000000..1c7ecc8 --- /dev/null +++ b/geeksbot_web/contrib/__init__.py @@ -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 +""" diff --git a/geeksbot_web/contrib/sites/__init__.py b/geeksbot_web/contrib/sites/__init__.py new file mode 100644 index 0000000..1c7ecc8 --- /dev/null +++ b/geeksbot_web/contrib/sites/__init__.py @@ -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 +""" diff --git a/geeksbot_web/contrib/sites/migrations/0001_initial.py b/geeksbot_web/contrib/sites/migrations/0001_initial.py new file mode 100644 index 0000000..c0c8906 --- /dev/null +++ b/geeksbot_web/contrib/sites/migrations/0001_initial.py @@ -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())], + ) + ] \ No newline at end of file diff --git a/geeksbot_web/contrib/sites/migrations/0002_alter_domain_unique.py b/geeksbot_web/contrib/sites/migrations/0002_alter_domain_unique.py new file mode 100644 index 0000000..2fcbc4e --- /dev/null +++ b/geeksbot_web/contrib/sites/migrations/0002_alter_domain_unique.py @@ -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", + ), + ) + ] \ No newline at end of file diff --git a/geeksbot_web/contrib/sites/migrations/0003_set_site_domain_and_name.py b/geeksbot_web/contrib/sites/migrations/0003_set_site_domain_and_name.py new file mode 100644 index 0000000..93aab61 --- /dev/null +++ b/geeksbot_web/contrib/sites/migrations/0003_set_site_domain_and_name.py @@ -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)] \ No newline at end of file diff --git a/geeksbot_web/contrib/sites/migrations/__init__.py b/geeksbot_web/contrib/sites/migrations/__init__.py new file mode 100644 index 0000000..1c7ecc8 --- /dev/null +++ b/geeksbot_web/contrib/sites/migrations/__init__.py @@ -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 +""" diff --git a/geeksbot_web/dmessages/__init__.py b/geeksbot_web/dmessages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/dmessages/admin.py b/geeksbot_web/dmessages/admin.py new file mode 100644 index 0000000..e14fb8b --- /dev/null +++ b/geeksbot_web/dmessages/admin.py @@ -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) \ No newline at end of file diff --git a/geeksbot_web/dmessages/api_urls.py b/geeksbot_web/dmessages/api_urls.py new file mode 100644 index 0000000..5891225 --- /dev/null +++ b/geeksbot_web/dmessages/api_urls.py @@ -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("/", view=MessageDetailAPI.as_view(), name='message_detail'), + path("/requests/", view=RequestsAPI.as_view(), name="requests_list"), + path("/requests//", view=RequestDetailAPI.as_view(), name='request_detail'), + path("/requests//comments/", view=CommentsAPI.as_view(), name="comments_list"), + path("/requests//comments/count/", view=CommentsCountAPI.as_view(), name="comments_count"), + path("/requests//comments//", view=CommentDetailAPI.as_view(), name='comment_detail'), + path("/requests/user//", view=UserRequestsAPI.as_view(), name='user_requests_list'), + path("/wait/", view=WaitForMessageAPI.as_view(), name='wait_for_message'), + path("/wait//", view=WaitForMessageAPI.as_view(), name='wait_for_message_timeout'), +] diff --git a/geeksbot_web/dmessages/apps.py b/geeksbot_web/dmessages/apps.py new file mode 100644 index 0000000..9020d70 --- /dev/null +++ b/geeksbot_web/dmessages/apps.py @@ -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") diff --git a/geeksbot_web/dmessages/migrations/0001_initial.py b/geeksbot_web/dmessages/migrations/0001_initial.py new file mode 100644 index 0000000..bdb189d --- /dev/null +++ b/geeksbot_web/dmessages/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/geeksbot_web/dmessages/migrations/0002_auto_20190920_2139.py b/geeksbot_web/dmessages/migrations/0002_auto_20190920_2139.py new file mode 100644 index 0000000..4258381 --- /dev/null +++ b/geeksbot_web/dmessages/migrations/0002_auto_20190920_2139.py @@ -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'), + ), + ] diff --git a/geeksbot_web/dmessages/migrations/0003_auto_20190921_0721.py b/geeksbot_web/dmessages/migrations/0003_auto_20190921_0721.py new file mode 100644 index 0000000..5f3b8f3 --- /dev/null +++ b/geeksbot_web/dmessages/migrations/0003_auto_20190921_0721.py @@ -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), + ), + ] diff --git a/geeksbot_web/dmessages/migrations/__init__.py b/geeksbot_web/dmessages/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/dmessages/models.py b/geeksbot_web/dmessages/models.py new file mode 100644 index 0000000..5316117 --- /dev/null +++ b/geeksbot_web/dmessages/models.py @@ -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') diff --git a/geeksbot_web/dmessages/serializers.py b/geeksbot_web/dmessages/serializers.py new file mode 100644 index 0000000..082cb25 --- /dev/null +++ b/geeksbot_web/dmessages/serializers.py @@ -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__" diff --git a/geeksbot_web/dmessages/tests.py b/geeksbot_web/dmessages/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/geeksbot_web/dmessages/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/geeksbot_web/dmessages/utils.py b/geeksbot_web/dmessages/utils.py new file mode 100644 index 0000000..ab5c646 --- /dev/null +++ b/geeksbot_web/dmessages/utils.py @@ -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) diff --git a/geeksbot_web/dmessages/views.py b/geeksbot_web/dmessages/views.py new file mode 100644 index 0000000..02b8a9b --- /dev/null +++ b/geeksbot_web/dmessages/views.py @@ -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) diff --git a/geeksbot_web/entrypoint b/geeksbot_web/entrypoint new file mode 100755 index 0000000..9cee30a --- /dev/null +++ b/geeksbot_web/entrypoint @@ -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 diff --git a/geeksbot_web/guilds/__init__.py b/geeksbot_web/guilds/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/guilds/admin.py b/geeksbot_web/guilds/admin.py new file mode 100644 index 0000000..e5cc27a --- /dev/null +++ b/geeksbot_web/guilds/admin.py @@ -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) \ No newline at end of file diff --git a/geeksbot_web/guilds/api_urls.py b/geeksbot_web/guilds/api_urls.py new file mode 100644 index 0000000..6b2f3d7 --- /dev/null +++ b/geeksbot_web/guilds/api_urls.py @@ -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("/", view=GuildDetail.as_view(), name='detail'), + path("/roles/", view=RolesAPI.as_view(), name="list"), + path("/roles/admin/", view=AdminRolesAPI.as_view(), name='admin'), + path("/roles//", view=RoleDetailAPI.as_view(), name='detail'), +] diff --git a/geeksbot_web/guilds/apps.py b/geeksbot_web/guilds/apps.py new file mode 100644 index 0000000..14ed662 --- /dev/null +++ b/geeksbot_web/guilds/apps.py @@ -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") diff --git a/geeksbot_web/guilds/migrations/0001_initial.py b/geeksbot_web/guilds/migrations/0001_initial.py new file mode 100644 index 0000000..cf97b8b --- /dev/null +++ b/geeksbot_web/guilds/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/geeksbot_web/guilds/migrations/0002_auto_20190921_0250.py b/geeksbot_web/guilds/migrations/0002_auto_20190921_0250.py new file mode 100644 index 0000000..49a4932 --- /dev/null +++ b/geeksbot_web/guilds/migrations/0002_auto_20190921_0250.py @@ -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', + ), + ] diff --git a/geeksbot_web/guilds/migrations/__init__.py b/geeksbot_web/guilds/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/guilds/models.py b/geeksbot_web/guilds/models.py new file mode 100644 index 0000000..56afbac --- /dev/null +++ b/geeksbot_web/guilds/models.py @@ -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}" diff --git a/geeksbot_web/guilds/permissions.py b/geeksbot_web/guilds/permissions.py new file mode 100644 index 0000000..50d64b0 --- /dev/null +++ b/geeksbot_web/guilds/permissions.py @@ -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) diff --git a/geeksbot_web/guilds/serializers.py b/geeksbot_web/guilds/serializers.py new file mode 100644 index 0000000..82d5e9f --- /dev/null +++ b/geeksbot_web/guilds/serializers.py @@ -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"] diff --git a/geeksbot_web/guilds/tests.py b/geeksbot_web/guilds/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/geeksbot_web/guilds/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/geeksbot_web/guilds/urls.py b/geeksbot_web/guilds/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/guilds/utils.py b/geeksbot_web/guilds/utils.py new file mode 100644 index 0000000..78314d8 --- /dev/null +++ b/geeksbot_web/guilds/utils.py @@ -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) diff --git a/geeksbot_web/guilds/views.py b/geeksbot_web/guilds/views.py new file mode 100644 index 0000000..2d9200d --- /dev/null +++ b/geeksbot_web/guilds/views.py @@ -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) diff --git a/geeksbot_web/manage.py b/geeksbot_web/manage.py new file mode 100755 index 0000000..28c92b9 --- /dev/null +++ b/geeksbot_web/manage.py @@ -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) diff --git a/geeksbot_web/patreon/__init__.py b/geeksbot_web/patreon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/patreon/admin.py b/geeksbot_web/patreon/admin.py new file mode 100644 index 0000000..6e73988 --- /dev/null +++ b/geeksbot_web/patreon/admin.py @@ -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) \ No newline at end of file diff --git a/geeksbot_web/patreon/apps.py b/geeksbot_web/patreon/apps.py new file mode 100644 index 0000000..02d02ed --- /dev/null +++ b/geeksbot_web/patreon/apps.py @@ -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") diff --git a/geeksbot_web/patreon/migrations/0001_initial.py b/geeksbot_web/patreon/migrations/0001_initial.py new file mode 100644 index 0000000..6abcf6e --- /dev/null +++ b/geeksbot_web/patreon/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/geeksbot_web/patreon/migrations/__init__.py b/geeksbot_web/patreon/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/patreon/models.py b/geeksbot_web/patreon/models.py new file mode 100644 index 0000000..ca07ecd --- /dev/null +++ b/geeksbot_web/patreon/models.py @@ -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}" diff --git a/geeksbot_web/patreon/patron.py b/geeksbot_web/patreon/patron.py new file mode 100644 index 0000000..6a4e430 --- /dev/null +++ b/geeksbot_web/patreon/patron.py @@ -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) diff --git a/geeksbot_web/patreon/serializers.py b/geeksbot_web/patreon/serializers.py new file mode 100644 index 0000000..087a14f --- /dev/null +++ b/geeksbot_web/patreon/serializers.py @@ -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__" diff --git a/geeksbot_web/patreon/tests.py b/geeksbot_web/patreon/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/geeksbot_web/patreon/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/geeksbot_web/patreon/utils.py b/geeksbot_web/patreon/utils.py new file mode 100644 index 0000000..c738729 --- /dev/null +++ b/geeksbot_web/patreon/utils.py @@ -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) diff --git a/geeksbot_web/patreon/views.py b/geeksbot_web/patreon/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/geeksbot_web/patreon/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/geeksbot_web/rcon/__init__.py b/geeksbot_web/rcon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/rcon/admin.py b/geeksbot_web/rcon/admin.py new file mode 100644 index 0000000..7209c51 --- /dev/null +++ b/geeksbot_web/rcon/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from .models import RconServer + +# Register your models here. +admin.site.register(RconServer) \ No newline at end of file diff --git a/geeksbot_web/rcon/api_urls.py b/geeksbot_web/rcon/api_urls.py new file mode 100644 index 0000000..6496d3c --- /dev/null +++ b/geeksbot_web/rcon/api_urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from .views import RCONServersAPI, RCONServerDetailAPI, ListPlayers + +app_name = "rcon_api" +urlpatterns = [ + path("/", view=RCONServersAPI.as_view(), name='guild_servers'), + path("//", view=RCONServerDetailAPI.as_view(), name="server_detail"), + path("//listplayers", view=ListPlayers.as_view(), name='listplayers'), +] diff --git a/geeksbot_web/rcon/apps.py b/geeksbot_web/rcon/apps.py new file mode 100644 index 0000000..3ac65ea --- /dev/null +++ b/geeksbot_web/rcon/apps.py @@ -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") diff --git a/geeksbot_web/rcon/migrations/0001_initial.py b/geeksbot_web/rcon/migrations/0001_initial.py new file mode 100644 index 0000000..ea652ae --- /dev/null +++ b/geeksbot_web/rcon/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/geeksbot_web/rcon/migrations/0002_rconserver_whitelist.py b/geeksbot_web/rcon/migrations/0002_rconserver_whitelist.py new file mode 100644 index 0000000..9b5aa20 --- /dev/null +++ b/geeksbot_web/rcon/migrations/0002_rconserver_whitelist.py @@ -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), + ), + ] diff --git a/geeksbot_web/rcon/migrations/__init__.py b/geeksbot_web/rcon/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/rcon/models.py b/geeksbot_web/rcon/models.py new file mode 100644 index 0000000..35995b3 --- /dev/null +++ b/geeksbot_web/rcon/models.py @@ -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}" diff --git a/geeksbot_web/rcon/rcon_lib/__init__.py b/geeksbot_web/rcon/rcon_lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/rcon/rcon_lib/arcon.py b/geeksbot_web/rcon/rcon_lib/arcon.py new file mode 100644 index 0000000..71d776a --- /dev/null +++ b/geeksbot_web/rcon/rcon_lib/arcon.py @@ -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 diff --git a/geeksbot_web/rcon/rcon_lib/rcon.py b/geeksbot_web/rcon/rcon_lib/rcon.py new file mode 100644 index 0000000..deeafee --- /dev/null +++ b/geeksbot_web/rcon/rcon_lib/rcon.py @@ -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')) diff --git a/geeksbot_web/rcon/serializers.py b/geeksbot_web/rcon/serializers.py new file mode 100644 index 0000000..1fd4373 --- /dev/null +++ b/geeksbot_web/rcon/serializers.py @@ -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__" diff --git a/geeksbot_web/rcon/tests.py b/geeksbot_web/rcon/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/geeksbot_web/rcon/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/geeksbot_web/rcon/utils.py b/geeksbot_web/rcon/utils.py new file mode 100644 index 0000000..1d78983 --- /dev/null +++ b/geeksbot_web/rcon/utils.py @@ -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) diff --git a/geeksbot_web/rcon/views.py b/geeksbot_web/rcon/views.py new file mode 100644 index 0000000..ecff7c0 --- /dev/null +++ b/geeksbot_web/rcon/views.py @@ -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) diff --git a/geeksbot_web/shared_libs/TicTacToe/__init__.py b/geeksbot_web/shared_libs/TicTacToe/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/shared_libs/TicTacToe/board.py b/geeksbot_web/shared_libs/TicTacToe/board.py new file mode 100644 index 0000000..35365c6 --- /dev/null +++ b/geeksbot_web/shared_libs/TicTacToe/board.py @@ -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'' + + 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 + diff --git a/geeksbot_web/shared_libs/TicTacToe/player.py b/geeksbot_web/shared_libs/TicTacToe/player.py new file mode 100644 index 0000000..e90278b --- /dev/null +++ b/geeksbot_web/shared_libs/TicTacToe/player.py @@ -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'' + + 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 diff --git a/geeksbot_web/shared_libs/__init__.py b/geeksbot_web/shared_libs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/static/css/project.css b/geeksbot_web/static/css/project.css new file mode 100644 index 0000000..f1d543d --- /dev/null +++ b/geeksbot_web/static/css/project.css @@ -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; +} diff --git a/geeksbot_web/static/fonts/.gitkeep b/geeksbot_web/static/fonts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/static/images/favicons/favicon.ico b/geeksbot_web/static/images/favicons/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e1c1dd1a32a3a077c41a21e52bc7fb5ac90d3afb GIT binary patch literal 8348 zcmeHLX-gGh6rQ3V&`92%;lcmGu`Jl^WK@OV`^XKh08PZoaH%l?rdiiWq>kJ2@6vM zhAH8L6=kTRD1!xR`-2o^hS&}loN!U1#gF+=YxEq~uqf5#iBw%p0;w;5ehm+6a!sSu zh;X+$+}oF$X1Q6DwS~>Y_Hpw^(&QvJO<5ZCPrn5laNp%s{B3x1hf%*?eW>oS{<{4s^u_yG zpDyG!_iV#~G=q<~slG@0Sx47XXQ%O;HY7ILVf}B-)R$6GV^A78)t0tN9`tu3r9Z+xM?NgUd8geu=c`0?r;-KR&IQjKs zSB#VCpg8CPW&M}$sth@H=VS%t;23!!j};F)bb;W3EkA!4QmCsY_p81^TK0{;$g>e1Hl998^0M+r0-7ZSN+l_cMSRuEALTE@|d6+3{GMP^;_|<yhubb{FF1IPgH|0>SH%pC!c)uFI)H z?jv4y0uO{P5WE>~I<$r!RFo0lN4r{xm;Jy4p$h~b3S*a#)$Z*nI}&Kc_C+*zZHz1v zIR8TBVH7Y?Mp~ zofpsr+SP@>EX4e*bQ{nAUYtKrQ&-5d4j;FF{^-^Dt2^5I`HSb!|22PN26n3>hKPOy zW2D*A*v+c{bFKCwozbB{dObp~e&1Nxrj<4Ih4~w)M`nl08o}Wq2 z#Jt(k+M>+}JY(=2gm(gdUq)^@e%tX(F)RBtotnB2^y>YKz-5eg_qO&n%lJ1nuQmTY zxmyE1NWjl1EGvD?Z|n;neT;sa?Q;Ef-#%$BYxXAhD4xF+@M>*qrR!x^sPN98|BX4; z!$NJcKF`^C7f(<_vlp%b>`pxL^8Y<&?KFx{n_!5C9VqLA*CP@zhXs2t#dmrAKu?d_ y^&_r5_e@uesG}CO*g!3o?-kB+I^cA`>44J#rvpw0oDMi0a5~_0!0Eu>4*Uj$LD0AW literal 0 HcmV?d00001 diff --git a/geeksbot_web/static/js/project.js b/geeksbot_web/static/js/project.js new file mode 100644 index 0000000..d26d23b --- /dev/null +++ b/geeksbot_web/static/js/project.js @@ -0,0 +1 @@ +/* Project specific Javascript goes here. */ diff --git a/geeksbot_web/static/sass/custom_bootstrap_vars.scss b/geeksbot_web/static/sass/custom_bootstrap_vars.scss new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/static/sass/project.scss b/geeksbot_web/static/sass/project.scss new file mode 100644 index 0000000..3c8f261 --- /dev/null +++ b/geeksbot_web/static/sass/project.scss @@ -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; +} diff --git a/geeksbot_web/templates/403.html b/geeksbot_web/templates/403.html new file mode 100644 index 0000000..77db8ae --- /dev/null +++ b/geeksbot_web/templates/403.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block title %}Forbidden (403){% endblock %} + +{% block content %} +

Forbidden (403)

+ +

CSRF verification failed. Request aborted.

+{% endblock content %} diff --git a/geeksbot_web/templates/404.html b/geeksbot_web/templates/404.html new file mode 100644 index 0000000..98327cd --- /dev/null +++ b/geeksbot_web/templates/404.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block title %}Page not found{% endblock %} + +{% block content %} +

Page not found

+ +

This is not the page you were looking for.

+{% endblock content %} diff --git a/geeksbot_web/templates/500.html b/geeksbot_web/templates/500.html new file mode 100644 index 0000000..21df606 --- /dev/null +++ b/geeksbot_web/templates/500.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block title %}Server Error{% endblock %} + +{% block content %} +

Ooops!!! 500

+ +

Looks like something went wrong!

+ +

We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.

+{% endblock content %} + + diff --git a/geeksbot_web/templates/account/account_inactive.html b/geeksbot_web/templates/account/account_inactive.html new file mode 100644 index 0000000..17c2157 --- /dev/null +++ b/geeksbot_web/templates/account/account_inactive.html @@ -0,0 +1,12 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Account Inactive" %}{% endblock %} + +{% block inner %} +

{% trans "Account Inactive" %}

+ +

{% trans "This account is inactive." %}

+{% endblock %} + diff --git a/geeksbot_web/templates/account/base.html b/geeksbot_web/templates/account/base.html new file mode 100644 index 0000000..8e1f260 --- /dev/null +++ b/geeksbot_web/templates/account/base.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %} + +{% block content %} +
+
+ {% block inner %}{% endblock %} +
+
+{% endblock %} diff --git a/geeksbot_web/templates/account/email.html b/geeksbot_web/templates/account/email.html new file mode 100644 index 0000000..0dc8d14 --- /dev/null +++ b/geeksbot_web/templates/account/email.html @@ -0,0 +1,80 @@ + +{% extends "account/base.html" %} + +{% load i18n %} +{% load crispy_forms_tags %} + +{% block head_title %}{% trans "Account" %}{% endblock %} + +{% block inner %} +

{% trans "E-mail Addresses" %}

+ +{% if user.emailaddress_set.all %} +

{% trans 'The following e-mail addresses are associated with your account:' %}

+ + + +{% else %} +

{% trans 'Warning:'%} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}

+ +{% endif %} + + +

{% trans "Add E-mail Address" %}

+ +
+ {% csrf_token %} + {{ form|crispy }} + +
+ +{% endblock %} + + +{% block javascript %} +{{ block.super }} + +{% endblock %} + diff --git a/geeksbot_web/templates/account/email_confirm.html b/geeksbot_web/templates/account/email_confirm.html new file mode 100644 index 0000000..46c7812 --- /dev/null +++ b/geeksbot_web/templates/account/email_confirm.html @@ -0,0 +1,32 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load account %} + +{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} + + +{% block inner %} +

{% trans "Confirm E-mail Address" %}

+ +{% if confirmation %} + +{% user_display confirmation.email_address.user as user_display %} + +

{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}

+ +
+{% csrf_token %} + +
+ +{% else %} + +{% url 'account_email' as email_url %} + +

{% blocktrans %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request.{% endblocktrans %}

+ +{% endif %} + +{% endblock %} + diff --git a/geeksbot_web/templates/account/login.html b/geeksbot_web/templates/account/login.html new file mode 100644 index 0000000..2cadea6 --- /dev/null +++ b/geeksbot_web/templates/account/login.html @@ -0,0 +1,48 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load account socialaccount %} +{% load crispy_forms_tags %} + +{% block head_title %}{% trans "Sign In" %}{% endblock %} + +{% block inner %} + +

{% trans "Sign In" %}

+ +{% get_providers as socialaccount_providers %} + +{% if socialaccount_providers %} +

{% blocktrans with site.name as site_name %}Please sign in with one +of your existing third party accounts. Or, sign up +for a {{ site_name }} account and sign in below:{% endblocktrans %}

+ +
+ +
    + {% include "socialaccount/snippets/provider_list.html" with process="login" %} +
+ + + +
+ +{% include "socialaccount/snippets/login_extra.html" %} + +{% else %} +

{% blocktrans %}If you have not created an account yet, then please +sign up first.{% endblocktrans %}

+{% endif %} + + + +{% endblock %} + diff --git a/geeksbot_web/templates/account/logout.html b/geeksbot_web/templates/account/logout.html new file mode 100644 index 0000000..8e2e675 --- /dev/null +++ b/geeksbot_web/templates/account/logout.html @@ -0,0 +1,22 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Sign Out" %}{% endblock %} + +{% block inner %} +

{% trans "Sign Out" %}

+ +

{% trans 'Are you sure you want to sign out?' %}

+ +
+ {% csrf_token %} + {% if redirect_field_value %} + + {% endif %} + +
+ + +{% endblock %} + diff --git a/geeksbot_web/templates/account/password_change.html b/geeksbot_web/templates/account/password_change.html new file mode 100644 index 0000000..b72ca06 --- /dev/null +++ b/geeksbot_web/templates/account/password_change.html @@ -0,0 +1,17 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load crispy_forms_tags %} + +{% block head_title %}{% trans "Change Password" %}{% endblock %} + +{% block inner %} +

{% trans "Change Password" %}

+ +
+ {% csrf_token %} + {{ form|crispy }} + +
+{% endblock %} + diff --git a/geeksbot_web/templates/account/password_reset.html b/geeksbot_web/templates/account/password_reset.html new file mode 100644 index 0000000..845bbda --- /dev/null +++ b/geeksbot_web/templates/account/password_reset.html @@ -0,0 +1,26 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load account %} +{% load crispy_forms_tags %} + +{% block head_title %}{% trans "Password Reset" %}{% endblock %} + +{% block inner %} + +

{% trans "Password Reset" %}

+ {% if user.is_authenticated %} + {% include "account/snippets/already_logged_in.html" %} + {% endif %} + +

{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}

+ +
+ {% csrf_token %} + {{ form|crispy }} + +
+ +

{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}

+{% endblock %} + diff --git a/geeksbot_web/templates/account/password_reset_done.html b/geeksbot_web/templates/account/password_reset_done.html new file mode 100644 index 0000000..c59534a --- /dev/null +++ b/geeksbot_web/templates/account/password_reset_done.html @@ -0,0 +1,17 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load account %} + +{% block head_title %}{% trans "Password Reset" %}{% endblock %} + +{% block inner %} +

{% trans "Password Reset" %}

+ + {% if user.is_authenticated %} + {% include "account/snippets/already_logged_in.html" %} + {% endif %} + +

{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

+{% endblock %} + diff --git a/geeksbot_web/templates/account/password_reset_from_key.html b/geeksbot_web/templates/account/password_reset_from_key.html new file mode 100644 index 0000000..4abdb56 --- /dev/null +++ b/geeksbot_web/templates/account/password_reset_from_key.html @@ -0,0 +1,25 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load crispy_forms_tags %} +{% block head_title %}{% trans "Change Password" %}{% endblock %} + +{% block inner %} +

{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}

+ + {% if token_fail %} + {% url 'account_reset_password' as passwd_reset_url %} +

{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset.{% endblocktrans %}

+ {% else %} + {% if form %} +
+ {% csrf_token %} + {{ form|crispy }} + +
+ {% else %} +

{% trans 'Your password is now changed.' %}

+ {% endif %} + {% endif %} +{% endblock %} + diff --git a/geeksbot_web/templates/account/password_reset_from_key_done.html b/geeksbot_web/templates/account/password_reset_from_key_done.html new file mode 100644 index 0000000..89be086 --- /dev/null +++ b/geeksbot_web/templates/account/password_reset_from_key_done.html @@ -0,0 +1,10 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% block head_title %}{% trans "Change Password" %}{% endblock %} + +{% block inner %} +

{% trans "Change Password" %}

+

{% trans 'Your password is now changed.' %}

+{% endblock %} + diff --git a/geeksbot_web/templates/account/password_set.html b/geeksbot_web/templates/account/password_set.html new file mode 100644 index 0000000..2232223 --- /dev/null +++ b/geeksbot_web/templates/account/password_set.html @@ -0,0 +1,17 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load crispy_forms_tags %} + +{% block head_title %}{% trans "Set Password" %}{% endblock %} + +{% block inner %} +

{% trans "Set Password" %}

+ +
+ {% csrf_token %} + {{ form|crispy }} + +
+{% endblock %} + diff --git a/geeksbot_web/templates/account/signup.html b/geeksbot_web/templates/account/signup.html new file mode 100644 index 0000000..6a2954e --- /dev/null +++ b/geeksbot_web/templates/account/signup.html @@ -0,0 +1,23 @@ +{% extends "account/base.html" %} + +{% load i18n %} +{% load crispy_forms_tags %} + +{% block head_title %}{% trans "Signup" %}{% endblock %} + +{% block inner %} +

{% trans "Sign Up" %}

+ +

{% blocktrans %}Already have an account? Then please sign in.{% endblocktrans %}

+ + + +{% endblock %} + diff --git a/geeksbot_web/templates/account/signup_closed.html b/geeksbot_web/templates/account/signup_closed.html new file mode 100644 index 0000000..2322f17 --- /dev/null +++ b/geeksbot_web/templates/account/signup_closed.html @@ -0,0 +1,12 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Sign Up Closed" %}{% endblock %} + +{% block inner %} +

{% trans "Sign Up Closed" %}

+ +

{% trans "We are sorry, but the sign up is currently closed." %}

+{% endblock %} + diff --git a/geeksbot_web/templates/account/verification_sent.html b/geeksbot_web/templates/account/verification_sent.html new file mode 100644 index 0000000..ad093fd --- /dev/null +++ b/geeksbot_web/templates/account/verification_sent.html @@ -0,0 +1,13 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} + +{% block inner %} +

{% trans "Verify Your E-mail Address" %}

+ +

{% blocktrans %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

+ +{% endblock %} + diff --git a/geeksbot_web/templates/account/verified_email_required.html b/geeksbot_web/templates/account/verified_email_required.html new file mode 100644 index 0000000..09d4fde --- /dev/null +++ b/geeksbot_web/templates/account/verified_email_required.html @@ -0,0 +1,24 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} + +{% block inner %} +

{% trans "Verify Your E-mail Address" %}

+ +{% url 'account_email' as email_url %} + +

{% blocktrans %}This part of the site requires us to verify that +you are who you claim to be. For this purpose, we require that you +verify ownership of your e-mail address. {% endblocktrans %}

+ +

{% blocktrans %}We have sent an e-mail to you for +verification. Please click on the link inside this e-mail. Please +contact us if you do not receive it within a few minutes.{% endblocktrans %}

+ +

{% blocktrans %}Note: you can still change your e-mail address.{% endblocktrans %}

+ + +{% endblock %} + diff --git a/geeksbot_web/templates/base.html b/geeksbot_web/templates/base.html new file mode 100644 index 0000000..547b0c1 --- /dev/null +++ b/geeksbot_web/templates/base.html @@ -0,0 +1,110 @@ +{% load static i18n %} + + + + + {% block title %}geeksbot{% endblock title %} + + + + + + + + + + {% block css %} + + + + + + + + + + + + + {% endblock %} + + + + + +
+ + +
+ +
+ + {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + + {% block content %} +

Use this document as a way to quick start any new project.

+ {% endblock content %} + +
+ + {% block modal %}{% endblock modal %} + + + + {% block javascript %} + + + + + + + + + + + + + + + {% endblock javascript %} + + + diff --git a/geeksbot_web/templates/pages/about.html b/geeksbot_web/templates/pages/about.html new file mode 100644 index 0000000..63913c1 --- /dev/null +++ b/geeksbot_web/templates/pages/about.html @@ -0,0 +1 @@ +{% extends "base.html" %} \ No newline at end of file diff --git a/geeksbot_web/templates/pages/home.html b/geeksbot_web/templates/pages/home.html new file mode 100644 index 0000000..63913c1 --- /dev/null +++ b/geeksbot_web/templates/pages/home.html @@ -0,0 +1 @@ +{% extends "base.html" %} \ No newline at end of file diff --git a/geeksbot_web/templates/users/user_detail.html b/geeksbot_web/templates/users/user_detail.html new file mode 100644 index 0000000..c8c5d13 --- /dev/null +++ b/geeksbot_web/templates/users/user_detail.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}User: {{ object.username }}{% endblock %} + +{% block content %} +
+ +
+
+ +

{{ object.username }}

+ {% if object.name %} +

{{ object.name }}

+ {% endif %} + {{ user.auth_token }} +
+
+ +{% if object == request.user %} + +
+ +
+ My Info + E-Mail + +
+ +
+ +{% endif %} + + +
+{% endblock content %} + diff --git a/geeksbot_web/templates/users/user_form.html b/geeksbot_web/templates/users/user_form.html new file mode 100644 index 0000000..a054047 --- /dev/null +++ b/geeksbot_web/templates/users/user_form.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% load crispy_forms_tags %} + +{% block title %}{{ user.username }}{% endblock %} + +{% block content %} +

{{ user.username }}

+
+ {% csrf_token %} + {{ form|crispy }} +
+
+ +
+
+
+{% endblock %} diff --git a/geeksbot_web/users/__init__.py b/geeksbot_web/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/users/adapters.py b/geeksbot_web/users/adapters.py new file mode 100644 index 0000000..41ee20e --- /dev/null +++ b/geeksbot_web/users/adapters.py @@ -0,0 +1,35 @@ +from typing import Any + +from allauth.account.adapter import DefaultAccountAdapter +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from allauth.account.utils import user_email, user_username, user_field +from allauth.utils import valid_email_or_none +from django.conf import settings +from django.http import HttpRequest + + +class AccountAdapter(DefaultAccountAdapter): + def is_open_for_signup(self, request: HttpRequest): + return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", False) + + +class SocialAccountAdapter(DefaultSocialAccountAdapter): + def is_open_for_signup(self, request: HttpRequest, sociallogin: Any): + return getattr(settings, "SOCIAL_ACCOUNT_ALLOW_REGISTRATION", True) + + def populate_user(self, request, sociallogin, data): +# print(sociallogin.account.extra_data) + first_name = data.get('first_name') + last_name = data.get('last_name') + name = data.get('name') + id = sociallogin.account.extra_data.get('id') + user = sociallogin.user + user_username(user, data.get('username', '')) + user_email(user, valid_email_or_none(data.get('email')) or '') + name_parts = (name or '').partition(' ') + user_field(user, 'first_name', first_name or name_parts[0]) + user_field(user, 'last_name', last_name or name_parts[2]) + user_field(user, 'id', id or '') + user_field(user, 'avatar', sociallogin.account.extra_data.get('avatar', '')) + user_field(user, 'discriminator', sociallogin.account.extra_data.get('discriminator', '')) + return user diff --git a/geeksbot_web/users/admin.py b/geeksbot_web/users/admin.py new file mode 100644 index 0000000..c3af8e4 --- /dev/null +++ b/geeksbot_web/users/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from django.contrib.auth import admin as auth_admin + +from .forms import UserChangeForm, UserCreateForm +from .models import User + + +class UserAdmin(auth_admin.UserAdmin): + model = User + form = UserChangeForm + add_form = UserCreateForm + add_fieldsets = auth_admin.UserAdmin.add_fieldsets + ( + (None, {'fields': ('id')}), + ) + + +admin.site.register(User, UserAdmin) diff --git a/geeksbot_web/users/api_urls.py b/geeksbot_web/users/api_urls.py new file mode 100644 index 0000000..3e7cfa5 --- /dev/null +++ b/geeksbot_web/users/api_urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from geeksbot_web.users.views import UsersAPI, UserDetail, UserLogList, UserLogDetail + +app_name = "users_api" +urlpatterns = [ + path("", view=UsersAPI.as_view(), name="list"), + path("/", view=UserDetail.as_view(), name="detail"), + path("/logs/", view=UserLogList.as_view(), name="log_list"), + path("/logs/", view=UserLogDetail.as_view(), name="log_detail"), +] diff --git a/geeksbot_web/users/apps.py b/geeksbot_web/users/apps.py new file mode 100644 index 0000000..0684c07 --- /dev/null +++ b/geeksbot_web/users/apps.py @@ -0,0 +1,13 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class UsersConfig(AppConfig): + name = "geeksbot_web.users" + verbose_name = _("Users") + + def ready(self): + try: + import geeksbot_web.users.signals # noqa F401 + except ImportError: + pass diff --git a/geeksbot_web/users/forms.py b/geeksbot_web/users/forms.py new file mode 100644 index 0000000..74227a8 --- /dev/null +++ b/geeksbot_web/users/forms.py @@ -0,0 +1,20 @@ +from django.contrib.auth import forms +from django.forms import CharField +from allauth.account.forms import SignupForm + +from .models import User + + +class UserCreateForm(SignupForm): + id = CharField(max_length=30, label='Discord ID') + + def save(self, request): + user = super(UserCreateForm, self).save(request) + user.id = self.cleaned_data['id'] + user.save() + return user + + +class UserChangeForm(forms.UserChangeForm): + class Meta(forms.UserChangeForm.Meta): + model = User diff --git a/geeksbot_web/users/migrations/0001_initial.py b/geeksbot_web/users/migrations/0001_initial.py new file mode 100644 index 0000000..5b7996a --- /dev/null +++ b/geeksbot_web/users/migrations/0001_initial.py @@ -0,0 +1,70 @@ +# Generated by Django 2.2.4 on 2019-09-20 21:39 + +from django.conf import settings +import django.contrib.auth.models +import django.contrib.auth.validators +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + ('guilds', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('name', models.CharField(blank=True, max_length=255, verbose_name='Name of User')), + ('username', models.CharField(help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('id', models.CharField(max_length=30, primary_key=True, serialize=False)), + ('discord_username', models.CharField(max_length=100, null=True)), + ('previous_discord_usernames', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, null=True, size=None)), + ('discriminator', models.CharField(max_length=4, null=True)), + ('previous_discriminators', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=4), blank=True, null=True, size=None)), + ('steam_id', models.CharField(blank=True, max_length=30, null=True)), + ('animated', models.BooleanField(blank=True, null=True)), + ('avatar', models.CharField(blank=True, max_length=100, null=True)), + ('bot', models.BooleanField(blank=True, null=True)), + ('banned', models.BooleanField(default=False)), + ('logging_enabled', models.BooleanField(default=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('guilds', models.ManyToManyField(blank=True, null=True, to='guilds.Guild')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='UserLog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.DateTimeField(auto_now_add=True)), + ('action', models.IntegerField()), + ('description', models.CharField(blank=True, max_length=100, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/geeksbot_web/users/migrations/__init__.py b/geeksbot_web/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/users/models.py b/geeksbot_web/users/models.py new file mode 100644 index 0000000..aae41ff --- /dev/null +++ b/geeksbot_web/users/models.py @@ -0,0 +1,207 @@ +from django.contrib.auth.models import AbstractUser +from django.contrib.auth.validators import UnicodeUsernameValidator +from django.db.models import CharField +from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ +from django.db import models +from django.contrib.postgres.fields import ArrayField +from django.conf import settings +from django.db.models.signals import post_save +from django.dispatch import receiver +from rest_framework.authtoken.models import Token +from django.core.exceptions import ObjectDoesNotExist +from rest_framework import status + +from geeksbot_web.guilds.models import Guild +from .utils import verify_user_data +from .utils import create_error_response +from .utils import create_log_success_response +from .utils import create_success_response + + +@receiver(post_save, sender=settings.AUTH_USER_MODEL) +def create_auth_token(sender, instance=None, created=False, **kwargs): + if created: + Token.objects.create(user=instance) + + +class User(AbstractUser): + + # First Name and Last Name do not cover name patterns + # around the globe. + name = CharField(_("Name of User"), blank=True, max_length=255) + username = models.CharField( + _('username'), + max_length=150, + unique=False, + help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'), + validators=[UnicodeUsernameValidator()], + ) + id = models.CharField(max_length=30, primary_key=True) + discord_username = models.CharField(max_length=100, null=True) + previous_discord_usernames = ArrayField(models.CharField(max_length=100), blank=True, null=True) + discriminator = models.CharField(max_length=4, null=True) + previous_discriminators = ArrayField(models.CharField(max_length=4), blank=True, null=True) + guilds = models.ManyToManyField(Guild, blank=True, null=True) + steam_id = models.CharField(max_length=30, blank=True, null=True) + animated = models.BooleanField(blank=True, null=True) + avatar = models.CharField(max_length=100, blank=True, null=True) + bot = models.BooleanField(blank=True, null=True) + banned = models.BooleanField(default=False) + logging_enabled = models.BooleanField(default=True) + + @classmethod + def add_new_user(cls, data): + if not verify_user_data(data): + return create_error_response("Not all required fields are present.", + status=status.HTTP_400_BAD_REQUEST) + id = data.get('id') + if id: + if User.objects.filter(id=id).exists(): + return create_error_response("User Exists please update instead of create", + status=status.HTTP_409_CONFLICT) + discord_username = data.get('username') + discriminator = data.get('discriminator') + guild_id = data.get('guild') + try: + guild = Guild.objects.get(id=str(guild_id)) + except ObjectDoesNotExist: + return create_error_response("That is not a valid Guild", + status=status.HTTP_400_BAD_REQUEST) + animated = data.get('animated') + avatar = data.get('avatar') + bot = data.get('bot') + banned = data.get('banned') + logging = data.get('logging') + if not (avatar and (animated is not None) and (bot is not None)): + return create_error_response("All required fields must contain a value", + status.HTTP_400_BAD_REQUEST) + + user = User( + id=id, + discord_username=discord_username, + discriminator=discriminator, + animated=animated, + avatar=avatar, + bot=bot, + banned=banned or False, + logging_enabled=logging or True + ) + user.save() + user.guilds.add(guild) + return create_success_response(user, status.HTTP_201_CREATED, many=False) + + def update_user(self, data): + if data.get('username') and data.get('username') != self.discord_username: + if isinstance(self.previous_discord_usernames, list): + self.previous_discord_usernames.append(self.discord_username) + else: + self.previous_discord_usernames = [self.discord_username, ] + self.discord_username = data.get('username') + if data.get('discriminator') and data.get('discriminator') != self.discriminator: + if isinstance(self.previous_discriminators, list): + self.previous_discriminators.append(self.discriminator) + else: + self.previous_discriminators = [self.discriminator, ] + self.discriminator = data.get('discriminator') + if data.get('guild'): + guild = Guild.get_guild_by_id(data.get('guild')) + if not isinstance(guild, Guild): + return create_error_response("That is not a valid Guild", + status=status.HTTP_400_BAD_REQUEST) + self.guilds.add(guild) + if data.get('steam_id'): + self.steam_id = data.get('steam_id') + if data.get('animated'): + self.animated = data.get('animated') + if data.get('avatar'): + self.avatar = data.get('avatar') + if data.get('bot'): + self.bot = data.get('bot') + if data.get('banned'): + self.banned = data.get('banned') + if data.get('logging'): + self.logging_enabled = data.get('logging') + + self.save() + return create_success_response(self, status.HTTP_202_ACCEPTED, many=False) + + @classmethod + def get_user_by_id(cls, id): + try: + return cls.objects.get(id=id) + except ObjectDoesNotExist: + return None + + def get_absolute_url(self): + return reverse("users:detail", kwargs={"username": self.username}) + + +class UserLog(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + time = models.DateTimeField(auto_now_add=True, blank=True) + action = models.IntegerField() + description = models.CharField(max_length=100, null=True, blank=True) + + @classmethod + def add_new_log(cls, user, data): + user_id = data.get('user') + action = data.get('action') + description = data.get('description') + if not (user_id and action): + return create_error_response("User and Action are required.", + status=status.HTTP_400_BAD_REQUEST) + 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) + try: + action = int(action) + except ValueError: + return create_error_response("The Action must be a number", + status=status.HTTP_400_BAD_REQUEST) + log = cls( + user=user, + action=action, + description=description + ) + log.save() + return create_log_success_response(log, status.HTTP_201_CREATED, many=False) + + @classmethod + def get_log_by_id(cls, id): + try: + return cls.objects.get(id=id) + except ObjectDoesNotExist: + return None + + @classmethod + def get_logs_by_user(cls, user_id, count: int = None): + user = User.get_user_by_id(user_id) + if isinstance(user, User): + user_logs = cls.objects.filter(user=user).order_by('-time') + if count: + user_logs = user_logs[:count] + if len(user_logs) > 0: + return user_logs + else: + return [] + else: + return [] + + @classmethod + def get_logs_by_user_action(cls, user_id, action, count: int = None): + user = User.get_user_by_id(user_id) + if isinstance(user, User): + user_logs = cls.objects.filter(user=user, action=action).order_by('-time') + if count: + user_logs = user_logs[:count] + if len(user_logs) > 0: + return user_logs + else: + return [] + else: + return [] + + def __str__(self): + return f"{self.time} | {self.user.id} | {self.action}" diff --git a/geeksbot_web/users/serializers.py b/geeksbot_web/users/serializers.py new file mode 100644 index 0000000..67fd66f --- /dev/null +++ b/geeksbot_web/users/serializers.py @@ -0,0 +1,57 @@ +from rest_framework import serializers + +from geeksbot_web.users.models import User +from geeksbot_web.users.models import UserLog + + +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = [ + 'id', + 'username', + 'name', + 'discord_username', + 'previous_discord_usernames', + 'discriminator', + 'previous_discriminators', + 'guilds', + 'steam_id', + 'animated', + 'avatar', + 'bot', + 'banned', + 'logging_enabled', + 'is_staff', + 'is_superuser', + 'url' + ] + extra_kwargs = { + 'url': { + 'view_name': 'users_api:detail', + 'lookup_field': 'id' + }, + 'guilds': { + 'view_name': 'guilds_api:detail', + 'lookup_field': 'id' + } + } + + +class UserLogSerializer(serializers.ModelSerializer): + class Meta: + model = UserLog + fields = [ + 'user', + 'time', + 'action', + 'description', + 'url' + ] + extra_fields = { + 'url': { + 'view_name': 'users_api:log_detail', + 'lookup_field': 'id', + 'lookup_url_kwarg': 'log' + } + } diff --git a/geeksbot_web/users/tests/__init__.py b/geeksbot_web/users/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geeksbot_web/users/tests/factories.py b/geeksbot_web/users/tests/factories.py new file mode 100644 index 0000000..b537136 --- /dev/null +++ b/geeksbot_web/users/tests/factories.py @@ -0,0 +1,27 @@ +from typing import Any, Sequence + +from django.contrib.auth import get_user_model +from factory import DjangoModelFactory, Faker, post_generation + + +class UserFactory(DjangoModelFactory): + + username = Faker("user_name") + email = Faker("email") + name = Faker("name") + + @post_generation + def password(self, create: bool, extracted: Sequence[Any], **kwargs): + password = Faker( + "password", + length=42, + special_chars=True, + digits=True, + upper_case=True, + lower_case=True, + ).generate(extra_kwargs={}) + self.set_password(password) + + class Meta: + model = get_user_model() + django_get_or_create = ["username"] diff --git a/geeksbot_web/users/tests/test_forms.py b/geeksbot_web/users/tests/test_forms.py new file mode 100644 index 0000000..9e80054 --- /dev/null +++ b/geeksbot_web/users/tests/test_forms.py @@ -0,0 +1,40 @@ +import pytest + +from geeksbot_web.users.forms import UserCreationForm +from geeksbot_web.users.tests.factories import UserFactory + +pytestmark = pytest.mark.django_db + + +class TestUserCreationForm: + def test_clean_username(self): + # A user with proto_user params does not exist yet. + proto_user = UserFactory.build() + + form = UserCreationForm( + { + "username": proto_user.username, + "password1": proto_user._password, + "password2": proto_user._password, + } + ) + + assert form.is_valid() + assert form.clean_username() == proto_user.username + + # Creating a user. + form.save() + + # The user with proto_user params already exists, + # hence cannot be created. + form = UserCreationForm( + { + "username": proto_user.username, + "password1": proto_user._password, + "password2": proto_user._password, + } + ) + + assert not form.is_valid() + assert len(form.errors) == 1 + assert "username" in form.errors diff --git a/geeksbot_web/users/tests/test_models.py b/geeksbot_web/users/tests/test_models.py new file mode 100644 index 0000000..5486363 --- /dev/null +++ b/geeksbot_web/users/tests/test_models.py @@ -0,0 +1,8 @@ +import pytest +from django.conf import settings + +pytestmark = pytest.mark.django_db + + +def test_user_get_absolute_url(user: settings.AUTH_USER_MODEL): + assert user.get_absolute_url() == f"/users/{user.username}/" diff --git a/geeksbot_web/users/tests/test_urls.py b/geeksbot_web/users/tests/test_urls.py new file mode 100644 index 0000000..c636192 --- /dev/null +++ b/geeksbot_web/users/tests/test_urls.py @@ -0,0 +1,23 @@ +import pytest +from django.conf import settings +from django.urls import reverse, resolve + +pytestmark = pytest.mark.django_db + + +def test_detail(user: settings.AUTH_USER_MODEL): + assert ( + reverse("users:detail", kwargs={"username": user.username}) + == f"/users/{user.username}/" + ) + assert resolve(f"/users/{user.username}/").view_name == "users:detail" + + +def test_update(): + assert reverse("users:update") == "/users/~update/" + assert resolve("/users/~update/").view_name == "users:update" + + +def test_redirect(): + assert reverse("users:redirect") == "/users/~redirect/" + assert resolve("/users/~redirect/").view_name == "users:redirect" diff --git a/geeksbot_web/users/tests/test_views.py b/geeksbot_web/users/tests/test_views.py new file mode 100644 index 0000000..025f44d --- /dev/null +++ b/geeksbot_web/users/tests/test_views.py @@ -0,0 +1,52 @@ +import pytest +from django.conf import settings +from django.test import RequestFactory + +from geeksbot_web.users.views import UserRedirectView, UserUpdateView + +pytestmark = pytest.mark.django_db + + +class TestUserUpdateView: + """ + TODO: + extracting view initialization code as class-scoped fixture + would be great if only pytest-django supported non-function-scoped + fixture db access -- this is a work-in-progress for now: + https://github.com/pytest-dev/pytest-django/pull/258 + """ + + def test_get_success_url( + self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory + ): + view = UserUpdateView() + request = request_factory.get("/fake-url/") + request.user = user + + view.request = request + + assert view.get_success_url() == f"/users/{user.username}/" + + def test_get_object( + self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory + ): + view = UserUpdateView() + request = request_factory.get("/fake-url/") + request.user = user + + view.request = request + + assert view.get_object() == user + + +class TestUserRedirectView: + def test_get_redirect_url( + self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory + ): + view = UserRedirectView() + request = request_factory.get("/fake-url") + request.user = user + + view.request = request + + assert view.get_redirect_url() == f"/users/{user.username}/" diff --git a/geeksbot_web/users/urls.py b/geeksbot_web/users/urls.py new file mode 100644 index 0000000..8620294 --- /dev/null +++ b/geeksbot_web/users/urls.py @@ -0,0 +1,14 @@ +from django.urls import path + +from geeksbot_web.users.views import ( + user_redirect_view, + user_update_view, + user_detail_view, +) + +app_name = "users" +urlpatterns = [ + path("~redirect/", view=user_redirect_view, name="redirect"), + path("~update/", view=user_update_view, name="update"), + path("/", view=user_detail_view, name="detail"), +] diff --git a/geeksbot_web/users/utils.py b/geeksbot_web/users/utils.py new file mode 100644 index 0000000..393d9bd --- /dev/null +++ b/geeksbot_web/users/utils.py @@ -0,0 +1,36 @@ +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(user_data, status, many: bool = False): + from .serializers import UserSerializer + + return Response(UserSerializer(user_data, many=many).data, + status=status) + + +def create_log_success_response(log_data, status, many: bool = False): + from .serializers import UserLogSerializer + + return Response(UserLogSerializer(log_data, many=many).data, + status=status) + + +required_fields = [ + 'id', + 'username', + 'discriminator', + 'guild', + 'animated', + 'avatar', + 'bot', +] + + +def verify_user_data(data): + return all([field in data.keys() for field in required_fields]) diff --git a/geeksbot_web/users/views.py b/geeksbot_web/users/views.py new file mode 100644 index 0000000..af7c437 --- /dev/null +++ b/geeksbot_web/users/views.py @@ -0,0 +1,163 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse +from django.views.generic import DetailView, RedirectView, UpdateView +from django.contrib import messages +from django.utils.translation import ugettext_lazy as _ +from rest_framework.views import APIView +from rest_framework import generics +from rest_framework.permissions import IsAuthenticated +from rest_framework import status + + +from .models import UserLog +from geeksbot_web.utils.api_utils import PaginatedAPIView +from .models import User +from .serializers import UserSerializer +from .serializers import UserLogSerializer +from geeksbot_web.utils.permissions import CustomDjangoModelPermissions +from geeksbot_web.utils.permissions import CustomDjangoObjectPermissions +from .utils import create_error_response +from .utils import create_success_response +from .utils import create_log_success_response + + +class UserDetailView(LoginRequiredMixin, DetailView): + + model = User + slug_field = "username" + slug_url_kwarg = "username" + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + + context = self.get_context_data(object=self.object, user=request.user) + return self.render_to_response(context) + + +user_detail_view = UserDetailView.as_view() + + +class UserUpdateView(LoginRequiredMixin, UpdateView): + + model = User + fields = ["name"] + + def get_success_url(self): + return reverse("users:detail", kwargs={"username": self.request.user.username}) + + def get_object(self): + return User.objects.get(username=self.request.user.username) + + def form_valid(self, form): + messages.add_message( + self.request, messages.INFO, _("Infos successfully updated") + ) + return super().form_valid(form) + + +user_update_view = UserUpdateView.as_view() + + +class UserRedirectView(LoginRequiredMixin, RedirectView): + + permanent = False + + def get_redirect_url(self): + return reverse("users:detail", kwargs={"username": self.request.user.username}) + + +user_redirect_view = UserRedirectView.as_view() + +# API Views + + +class UsersAPI(generics.ListCreateAPIView): + permission_classes = [IsAuthenticated] + serializer_class = UserSerializer + + def get_queryset(self): + return User.objects.filter(guilds__id=self.request.data.get('guild')) + + # def get(self, request, guild=None, format=None): + # if guild: + # users = User.objects.filter(guilds__id=guild) + # else: + # users = User.objects.all() + # page = self.paginate_queryset(users) + # if page is not None: + # return create_success_response(page, status.HTTP_200_OK, many=True) + # + # return create_success_response(users, status.HTTP_200_OK, many=True) + # + # def post(self, request, format=None): + # data = dict(request.data) + # return User.add_new_user(data) + + +class UserDetail(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [IsAuthenticated] + serializer_class = UserSerializer + lookup_field = 'id' + + def get_queryset(self): + return User.objects.all() + + # def get(self, request, id, format=None): + # user = User.get_user_by_id(id) + # if not isinstance(user, User): + # return create_error_response("User Does not Exist", + # status=status.HTTP_404_NOT_FOUND) + # return create_success_response(user, + # status=status.HTTP_200_OK) + # + # def put(self, request, id, format=None): + # user = User.get_user_by_id(id) + # if isinstance(user, User): + # data = dict(request.data) + # return user.update_user(data) + # else: + # return create_error_response("User Does Not Exist", + # status=status.HTTP_404_NOT_FOUND) + + +class UserLogList(generics.ListCreateAPIView): + permission_classes = [IsAuthenticated] + serializer_class = UserLogSerializer + + def get_queryset(self): + return UserLog.objects.all() + + # def get(self, request, user, action=None, format=None): + # if action: + # user_logs = UserLog.get_logs_by_user_action(user, action) + # else: + # user_logs = UserLog.get_logs_by_user(user) + # + # page = self.paginate_queryset(user_logs) + # if page is not None: + # return create_log_success_response(page, status.HTTP_200_OK, many=True) + # + # return create_log_success_response(user_logs, status.HTTP_200_OK, many=True) + # + # def post(self, request, user, format=None): + # data = dict(request.data) + # return UserLog.add_new_log(user, data) + + +class UserLogDetail(generics.RetrieveUpdateAPIView): + permission_classes = [IsAuthenticated] + serializer_class = UserLogSerializer + lookup_url_kwarg = 'log' + lookup_field = 'id' + + def get_queryset(self): + user_id = self.kwargs['id'] + return UserLog.objects.filter(user__id=user_id) + + # def get(self, request, id, format=None): + # user_log = UserLog.get_log_by_id(id) + # if isinstance(user_log, UserLog): + # return create_log_success_response(user_log, status.HTTP_200_OK, many=False) + # else: + # return create_error_response("Log Does Not Exist", + # status=status.HTTP_404_NOT_FOUND) diff --git a/geeksbot_web/utils/api_utils.py b/geeksbot_web/utils/api_utils.py new file mode 100644 index 0000000..62a1775 --- /dev/null +++ b/geeksbot_web/utils/api_utils.py @@ -0,0 +1,33 @@ +from rest_framework.views import APIView +from rest_framework.settings import api_settings + + +class PaginatedAPIView(APIView): + pagination_class = api_settings.DEFAULT_PAGINATION_CLASS + + @property + def paginator(self): + """ + The paginator instance associated with the view, or `None`. + """ + if not hasattr(self, "_paginator"): + if self.pagination_class is None: + self._paginator = None + else: + self._paginator = self.pagination_class() + return self._paginator + + def paginate_queryset(self, queryset): + """ + Return a single page of results, or `None` if pagination is disabled. + """ + if self.paginator is None: + return None + return self.paginator.paginate_queryset(queryset, self.request, view=self) + + def get_paginated_response(self, data): + """ + Return a paginated style `Response` object for the given output data. + """ + assert self.paginator is not None + return self.paginator.get_paginated_response(data) diff --git a/geeksbot_web/utils/permissions.py b/geeksbot_web/utils/permissions.py new file mode 100644 index 0000000..fb55609 --- /dev/null +++ b/geeksbot_web/utils/permissions.py @@ -0,0 +1,27 @@ +from rest_framework.permissions import DjangoModelPermissions, DjangoObjectPermissions + + +class CustomDjangoModelPermissions(DjangoModelPermissions): + # Overriding to require view permissions + perms_map = { + 'GET': ['%(app_label)s.view_%(model_name)s'], + 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], + 'HEAD': ['%(app_label)s.view_%(model_name)s'], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + } + + +class CustomDjangoObjectPermissions(DjangoObjectPermissions): + # Overriding to require view permissions + perms_map = { + 'GET': ['%(app_label)s.view_%(model_name)s'], + 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], + 'HEAD': ['%(app_label)s.view_%(model_name)s'], + 'POST': ['%(app_label)s.add_%(model_name)s'], + 'PUT': ['%(app_label)s.change_%(model_name)s'], + 'PATCH': ['%(app_label)s.change_%(model_name)s'], + 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + } diff --git a/locale/README.rst b/locale/README.rst new file mode 100644 index 0000000..c2f1dcd --- /dev/null +++ b/locale/README.rst @@ -0,0 +1,6 @@ +Translations +============ + +Translations will be placed in this folder when running:: + + python manage.py makemessages diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000..d28608c --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,5 @@ +pytz==2019.2 # https://github.com/stub42/pytz +python-slugify==3.0.3 # https://github.com/un33k/python-slugify +Pillow==6.1.0 # https://github.com/python-pillow/Pillow +argon2-cffi==19.1.0 # https://github.com/hynek/argon2_cffi +redis==3.3.8 # https://github.com/antirez/redis diff --git a/requirements/local.txt b/requirements/local.txt new file mode 100644 index 0000000..dfe358b --- /dev/null +++ b/requirements/local.txt @@ -0,0 +1,30 @@ +-r ./base.txt + +docker-compose + +Werkzeug==0.14.1 # pyup: < 0.15 # https://github.com/pallets/werkzeug +ipdb==0.12.2 # https://github.com/gotcha/ipdb +Sphinx==2.2.0 # https://github.com/sphinx-doc/sphinx +psycopg2==2.8.3 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 + +# Testing +# ------------------------------------------------------------------------------ +mypy==0.720 # https://github.com/python/mypy +pytest==5.1.1 # https://github.com/pytest-dev/pytest +pytest-sugar==0.9.2 # https://github.com/Frozenball/pytest-sugar + +# Code quality +# ------------------------------------------------------------------------------ +flake8==3.7.8 # https://github.com/PyCQA/flake8 +coverage==4.5.4 # https://github.com/nedbat/coveragepy +black==19.3b0 # https://github.com/ambv/black +pylint-django==2.0.11 # https://github.com/PyCQA/pylint-django + +# Django +# ------------------------------------------------------------------------------ +factory-boy==2.12.0 # https://github.com/FactoryBoy/factory_boy + +django-debug-toolbar==2.0 # https://github.com/jazzband/django-debug-toolbar +django-extensions==2.2.1 # https://github.com/django-extensions/django-extensions +django-coverage-plugin==1.6.0 # https://github.com/nedbat/django_coverage_plugin +pytest-django==3.5.1 # https://github.com/pytest-dev/pytest-django diff --git a/requirements/production.txt b/requirements/production.txt new file mode 100644 index 0000000..94a5caa --- /dev/null +++ b/requirements/production.txt @@ -0,0 +1,4 @@ +# PRECAUTION: avoid production dependencies that aren't in development + +-r ./base.txt + diff --git a/requirements/web.txt b/requirements/web.txt new file mode 100644 index 0000000..8298e9d --- /dev/null +++ b/requirements/web.txt @@ -0,0 +1,22 @@ +# Django +# ------------------------------------------------------------------------------ +django==2.2.4 # pyup: < 3.0 # https://www.djangoproject.com/ +django-environ==0.4.5 # https://github.com/joke2k/django-environ +django-model-utils==3.2.0 # https://github.com/jazzband/django-model-utils +django-allauth==0.39.1 # https://github.com/pennersr/django-allauth +django-crispy-forms==1.7.2 # https://github.com/django-crispy-forms/django-crispy-forms +django-redis==4.10.0 # https://github.com/niwinz/django-redis +django-anymail[mailgun]==6.1.0 # https://github.com/anymail/django-anymail +django-debug-toolbar +django-extensions + +# Django REST Framework +djangorestframework==3.10.2 # https://github.com/encode/django-rest-framework +coreapi==2.3.3 # https://github.com/core-api/python-client + +gevent +gunicorn==19.9.0 # https://github.com/benoitc/gunicorn +psycopg2==2.8.3 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 +Collectfast==1.0.0 # https://github.com/antonagestam/collectfast + +python-valve diff --git a/services/Dockerfile-base b/services/Dockerfile-base new file mode 100644 index 0000000..f2b0e0a --- /dev/null +++ b/services/Dockerfile-base @@ -0,0 +1,30 @@ +FROM python:3.7-alpine AS geeksbot-base + +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 +WORKDIR /code + +ENV LC_ALL C.UTF-8 +ENV LANG C.UTF-8 + +RUN pip install --upgrade pip +RUN pip install virtualenv + +RUN apk update && apk add postgresql-client diff --git a/services/geeksbot.conf b/services/geeksbot.conf new file mode 100644 index 0000000..e490149 --- /dev/null +++ b/services/geeksbot.conf @@ -0,0 +1,48 @@ +upstream app_server { + server 127.0.0.1:8000 fail_timeout=0; +} + +server { + listen 443 ssl; + keepalive_timeout 5; + + ssl_certificate /etc/ssl/geeksbot_app_cert_chain.crt; + ssl_certificate_key /etc/ssl/geeksbot.app.key; + + access_log /tmp/logs/geeksbot/access.log; + error_log /tmp/logs/geeksbot/error.log; + + location /static/ { + alias /code/geeksbot_web/staticfiles/; + } + + location /error/ { + alias /code/geeksbot_web/staticfiles/errors/; + } + + location / { + try_files $uri @proxy_to_app; + } + + location @proxy_to_app { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + proxy_buffering off; + proxy_read_timeout 180; + proxy_connect_timeout 180; + + if (!-f $request_filename) { + proxy_pass http://app_server; + break; + } + } + + error_page 500 502 503 504 /error/maintenance.html; +} + +server { + listen 80 default_server; + return 301 https://$host$request_uri; +} \ No newline at end of file diff --git a/services/gunicorn.conf b/services/gunicorn.conf new file mode 100644 index 0000000..6636983 --- /dev/null +++ b/services/gunicorn.conf @@ -0,0 +1,10 @@ +import multiprocessing + +bind = "0.0.0.0:8000" +workers = multiprocessing.cpu_count() * 2 + 1 +worker_class = "gevent" +worker_connections = 4096 +timeout = 180 +backlog = 2048 +pidfile = "/tmp/geeksbot.pid" +reload = True \ No newline at end of file diff --git a/services/nginx.conf b/services/nginx.conf new file mode 100644 index 0000000..14446ff --- /dev/null +++ b/services/nginx.conf @@ -0,0 +1,44 @@ +user geeksbot; +master_process off; +# set open fd limit to 30000 +worker_rlimit_nofile 30000; +pid /var/run/nginx.pid; +daemon off; + +events { + worker_connections 4096; + accept_mutex off; +} + +http { + # Basic Settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 200M; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + access_log /tmp/logs/nginx/access.log; + error_log /tmp/logs/nginx/error.log; + + # Gzip + + gzip on; + gzip_proxied any; + gzip_comp_level 2; + gzip_http_version 1.1; + gzip_buffers 16 8k; + gzip_types text/plain text/css application/json application/x-javascript text/xml applicaion/xml application/xml-rss text/javascript; + gzip_disable "msie6"; + gzip_vary on; + + # Virtual Host Configs + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} \ No newline at end of file diff --git a/services/postgresql/postgres.conf b/services/postgresql/postgres.conf new file mode 100644 index 0000000..44c7a46 --- /dev/null +++ b/services/postgresql/postgres.conf @@ -0,0 +1,690 @@ +# ----------------------------- +# PostgreSQL configuration file +# ----------------------------- +# +# This file consists of lines of the form: +# +# name = value +# +# (The "=" is optional.) Whitespace may be used. Comments are introduced with +# "#" anywhere on a line. The complete list of parameter names and allowed +# values can be found in the PostgreSQL documentation. +# +# The commented-out settings shown in this file represent the default values. +# Re-commenting a setting is NOT sufficient to revert it to the default value; +# you need to reload the server. +# +# This file is read on server startup and when the server receives a SIGHUP +# signal. If you edit the file on a running system, you have to SIGHUP the +# server for the changes to take effect, run "pg_ctl reload", or execute +# "SELECT pg_reload_conf()". Some parameters, which are marked below, +# require a server shutdown and restart to take effect. +# +# Any parameter can also be given as a command-line option to the server, e.g., +# "postgres -c log_connections=on". Some parameters can be changed at run time +# with the "SET" SQL command. +# +# Memory units: kB = kilobytes Time units: ms = milliseconds +# MB = megabytes s = seconds +# GB = gigabytes min = minutes +# TB = terabytes h = hours +# d = days + + +#------------------------------------------------------------------------------ +# FILE LOCATIONS +#------------------------------------------------------------------------------ + +# The default values of these variables are driven from the -D command-line +# option or PGDATA environment variable, represented here as ConfigDir. + +#data_directory = 'ConfigDir' # use data in another directory + # (change requires restart) +#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file + # (change requires restart) +#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file + # (change requires restart) + +# If external_pid_file is not explicitly set, no extra PID file is written. +#external_pid_file = '' # write an extra PID file + # (change requires restart) + + +#------------------------------------------------------------------------------ +# CONNECTIONS AND AUTHENTICATION +#------------------------------------------------------------------------------ + +# - Connection Settings - + +listen_addresses = '*' + # comma-separated list of addresses; + # defaults to 'localhost'; use '*' for all + # (change requires restart) +#port = 5432 # (change requires restart) +max_connections = 1000 # (change requires restart) +#superuser_reserved_connections = 3 # (change requires restart) +#unix_socket_directories = '/tmp' # comma-separated list of directories + # (change requires restart) +#unix_socket_group = '' # (change requires restart) +#unix_socket_permissions = 0777 # begin with 0 to use octal notation + # (change requires restart) +#bonjour = off # advertise server via Bonjour + # (change requires restart) +#bonjour_name = '' # defaults to the computer name + # (change requires restart) + +# - TCP Keepalives - +# see "man 7 tcp" for details + +#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds; + # 0 selects the system default +#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds; + # 0 selects the system default +#tcp_keepalives_count = 0 # TCP_KEEPCNT; + # 0 selects the system default + +# - Authentication - + +#authentication_timeout = 1min # 1s-600s +#password_encryption = md5 # md5 or scram-sha-256 +#db_user_namespace = off + +# GSSAPI using Kerberos +#krb_server_keyfile = '' +#krb_caseins_users = off + +# - SSL - + +#ssl = off +#ssl_ca_file = '' +#ssl_cert_file = 'server.crt' +#ssl_crl_file = '' +#ssl_key_file = 'server.key' +#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers +#ssl_prefer_server_ciphers = on +#ssl_ecdh_curve = 'prime256v1' +#ssl_dh_params_file = '' +#ssl_passphrase_command = '' +#ssl_passphrase_command_supports_reload = off + + +#------------------------------------------------------------------------------ +# RESOURCE USAGE (except WAL) +#------------------------------------------------------------------------------ + +# - Memory - + +#shared_buffers = 32MB # min 128kB + # (change requires restart) +#huge_pages = try # on, off, or try + # (change requires restart) +#temp_buffers = 8MB # min 800kB +#max_prepared_transactions = 0 # zero disables the feature + # (change requires restart) +# Caution: it is not advisable to set max_prepared_transactions nonzero unless +# you actively intend to use prepared transactions. +#work_mem = 4MB # min 64kB +#maintenance_work_mem = 64MB # min 1MB +#autovacuum_work_mem = -1 # min 1MB, or -1 to use maintenance_work_mem +#max_stack_depth = 2MB # min 100kB +#dynamic_shared_memory_type = posix # the default is the first option + # supported by the operating system: + # posix + # sysv + # windows + # mmap + # use none to disable dynamic shared memory + # (change requires restart) + +# - Disk - + +#temp_file_limit = -1 # limits per-process temp file space + # in kB, or -1 for no limit + +# - Kernel Resources - + +#max_files_per_process = 1000 # min 25 + # (change requires restart) + +# - Cost-Based Vacuum Delay - + +#vacuum_cost_delay = 0 # 0-100 milliseconds +#vacuum_cost_page_hit = 1 # 0-10000 credits +#vacuum_cost_page_miss = 10 # 0-10000 credits +#vacuum_cost_page_dirty = 20 # 0-10000 credits +#vacuum_cost_limit = 200 # 1-10000 credits + +# - Background Writer - + +#bgwriter_delay = 200ms # 10-10000ms between rounds +#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables +#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round +#bgwriter_flush_after = 0 # measured in pages, 0 disables + +# - Asynchronous Behavior - + +#effective_io_concurrency = 1 # 1-1000; 0 disables prefetching +#max_worker_processes = 8 # (change requires restart) +#max_parallel_maintenance_workers = 2 # taken from max_parallel_workers +#max_parallel_workers_per_gather = 2 # taken from max_parallel_workers +#parallel_leader_participation = on +#max_parallel_workers = 8 # maximum number of max_worker_processes that + # can be used in parallel operations +#old_snapshot_threshold = -1 # 1min-60d; -1 disables; 0 is immediate + # (change requires restart) +#backend_flush_after = 0 # measured in pages, 0 disables + + +#------------------------------------------------------------------------------ +# WRITE-AHEAD LOG +#------------------------------------------------------------------------------ + +# - Settings - + +#wal_level = replica # minimal, replica, or logical + # (change requires restart) +#fsync = on # flush data to disk for crash safety + # (turning this off can cause + # unrecoverable data corruption) +#synchronous_commit = on # synchronization level; + # off, local, remote_write, remote_apply, or on +#wal_sync_method = fsync # the default is the first option + # supported by the operating system: + # open_datasync + # fdatasync (default on Linux) + # fsync + # fsync_writethrough + # open_sync +#full_page_writes = on # recover from partial page writes +#wal_compression = off # enable compression of full-page writes +#wal_log_hints = off # also do full page writes of non-critical updates + # (change requires restart) +#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers + # (change requires restart) +#wal_writer_delay = 200ms # 1-10000 milliseconds +#wal_writer_flush_after = 1MB # measured in pages, 0 disables + +#commit_delay = 0 # range 0-100000, in microseconds +#commit_siblings = 5 # range 1-1000 + +# - Checkpoints - + +#checkpoint_timeout = 5min # range 30s-1d +#max_wal_size = 1GB +#min_wal_size = 80MB +#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0 +#checkpoint_flush_after = 0 # measured in pages, 0 disables +#checkpoint_warning = 30s # 0 disables + +# - Archiving - + +#archive_mode = off # enables archiving; off, on, or always + # (change requires restart) +#archive_command = '' # command to use to archive a logfile segment + # placeholders: %p = path of file to archive + # %f = file name only + # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f' +#archive_timeout = 0 # force a logfile segment switch after this + # number of seconds; 0 disables + + +#------------------------------------------------------------------------------ +# REPLICATION +#------------------------------------------------------------------------------ + +# - Sending Servers - + +# Set these on the master and on any standby that will send replication data. + +#max_wal_senders = 10 # max number of walsender processes + # (change requires restart) +#wal_keep_segments = 0 # in logfile segments; 0 disables +#wal_sender_timeout = 60s # in milliseconds; 0 disables + +#max_replication_slots = 10 # max number of replication slots + # (change requires restart) +#track_commit_timestamp = off # collect timestamp of transaction commit + # (change requires restart) + +# - Master Server - + +# These settings are ignored on a standby server. + +#synchronous_standby_names = '' # standby servers that provide sync rep + # method to choose sync standbys, number of sync standbys, + # and comma-separated list of application_name + # from standby(s); '*' = all +#vacuum_defer_cleanup_age = 0 # number of xacts by which cleanup is delayed + +# - Standby Servers - + +# These settings are ignored on a master server. + +#hot_standby = on # "off" disallows queries during recovery + # (change requires restart) +#max_standby_archive_delay = 30s # max delay before canceling queries + # when reading WAL from archive; + # -1 allows indefinite delay +#max_standby_streaming_delay = 30s # max delay before canceling queries + # when reading streaming WAL; + # -1 allows indefinite delay +#wal_receiver_status_interval = 10s # send replies at least this often + # 0 disables +#hot_standby_feedback = off # send info from standby to prevent + # query conflicts +#wal_receiver_timeout = 60s # time that receiver waits for + # communication from master + # in milliseconds; 0 disables +#wal_retrieve_retry_interval = 5s # time to wait before retrying to + # retrieve WAL after a failed attempt + +# - Subscribers - + +# These settings are ignored on a publisher. + +#max_logical_replication_workers = 4 # taken from max_worker_processes + # (change requires restart) +#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers + + +#------------------------------------------------------------------------------ +# QUERY TUNING +#------------------------------------------------------------------------------ + +# - Planner Method Configuration - + +#enable_bitmapscan = on +#enable_hashagg = on +#enable_hashjoin = on +#enable_indexscan = on +#enable_indexonlyscan = on +#enable_material = on +#enable_mergejoin = on +#enable_nestloop = on +#enable_parallel_append = on +#enable_seqscan = on +#enable_sort = on +#enable_tidscan = on +#enable_partitionwise_join = off +#enable_partitionwise_aggregate = off +#enable_parallel_hash = on +#enable_partition_pruning = on + +# - Planner Cost Constants - + +#seq_page_cost = 1.0 # measured on an arbitrary scale +#random_page_cost = 4.0 # same scale as above +#cpu_tuple_cost = 0.01 # same scale as above +#cpu_index_tuple_cost = 0.005 # same scale as above +#cpu_operator_cost = 0.0025 # same scale as above +#parallel_tuple_cost = 0.1 # same scale as above +#parallel_setup_cost = 1000.0 # same scale as above + +#jit_above_cost = 100000 # perform JIT compilation if available + # and query more expensive than this; + # -1 disables +#jit_inline_above_cost = 500000 # inline small functions if query is + # more expensive than this; -1 disables +#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if + # query is more expensive than this; + # -1 disables + +#min_parallel_table_scan_size = 8MB +#min_parallel_index_scan_size = 512kB +#effective_cache_size = 4GB + +# - Genetic Query Optimizer - + +#geqo = on +#geqo_threshold = 12 +#geqo_effort = 5 # range 1-10 +#geqo_pool_size = 0 # selects default based on effort +#geqo_generations = 0 # selects default based on effort +#geqo_selection_bias = 2.0 # range 1.5-2.0 +#geqo_seed = 0.0 # range 0.0-1.0 + +# - Other Planner Options - + +#default_statistics_target = 100 # range 1-10000 +#constraint_exclusion = partition # on, off, or partition +#cursor_tuple_fraction = 0.1 # range 0.0-1.0 +#from_collapse_limit = 8 +#join_collapse_limit = 8 # 1 disables collapsing of explicit + # JOIN clauses +#force_parallel_mode = off +#jit = off # allow JIT compilation + + +#------------------------------------------------------------------------------ +# REPORTING AND LOGGING +#------------------------------------------------------------------------------ + +# - Where to Log - + +#log_destination = 'stderr' # Valid values are combinations of + # stderr, csvlog, syslog, and eventlog, + # depending on platform. csvlog + # requires logging_collector to be on. + +# This is used when logging to stderr: +#logging_collector = off # Enable capturing of stderr and csvlog + # into log files. Required to be on for + # csvlogs. + # (change requires restart) + +# These are only used if logging_collector is on: +#log_directory = 'log' # directory where log files are written, + # can be absolute or relative to PGDATA +#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, + # can include strftime() escapes +#log_file_mode = 0600 # creation mode for log files, + # begin with 0 to use octal notation +#log_truncate_on_rotation = off # If on, an existing log file with the + # same name as the new log file will be + # truncated rather than appended to. + # But such truncation only occurs on + # time-driven rotation, not on restarts + # or size-driven rotation. Default is + # off, meaning append to existing files + # in all cases. +#log_rotation_age = 1d # Automatic rotation of logfiles will + # happen after that time. 0 disables. +#log_rotation_size = 10MB # Automatic rotation of logfiles will + # happen after that much log output. + # 0 disables. + +# These are relevant when logging to syslog: +#syslog_facility = 'LOCAL0' +#syslog_ident = 'postgres' +#syslog_sequence_numbers = on +#syslog_split_messages = on + +# This is only relevant when logging to eventlog (win32): +# (change requires restart) +#event_source = 'PostgreSQL' + +# - When to Log - + +#log_min_messages = warning # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # info + # notice + # warning + # error + # log + # fatal + # panic + +#log_min_error_statement = error # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # info + # notice + # warning + # error + # log + # fatal + # panic (effectively off) + +#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements + # and their durations, > 0 logs only + # statements running at least this number + # of milliseconds + + +# - What to Log - + +#debug_print_parse = off +#debug_print_rewritten = off +#debug_print_plan = off +#debug_pretty_print = on +#log_checkpoints = off +#log_connections = off +#log_disconnections = off +#log_duration = off +#log_error_verbosity = default # terse, default, or verbose messages +#log_hostname = off +#log_line_prefix = '%m [%p] ' # special values: + # %a = application name + # %u = user name + # %d = database name + # %r = remote host and port + # %h = remote host + # %p = process ID + # %t = timestamp without milliseconds + # %m = timestamp with milliseconds + # %n = timestamp with milliseconds (as a Unix epoch) + # %i = command tag + # %e = SQL state + # %c = session ID + # %l = session line number + # %s = session start timestamp + # %v = virtual transaction ID + # %x = transaction ID (0 if none) + # %q = stop here in non-session + # processes + # %% = '%' + # e.g. '<%u%%%d> ' +#log_lock_waits = off # log lock waits >= deadlock_timeout +#log_statement = 'none' # none, ddl, mod, all +#log_replication_commands = off +#log_temp_files = -1 # log temporary files equal or larger + # than the specified size in kilobytes; + # -1 disables, 0 logs all temp files +#log_timezone = 'GMT' + +#------------------------------------------------------------------------------ +# PROCESS TITLE +#------------------------------------------------------------------------------ + +#cluster_name = '' # added to process titles if nonempty + # (change requires restart) +#update_process_title = on + + +#------------------------------------------------------------------------------ +# STATISTICS +#------------------------------------------------------------------------------ + +# - Query and Index Statistics Collector - + +#track_activities = on +#track_counts = on +#track_io_timing = off +#track_functions = none # none, pl, all +#track_activity_query_size = 1024 # (change requires restart) +#stats_temp_directory = 'pg_stat_tmp' + + +# - Monitoring - + +#log_parser_stats = off +#log_planner_stats = off +#log_executor_stats = off +#log_statement_stats = off + + +#------------------------------------------------------------------------------ +# AUTOVACUUM +#------------------------------------------------------------------------------ + +#autovacuum = on # Enable autovacuum subprocess? 'on' + # requires track_counts to also be on. +#log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and + # their durations, > 0 logs only + # actions running at least this number + # of milliseconds. +#autovacuum_max_workers = 3 # max number of autovacuum subprocesses + # (change requires restart) +#autovacuum_naptime = 1min # time between autovacuum runs +#autovacuum_vacuum_threshold = 50 # min number of row updates before + # vacuum +#autovacuum_analyze_threshold = 50 # min number of row updates before + # analyze +#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum +#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze +#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum + # (change requires restart) +#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age + # before forced vacuum + # (change requires restart) +#autovacuum_vacuum_cost_delay = 20ms # default vacuum cost delay for + # autovacuum, in milliseconds; + # -1 means use vacuum_cost_delay +#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for + # autovacuum, -1 means use + # vacuum_cost_limit + + +#------------------------------------------------------------------------------ +# CLIENT CONNECTION DEFAULTS +#------------------------------------------------------------------------------ + +# - Statement Behavior - + +#client_min_messages = notice # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # log + # notice + # warning + # error +#search_path = '"$user", public' # schema names +#row_security = on +#default_tablespace = '' # a tablespace name, '' uses the default +#temp_tablespaces = '' # a list of tablespace names, '' uses + # only default tablespace +#check_function_bodies = on +#default_transaction_isolation = 'read committed' +#default_transaction_read_only = off +#default_transaction_deferrable = off +#session_replication_role = 'origin' +#statement_timeout = 0 # in milliseconds, 0 is disabled +#lock_timeout = 0 # in milliseconds, 0 is disabled +#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled +#vacuum_freeze_min_age = 50000000 +#vacuum_freeze_table_age = 150000000 +#vacuum_multixact_freeze_min_age = 5000000 +#vacuum_multixact_freeze_table_age = 150000000 +#vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples + # before index cleanup, 0 always performs + # index cleanup +#bytea_output = 'hex' # hex, escape +#xmlbinary = 'base64' +#xmloption = 'content' +#gin_fuzzy_search_limit = 0 +#gin_pending_list_limit = 4MB + +# - Locale and Formatting - + +#datestyle = 'iso, mdy' +#intervalstyle = 'postgres' +#timezone = 'GMT' +#timezone_abbreviations = 'Default' # Select the set of available time zone + # abbreviations. Currently, there are + # Default + # Australia (historical usage) + # India + # You can create your own file in + # share/timezonesets/. +#extra_float_digits = 0 # min -15, max 3 +#client_encoding = sql_ascii # actually, defaults to database + # encoding + +# These settings are initialized by initdb, but they can be changed. +#lc_messages = 'C' # locale for system error message + # strings +#lc_monetary = 'C' # locale for monetary formatting +#lc_numeric = 'C' # locale for number formatting +#lc_time = 'C' # locale for time formatting + +# default configuration for text search +#default_text_search_config = 'pg_catalog.simple' + +# - Shared Library Preloading - + +#shared_preload_libraries = '' # (change requires restart) +#local_preload_libraries = '' +#session_preload_libraries = '' +#jit_provider = 'llvmjit' # JIT library to use + +# - Other Defaults - + +#dynamic_library_path = '$libdir' + + +#------------------------------------------------------------------------------ +# LOCK MANAGEMENT +#------------------------------------------------------------------------------ + +#deadlock_timeout = 1s +#max_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_relation = -2 # negative values mean + # (max_pred_locks_per_transaction + # / -max_pred_locks_per_relation) - 1 +#max_pred_locks_per_page = 2 # min 0 + + +#------------------------------------------------------------------------------ +# VERSION AND PLATFORM COMPATIBILITY +#------------------------------------------------------------------------------ + +# - Previous PostgreSQL Versions - + +#array_nulls = on +#backslash_quote = safe_encoding # on, off, or safe_encoding +#default_with_oids = off +#escape_string_warning = on +#lo_compat_privileges = off +#operator_precedence_warning = off +#quote_all_identifiers = off +#standard_conforming_strings = on +#synchronize_seqscans = on + +# - Other Platforms and Clients - + +#transform_null_equals = off + + +#------------------------------------------------------------------------------ +# ERROR HANDLING +#------------------------------------------------------------------------------ + +#exit_on_error = off # terminate session on any error? +#restart_after_crash = on # reinitialize after backend crash? +#data_sync_retry = off # retry or panic on failure to fsync + # data? + # (change requires restart) + + +#------------------------------------------------------------------------------ +# CONFIG FILE INCLUDES +#------------------------------------------------------------------------------ + +# These options allow settings to be loaded from files other than the +# default postgresql.conf. + +#include_dir = '' # include files ending in '.conf' from + # a directory, e.g., 'conf.d' +#include_if_exists = '' # include file only if it exists +#include = '' # include file + + +#------------------------------------------------------------------------------ +# CUSTOMIZED OPTIONS +#------------------------------------------------------------------------------ + +# Add settings for extensions here diff --git a/services/supervisor_geeksbot.conf b/services/supervisor_geeksbot.conf new file mode 100644 index 0000000..12c4b2b --- /dev/null +++ b/services/supervisor_geeksbot.conf @@ -0,0 +1,15 @@ +[program:geeksbot] +command=/usr/local/bin/gunicorn config.wsgi:application -c /etc/gunicorn.conf +directory=/code/geeksbot_web +stdout_logfile=/tmp/logs/geeksbot/gunicorn.log +autostart=true +autorestart=true +redirect_stderr=true +user=geeksbot + +[program:nginx] +command=/usr/sbin/nginx +stdout_logfile=/tmp/logs/nginx/access.log +stderr_logfile=/tmp/logs/nginx/error.log +autostart=true +autorestart=true diff --git a/services/supervisord.conf b/services/supervisord.conf new file mode 100644 index 0000000..14ce905 --- /dev/null +++ b/services/supervisord.conf @@ -0,0 +1,21 @@ +[unix_http_server] +file=/tmp/supervisor.sock + +[supervisord] +nodaemon=true +logfile=/tmp/logs/supervisord.log +logfile_maxbytes=50MB +logfile_backups=0 +loglevel=info +pidfile=/tmp/supervisord.pid +minfds=1024 ; min available startup file descriptors +minprocs=200 ; min available process descriptors + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///tmp/supervisor.sock + +[include] +files = /etc/supervisor/conf.d/*.conf \ No newline at end of file