diff --git a/requirements.txt b/requirements.txt index 01126a132b6ef30fbbf2b478498be9881e20c43f..5d8cb701c6fea10df23c5bd6122cac3b8738f7d2 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 1d4684f13d888b7fc010fbc1692f4bde21d8b77a..f6c00eaa0b71a46374118822189d24219fd222fb 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 0000000000000000000000000000000000000000..a025e929346014021b1e4812bea4c1c9f79080e6 --- /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 f2d26f07422e0ed206c4d00d6ca567c530a638f8..2d69277d2cd075cbfbda8021e17a38f449d33420 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 aba6f9e3a682e333499f0405631b610e986b9976..6f7bfc778935a7939baaa2972243ec4a88e40985 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 0000000000000000000000000000000000000000..7588a5c178692f4b43493f9375af555be101df67 --- /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 0000000000000000000000000000000000000000..9f288eb4b929b095dc337e02ce6d02b2e249ba3a --- /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 6657a4bf02dc42fa9d3fa5b9e6b566d41f055432..0d0ec7df47aa37768f15bae8577c3f51891a548d 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 0165c52e3727fdff96522877da8216843ecdd773..84da7692e103272a36713e48ec931d36e68cac36 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 c23f32ccc75de1d82a443b914ff9ddf35946a80b..2dc9e033f3e15eb4c8ee10d1812d2ecc5b9f4db0 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 0000000000000000000000000000000000000000..6cb4adc64fdba26ff4eebe23cbb54773563cd4cf --- /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])