From 7863d342819f8cecd1852b8d8e89e31a8c7ed31a Mon Sep 17 00:00:00 2001
From: dnns01 <git@dnns01.de>
Date: Tue, 4 Oct 2022 15:15:41 +0200
Subject: [PATCH] Add functionality to Timers

---
 requirements.txt                              |   4 +-
 strolchibot/forms.py                          |  14 ++-
 .../migrations/0013_config_timers_interval.py |  18 +++
 strolchibot/models.py                         |   1 +
 strolchibot/static/js/scripts.js              |  37 ++++++
 strolchibot/templates/timers/edit.html        |   9 ++
 strolchibot/templates/timers/list.html        |  49 ++++++++
 strolchibot/urls.py                           |   6 +-
 strolchibot/views.py                          | 111 ++++++++++++++----
 twitchbot/strolchibot.py                      |   7 +-
 twitchbot/timers.py                           |  30 +++++
 11 files changed, 255 insertions(+), 31 deletions(-)
 create mode 100644 strolchibot/migrations/0013_config_timers_interval.py
 create mode 100644 strolchibot/templates/timers/edit.html
 create mode 100644 strolchibot/templates/timers/list.html
 create mode 100644 twitchbot/timers.py

diff --git a/requirements.txt b/requirements.txt
index 01126a1..5d8cb70 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,7 +5,7 @@ attrs==20.3.0
 certifi==2020.12.5
 chardet==3.0.4
 charset-normalizer==2.0.12
-Django==4.1
+Django==4.1.2
 fontawesome-free==5.15.1
 gql==2.0.0
 graphql-core==2.3.2
@@ -19,7 +19,7 @@ requests==2.25.1
 Rx==1.6.1
 six==1.16.0
 sqlparse==0.4.2
-twitchio==2.4.0
+twitchio @ git+https://github.com/TwitchIO/TwitchIO.git@71eabf6b6003608c08f208fc8a83765e08c38708
 typing-extensions==3.7.4.3
 urllib3==1.26.5
 websockets==9.1
diff --git a/strolchibot/forms.py b/strolchibot/forms.py
index 1d4684f..f6c00ea 100644
--- a/strolchibot/forms.py
+++ b/strolchibot/forms.py
@@ -1,6 +1,6 @@
 from django import forms
 
-from .models import Config, Command, Spotify, Counter
+from .models import Config, Command, Spotify, Counter, Timer
 
 
 class BaseModelForm(forms.ModelForm):
@@ -39,6 +39,18 @@ class CommandForm(BaseModelForm):
         exclude = ['active']
 
 
+class TimerConfigForm(BaseModelForm):
+    class Meta:
+        model = Config
+        fields = ['timers_interval']
+
+
+class TimerForm(BaseModelForm):
+    class Meta:
+        model = Timer
+        exclude = ['active']
+
+
 class SpotifyForm(BaseModelForm):
     class Meta:
         model = Spotify
diff --git a/strolchibot/migrations/0013_config_timers_interval.py b/strolchibot/migrations/0013_config_timers_interval.py
new file mode 100644
index 0000000..a025e92
--- /dev/null
+++ b/strolchibot/migrations/0013_config_timers_interval.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.2 on 2022-10-04 10:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('strolchibot', '0012_command_permissions'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='config',
+            name='timers_interval',
+            field=models.IntegerField(default=10, verbose_name='Interval'),
+        ),
+    ]
diff --git a/strolchibot/models.py b/strolchibot/models.py
index f2d26f0..2d69277 100644
--- a/strolchibot/models.py
+++ b/strolchibot/models.py
@@ -47,6 +47,7 @@ class LinkBlacklist(models.Model):
 
 
 class Config(models.Model):
+    timers_interval = models.IntegerField(default=10, verbose_name="Interval")
     link_protection_active = models.BooleanField(default=True, verbose_name="Active")
     link_protection_permit_subs = models.BooleanField(default=True, verbose_name="Permit Subs")
     streamer = models.TextField(max_length=20, null=True)
diff --git a/strolchibot/static/js/scripts.js b/strolchibot/static/js/scripts.js
index aba6f9e..6f7bfc7 100644
--- a/strolchibot/static/js/scripts.js
+++ b/strolchibot/static/js/scripts.js
@@ -163,4 +163,41 @@ function setInLoop(clip_id, visible) {
         credentials: 'same-origin',
         body: JSON.stringify(payload)
     }).then(response => response.json()).then(data => location.reload());
+}
+
+
+function timerSetActive(checkbox, timer) {
+    const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
+    let payload = {
+        timer: timer,
+        active: checkbox.checked
+    };
+
+    fetch('/timers/active', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json',
+            'X-CSRFToken': csrftoken
+        },
+        credentials: 'same-origin',
+        body: JSON.stringify(payload)
+    }).then(response => response.json()).then(data => checkbox.checked = data.active);
+}
+
+
+function timerRemove(timer_id) {
+    const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
+    let payload = {
+        id: timer_id
+    };
+
+    fetch('/timers/remove', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json',
+            'X-CSRFToken': csrftoken
+        },
+        credentials: 'same-origin',
+        body: JSON.stringify(payload)
+    }).then(response => response.json()).then(data => location.reload());
 }
\ No newline at end of file
diff --git a/strolchibot/templates/timers/edit.html b/strolchibot/templates/timers/edit.html
new file mode 100644
index 0000000..7588a5c
--- /dev/null
+++ b/strolchibot/templates/timers/edit.html
@@ -0,0 +1,9 @@
+{% extends 'layout.html' %}
+
+{% block content %}
+    <form method="post">
+        {% csrf_token %}
+        {% include 'card_form.html' %}
+        <input type="submit" value="Add/Save" class="w3-button w3-strolchpink w3-block">
+    </form>
+{% endblock %}
\ No newline at end of file
diff --git a/strolchibot/templates/timers/list.html b/strolchibot/templates/timers/list.html
new file mode 100644
index 0000000..9f288eb
--- /dev/null
+++ b/strolchibot/templates/timers/list.html
@@ -0,0 +1,49 @@
+{% extends 'layout.html' %}
+
+{% block content %}
+    {% csrf_token %}
+    <div class="w3-card w3-white">
+    <div style="display: block; width: 75%" class="w3-left">
+        <form method="post">
+            {% csrf_token %}
+            <p>
+                {{ form.message.errors }}
+                <label for="{{ form.timers_interval.id_for_label }}">Interval:</label>
+                {{ form.timers_interval }}
+                <input type="submit" value="Save" class="w3-button w3-strolchpink w3-block">
+            </p>
+        </form>
+    </div>
+        <div>
+            <a href="{% url "timers_new" %}" class="w3-button w3-strolchpink w3-right w3-margin"><i
+                    class="fas fa-plus"></i> Add Timer</a>
+        </div>
+        <div class="w3-padding">
+            <table class="w3-table w3-striped">
+                <tbody>
+                <tr>
+                    <th>Timer</th>
+                    <th style="width: 1px;">Active</th>
+                    <th style="width: 1px;"></th>
+                    <th style="width: 1px;"></th>
+                </tr>
+                {% for timer in timers %}
+                    <tr>
+                        <td>{{ timer.text }}</td>
+                        <td>
+                            <label class="switch">
+                                <input class="w3-switch" type="checkbox" {% if timer.active %}checked{% endif %}
+                                       onchange="timerSetActive(this, '{{ timer.id }}')">
+                                <span class="slider"></span>
+                            </label>
+                        </td>
+                        <td><a href="#" onclick="timerRemove({{ timer.id }})"><em class="fas fa-trash-alt"></em></a>
+                        </td>
+                        <td><a href="{% url "timers_edit" timer.id %}"><em class="fas fa-pencil-alt"></em></a></td>
+                    </tr>
+                {% endfor %}
+                </tbody>
+            </table>
+        </div>
+    </div>
+{% endblock %}
\ No newline at end of file
diff --git a/strolchibot/urls.py b/strolchibot/urls.py
index 6657a4b..0d0ec7d 100644
--- a/strolchibot/urls.py
+++ b/strolchibot/urls.py
@@ -37,8 +37,10 @@ urlpatterns = [
     path('klassenbuch/', views.klassenbuch, name="klassenbuch"),
     path('klassenbuch/remove/<int:id>', views.klassenbuch_remove, name="klassenbuch_remove"),
     path('timers/', views.timers, name="timers"),
-    path('timers/remove/<int:id>', views.timers_remove, name="timers_remove"),
-    path('timers/activate/<int:id>', views.timers_activate, name="timers_activate"),
+    path('timers/new', views.timers_new, name="timers_new"),
+    path('timers/remove', views.timers_remove, name="timers_remove"),
+    path('timers/edit/<int:timer_id>', views.timers_edit, name="timers_edit"),
+    path('timers/active', views.timers_set_active, name="timers_set_activate"),
     path('link_protection/', views.link_protection, name="link_protection"),
     path('link_protection/permit/remove/<int:id>', views.link_protection_permit_remove,
          name="link_protection_permit_remove"),
diff --git a/strolchibot/views.py b/strolchibot/views.py
index 0165c52..84da769 100644
--- a/strolchibot/views.py
+++ b/strolchibot/views.py
@@ -8,7 +8,8 @@ from django.forms import modelformset_factory, modelform_factory
 from django.http import Http404, JsonResponse, HttpResponse, HttpRequest
 from django.shortcuts import render, redirect, get_object_or_404
 
-from .forms import BaseModelForm, LinkProtectionConfigForm, CommandForm, SpotifyForm, CounterForm
+from .forms import BaseModelForm, LinkProtectionConfigForm, CommandForm, SpotifyForm, CounterForm, TimerForm, \
+    TimerConfigForm
 from .models import Command, Klassenbuch, Timer, Config, LinkPermit, LinkWhitelist, LinkBlacklist, Spotify, Counter
 
 
@@ -44,28 +45,6 @@ def klassenbuch_remove(request, id):
     return redirect("/klassenbuch")
 
 
-@login_required(login_url="/login")
-def timers(request):
-    TimerFormSet = modelformset_factory(Timer, form=BaseModelForm, fields=("text", "active"))
-    if request.method == "POST":
-        formset = TimerFormSet(request.POST, request.FILES)
-        if formset.is_valid():
-            formset.save()
-
-    forms = {
-        "Basic Configuration": {
-            "display": "card",
-            "type": "formset",
-            "name": "timers",
-            "formset": TimerFormSet(),
-            "remove_url": "timers_remove",
-            "activate_url": "timers_activate",
-        },
-    }
-
-    return render(request, "form.html", {"title": "Timers", "forms": forms, "active": "timers"})
-
-
 @login_required(login_url="/login")
 def timers_remove(request, id):
     Timer.objects.filter(pk=id).delete()
@@ -355,7 +334,7 @@ def spotify_login_redirect(request: HttpRequest) -> JsonResponse:
 # </editor-fold>
 
 
-# <editor-fold desc="Counter">
+# <editor-fold desc="Counters">
 @login_required(login_url="/login")
 def counters(request: HttpRequest) -> HttpResponse:
     return render(request, "counters/list.html",
@@ -407,4 +386,88 @@ def counters_edit(request: HttpRequest, counter_id: int) -> HttpResponse:
 
     return render(request, "counters/edit.html",
                   {"title": "Counters", "form": {"form": form}})
+
+
+# </editor-fold>
+
+
+# <editor-fold desc="Timers">
+@login_required(login_url="/login")
+def timers(request: HttpRequest) -> HttpResponse:
+    config = Config.objects.first()
+
+    if request.method == "POST":
+        form = TimerConfigForm(request.POST, instance=config)
+
+        if form.is_valid():
+            form.save()
+
+    form =  TimerConfigForm(instance=config)
+
+    return render(request, "timers/list.html",
+                  {"title": "Timers", "timers": Timer.objects.all(), "form": form, "active": "timers"})
+
+
+@login_required(login_url="/login")
+def timers_new(request: HttpRequest) -> HttpResponse:
+    if request.method == "POST":
+        form = TimerForm(request.POST)
+
+        if form.is_valid():
+            form.save()
+            return redirect("/timers")
+
+    form = TimerForm()
+
+    return render(request, "timers/edit.html",
+                  {"title": "Timers", "form": {"form": form}})
+
+
+@login_required(login_url="/login")
+def timers_remove(request: HttpRequest) -> HttpResponse:
+    if request.method == "POST":
+        try:
+            payload = json.loads(request.body)
+            timer = get_object_or_404(Timer, pk=payload["id"])
+            timer.delete()
+        except (json.decoder.JSONDecodeError, KeyError):
+            pass
+
+        return JsonResponse({})
+
+    raise Http404
+
+
+@login_required(login_url="/login")
+def timers_edit(request: HttpRequest, timer_id: int) -> HttpResponse:
+    timer = get_object_or_404(Timer, pk=timer_id)
+
+    if request.method == "POST":
+        form = TimerForm(request.POST, instance=timer)
+
+        if form.is_valid():
+            form.save()
+            return redirect("/timers")
+
+    form = TimerForm(instance=timer)
+
+    return render(request, "timers/edit.html",
+                  {"title": "Timers", "form": {"form": form}})
+
+
+@login_required(login_url="/login")
+def timers_set_active(request: HttpRequest) -> JsonResponse:
+    if request.method == "POST":
+        try:
+            payload = json.loads(request.body)
+            timer = get_object_or_404(Timer, id=payload["timer"])
+            timer.active = payload["active"]
+            timer.save()
+
+            return JsonResponse({"active": timer.active})
+        except (json.decoder.JSONDecodeError, KeyError):
+            pass
+
+    raise Http404
+
 # </editor-fold>
diff --git a/twitchbot/strolchibot.py b/twitchbot/strolchibot.py
index c23f32c..2dc9e03 100644
--- a/twitchbot/strolchibot.py
+++ b/twitchbot/strolchibot.py
@@ -9,7 +9,7 @@ from twitchio import Channel, Message
 from twitchio.ext import commands
 from twitchio.ext.commands import Context
 
-import chat_commands, giveaway, klassenbuch, link_protection, spotify_cog, vote_cog, countdown, einkaufsliste
+import chat_commands, giveaway, klassenbuch, link_protection, spotify_cog, vote_cog, countdown, einkaufsliste, timers
 
 load_dotenv()
 
@@ -36,6 +36,7 @@ class StrolchiBot(commands.Bot, ABC):
         self.add_cog(chat_commands.Commands(self))
         self.add_cog(countdown.Countdown(self))
         self.add_cog(einkaufsliste.Einkaufsliste(self))
+        self.add_cog(timers.Timers(self))
 
     @staticmethod
     async def send_me(ctx, content):
@@ -60,6 +61,8 @@ class StrolchiBot(commands.Bot, ABC):
 
         if vote_cog := self.cogs.get("VoteCog"):
             vote_cog.manage_vote.start()
+        if timers := self.cogs.get("Timers"):
+            timers.timer_loop.start()
 
     @staticmethod
     def get_percentage(part, total):
@@ -76,7 +79,7 @@ class StrolchiBot(commands.Bot, ABC):
         return await self.get_chatters(self.CHANNEL)
 
     async def stream(self):
-        return await self.get_stream(self.CHANNEL)
+        return await self.fetch_streams(user_logins=[self.CHANNEL])
 
 
 bot = StrolchiBot()
diff --git a/twitchbot/timers.py b/twitchbot/timers.py
new file mode 100644
index 0000000..6cb4adc
--- /dev/null
+++ b/twitchbot/timers.py
@@ -0,0 +1,30 @@
+import sqlite3
+
+from twitchio.ext import commands, routines
+
+from twitchbot import config
+
+
+class Timers(commands.Cog):
+    def __init__(self, bot):
+        self.bot = bot
+        self.item = -1
+        self.interval = config.get("timers_interval")
+
+    @routines.routine(minutes=config.get("timers_interval"))
+    async def timer_loop(self):
+        new_interval = config.get("timers_interval")
+        if new_interval != self.interval:
+            self.interval = new_interval
+            self.timer_loop.change_interval(minutes=self.interval)
+        else:
+            if streams := await self.bot.stream():
+                conn = sqlite3.connect("db.sqlite3")
+
+                c = conn.cursor()
+                c.execute(f"SELECT text FROM strolchibot_timer where active = 1")
+                timers = list(c.fetchall())
+                conn.close()
+
+                self.item = (self.item + 1) % len(timers)
+                await self.bot.send_announce(self.bot.channel(), timers[self.item][0])
-- 
GitLab