From adbea1e129de4faf7170fa66b9f7437e453a5953 Mon Sep 17 00:00:00 2001
From: dnns01 <mail@dnns01.de>
Date: Thu, 7 Jan 2021 01:00:17 +0100
Subject: [PATCH] Create Webinterface around HaugeBot

---
 .idea/encodings.xml                           |    6 -
 .idea/haugebot.iml                            |   30 +
 .idea/misc.xml                                |    2 +-
 .idea/modules.xml                             |    2 +-
 .idea/pipimeter.iml                           |   10 -
 .idea/sqldialects.xml                         |    7 +
 .idea/vagrant.xml                             |    7 -
 haugebot/__init__.py                          |    0
 haugebot/asgi.py                              |   16 +
 haugebot/settings.py                          |  125 ++
 haugebot/urls.py                              |   29 +
 haugebot/wsgi.py                              |   16 +
 .../giveaway_cog.py                           |    2 +-
 hausgeist.py => haugebot_twitch/haugebot.py   |   36 +-
 haugebot_twitch/info_cog.py                   |   37 +
 pipi_cog.py => haugebot_twitch/pipi_cog.py    |    0
 vote_cog.py => haugebot_twitch/vote_cog.py    |    3 +-
 .../vote_redis.py                             |    0
 haugebot_web/__init__.py                      |    0
 haugebot_web/admin.py                         |    1 +
 haugebot_web/apps.py                          |    5 +
 haugebot_web/auth.py                          |   18 +
 haugebot_web/forms.py                         |   53 +
 haugebot_web/managers.py                      |   12 +
 haugebot_web/migrations/0001_initial.py       |  109 +
 haugebot_web/migrations/__init__.py           |    0
 haugebot_web/models.py                        |   47 +
 haugebot_web/static/css/styles.css            |   47 +
 haugebot_web/static/css/w3.css                | 1749 +++++++++++++++++
 haugebot_web/static/images/logo.png           |  Bin 0 -> 17377 bytes
 .../templates/color_select_option.html        |    2 +
 haugebot_web/templates/form.html              |   52 +
 haugebot_web/templates/home.html              |    5 +
 haugebot_web/templates/layout.html            |   36 +
 haugebot_web/tests.py                         |    1 +
 haugebot_web/twitch_api.py                    |  109 +
 haugebot_web/views.py                         |   88 +
 info.json                                     |   25 -
 info_cog.py                                   |   46 -
 manage.py                                     |   22 +
 requirements.txt                              |   20 +-
 41 files changed, 2658 insertions(+), 117 deletions(-)
 delete mode 100644 .idea/encodings.xml
 create mode 100644 .idea/haugebot.iml
 delete mode 100644 .idea/pipimeter.iml
 create mode 100644 .idea/sqldialects.xml
 delete mode 100644 .idea/vagrant.xml
 create mode 100644 haugebot/__init__.py
 create mode 100644 haugebot/asgi.py
 create mode 100644 haugebot/settings.py
 create mode 100644 haugebot/urls.py
 create mode 100644 haugebot/wsgi.py
 rename giveaway_cog.py => haugebot_twitch/giveaway_cog.py (99%)
 rename hausgeist.py => haugebot_twitch/haugebot.py (81%)
 create mode 100644 haugebot_twitch/info_cog.py
 rename pipi_cog.py => haugebot_twitch/pipi_cog.py (100%)
 rename vote_cog.py => haugebot_twitch/vote_cog.py (99%)
 rename vote_redis.py => haugebot_twitch/vote_redis.py (100%)
 create mode 100644 haugebot_web/__init__.py
 create mode 100644 haugebot_web/admin.py
 create mode 100644 haugebot_web/apps.py
 create mode 100644 haugebot_web/auth.py
 create mode 100644 haugebot_web/forms.py
 create mode 100644 haugebot_web/managers.py
 create mode 100644 haugebot_web/migrations/0001_initial.py
 create mode 100644 haugebot_web/migrations/__init__.py
 create mode 100644 haugebot_web/models.py
 create mode 100644 haugebot_web/static/css/styles.css
 create mode 100644 haugebot_web/static/css/w3.css
 create mode 100644 haugebot_web/static/images/logo.png
 create mode 100644 haugebot_web/templates/color_select_option.html
 create mode 100644 haugebot_web/templates/form.html
 create mode 100644 haugebot_web/templates/home.html
 create mode 100644 haugebot_web/templates/layout.html
 create mode 100644 haugebot_web/tests.py
 create mode 100644 haugebot_web/twitch_api.py
 create mode 100644 haugebot_web/views.py
 delete mode 100644 info.json
 delete mode 100644 info_cog.py
 create mode 100644 manage.py

diff --git a/.idea/encodings.xml b/.idea/encodings.xml
deleted file mode 100644
index e23123f..0000000
--- a/.idea/encodings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="Encoding">
-    <file url="file://$PROJECT_DIR$/info.json" charset="UTF-8" />
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/haugebot.iml b/.idea/haugebot.iml
new file mode 100644
index 0000000..6190690
--- /dev/null
+++ b/.idea/haugebot.iml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="django" name="Django">
+      <configuration>
+        <option name="rootFolder" value="$MODULE_DIR$" />
+        <option name="settingsModule" value="haugebot/settings.py" />
+        <option name="manageScript" value="$MODULE_DIR$/manage.py" />
+        <option name="environment" value="&lt;map/&gt;" />
+        <option name="doNotUseTestRunner" value="false" />
+        <option name="trackFilePattern" value="migrations" />
+      </configuration>
+    </facet>
+  </component>
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/venv" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+  <component name="TemplatesService">
+    <option name="TEMPLATE_CONFIGURATION" value="Django" />
+    <option name="TEMPLATE_FOLDERS">
+      <list>
+        <option value="$MODULE_DIR$/../haugebot\templates" />
+      </list>
+    </option>
+  </component>
+</module>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index da49fd6..e32c50e 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,5 +3,5 @@
   <component name="JavaScriptSettings">
     <option name="languageLevel" value="ES6" />
   </component>
-  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (TwitchHausGeist)" project-jdk-type="Python SDK" />
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (haugebot)" project-jdk-type="Python SDK" />
 </project>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
index f7b4bc4..e9d8211 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,7 +2,7 @@
 <project version="4">
   <component name="ProjectModuleManager">
     <modules>
-      <module fileurl="file://$PROJECT_DIR$/.idea/pipimeter.iml" filepath="$PROJECT_DIR$/.idea/pipimeter.iml" />
+      <module fileurl="file://$PROJECT_DIR$/.idea/haugebot.iml" filepath="$PROJECT_DIR$/.idea/haugebot.iml" />
     </modules>
   </component>
 </project>
\ No newline at end of file
diff --git a/.idea/pipimeter.iml b/.idea/pipimeter.iml
deleted file mode 100644
index 6b70e77..0000000
--- a/.idea/pipimeter.iml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="PYTHON_MODULE" version="4">
-  <component name="NewModuleRootManager">
-    <content url="file://$MODULE_DIR$">
-      <excludeFolder url="file://$MODULE_DIR$/venv" />
-    </content>
-    <orderEntry type="jdk" jdkName="Python 3.8 (TwitchHausGeist)" jdkType="Python SDK" />
-    <orderEntry type="sourceFolder" forTests="false" />
-  </component>
-</module>
\ No newline at end of file
diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml
new file mode 100644
index 0000000..18091cb
--- /dev/null
+++ b/.idea/sqldialects.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="SqlDialectMappings">
+    <file url="file://$PROJECT_DIR$/haugebot_web/migrations/0001_initial.py" dialect="GenericSQL" />
+    <file url="PROJECT" dialect="SQLite" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/vagrant.xml b/.idea/vagrant.xml
deleted file mode 100644
index a5aa786..0000000
--- a/.idea/vagrant.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="VagrantProjectSettings">
-    <option name="instanceFolder" value="" />
-    <option name="provider" value="" />
-  </component>
-</project>
\ No newline at end of file
diff --git a/haugebot/__init__.py b/haugebot/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/haugebot/asgi.py b/haugebot/asgi.py
new file mode 100644
index 0000000..abb3541
--- /dev/null
+++ b/haugebot/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for haugebot project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'haugebot.settings')
+
+application = get_asgi_application()
diff --git a/haugebot/settings.py b/haugebot/settings.py
new file mode 100644
index 0000000..04587d7
--- /dev/null
+++ b/haugebot/settings.py
@@ -0,0 +1,125 @@
+"""
+Django settings for haugebot project.
+
+Generated by 'django-admin startproject' using Django 3.1.5.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.1/ref/settings/
+"""
+
+import os
+from pathlib import Path
+
+from dotenv import load_dotenv
+
+load_dotenv()
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = os.getenv("DJANGO_SECRET")
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+AUTHENTICATION_BACKENDS = [
+    'haugebot_web.auth.TwitchAuthenticationBackend',
+]
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'haugebot_web',
+]
+
+MIDDLEWARE = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'haugebot.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [os.path.join(BASE_DIR, 'templates')]
+        ,
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'haugebot.wsgi.application'
+
+# Database
+# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': BASE_DIR / 'db.sqlite3',
+    }
+}
+
+# Password validation
+# https://docs.djangoproject.com/en/3.1/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',
+    },
+]
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.1/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.1/howto/static-files/
+
+STATIC_URL = '/static/'
diff --git a/haugebot/urls.py b/haugebot/urls.py
new file mode 100644
index 0000000..971b78f
--- /dev/null
+++ b/haugebot/urls.py
@@ -0,0 +1,29 @@
+"""haugebot URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/3.1/topics/http/urls/
+Examples:
+Function views
+    1. Add an import:  from my_app import views
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
+Class-based views
+    1. Add an import:  from other_app.views import Home
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
+Including another URLconf
+    1. Import the include() function: from django.urls import include, path
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path
+
+from haugebot_web import views
+
+urlpatterns = [
+    path('admin/', admin.site.urls),
+    path('', views.home, name="home"),
+    path('login/', views.login, name="login"),
+    path('logout/', views.logout, name="logout"),
+    path('login/redirect/', views.login_redirect, name="login_redirect"),
+    path('wusstest_du_schon/', views.wusstest_du_schon, name="wusstest_du_schon"),
+    path('wusstest_du_schon/remove/<int:id>', views.wusstest_du_schon_remove, name="wusstest_du_schon_remove"),
+]
diff --git a/haugebot/wsgi.py b/haugebot/wsgi.py
new file mode 100644
index 0000000..cb12bbf
--- /dev/null
+++ b/haugebot/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for haugebot project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'haugebot.settings')
+
+application = get_wsgi_application()
diff --git a/giveaway_cog.py b/haugebot_twitch/giveaway_cog.py
similarity index 99%
rename from giveaway_cog.py
rename to haugebot_twitch/giveaway_cog.py
index 3c0d09f..31941bb 100644
--- a/giveaway_cog.py
+++ b/haugebot_twitch/giveaway_cog.py
@@ -5,7 +5,7 @@ from twitchio.ext import commands
 
 
 @commands.core.cog(name="GiveawayCog")
-class GiveawayGog:
+class GiveawayCog:
     def __init__(self, bot):
         self.bot = bot
         self.giveaway_enabled = False
diff --git a/hausgeist.py b/haugebot_twitch/haugebot.py
similarity index 81%
rename from hausgeist.py
rename to haugebot_twitch/haugebot.py
index 16d6d01..0feb0e3 100644
--- a/hausgeist.py
+++ b/haugebot_twitch/haugebot.py
@@ -1,18 +1,21 @@
 import asyncio
-import logging
 import os
+import sqlite3
 from abc import ABC
 
 from dotenv import load_dotenv
-from twitchio.dataclasses import Context, Message, Channel
-from twitchio.ext import commands
-
-from giveaway_cog import GiveawayGog
+from giveaway_cog import GiveawayCog
+# from giveaway_cog import GiveawayGog
 from info_cog import InfoCog
 from pipi_cog import PipiCog
+from twitchio.dataclasses import Context, Message, Channel
+from twitchio.ext import commands
 from vote_cog import VoteCog
 
-logging.basicConfig(level=logging.INFO, filename='hausgeist.log')
+# from pipi_cog import PipiCog
+# from vote_cog import VoteCog
+
+# logging.basicConfig(level=logging.INFO, filename='hausgeist.log')
 
 load_dotenv()
 IRC_TOKEN = os.getenv("IRC_TOKEN")
@@ -33,14 +36,12 @@ class HaugeBot(commands.Bot, ABC):
         self.PREFIX = os.getenv("PREFIX")
         super().__init__(irc_token=IRC_TOKEN, prefix=PREFIX, nick=NICK, initial_channels=[CHANNEL], client_id=CLIENT_ID,
                          client_secret=CLIENT_SECRET)
-        self.pipi_cog = PipiCog(self)
-        self.giveaway_cog = GiveawayGog(self)
-        self.vote_cog = VoteCog(self)
         self.info_cog = InfoCog(self)
-        self.add_cog(self.pipi_cog)
-        self.add_cog(self.giveaway_cog)
-        self.add_cog(self.vote_cog)
+        self.pipi_cog = PipiCog(self)
+        self.add_cog(GiveawayCog(self))
+        self.add_cog(VoteCog(self))
         self.add_cog(self.info_cog)
+        self.add_cog(self.pipi_cog)
 
     @staticmethod
     async def send_me(ctx, content, color):
@@ -55,6 +56,7 @@ class HaugeBot(commands.Bot, ABC):
 
     async def event_ready(self):
         print('Logged in')
+
         asyncio.create_task(self.info_cog.info_loop())
         asyncio.create_task(self.pipi_cog.pipimeter_loop())
 
@@ -75,6 +77,16 @@ class HaugeBot(commands.Bot, ABC):
     async def stream(self):
         return await self.get_stream(self.CHANNEL)
 
+    @staticmethod
+    def get_setting(key):
+        conn = sqlite3.connect("db.sqlite3")
+
+        c = conn.cursor()
+        c.execute('SELECT value from haugebot_web_setting where key = ?', (key,))
+        value = c.fetchone()[0]
+        conn.close()
+        return value
+
 
 bot = HaugeBot()
 
diff --git a/haugebot_twitch/info_cog.py b/haugebot_twitch/info_cog.py
new file mode 100644
index 0000000..2e6dc98
--- /dev/null
+++ b/haugebot_twitch/info_cog.py
@@ -0,0 +1,37 @@
+import asyncio
+import random
+import sqlite3
+
+from twitchio.ext import commands
+
+
+@commands.core.cog()
+class InfoCog:
+    def __init__(self, bot):
+        self.bot = bot
+
+    async def info_loop(self):
+        while True:
+            sleep_duration = int(self.bot.get_setting("WusstestDuSchonLoop"))
+            await asyncio.sleep(sleep_duration * 60)
+
+            if await self.bot.stream():
+                channel = self.bot.channel()
+                color = self.bot.get_setting("WusstestDuSchonColor")
+                prefix = self.bot.get_setting("WusstestDuSchonPrefix")
+                message = self.get_random_message(prefix)
+                await self.bot.send_me(channel, message, color)
+
+    @staticmethod
+    def get_random_message(prefix):
+        conn = sqlite3.connect("db.sqlite3")
+
+        c = conn.cursor()
+        c.execute('SELECT text, use_prefix from haugebot_web_wusstestduschon where active is true')
+        wusstestduschon = random.choice(c.fetchall())
+        conn.close()
+
+        if wusstestduschon[1] == 1:
+            return prefix.strip() + " " + wusstestduschon[0].strip()
+        else:
+            return wusstestduschon[0]
diff --git a/pipi_cog.py b/haugebot_twitch/pipi_cog.py
similarity index 100%
rename from pipi_cog.py
rename to haugebot_twitch/pipi_cog.py
diff --git a/vote_cog.py b/haugebot_twitch/vote_cog.py
similarity index 99%
rename from vote_cog.py
rename to haugebot_twitch/vote_cog.py
index 3640aa6..21b45f2 100644
--- a/vote_cog.py
+++ b/haugebot_twitch/vote_cog.py
@@ -2,9 +2,8 @@ import asyncio
 import os
 import time
 
-from twitchio.ext import commands
-
 import vote_redis
+from twitchio.ext import commands
 
 
 @commands.core.cog(name="VoteCog")
diff --git a/vote_redis.py b/haugebot_twitch/vote_redis.py
similarity index 100%
rename from vote_redis.py
rename to haugebot_twitch/vote_redis.py
diff --git a/haugebot_web/__init__.py b/haugebot_web/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/haugebot_web/admin.py b/haugebot_web/admin.py
new file mode 100644
index 0000000..846f6b4
--- /dev/null
+++ b/haugebot_web/admin.py
@@ -0,0 +1 @@
+# Register your models here.
diff --git a/haugebot_web/apps.py b/haugebot_web/apps.py
new file mode 100644
index 0000000..39b5068
--- /dev/null
+++ b/haugebot_web/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class HaugebotWebConfig(AppConfig):
+    name = 'haugebot_web'
diff --git a/haugebot_web/auth.py b/haugebot_web/auth.py
new file mode 100644
index 0000000..28b3e4b
--- /dev/null
+++ b/haugebot_web/auth.py
@@ -0,0 +1,18 @@
+from django.contrib.auth.backends import BaseBackend
+
+from .models import TwitchUser
+
+
+class TwitchAuthenticationBackend(BaseBackend):
+    def authenticated(self, request, user) -> TwitchUser:
+        find_user = TwitchUser.objects.filter(id=user['id'])
+        if len(find_user) == 0:
+            TwitchUser.objects.create_new_twitch_user(user)
+            return self.authenticate(request, user)
+        return find_user
+
+    def get_user(self, user_id):
+        try:
+            return TwitchUser.objects.get(pk=user_id)
+        except TwitchUser.DoesNotExist:
+            return None
diff --git a/haugebot_web/forms.py b/haugebot_web/forms.py
new file mode 100644
index 0000000..b34be0d
--- /dev/null
+++ b/haugebot_web/forms.py
@@ -0,0 +1,53 @@
+from django import forms
+
+from .models import Setting, TwitchColor
+
+
+class BaseForm(forms.ModelForm):
+    def __init__(self, *args, **kwargs):
+        super(BaseForm, self).__init__(*args, **kwargs)
+        for field_name, field in self.fields.items():
+            if type(field) is forms.fields.BooleanField:
+                field.widget.attrs['class'] = ' w3-check '
+                field.label_suffix = ""
+            else:
+                field.widget.attrs['class'] = ' w3-input '
+            field.widget.attrs['placeholder'] = field.label
+
+
+class WusstestDuSchonSettingsForm(forms.Form):
+    prefix_field = forms.CharField(max_length=50, initial=Setting.objects.get(key="WusstestDuSchonPrefix").value,
+                                   label="Präfix")
+    loop_field = forms.IntegerField(initial=Setting.objects.get(key="WusstestDuSchonLoop").value,
+                                    label="Pause (in Minuten)")
+    color_field = forms.ChoiceField(choices=[(color.color, color.display_name) for color in TwitchColor.objects.all()],
+                                    label="Text Farbe")
+
+    def __init__(self, *args, **kwargs):
+        super(WusstestDuSchonSettingsForm, self).__init__(*args, **kwargs)
+
+        self.fields["prefix_field"].initial = Setting.objects.get(key="WusstestDuSchonPrefix").value
+        self.fields["loop_field"].initial = Setting.objects.get(key="WusstestDuSchonLoop").value
+        self.fields["color_field"].initial = TwitchColor.objects.get(
+            twitch_name=Setting.objects.get(key="WusstestDuSchonColor").value).color
+
+        for field_name, field in self.fields.items():
+            if type(field) is forms.fields.BooleanField:
+                field.widget.attrs['class'] = ' w3-check '
+                field.label_suffix = ""
+            else:
+                field.widget.attrs['class'] = ' w3-input '
+            field.widget.attrs['placeholder'] = field.label
+
+    def save(self):
+        prefix = Setting.objects.get(key="WusstestDuSchonPrefix")
+        prefix.value = self.cleaned_data["prefix_field"]
+        prefix.save()
+
+        loop = Setting.objects.get(key="WusstestDuSchonLoop")
+        loop.value = self.cleaned_data["loop_field"]
+        loop.save()
+
+        color = Setting.objects.get(key="WusstestDuSchonColor")
+        color.value = TwitchColor.objects.get(color=self.cleaned_data["color_field"]).twitch_name
+        color.save()
diff --git a/haugebot_web/managers.py b/haugebot_web/managers.py
new file mode 100644
index 0000000..d082c72
--- /dev/null
+++ b/haugebot_web/managers.py
@@ -0,0 +1,12 @@
+from django.contrib.auth import models
+
+
+class TwitchUserManager(models.UserManager):
+    def create_new_twitch_user(self, user):
+        new_user = self.create(
+            id=user['id'],
+            login=user['login'],
+            access_token=user['access_token'],
+            refresh_token=user['refresh_token']
+        )
+        return new_user
diff --git a/haugebot_web/migrations/0001_initial.py b/haugebot_web/migrations/0001_initial.py
new file mode 100644
index 0000000..0dead91
--- /dev/null
+++ b/haugebot_web/migrations/0001_initial.py
@@ -0,0 +1,109 @@
+# Generated by Django 3.1.5 on 2021-01-05 12:27
+
+from django.db import migrations, models
+
+import haugebot_web.managers
+
+
+class Migration(migrations.Migration):
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Setting',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('key', models.CharField(max_length=50)),
+                ('value', models.CharField(max_length=50)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='TwitchColor',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('twitch_name', models.CharField(max_length=20)),
+                ('display_name', models.CharField(max_length=20)),
+                ('color', models.CharField(max_length=7)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='TwitchUser',
+            fields=[
+                ('id', models.BigIntegerField(primary_key=True, serialize=False)),
+                ('login', models.CharField(max_length=50)),
+                ('access_token', models.CharField(max_length=50)),
+                ('refresh_token', models.CharField(max_length=50)),
+                ('last_login', models.DateTimeField(null=True)),
+            ],
+            managers=[
+                ('objects', haugebot_web.managers.TwitchUserManager()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='WusstestDuSchon',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('advertised_command', models.CharField(max_length=20)),
+                ('text', models.TextField(max_length=450)),
+                ('use_prefix', models.BooleanField(default=True, verbose_name='Präfix verwenden')),
+                ('active', models.BooleanField(default=True, verbose_name='Aktiv')),
+            ],
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_setting (key, value) VALUES('WusstestDuSchonPrefix', 'Psssst... wusstest du eigentlich schon,')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_setting (key, value) VALUES('WusstestDuSchonLoop', '10')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_setting (key, value) VALUES('WusstestDuSchonColor', 'HotPink')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (1, 'Red', 'Rot', '#ff0000')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (2, 'Blue', 'Blau', '#0000ff')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (3, 'Green', 'Grün', '#008000')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (4, 'FireBrick', 'Backstein', '#b22222')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (5, 'Coral', 'Korallenrot', '#ff7f50')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (6, 'YellowGreen', 'Gelbliches Grün', '#9acd32')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (7, 'OrangeRed', 'Orangenrot', '#ff4500')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (8, 'SeaGreen', 'Meeresgrün', '#2e8b57')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (9, 'GoldenRod', 'Goldrute', '#daa520')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (10, 'Chocolate', 'Schokoladenbraun', '#d2691e')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (11, 'CadetBlue', 'Marineblau', '#5f9ea0')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (12, 'DodgerBlue', 'Dodgerblau', '#1e90ff')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (13, 'HotPink', 'Leuchtendes Rosa', '#ff69b4')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (14, 'BlueViolet', 'Blauviolett', '#8a2be2')"
+        ),
+        migrations.RunSQL(
+            "INSERT INTO haugebot_web_twitchcolor (id, twitch_name, display_name, color) VALUES (15, 'SpringGreen', 'Frühlingsgrün', '#00ff7f')"
+        ),
+    ]
diff --git a/haugebot_web/migrations/__init__.py b/haugebot_web/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/haugebot_web/models.py b/haugebot_web/models.py
new file mode 100644
index 0000000..a65a857
--- /dev/null
+++ b/haugebot_web/models.py
@@ -0,0 +1,47 @@
+import os
+
+from django.db import models
+
+from haugebot_web import twitch_api
+from .managers import TwitchUserManager
+
+
+class Setting(models.Model):
+    key = models.CharField(max_length=50)
+    value = models.CharField(max_length=50)
+
+
+class TwitchColor(models.Model):
+    twitch_name = models.CharField(max_length=20)
+    display_name = models.CharField(max_length=20)
+    color = models.CharField(max_length=7)
+
+
+class WusstestDuSchon(models.Model):
+    advertised_command = models.CharField(max_length=20)
+    text = models.TextField(max_length=450)
+    use_prefix = models.BooleanField(default=True, verbose_name="Präfix verwenden")
+    active = models.BooleanField(default=True, verbose_name="Aktiv")
+
+
+class TwitchUser(models.Model):
+    objects = TwitchUserManager()
+
+    id = models.BigIntegerField(primary_key=True)
+    login = models.CharField(max_length=50)
+    access_token = models.CharField(max_length=50)
+    refresh_token = models.CharField(max_length=50)
+    last_login = models.DateTimeField(null=True)
+
+    def update_tokens(self, access_token, refresh_token):
+        self.access_token = access_token
+        self.refresh_token = refresh_token
+        self.save()
+
+    def is_authenticated(self):
+        broadcaster_id = os.getenv("BROADCASTER_ID")
+        try:
+            broadcaster = TwitchUser.objects.get(pk=broadcaster_id)
+            return twitch_api.is_mod(self, broadcaster)
+        except TwitchUser.DoesNotExist:
+            return False
diff --git a/haugebot_web/static/css/styles.css b/haugebot_web/static/css/styles.css
new file mode 100644
index 0000000..ba369ff
--- /dev/null
+++ b/haugebot_web/static/css/styles.css
@@ -0,0 +1,47 @@
+#content {
+    margin-left: 200px;
+}
+
+.w3-haugedark {
+    background: #222222;
+    color: #fca4ba;
+}
+
+.w3-haugepink {
+    background: #fca4ba;
+    color: #000e47;
+}
+
+.w3-haugepinkdark {
+    background: #f65656;
+}
+
+.w3-haugeblue {
+    background: #000e47;
+}
+
+.w3-haugepink:hover {
+    background: #f65656 !important;
+    color: #000e47;
+}
+
+#logo {
+    max-width: 100%;
+}
+
+.w3-form-container {
+    margin-right: 50px;
+}
+
+.w3-display-topright .w3-button {
+    margin-right: 1em;
+}
+
+.w3-card {
+    margin-bottom: 20px;
+}
+
+textarea {
+    max-height: 62px;
+    resize: none;
+}
\ No newline at end of file
diff --git a/haugebot_web/static/css/w3.css b/haugebot_web/static/css/w3.css
new file mode 100644
index 0000000..5bc5502
--- /dev/null
+++ b/haugebot_web/static/css/w3.css
@@ -0,0 +1,1749 @@
+/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
+html {
+    box-sizing: border-box
+}
+
+*, *:before, *:after {
+    box-sizing: inherit
+}
+
+/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
+html {
+    -ms-text-size-adjust: 100%;
+    -webkit-text-size-adjust: 100%
+}
+
+body {
+    margin: 0
+}
+
+article, aside, details, figcaption, figure, footer, header, main, menu, nav, section {
+    display: block
+}
+
+summary {
+    display: list-item
+}
+
+audio, canvas, progress, video {
+    display: inline-block
+}
+
+progress {
+    vertical-align: baseline
+}
+
+audio:not([controls]) {
+    display: none;
+    height: 0
+}
+
+[hidden], template {
+    display: none
+}
+
+a {
+    background-color: transparent
+}
+
+a:active, a:hover {
+    outline-width: 0
+}
+
+abbr[title] {
+    border-bottom: none;
+    text-decoration: underline;
+    text-decoration: underline dotted
+}
+
+b, strong {
+    font-weight: bolder
+}
+
+dfn {
+    font-style: italic
+}
+
+mark {
+    background: #ff0;
+    color: #000
+}
+
+small {
+    font-size: 80%
+}
+
+sub, sup {
+    font-size: 75%;
+    line-height: 0;
+    position: relative;
+    vertical-align: baseline
+}
+
+sub {
+    bottom: -0.25em
+}
+
+sup {
+    top: -0.5em
+}
+
+figure {
+    margin: 1em 40px
+}
+
+img {
+    border-style: none
+}
+
+code, kbd, pre, samp {
+    font-family: monospace, monospace;
+    font-size: 1em
+}
+
+hr {
+    box-sizing: content-box;
+    height: 0;
+    overflow: visible
+}
+
+button, input, select, textarea, optgroup {
+    font: inherit;
+    margin: 0
+}
+
+optgroup {
+    font-weight: bold
+}
+
+button, input {
+    overflow: visible
+}
+
+button, select {
+    text-transform: none
+}
+
+button, [type=button], [type=reset], [type=submit] {
+    -webkit-appearance: button
+}
+
+button::-moz-focus-inner, [type=button]::-moz-focus-inner, [type=reset]::-moz-focus-inner, [type=submit]::-moz-focus-inner {
+    border-style: none;
+    padding: 0
+}
+
+button:-moz-focusring, [type=button]:-moz-focusring, [type=reset]:-moz-focusring, [type=submit]:-moz-focusring {
+    outline: 1px dotted ButtonText
+}
+
+fieldset {
+    border: 1px solid #c0c0c0;
+    margin: 0 2px;
+    padding: .35em .625em .75em
+}
+
+legend {
+    color: inherit;
+    display: table;
+    max-width: 100%;
+    padding: 0;
+    white-space: normal
+}
+
+textarea {
+    overflow: auto
+}
+
+[type=checkbox], [type=radio] {
+    padding: 0
+}
+
+[type=number]::-webkit-inner-spin-button, [type=number]::-webkit-outer-spin-button {
+    height: auto
+}
+
+[type=search] {
+    -webkit-appearance: textfield;
+    outline-offset: -2px
+}
+
+[type=search]::-webkit-search-decoration {
+    -webkit-appearance: none
+}
+
+::-webkit-file-upload-button {
+    -webkit-appearance: button;
+    font: inherit
+}
+
+/* End extract */
+html, body {
+    font-family: Verdana, sans-serif;
+    font-size: 15px;
+    line-height: 1.5
+}
+
+html {
+    overflow-x: hidden
+}
+
+h1 {
+    font-size: 36px
+}
+
+h2 {
+    font-size: 30px
+}
+
+h3 {
+    font-size: 24px
+}
+
+h4 {
+    font-size: 20px
+}
+
+h5 {
+    font-size: 18px
+}
+
+h6 {
+    font-size: 16px
+}
+
+.w3-serif {
+    font-family: serif
+}
+
+.w3-sans-serif {
+    font-family: sans-serif
+}
+
+.w3-cursive {
+    font-family: cursive
+}
+
+.w3-monospace {
+    font-family: monospace
+}
+
+h1, h2, h3, h4, h5, h6 {
+    font-family: "Segoe UI", Arial, sans-serif;
+    font-weight: 400;
+    margin: 10px 0
+}
+
+.w3-wide {
+    letter-spacing: 4px
+}
+
+hr {
+    border: 0;
+    border-top: 1px solid #eee;
+    margin: 20px 0
+}
+
+.w3-image {
+    max-width: 100%;
+    height: auto
+}
+
+img {
+    vertical-align: middle
+}
+
+a {
+    color: inherit
+}
+
+.w3-table, .w3-table-all {
+    border-collapse: collapse;
+    border-spacing: 0;
+    width: 100%;
+    display: table
+}
+
+.w3-table-all {
+    border: 1px solid #ccc
+}
+
+.w3-bordered tr, .w3-table-all tr {
+    border-bottom: 1px solid #ddd
+}
+
+.w3-striped tbody tr:nth-child(even) {
+    background-color: #f1f1f1
+}
+
+.w3-table-all tr:nth-child(odd) {
+    background-color: #fff
+}
+
+.w3-table-all tr:nth-child(even) {
+    background-color: #f1f1f1
+}
+
+.w3-hoverable tbody tr:hover, .w3-ul.w3-hoverable li:hover {
+    background-color: #ccc
+}
+
+.w3-centered tr th, .w3-centered tr td {
+    text-align: center
+}
+
+.w3-table td, .w3-table th, .w3-table-all td, .w3-table-all th {
+    padding: 8px 8px;
+    display: table-cell;
+    text-align: left;
+    vertical-align: top
+}
+
+.w3-table th:first-child, .w3-table td:first-child, .w3-table-all th:first-child, .w3-table-all td:first-child {
+    padding-left: 16px
+}
+
+.w3-btn, .w3-button {
+    border: none;
+    display: inline-block;
+    padding: 8px 16px;
+    vertical-align: middle;
+    overflow: hidden;
+    text-decoration: none;
+    color: inherit;
+    background-color: inherit;
+    text-align: center;
+    cursor: pointer;
+    white-space: nowrap
+}
+
+.w3-btn:hover {
+    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)
+}
+
+.w3-btn, .w3-button {
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+    -khtml-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none
+}
+
+.w3-disabled, .w3-btn:disabled, .w3-button:disabled {
+    cursor: not-allowed;
+    opacity: 0.3
+}
+
+.w3-disabled *, :disabled * {
+    pointer-events: none
+}
+
+.w3-btn.w3-disabled:hover, .w3-btn:disabled:hover {
+    box-shadow: none
+}
+
+.w3-badge, .w3-tag {
+    background-color: #000;
+    color: #fff;
+    display: inline-block;
+    padding-left: 8px;
+    padding-right: 8px;
+    text-align: center
+}
+
+.w3-badge {
+    border-radius: 50%
+}
+
+.w3-ul {
+    list-style-type: none;
+    padding: 0;
+    margin: 0
+}
+
+.w3-ul li {
+    padding: 8px 16px;
+    border-bottom: 1px solid #ddd
+}
+
+.w3-ul li:last-child {
+    border-bottom: none
+}
+
+.w3-tooltip, .w3-display-container {
+    position: relative
+}
+
+.w3-tooltip .w3-text {
+    display: none
+}
+
+.w3-tooltip:hover .w3-text {
+    display: inline-block
+}
+
+.w3-ripple:active {
+    opacity: 0.5
+}
+
+.w3-ripple {
+    transition: opacity 0s
+}
+
+.w3-input {
+    padding: 8px;
+    display: block;
+    border: none;
+    border-bottom: 1px solid #ccc;
+    width: 100%
+}
+
+.w3-select {
+    padding: 9px 0;
+    width: 100%;
+    border: none;
+    border-bottom: 1px solid #ccc
+}
+
+.w3-dropdown-click, .w3-dropdown-hover {
+    position: relative;
+    display: inline-block;
+    cursor: pointer
+}
+
+.w3-dropdown-hover:hover .w3-dropdown-content {
+    display: block
+}
+
+.w3-dropdown-hover:first-child, .w3-dropdown-click:hover {
+    background-color: #ccc;
+    color: #000
+}
+
+.w3-dropdown-hover:hover > .w3-button:first-child, .w3-dropdown-click:hover > .w3-button:first-child {
+    background-color: #ccc;
+    color: #000
+}
+
+.w3-dropdown-content {
+    cursor: auto;
+    color: #000;
+    background-color: #fff;
+    display: none;
+    position: absolute;
+    min-width: 160px;
+    margin: 0;
+    padding: 0;
+    z-index: 1
+}
+
+.w3-check, .w3-radio {
+    width: 24px;
+    height: 24px;
+    position: relative;
+    top: 6px
+}
+
+.w3-sidebar {
+    height: 100%;
+    width: 200px;
+    background-color: #fff;
+    position: fixed !important;
+    z-index: 1;
+    overflow: auto
+}
+
+.w3-bar-block .w3-dropdown-hover, .w3-bar-block .w3-dropdown-click {
+    width: 100%
+}
+
+.w3-bar-block .w3-dropdown-hover .w3-dropdown-content, .w3-bar-block .w3-dropdown-click .w3-dropdown-content {
+    min-width: 100%
+}
+
+.w3-bar-block .w3-dropdown-hover .w3-button, .w3-bar-block .w3-dropdown-click .w3-button {
+    width: 100%;
+    text-align: left;
+    padding: 8px 16px
+}
+
+.w3-main, #main {
+    transition: margin-left .4s
+}
+
+.w3-modal {
+    z-index: 3;
+    display: none;
+    padding-top: 100px;
+    position: fixed;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    overflow: auto;
+    background-color: rgb(0, 0, 0);
+    background-color: rgba(0, 0, 0, 0.4)
+}
+
+.w3-modal-content {
+    margin: auto;
+    background-color: #fff;
+    position: relative;
+    padding: 0;
+    outline: 0;
+    width: 600px
+}
+
+.w3-bar {
+    width: 100%;
+    overflow: hidden
+}
+
+.w3-center .w3-bar {
+    display: inline-block;
+    width: auto
+}
+
+.w3-bar .w3-bar-item {
+    padding: 8px 16px;
+    float: left;
+    width: auto;
+    border: none;
+    display: block;
+    outline: 0
+}
+
+.w3-bar .w3-dropdown-hover, .w3-bar .w3-dropdown-click {
+    position: static;
+    float: left
+}
+
+.w3-bar .w3-button {
+    white-space: normal
+}
+
+.w3-bar-block .w3-bar-item {
+    width: 100%;
+    display: block;
+    padding: 8px 16px;
+    text-align: left;
+    border: none;
+    white-space: normal;
+    float: none;
+    outline: 0
+}
+
+.w3-bar-block.w3-center .w3-bar-item {
+    text-align: center
+}
+
+.w3-block {
+    display: block;
+    width: 100%
+}
+
+.w3-responsive {
+    display: block;
+    overflow-x: auto
+}
+
+.w3-container:after, .w3-container:before, .w3-panel:after, .w3-panel:before, .w3-row:after, .w3-row:before, .w3-row-padding:after, .w3-row-padding:before,
+.w3-cell-row:before, .w3-cell-row:after, .w3-clear:after, .w3-clear:before, .w3-bar:before, .w3-bar:after {
+    content: "";
+    display: table;
+    clear: both
+}
+
+.w3-col, .w3-half, .w3-third, .w3-twothird, .w3-threequarter, .w3-quarter {
+    float: left;
+    width: 100%
+}
+
+.w3-col.s1 {
+    width: 8.33333%
+}
+
+.w3-col.s2 {
+    width: 16.66666%
+}
+
+.w3-col.s3 {
+    width: 24.99999%
+}
+
+.w3-col.s4 {
+    width: 33.33333%
+}
+
+.w3-col.s5 {
+    width: 41.66666%
+}
+
+.w3-col.s6 {
+    width: 49.99999%
+}
+
+.w3-col.s7 {
+    width: 58.33333%
+}
+
+.w3-col.s8 {
+    width: 66.66666%
+}
+
+.w3-col.s9 {
+    width: 74.99999%
+}
+
+.w3-col.s10 {
+    width: 83.33333%
+}
+
+.w3-col.s11 {
+    width: 91.66666%
+}
+
+.w3-col.s12 {
+    width: 99.99999%
+}
+
+@media (min-width: 601px) {
+    .w3-col.m1 {
+        width: 8.33333%
+    }
+
+    .w3-col.m2 {
+        width: 16.66666%
+    }
+
+    .w3-col.m3, .w3-quarter {
+        width: 24.99999%
+    }
+
+    .w3-col.m4, .w3-third {
+        width: 33.33333%
+    }
+
+    .w3-col.m5 {
+        width: 41.66666%
+    }
+
+    .w3-col.m6, .w3-half {
+        width: 49.99999%
+    }
+
+    .w3-col.m7 {
+        width: 58.33333%
+    }
+
+    .w3-col.m8, .w3-twothird {
+        width: 66.66666%
+    }
+
+    .w3-col.m9, .w3-threequarter {
+        width: 74.99999%
+    }
+
+    .w3-col.m10 {
+        width: 83.33333%
+    }
+
+    .w3-col.m11 {
+        width: 91.66666%
+    }
+
+    .w3-col.m12 {
+        width: 99.99999%
+    }
+}
+
+@media (min-width: 993px) {
+    .w3-col.l1 {
+        width: 8.33333%
+    }
+
+    .w3-col.l2 {
+        width: 16.66666%
+    }
+
+    .w3-col.l3 {
+        width: 24.99999%
+    }
+
+    .w3-col.l4 {
+        width: 33.33333%
+    }
+
+    .w3-col.l5 {
+        width: 41.66666%
+    }
+
+    .w3-col.l6 {
+        width: 49.99999%
+    }
+
+    .w3-col.l7 {
+        width: 58.33333%
+    }
+
+    .w3-col.l8 {
+        width: 66.66666%
+    }
+
+    .w3-col.l9 {
+        width: 74.99999%
+    }
+
+    .w3-col.l10 {
+        width: 83.33333%
+    }
+
+    .w3-col.l11 {
+        width: 91.66666%
+    }
+
+    .w3-col.l12 {
+        width: 99.99999%
+    }
+}
+
+.w3-rest {
+    overflow: hidden
+}
+
+.w3-stretch {
+    margin-left: -16px;
+    margin-right: -16px
+}
+
+.w3-content, .w3-auto {
+    margin-left: auto;
+    margin-right: auto
+}
+
+.w3-content {
+    max-width: 980px
+}
+
+.w3-auto {
+    max-width: 1140px
+}
+
+.w3-cell-row {
+    display: table;
+    width: 100%
+}
+
+.w3-cell {
+    display: table-cell
+}
+
+.w3-cell-top {
+    vertical-align: top
+}
+
+.w3-cell-middle {
+    vertical-align: middle
+}
+
+.w3-cell-bottom {
+    vertical-align: bottom
+}
+
+.w3-hide {
+    display: none !important
+}
+
+.w3-show-block, .w3-show {
+    display: block !important
+}
+
+.w3-show-inline-block {
+    display: inline-block !important
+}
+
+@media (max-width: 1205px) {
+    .w3-auto {
+        max-width: 95%
+    }
+}
+
+@media (max-width: 600px) {
+    .w3-modal-content {
+        margin: 0 10px;
+        width: auto !important
+    }
+
+    .w3-modal {
+        padding-top: 30px
+    }
+
+    .w3-dropdown-hover.w3-mobile .w3-dropdown-content, .w3-dropdown-click.w3-mobile .w3-dropdown-content {
+        position: relative
+    }
+
+    .w3-hide-small {
+        display: none !important
+    }
+
+    .w3-mobile {
+        display: block;
+        width: 100% !important
+    }
+
+    .w3-bar-item.w3-mobile, .w3-dropdown-hover.w3-mobile, .w3-dropdown-click.w3-mobile {
+        text-align: center
+    }
+
+    .w3-dropdown-hover.w3-mobile, .w3-dropdown-hover.w3-mobile .w3-btn, .w3-dropdown-hover.w3-mobile .w3-button, .w3-dropdown-click.w3-mobile, .w3-dropdown-click.w3-mobile .w3-btn, .w3-dropdown-click.w3-mobile .w3-button {
+        width: 100%
+    }
+}
+
+@media (max-width: 768px) {
+    .w3-modal-content {
+        width: 500px
+    }
+
+    .w3-modal {
+        padding-top: 50px
+    }
+}
+
+@media (min-width: 993px) {
+    .w3-modal-content {
+        width: 900px
+    }
+
+    .w3-hide-large {
+        display: none !important
+    }
+
+    .w3-sidebar.w3-collapse {
+        display: block !important
+    }
+}
+
+@media (max-width: 992px) and (min-width: 601px) {
+    .w3-hide-medium {
+        display: none !important
+    }
+}
+
+@media (max-width: 992px) {
+    .w3-sidebar.w3-collapse {
+        display: none
+    }
+
+    .w3-main {
+        margin-left: 0 !important;
+        margin-right: 0 !important
+    }
+
+    .w3-auto {
+        max-width: 100%
+    }
+}
+
+.w3-top, .w3-bottom {
+    position: fixed;
+    width: 100%;
+    z-index: 1
+}
+
+.w3-top {
+    top: 0
+}
+
+.w3-bottom {
+    bottom: 0
+}
+
+.w3-overlay {
+    position: fixed;
+    display: none;
+    width: 100%;
+    height: 100%;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(0, 0, 0, 0.5);
+    z-index: 2
+}
+
+.w3-display-topleft {
+    position: absolute;
+    left: 0;
+    top: 0
+}
+
+.w3-display-topright {
+    position: absolute;
+    right: 0;
+    top: 0
+}
+
+.w3-display-bottomleft {
+    position: absolute;
+    left: 0;
+    bottom: 0
+}
+
+.w3-display-bottomright {
+    position: absolute;
+    right: 0;
+    bottom: 0
+}
+
+.w3-display-middle {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    -ms-transform: translate(-50%, -50%)
+}
+
+.w3-display-left {
+    position: absolute;
+    top: 50%;
+    left: 0%;
+    transform: translate(0%, -50%);
+    -ms-transform: translate(-0%, -50%)
+}
+
+.w3-display-right {
+    position: absolute;
+    top: 50%;
+    right: 0%;
+    transform: translate(0%, -50%);
+    -ms-transform: translate(0%, -50%)
+}
+
+.w3-display-topmiddle {
+    position: absolute;
+    left: 50%;
+    top: 0;
+    transform: translate(-50%, 0%);
+    -ms-transform: translate(-50%, 0%)
+}
+
+.w3-display-bottommiddle {
+    position: absolute;
+    left: 50%;
+    bottom: 0;
+    transform: translate(-50%, 0%);
+    -ms-transform: translate(-50%, 0%)
+}
+
+.w3-display-container:hover .w3-display-hover {
+    display: block
+}
+
+.w3-display-container:hover span.w3-display-hover {
+    display: inline-block
+}
+
+.w3-display-hover {
+    display: none
+}
+
+.w3-display-position {
+    position: absolute
+}
+
+.w3-circle {
+    border-radius: 50%
+}
+
+.w3-round-small {
+    border-radius: 2px
+}
+
+.w3-round, .w3-round-medium {
+    border-radius: 4px
+}
+
+.w3-round-large {
+    border-radius: 8px
+}
+
+.w3-round-xlarge {
+    border-radius: 16px
+}
+
+.w3-round-xxlarge {
+    border-radius: 32px
+}
+
+.w3-row-padding, .w3-row-padding > .w3-half, .w3-row-padding > .w3-third, .w3-row-padding > .w3-twothird, .w3-row-padding > .w3-threequarter, .w3-row-padding > .w3-quarter, .w3-row-padding > .w3-col {
+    padding: 0 8px
+}
+
+.w3-container, .w3-panel {
+    padding: 0.01em 16px
+}
+
+.w3-panel {
+    margin-top: 16px;
+    margin-bottom: 16px
+}
+
+.w3-code, .w3-codespan {
+    font-family: Consolas, "courier new";
+    font-size: 16px
+}
+
+.w3-code {
+    width: auto;
+    background-color: #fff;
+    padding: 8px 12px;
+    border-left: 4px solid #4CAF50;
+    word-wrap: break-word
+}
+
+.w3-codespan {
+    color: crimson;
+    background-color: #f1f1f1;
+    padding-left: 4px;
+    padding-right: 4px;
+    font-size: 110%
+}
+
+.w3-card, .w3-card-2 {
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12)
+}
+
+.w3-card-4, .w3-hover-shadow:hover {
+    box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.2), 0 4px 20px 0 rgba(0, 0, 0, 0.19)
+}
+
+.w3-spin {
+    animation: w3-spin 2s infinite linear
+}
+
+@keyframes w3-spin {
+    0% {
+        transform: rotate(0deg)
+    }
+    100% {
+        transform: rotate(359deg)
+    }
+}
+
+.w3-animate-fading {
+    animation: fading 10s infinite
+}
+
+@keyframes fading {
+    0% {
+        opacity: 0
+    }
+    50% {
+        opacity: 1
+    }
+    100% {
+        opacity: 0
+    }
+}
+
+.w3-animate-opacity {
+    animation: opac 0.8s
+}
+
+@keyframes opac {
+    from {
+        opacity: 0
+    }
+    to {
+        opacity: 1
+    }
+}
+
+.w3-animate-top {
+    position: relative;
+    animation: animatetop 0.4s
+}
+
+@keyframes animatetop {
+    from {
+        top: -300px;
+        opacity: 0
+    }
+    to {
+        top: 0;
+        opacity: 1
+    }
+}
+
+.w3-animate-left {
+    position: relative;
+    animation: animateleft 0.4s
+}
+
+@keyframes animateleft {
+    from {
+        left: -300px;
+        opacity: 0
+    }
+    to {
+        left: 0;
+        opacity: 1
+    }
+}
+
+.w3-animate-right {
+    position: relative;
+    animation: animateright 0.4s
+}
+
+@keyframes animateright {
+    from {
+        right: -300px;
+        opacity: 0
+    }
+    to {
+        right: 0;
+        opacity: 1
+    }
+}
+
+.w3-animate-bottom {
+    position: relative;
+    animation: animatebottom 0.4s
+}
+
+@keyframes animatebottom {
+    from {
+        bottom: -300px;
+        opacity: 0
+    }
+    to {
+        bottom: 0;
+        opacity: 1
+    }
+}
+
+.w3-animate-zoom {
+    animation: animatezoom 0.6s
+}
+
+@keyframes animatezoom {
+    from {
+        transform: scale(0)
+    }
+    to {
+        transform: scale(1)
+    }
+}
+
+.w3-animate-input {
+    transition: width 0.4s ease-in-out
+}
+
+.w3-animate-input:focus {
+    width: 100% !important
+}
+
+.w3-opacity, .w3-hover-opacity:hover {
+    opacity: 0.60
+}
+
+.w3-opacity-off, .w3-hover-opacity-off:hover {
+    opacity: 1
+}
+
+.w3-opacity-max {
+    opacity: 0.25
+}
+
+.w3-opacity-min {
+    opacity: 0.75
+}
+
+.w3-greyscale-max, .w3-grayscale-max, .w3-hover-greyscale:hover, .w3-hover-grayscale:hover {
+    filter: grayscale(100%)
+}
+
+.w3-greyscale, .w3-grayscale {
+    filter: grayscale(75%)
+}
+
+.w3-greyscale-min, .w3-grayscale-min {
+    filter: grayscale(50%)
+}
+
+.w3-sepia {
+    filter: sepia(75%)
+}
+
+.w3-sepia-max, .w3-hover-sepia:hover {
+    filter: sepia(100%)
+}
+
+.w3-sepia-min {
+    filter: sepia(50%)
+}
+
+.w3-tiny {
+    font-size: 10px !important
+}
+
+.w3-small {
+    font-size: 12px !important
+}
+
+.w3-medium {
+    font-size: 15px !important
+}
+
+.w3-large {
+    font-size: 18px !important
+}
+
+.w3-xlarge {
+    font-size: 24px !important
+}
+
+.w3-xxlarge {
+    font-size: 36px !important
+}
+
+.w3-xxxlarge {
+    font-size: 48px !important
+}
+
+.w3-jumbo {
+    font-size: 64px !important
+}
+
+.w3-left-align {
+    text-align: left !important
+}
+
+.w3-right-align {
+    text-align: right !important
+}
+
+.w3-justify {
+    text-align: justify !important
+}
+
+.w3-center {
+    text-align: center !important
+}
+
+.w3-border-0 {
+    border: 0 !important
+}
+
+.w3-border {
+    border: 1px solid #ccc !important
+}
+
+.w3-border-top {
+    border-top: 1px solid #ccc !important
+}
+
+.w3-border-bottom {
+    border-bottom: 1px solid #ccc !important
+}
+
+.w3-border-left {
+    border-left: 1px solid #ccc !important
+}
+
+.w3-border-right {
+    border-right: 1px solid #ccc !important
+}
+
+.w3-topbar {
+    border-top: 6px solid #ccc !important
+}
+
+.w3-bottombar {
+    border-bottom: 6px solid #ccc !important
+}
+
+.w3-leftbar {
+    border-left: 6px solid #ccc !important
+}
+
+.w3-rightbar {
+    border-right: 6px solid #ccc !important
+}
+
+.w3-section, .w3-code {
+    margin-top: 16px !important;
+    margin-bottom: 16px !important
+}
+
+.w3-margin {
+    margin: 16px !important
+}
+
+.w3-margin-top {
+    margin-top: 16px !important
+}
+
+.w3-margin-bottom {
+    margin-bottom: 16px !important
+}
+
+.w3-margin-left {
+    margin-left: 16px !important
+}
+
+.w3-margin-right {
+    margin-right: 16px !important
+}
+
+.w3-padding-small {
+    padding: 4px 8px !important
+}
+
+.w3-padding {
+    padding: 8px 16px !important
+}
+
+.w3-padding-large {
+    padding: 12px 24px !important
+}
+
+.w3-padding-16 {
+    padding-top: 16px !important;
+    padding-bottom: 16px !important
+}
+
+.w3-padding-24 {
+    padding-top: 24px !important;
+    padding-bottom: 24px !important
+}
+
+.w3-padding-32 {
+    padding-top: 32px !important;
+    padding-bottom: 32px !important
+}
+
+.w3-padding-48 {
+    padding-top: 48px !important;
+    padding-bottom: 48px !important
+}
+
+.w3-padding-64 {
+    padding-top: 64px !important;
+    padding-bottom: 64px !important
+}
+
+.w3-padding-top-64 {
+    padding-top: 64px !important
+}
+
+.w3-padding-top-48 {
+    padding-top: 48px !important
+}
+
+.w3-padding-top-32 {
+    padding-top: 32px !important
+}
+
+.w3-padding-top-24 {
+    padding-top: 24px !important
+}
+
+.w3-left {
+    float: left !important
+}
+
+.w3-right {
+    float: right !important
+}
+
+.w3-button:hover {
+    color: #000 !important;
+    background-color: #ccc !important
+}
+
+.w3-transparent, .w3-hover-none:hover {
+    background-color: transparent !important
+}
+
+.w3-hover-none:hover {
+    box-shadow: none !important
+}
+
+/* Colors */
+.w3-amber, .w3-hover-amber:hover {
+    color: #000 !important;
+    background-color: #ffc107 !important
+}
+
+.w3-aqua, .w3-hover-aqua:hover {
+    color: #000 !important;
+    background-color: #00ffff !important
+}
+
+.w3-blue, .w3-hover-blue:hover {
+    color: #fff !important;
+    background-color: #2196F3 !important
+}
+
+.w3-light-blue, .w3-hover-light-blue:hover {
+    color: #000 !important;
+    background-color: #87CEEB !important
+}
+
+.w3-brown, .w3-hover-brown:hover {
+    color: #fff !important;
+    background-color: #795548 !important
+}
+
+.w3-cyan, .w3-hover-cyan:hover {
+    color: #000 !important;
+    background-color: #00bcd4 !important
+}
+
+.w3-blue-grey, .w3-hover-blue-grey:hover, .w3-blue-gray, .w3-hover-blue-gray:hover {
+    color: #fff !important;
+    background-color: #607d8b !important
+}
+
+.w3-green, .w3-hover-green:hover {
+    color: #fff !important;
+    background-color: #4CAF50 !important
+}
+
+.w3-light-green, .w3-hover-light-green:hover {
+    color: #000 !important;
+    background-color: #8bc34a !important
+}
+
+.w3-indigo, .w3-hover-indigo:hover {
+    color: #fff !important;
+    background-color: #3f51b5 !important
+}
+
+.w3-khaki, .w3-hover-khaki:hover {
+    color: #000 !important;
+    background-color: #f0e68c !important
+}
+
+.w3-lime, .w3-hover-lime:hover {
+    color: #000 !important;
+    background-color: #cddc39 !important
+}
+
+.w3-orange, .w3-hover-orange:hover {
+    color: #000 !important;
+    background-color: #ff9800 !important
+}
+
+.w3-deep-orange, .w3-hover-deep-orange:hover {
+    color: #fff !important;
+    background-color: #ff5722 !important
+}
+
+.w3-pink, .w3-hover-pink:hover {
+    color: #fff !important;
+    background-color: #e91e63 !important
+}
+
+.w3-purple, .w3-hover-purple:hover {
+    color: #fff !important;
+    background-color: #9c27b0 !important
+}
+
+.w3-deep-purple, .w3-hover-deep-purple:hover {
+    color: #fff !important;
+    background-color: #673ab7 !important
+}
+
+.w3-red, .w3-hover-red:hover {
+    color: #fff !important;
+    background-color: #f44336 !important
+}
+
+.w3-sand, .w3-hover-sand:hover {
+    color: #000 !important;
+    background-color: #fdf5e6 !important
+}
+
+.w3-teal, .w3-hover-teal:hover {
+    color: #fff !important;
+    background-color: #009688 !important
+}
+
+.w3-yellow, .w3-hover-yellow:hover {
+    color: #000 !important;
+    background-color: #ffeb3b !important
+}
+
+.w3-white, .w3-hover-white:hover {
+    color: #000 !important;
+    background-color: #fff !important
+}
+
+.w3-black, .w3-hover-black:hover {
+    color: #fff !important;
+    background-color: #000 !important
+}
+
+.w3-grey, .w3-hover-grey:hover, .w3-gray, .w3-hover-gray:hover {
+    color: #000 !important;
+    background-color: #9e9e9e !important
+}
+
+.w3-light-grey, .w3-hover-light-grey:hover, .w3-light-gray, .w3-hover-light-gray:hover {
+    color: #000 !important;
+    background-color: #f1f1f1 !important
+}
+
+.w3-dark-grey, .w3-hover-dark-grey:hover, .w3-dark-gray, .w3-hover-dark-gray:hover {
+    color: #fff !important;
+    background-color: #616161 !important
+}
+
+.w3-pale-red, .w3-hover-pale-red:hover {
+    color: #000 !important;
+    background-color: #ffdddd !important
+}
+
+.w3-pale-green, .w3-hover-pale-green:hover {
+    color: #000 !important;
+    background-color: #ddffdd !important
+}
+
+.w3-pale-yellow, .w3-hover-pale-yellow:hover {
+    color: #000 !important;
+    background-color: #ffffcc !important
+}
+
+.w3-pale-blue, .w3-hover-pale-blue:hover {
+    color: #000 !important;
+    background-color: #ddffff !important
+}
+
+.w3-text-amber, .w3-hover-text-amber:hover {
+    color: #ffc107 !important
+}
+
+.w3-text-aqua, .w3-hover-text-aqua:hover {
+    color: #00ffff !important
+}
+
+.w3-text-blue, .w3-hover-text-blue:hover {
+    color: #2196F3 !important
+}
+
+.w3-text-light-blue, .w3-hover-text-light-blue:hover {
+    color: #87CEEB !important
+}
+
+.w3-text-brown, .w3-hover-text-brown:hover {
+    color: #795548 !important
+}
+
+.w3-text-cyan, .w3-hover-text-cyan:hover {
+    color: #00bcd4 !important
+}
+
+.w3-text-blue-grey, .w3-hover-text-blue-grey:hover, .w3-text-blue-gray, .w3-hover-text-blue-gray:hover {
+    color: #607d8b !important
+}
+
+.w3-text-green, .w3-hover-text-green:hover {
+    color: #4CAF50 !important
+}
+
+.w3-text-light-green, .w3-hover-text-light-green:hover {
+    color: #8bc34a !important
+}
+
+.w3-text-indigo, .w3-hover-text-indigo:hover {
+    color: #3f51b5 !important
+}
+
+.w3-text-khaki, .w3-hover-text-khaki:hover {
+    color: #b4aa50 !important
+}
+
+.w3-text-lime, .w3-hover-text-lime:hover {
+    color: #cddc39 !important
+}
+
+.w3-text-orange, .w3-hover-text-orange:hover {
+    color: #ff9800 !important
+}
+
+.w3-text-deep-orange, .w3-hover-text-deep-orange:hover {
+    color: #ff5722 !important
+}
+
+.w3-text-pink, .w3-hover-text-pink:hover {
+    color: #e91e63 !important
+}
+
+.w3-text-purple, .w3-hover-text-purple:hover {
+    color: #9c27b0 !important
+}
+
+.w3-text-deep-purple, .w3-hover-text-deep-purple:hover {
+    color: #673ab7 !important
+}
+
+.w3-text-red, .w3-hover-text-red:hover {
+    color: #f44336 !important
+}
+
+.w3-text-sand, .w3-hover-text-sand:hover {
+    color: #fdf5e6 !important
+}
+
+.w3-text-teal, .w3-hover-text-teal:hover {
+    color: #009688 !important
+}
+
+.w3-text-yellow, .w3-hover-text-yellow:hover {
+    color: #d2be0e !important
+}
+
+.w3-text-white, .w3-hover-text-white:hover {
+    color: #fff !important
+}
+
+.w3-text-black, .w3-hover-text-black:hover {
+    color: #000 !important
+}
+
+.w3-text-grey, .w3-hover-text-grey:hover, .w3-text-gray, .w3-hover-text-gray:hover {
+    color: #757575 !important
+}
+
+.w3-text-light-grey, .w3-hover-text-light-grey:hover, .w3-text-light-gray, .w3-hover-text-light-gray:hover {
+    color: #f1f1f1 !important
+}
+
+.w3-text-dark-grey, .w3-hover-text-dark-grey:hover, .w3-text-dark-gray, .w3-hover-text-dark-gray:hover {
+    color: #3a3a3a !important
+}
+
+.w3-border-amber, .w3-hover-border-amber:hover {
+    border-color: #ffc107 !important
+}
+
+.w3-border-aqua, .w3-hover-border-aqua:hover {
+    border-color: #00ffff !important
+}
+
+.w3-border-blue, .w3-hover-border-blue:hover {
+    border-color: #2196F3 !important
+}
+
+.w3-border-light-blue, .w3-hover-border-light-blue:hover {
+    border-color: #87CEEB !important
+}
+
+.w3-border-brown, .w3-hover-border-brown:hover {
+    border-color: #795548 !important
+}
+
+.w3-border-cyan, .w3-hover-border-cyan:hover {
+    border-color: #00bcd4 !important
+}
+
+.w3-border-blue-grey, .w3-hover-border-blue-grey:hover, .w3-border-blue-gray, .w3-hover-border-blue-gray:hover {
+    border-color: #607d8b !important
+}
+
+.w3-border-green, .w3-hover-border-green:hover {
+    border-color: #4CAF50 !important
+}
+
+.w3-border-light-green, .w3-hover-border-light-green:hover {
+    border-color: #8bc34a !important
+}
+
+.w3-border-indigo, .w3-hover-border-indigo:hover {
+    border-color: #3f51b5 !important
+}
+
+.w3-border-khaki, .w3-hover-border-khaki:hover {
+    border-color: #f0e68c !important
+}
+
+.w3-border-lime, .w3-hover-border-lime:hover {
+    border-color: #cddc39 !important
+}
+
+.w3-border-orange, .w3-hover-border-orange:hover {
+    border-color: #ff9800 !important
+}
+
+.w3-border-deep-orange, .w3-hover-border-deep-orange:hover {
+    border-color: #ff5722 !important
+}
+
+.w3-border-pink, .w3-hover-border-pink:hover {
+    border-color: #e91e63 !important
+}
+
+.w3-border-purple, .w3-hover-border-purple:hover {
+    border-color: #9c27b0 !important
+}
+
+.w3-border-deep-purple, .w3-hover-border-deep-purple:hover {
+    border-color: #673ab7 !important
+}
+
+.w3-border-red, .w3-hover-border-red:hover {
+    border-color: #f44336 !important
+}
+
+.w3-border-sand, .w3-hover-border-sand:hover {
+    border-color: #fdf5e6 !important
+}
+
+.w3-border-teal, .w3-hover-border-teal:hover {
+    border-color: #009688 !important
+}
+
+.w3-border-yellow, .w3-hover-border-yellow:hover {
+    border-color: #ffeb3b !important
+}
+
+.w3-border-white, .w3-hover-border-white:hover {
+    border-color: #fff !important
+}
+
+.w3-border-black, .w3-hover-border-black:hover {
+    border-color: #000 !important
+}
+
+.w3-border-grey, .w3-hover-border-grey:hover, .w3-border-gray, .w3-hover-border-gray:hover {
+    border-color: #9e9e9e !important
+}
+
+.w3-border-light-grey, .w3-hover-border-light-grey:hover, .w3-border-light-gray, .w3-hover-border-light-gray:hover {
+    border-color: #f1f1f1 !important
+}
+
+.w3-border-dark-grey, .w3-hover-border-dark-grey:hover, .w3-border-dark-gray, .w3-hover-border-dark-gray:hover {
+    border-color: #616161 !important
+}
+
+.w3-border-pale-red, .w3-hover-border-pale-red:hover {
+    border-color: #ffe7e7 !important
+}
+
+.w3-border-pale-green, .w3-hover-border-pale-green:hover {
+    border-color: #e7ffe7 !important
+}
+
+.w3-border-pale-yellow, .w3-hover-border-pale-yellow:hover {
+    border-color: #ffffcc !important
+}
+
+.w3-border-pale-blue, .w3-hover-border-pale-blue:hover {
+    border-color: #e7ffff !important
+}
\ No newline at end of file
diff --git a/haugebot_web/static/images/logo.png b/haugebot_web/static/images/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..00a09cfe416e2047ca4cb5616e1930870c1e5ff5
GIT binary patch
literal 17377
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

literal 0
HcmV?d00001

diff --git a/haugebot_web/templates/color_select_option.html b/haugebot_web/templates/color_select_option.html
new file mode 100644
index 0000000..8ad45ca
--- /dev/null
+++ b/haugebot_web/templates/color_select_option.html
@@ -0,0 +1,2 @@
+<option value="{{ widget.value|stringformat:'s' }}"{% include "django/forms/widgets/attrs.html" %}
+        style="{{ widget.value|stringformat:'s' }}">{{ widget.label }}</option>
diff --git a/haugebot_web/templates/form.html b/haugebot_web/templates/form.html
new file mode 100644
index 0000000..5b2a1bd
--- /dev/null
+++ b/haugebot_web/templates/form.html
@@ -0,0 +1,52 @@
+{% extends 'layout.html' %}
+
+{% block content %}
+    <form method="post">
+        {% csrf_token %}
+        {% if form %}
+            <div class="w3-card w3-white w3-display-container">
+                <div class="w3-container w3-form-container">
+                    {{ form.non_field_errors }}
+                    {% for field in form %}
+                        <p>
+                            {{ field.errors }}
+                            {{ field.label_tag }}
+                            {{ field }}
+                        </p>
+                    {% endfor %}
+                </div>
+            </div>
+        {% endif %}
+        {{ formset.management_form }}
+        {% for form in formset %}
+            <div class="w3-card w3-white w3-display-container">
+                <div class="w3-container w3-form-container">
+                    {{ form.non_field_errors }}
+                    {% for field in form %}
+                        <p>
+                            {{ field.errors }}
+                            {{ field }}
+                            {% if field.widget_type == "checkbox" %}
+                                {{ field.label_tag }}
+                            {% endif %}
+                        </p>
+                    {% endfor %}
+                </div>
+                {% if form.fields.id.initial %}
+                    <div class="w3-display-topright">
+                        <p><a href="{% url remove_url form.fields.id.initial %}" class="w3-button w3-haugepink"><i
+                                class="fas fa-trash-alt"></i></a></p>
+                    </div>
+                {% endif %}
+            </div>
+        {% endfor %}
+        <input type="submit" value="Add/Save" class="w3-button w3-haugepink w3-block">
+    </form>
+
+    <script>
+        color_field_options = document.querySelectorAll("#id_color_field option");
+        color_field_options.forEach(value => {
+            value.style = "background: " + value.getAttribute("value") + ";"
+        })
+    </script>
+{% endblock %}
\ No newline at end of file
diff --git a/haugebot_web/templates/home.html b/haugebot_web/templates/home.html
new file mode 100644
index 0000000..e1011a6
--- /dev/null
+++ b/haugebot_web/templates/home.html
@@ -0,0 +1,5 @@
+{% extends 'layout.html' %}
+
+{% block content %}
+    {{ status }}
+{% endblock %}
\ No newline at end of file
diff --git a/haugebot_web/templates/layout.html b/haugebot_web/templates/layout.html
new file mode 100644
index 0000000..d5162df
--- /dev/null
+++ b/haugebot_web/templates/layout.html
@@ -0,0 +1,36 @@
+{% load static %}
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>{{ title }}</title>
+    <link rel="shortcut icon" type="image/png" href="{% static 'images/logo.png' %}">
+    <script src="https://kit.fontawesome.com/cdec119da7.js" crossorigin="anonymous"></script>
+    <link rel="stylesheet" href="{% static 'css/w3.css' %}" type="text/css">
+    <link rel="stylesheet" href="{% static 'css/styles.css' %}" type="text/css">
+</head>
+<body class="w3-light-gray">
+<nav>
+    <div class="w3-sidebar w3-bar-block w3-haugedark ">
+        <a href="{% url 'home' %}" class="logo"><img src="{% static 'images/logo.png' %}" id="logo"/></a>
+        {% if user.is_authenticated %}
+            <a href="{% url 'wusstest_du_schon' %}"
+               class="w3-bar-item w3-button {% if title == "Wusstest du Schon?" %}w3-light-gray{% endif %}">Wusstest du
+                Schon?</a>
+            <a href="{% url 'logout' %}" class="w3-bar-item w3-button">Logout</a>
+        {% else %}
+            <a href="{% url 'login' %}" class="w3-bar-item w3-button">Login</a>
+        {% endif %}
+    </div>
+</nav>
+<div id="content">
+    <div class="w3-container">
+        <h1>{{ title }}</h1>
+    </div>
+    <div class="w3-container">
+        {% block content %}{% endblock %}
+    </div>
+</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/haugebot_web/tests.py b/haugebot_web/tests.py
new file mode 100644
index 0000000..a39b155
--- /dev/null
+++ b/haugebot_web/tests.py
@@ -0,0 +1 @@
+# Create your tests here.
diff --git a/haugebot_web/twitch_api.py b/haugebot_web/twitch_api.py
new file mode 100644
index 0000000..c544f54
--- /dev/null
+++ b/haugebot_web/twitch_api.py
@@ -0,0 +1,109 @@
+import os
+from datetime import datetime
+
+import requests
+from django.utils.http import urlencode
+
+token_url = "https://id.twitch.tv/oauth2/token?client_id={0}&client_secret={1}&grant_type=client_credentials"
+clips_url = "https://api.twitch.tv/helix/clips?broadcaster_id={0}&first=100"
+broadcaster_id = os.getenv("BROADCASTER_ID")
+client_id = os.getenv("CLIENT_ID")
+client_secret = os.getenv("CLIENT_SECRET")
+access_token = None
+
+
+def get_access_token():
+    global access_token
+
+    response = requests.post(token_url.format(client_id, client_secret))
+    credentials = response.json()
+    access_token = credentials["access_token"]
+
+
+def call(url):
+    if access_token:
+        response = requests.get(url, headers={
+            'Authorization': 'Bearer {}'.format(access_token),
+            'Client-Id': client_id
+        })
+        if response.status_code == 200:
+            return response.json()
+        else:
+            get_access_token()
+            response = requests.get(url, headers={
+                'Authorization': 'Bearer {}'.format(access_token),
+                'Client-Id': client_id
+            })
+            if response.status_code == 200:
+                return response.json()
+    else:
+        get_access_token()
+        response = requests.get(url, headers={
+            'Authorization': 'Bearer {}'.format(access_token),
+            'Client-Id': client_id
+        })
+        if response.status_code == 200:
+            return response.json()
+
+
+def get_clips(cursor=None, all_clips=False, today=False):
+    clips = []
+    url = clips_url.format(broadcaster_id)
+    if cursor:
+        url += "&after=" + cursor
+    if today:
+        url += "&started_at=" + get_date()
+
+    clips_json = call(url)
+
+    if data := clips_json.get("data"):
+        for clip in data:
+            if thumbnail_url := clip.get("thumbnail_url"):
+                clip_url = thumbnail_url.split("-preview-")[0] + ".mp4"
+                clips.append(clip_url)
+
+    if all_clips:
+        if pagination := clips_json.get("pagination"):
+            if new_cursor := pagination.get("cursor"):
+                clips.extend(get_clips(cursor=new_cursor, all_clips=all_clips, today=today))
+    return clips
+
+
+def get_date():
+    now = datetime.now()
+    dt = datetime(day=now.day, month=now.month, year=now.year)
+    return dt.isoformat() + "Z"
+
+
+def is_mod(user, broadcaster):
+    if user.id == broadcaster.id:
+        return True
+
+    response = requests.get(
+        f"https://api.twitch.tv/helix/moderation/moderators?broadcaster_id={broadcaster.id}&user_id={user.id}",
+        headers={
+            'Authorization': f'Bearer {broadcaster.access_token}',
+            'Client-Id': client_id
+        })
+
+    if response.status_code == 401 and response.json().get("message") == "Invalid OAuth token":
+        if not refresh_access_token(broadcaster):
+            return False
+        return is_mod(user, broadcaster)
+
+    return response.json().get("data") is not None
+
+
+def refresh_access_token(broadcaster):
+    url = "https://id.twitch.tv/oauth2/token?" + urlencode(
+        {"grant_type": "refresh_token", "refresh_token": broadcaster.refresh_token, "client_id": client_id,
+         "client_secret": client_secret})
+    response = requests.post(url)
+
+    if response.status_code == 400 and response.json().get("message") == "Invalid refresh token":
+        return None
+
+    json = response.json()
+    access_token = json["access_token"]
+    refresh_token = json["refresh_token"]
+    broadcaster.update_tokens(access_token, refresh_token)
diff --git a/haugebot_web/views.py b/haugebot_web/views.py
new file mode 100644
index 0000000..688e965
--- /dev/null
+++ b/haugebot_web/views.py
@@ -0,0 +1,88 @@
+import os
+
+import requests
+from django.contrib.auth import authenticate, login as django_login, logout as django_logout
+from django.contrib.auth.decorators import login_required
+from django.forms import modelformset_factory
+from django.shortcuts import render, redirect
+
+from .forms import BaseForm, WusstestDuSchonSettingsForm
+from .models import WusstestDuSchon
+
+
+# Create your views here.
+def home(request):
+    return render(request, "home.html", {'title': 'HaugeBot'})
+
+
+@login_required(login_url="/login")
+def wusstest_du_schon(request):
+    WusstestDuSchonFormSet = modelformset_factory(WusstestDuSchon, form=BaseForm,
+                                                  fields=('advertised_command', 'text', 'use_prefix', 'active'),
+                                                  field_classes=[''])
+    if request.method == "POST":
+        settings_form = WusstestDuSchonSettingsForm(request.POST)
+        formset = WusstestDuSchonFormSet(request.POST, request.FILES)
+        if formset.is_valid():
+            formset.save()
+        if settings_form.is_valid():
+            settings_form.save()
+
+    formset = WusstestDuSchonFormSet()
+    settings_form = WusstestDuSchonSettingsForm()
+
+    return render(request, "form.html",
+                  {'title': 'Wusstest du Schon?', 'formset': formset, 'remove_url': 'wusstest_du_schon_remove',
+                   'form': settings_form})
+
+
+@login_required(login_url="/login")
+def wusstest_du_schon_remove(request, id):
+    WusstestDuSchon.objects.filter(pk=id).delete()
+
+    return redirect("/wusstest_du_schon")
+
+
+def login(request):
+    client_id = os.getenv("CLIENT_ID")
+    redirect_uri = os.getenv("REDIRECT_URI")
+    url = f"https://id.twitch.tv/oauth2/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope=moderation:read"
+    return redirect(url)
+
+
+def logout(request):
+    django_logout(request)
+    return redirect("/")
+
+
+def login_redirect(request):
+    code = request.GET.get('code')
+    user = exchange_code(code)
+    if user:
+        twitch_user = authenticate(request, user=user)
+        twitch_user = list(twitch_user).pop()
+        django_login(request, twitch_user)
+
+    return redirect("/")
+
+
+def exchange_code(code):
+    client_id = os.getenv("CLIENT_ID")
+    client_secret = os.getenv("CLIENT_SECRET")
+    redirect_uri = os.getenv("REDIRECT_URI")
+    url = f"https://id.twitch.tv/oauth2/token?client_id={client_id}&client_secret={client_secret}&code={code}&grant_type=authorization_code&redirect_uri={redirect_uri}"
+    response = requests.post(url)
+    if response.status_code == 200:
+        credentials = response.json()
+
+        response = requests.get("https://api.twitch.tv/helix/users", headers={
+            'Authorization': f'Bearer {credentials["access_token"]}',
+            'Client-Id': client_id
+        })
+
+        user = response.json()["data"][0]
+
+        return {'id': user['id'], 'login': user['login'], 'access_token': credentials['access_token'],
+                'refresh_token': credentials['refresh_token']}
+
+    return None
diff --git a/info.json b/info.json
deleted file mode 100644
index 7cc9087..0000000
--- a/info.json
+++ /dev/null
@@ -1,25 +0,0 @@
-[
-  "mit !subs kannst du dir anzeigen lassen, wie viele haugeSun -e Menschen diesen Kanal derzeit mit einem Sub unterstützen.",
-  "mit !time kannst du dir die aktuelle Uhrzeit bei Hauke in Tokyo anzeigen lassen.",
-  "mit !pipi kannst du Hauke einen Hinweis hinterlassen, dass du demnächst mal eine Pause bräuchtest, um zum Beispiel Pipi zu machen. Außerdem kannst du mit !warpipi mitteilen, dass sich dein Wunsch nach einer Pause inzwischen erledigt hat.",
-  "mit !wishlist erhälst du den Link zu Haukes Amazon Wishlist.",
-  "mit !zeit kannst du dir die aktuelle Uhrzeit bei Hauke in Tokyo anzeigen lassen.",
-  "mit !amazon erhälst du Haukes Amazon Affiliate Link.",
-  "mit !art erhälst du die Information, wer für das wunderbare Design dieses tollen Twitch-Kanals verantwortlich ist.",
-  "mit !artists erhälst du die Links zu allen Musik Artists, deren Musik hier während des Streams immer im Hintergrund läuft.",
-  "mit !campfire bekommst du weitere Informationen zur interaktiven Hörspielapp Campfire.",
-  "mit !discord erfährst du den Link zum Discord Server dieses Kanals.",
-  "mit !donate erhälst du weitere Informationen, wie du Hauke durch eine Spende unterstützen kannst.",
-  "mit !game bekommst du weitere Informationen zum Morriton Manor Spiel.",
-  "wenn du Hauke auf Instagram folgen möchtest, dann schreib einfach !instagram in den Chat und du bekommst den Link zu Haukes Profil.",
-  "wenn dir die Playlist, die gerade im Hintergrund läuft gefällt, dann schreib doch mal !playlist in den Chat und du erhälst den Link zur Spotify Playlist.",
-  "falls du nicht genug von Hauke und Japan bekommen kannst, einfach !podcast in den Chat und du bekommst den Link zu seinem FYEO Podcast.",
-  "als Besitzer von Amazon Prime kannst du jeden Monat einen Twitch Kanal deiner Wahl kostenlos abonnieren. Falls du wissen möchtest, wie das geht, !prime in den Chat und du bekommst alle notwendigen Informationen.",
-  "wenn du studierst und noch kein Amazon Prime hast, dann kannst du dies zu bevorzugten Konditionen abschließen. Falls dich das interessiert, gib einfach mal !primestudent in den Chat und du erhälst einen Link mit allen notwendigen Informationen.",
-  "als alte(r) Reisliebhaber(in) kannst du von Haukes Partnerschaft mit Reishunger profitieren. Alle Informationen hierzu: !reishunger",
-  "mit !song kannst du dir Informationen zum aktuell laufenden Song anzeigen lassen.",
-  "du kannst sogar subben, wenn du den Stream am Handy schaust. Gib mal !sub in den Chat ein, wenn dich interessiert, wie das geht.",
-  "falls du Hauke auch auf Twitter folgen möchtest, !twitter in den Chat, und du bekommst sofort einen Link dafür.",
-  "mit !wort kannst du dir das Wort des Tages anzeigen lassen.",
-  "Hauke hat nicht nur einen Twitch Kanal, auf dem er regelmäßig streamt. Regelmäßig erscheinen auf YouTube auch Videos mit Ausschnitten des Streams, oder extra dafür produzierten Videos. Mit !youtube erhälst du den Link zum YouTube Kanal."
-]
\ No newline at end of file
diff --git a/info_cog.py b/info_cog.py
deleted file mode 100644
index 51e6d89..0000000
--- a/info_cog.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import asyncio
-import json
-import logging
-import os
-import random
-from datetime import datetime
-
-from twitchio.ext import commands
-
-
-@commands.core.cog(name="InfoCog")
-class InfoCog:
-    def __init__(self, bot):
-        self.bot = bot
-        self.info_file = os.getenv("INFO_JSON")
-        self.INFO_LOOP = float(os.getenv("INFO_LOOP"))
-        self.INFO_COLOR = os.getenv("INFO_COLOR")
-        self.info = []
-        self.load_info()
-
-    def load_info(self):
-        """ Loads all info that should be sent to the chat regularly from INFO_JSON """
-
-        info_file = open(self.info_file, mode='r')
-        self.info = json.load(info_file)
-
-    async def info_loop(self):
-        """ Send !pipimeter into the chat every x Minutes. Also check, whether the stream was offline for x Minutes.
-         If this is true, reset the pipi counter, as you can assume, that the stream recently started."""
-
-        logging.log(logging.INFO, f"Info loop started. {datetime.now()} {self}")
-
-        while True:
-            logging.log(logging.INFO, f"Inside Info loop. Sleep for {self.INFO_LOOP} minutes. {datetime.now()} {self}")
-            await asyncio.sleep(self.INFO_LOOP * 60)
-            logging.log(logging.INFO, f"Inside Info loop finished sleeping now. {datetime.now()} {self}")
-
-            if await self.bot.stream():
-                logging.log(logging.INFO,
-                            f"Inside Info loop. Stream is online, so send a message now!!! {datetime.now()} {self}")
-                channel = self.bot.channel()
-                message = f"Psssst... wusstest du eigentlich schon, {random.choice(self.info)}"
-                await self.bot.send_me(channel, message, self.INFO_COLOR)
-
-            logging.log(logging.INFO,
-                        f"Inside Info loop. Ooooookay, Loop ended, let's continue with the next round!!! {datetime.now()} {self}")
diff --git a/manage.py b/manage.py
new file mode 100644
index 0000000..fb5aa8f
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+    """Run administrative tasks."""
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'haugebot.settings')
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/requirements.txt b/requirements.txt
index 57f9216..c794e3d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,19 @@
-aiohttp==3.6.2
+aiohttp==3.7.3
+asgiref==3.3.1
 async-timeout==3.0.1
-attrs==19.3.0
+attrs==20.3.0
+certifi==2020.12.5
 chardet==3.0.4
+Django==3.1.5
 idna==2.10
-multidict==4.7.6
-python-dotenv==0.14.0
+multidict==5.1.0
+python-dotenv==0.15.0
+pytz==2020.5
+redis==3.5.3
+requests==2.25.1
+sqlparse==0.4.1
 twitchio==1.1.0
+typing-extensions==3.7.4.3
+urllib3==1.26.2
 websockets==8.1
-yarl==1.4.2
-redis==3.5.3
+yarl==1.6.3
-- 
GitLab