Full Discord integration

This commit is contained in:
Corban-Lee Jones 2024-03-11 22:42:58 +00:00
parent dde32e4b6d
commit 8666a6094e
17 changed files with 406 additions and 304 deletions

View File

@ -4,7 +4,7 @@ import logging
from rest_framework import serializers
from apps.home.models import Subscription, SubscriptionChannel, TrackedContent
from apps.home.models import Subscription, TrackedContent
log = logging.getLogger(__name__)
@ -105,6 +105,16 @@ class DynamicModelSerializer(serializers.ModelSerializer):
abstract = True
# class SubscriptionTargetSerializer(DynamicModelSerializer):
# """
# Serializer for the Subscription Target Model.
# """
# class Meta:
# model = SubscriptionTarget
# fields = ("id", "creation_datetime")
class SubscriptionSerializer(DynamicModelSerializer):
"""
Serializer for the Subscription Model.
@ -112,37 +122,11 @@ class SubscriptionSerializer(DynamicModelSerializer):
image = serializers.ImageField(required=False)
server = serializers.CharField()
# targets = SubscriptionTargetSerializer(many=True, required=False)
class Meta:
model = Subscription
fields = ("uuid", "name", "rss_url", "image", "server", "creation_datetime")
class SubscriptionChannelSerializerGET(DynamicModelSerializer):
"""
Serializer for the SubscriptionChannel Model.
This serializer should be used with GET requests.
"""
subscription = SubscriptionSerializer()
class Meta:
model = SubscriptionChannel
fields = ("uuid", "id", "subscription", "creation_datetime")
class SubscriptionChannelSerializerPOST(DynamicModelSerializer):
"""
Serializer for the SubscriptionChannel Model.
This serializer should be used with POST requests.
"""
subscription = serializers.PrimaryKeyRelatedField(queryset=Subscription.objects.all())
class Meta:
model = SubscriptionChannel
fields = ("uuid", "id", "subscription", "creation_datetime")
fields = ("uuid", "name", "rss_url", "image", "server", "targets", "creation_datetime", "extra_notes")
class TrackedContentSerializer(DynamicModelSerializer):
"""

View File

@ -6,8 +6,6 @@ from rest_framework.authtoken.views import obtain_auth_token
from .views import (
Subscription_ListView,
Subscription_DetailView,
SubscriptionChannel_ListView,
SubscriptionChannel_DetailView,
TrackedContent_ListView,
TrackedContent_DetailView
)
@ -18,10 +16,6 @@ urlpatterns = [
path("api-token-auth/", obtain_auth_token),
path("subscription/", include([
path("channel/", include([
path("", SubscriptionChannel_ListView.as_view(), name="subscriptionchannel"),
path("<str:pk>/", SubscriptionChannel_DetailView.as_view(), name="subscriptionchannel-detail")
])),
path("", Subscription_ListView.as_view(), name="subscription"),
path("<str:pk>/", Subscription_DetailView.as_view(), name="subscription-detail")
])),

View File

@ -11,11 +11,9 @@ from rest_framework.pagination import PageNumberPagination
from rest_framework.authentication import SessionAuthentication, TokenAuthentication
from rest_framework.parsers import MultiPartParser, FormParser
from apps.home.models import Subscription, SubscriptionChannel, TrackedContent
from apps.home.models import Subscription, TrackedContent
from .serializers import (
SubscriptionSerializer,
SubscriptionChannelSerializerGET,
SubscriptionChannelSerializerPOST,
TrackedContentSerializer
)
@ -50,8 +48,8 @@ class Subscription_ListView(generics.ListCreateAPIView):
queryset = Subscription.objects.all().order_by("-creation_datetime")
filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ["uuid", "name", "rss_url", "server", "creation_datetime"]
search_fields = ["name"]
filterset_fields = ["uuid", "name", "rss_url", "server", "targets", "creation_datetime", "extra_notes"]
search_fields = ["name", "extra_notes"]
ordering_fields = ["creation_datetime"]
def post(self, request):
@ -86,88 +84,6 @@ class Subscription_DetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Subscription.objects.all().order_by("-creation_datetime")
# =================================================================================================
# SubscriptionChannel Views
class SubscriptionChannel_ListView(generics.ListCreateAPIView):
"""
View to provide a list of SubscriptionChannel model instances.
Can also be used to create a new instance.
Supports: GET, POST
"""
authentication_classes = [SessionAuthentication, TokenAuthentication]
permission_classes = [permissions.IsAuthenticated]
pagination_class = DefaultPagination
# serializer_class = SubscriptionChannelSerializer
queryset = SubscriptionChannel.objects.all().order_by("-creation_datetime")
filter_backends = [rest_filters.DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ["uuid", "id", "subscription", "subscription__server"]
ordering_fields = ["creation_datetime"]
def get_serializer(self, *args, **kwargs):
if self.request.method == "POST":
return SubscriptionChannelSerializerPOST(*args, **kwargs)
else:
return SubscriptionChannelSerializerGET(*args, **kwargs)
def post(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
self.perform_create(serializer)
except IntegrityError as exc:
log.error(exc)
return Response(
{"detail": "Duplicate or limit reached"},
status=status.HTTP_409_CONFLICT,
exception=True
)
# Switch to the GET serializer for return data, as it contains more data on the subscription.
# We can't modify the POST serializer to contain this data, otherwise it expects it in POST.
self.request.method = "GET"
serializer = self.get_serializer(serializer.instance)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class SubscriptionChannel_DetailView(generics.RetrieveDestroyAPIView):
"""
View to provide details on a particular SubscriptionChannel model instances.
Supports: GET, DELETE
"""
authentication_classes = [SessionAuthentication, TokenAuthentication]
permission_classes = [permissions.IsAuthenticated]
parser_classes = [MultiPartParser, FormParser]
serializer_class = SubscriptionChannelSerializerGET
queryset = SubscriptionChannel.objects.all().order_by("-creation_datetime")
def post(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
self.perform_create(serializer)
except IntegrityError:
return Response(
{"detail": "Duplicate or limit reached"},
status=status.HTTP_409_CONFLICT,
exception=True
)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
# =================================================================================================
# Tracked Content Views

View File

@ -34,6 +34,7 @@ class DiscordUserOAuth2Manager(models.UserManager):
flags=user["flags"],
locale=user["locale"],
mfa_enabled=user["mfa_enabled"],
access_token=user["access_token"],
**extra_fields
)

View File

@ -1,5 +1,7 @@
# Generated by Django 5.0.1 on 2024-03-10 22:22
# Generated by Django 5.0.1 on 2024-03-11 17:38
import apps.authentication.managers
import django.utils.timezone
from django.db import migrations, models
@ -14,15 +16,22 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='DiscordUser',
fields=[
('id', models.PositiveBigIntegerField(primary_key=True, serialize=False)),
('username', models.CharField(max_length=100)),
('global_name', models.CharField(max_length=100)),
('avatar', models.CharField(max_length=100)),
('public_flags', models.IntegerField()),
('flags', models.IntegerField()),
('locale', models.CharField(max_length=100)),
('mfa_enabled', models.BooleanField()),
('last_login', models.DateTimeField()),
('id', models.PositiveBigIntegerField(help_text="the user's id", primary_key=True, serialize=False)),
('username', models.CharField(help_text="the user's username, not unique across the platform", max_length=32, unique=True, verbose_name='username')),
('global_name', models.CharField(help_text="the user's display name, if it is set. For bots, this is the application name", max_length=32, verbose_name='nickname')),
('avatar', models.CharField(help_text="the user's avatar hash", max_length=64, verbose_name='avatar')),
('public_flags', models.IntegerField(help_text="the public flags on a user's account", verbose_name='public flags')),
('flags', models.IntegerField(help_text="the flags on a user's account", verbose_name='flags')),
('locale', models.CharField(help_text="the user's chosen language option", max_length=16, verbose_name='locale')),
('mfa_enabled', models.BooleanField(help_text='whether the user has two factor enabled on their account', verbose_name='mfa enabled')),
('last_login', models.DateTimeField(default=django.utils.timezone.now, help_text='datetime of the previous login', verbose_name='last login')),
('access_token', models.CharField(help_text='token for the application to make api calls on behalf of the user.', max_length=100, verbose_name='access token')),
('is_active', models.BooleanField(default=True, help_text='Use as a "soft delete" rather than deleting the user.', verbose_name='active status')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_superuser', models.BooleanField(default=False, help_text='Designates whether the user has unrestricted site control.', verbose_name='superuser status')),
],
managers=[
('objects', apps.authentication.managers.DiscordUserOAuth2Manager()),
],
),
]

View File

@ -1,81 +0,0 @@
# Generated by Django 5.0.1 on 2024-03-10 23:15
import apps.authentication.managers
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0001_initial'),
]
operations = [
migrations.AlterModelManagers(
name='discorduser',
managers=[
('objects', apps.authentication.managers.DiscordUserOAuth2Manager()),
],
),
migrations.AddField(
model_name='discorduser',
name='is_active',
field=models.BooleanField(default=True, help_text='Use as a "soft delete" rather than deleting the user.', verbose_name='active status'),
),
migrations.AddField(
model_name='discorduser',
name='is_staff',
field=models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status'),
),
migrations.AddField(
model_name='discorduser',
name='is_superuser',
field=models.BooleanField(default=False, help_text='Designates whether the user has unrestricted site control.', verbose_name='superuser status'),
),
migrations.AlterField(
model_name='discorduser',
name='avatar',
field=models.CharField(help_text="the user's avatar hash", max_length=64, verbose_name='avatar'),
),
migrations.AlterField(
model_name='discorduser',
name='flags',
field=models.IntegerField(help_text="the flags on a user's account", verbose_name='flags'),
),
migrations.AlterField(
model_name='discorduser',
name='global_name',
field=models.CharField(help_text="the user's display name, if it is set. For bots, this is the application name", max_length=32, verbose_name='nickname'),
),
migrations.AlterField(
model_name='discorduser',
name='id',
field=models.PositiveBigIntegerField(help_text="the user's id", primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='discorduser',
name='last_login',
field=models.DateTimeField(default=django.utils.timezone.now, help_text='datetime of the previous login', verbose_name='last login'),
),
migrations.AlterField(
model_name='discorduser',
name='locale',
field=models.CharField(help_text="the user's chosen language option", max_length=16, verbose_name='locale'),
),
migrations.AlterField(
model_name='discorduser',
name='mfa_enabled',
field=models.BooleanField(help_text='whether the user has two factor enabled on their account', verbose_name='mfa enabled'),
),
migrations.AlterField(
model_name='discorduser',
name='public_flags',
field=models.IntegerField(help_text="the public flags on a user's account", verbose_name='public flags'),
),
migrations.AlterField(
model_name='discorduser',
name='username',
field=models.CharField(help_text="the user's username, not unique across the platform", max_length=32, unique=True, verbose_name='username'),
),
]

View File

@ -58,6 +58,14 @@ class DiscordUser(models.Model):
help_text=_("datetime of the previous login")
)
# Access Token
access_token = models.CharField(
_("access token"),
max_length=100,
help_text=_("token for the application to make api calls on behalf of the user.")
)
# Custom Attributes
is_active = models.BooleanField(
@ -84,4 +92,8 @@ class DiscordUser(models.Model):
return True
def __str__(self):
return self.global_name or self.username
return self.global_name or self.username
@property
def avatar_url(self):
return f"https://cdn.discordapp.com/avatars/{self.id}/{self.avatar}.webp?size=128"

View File

@ -3,13 +3,16 @@
from django.urls import path
from django.contrib.auth.views import LogoutView
# from .views import login_view, register_user, profile_view, discord_login, discord_login_redirect
from . import views
from .views import DiscordLoginAction, DiscordLoginRedirect, Login
from .views import DiscordLoginAction, DiscordLoginRedirect, Login, GuildsView, GuildChannelsView
urlpatterns = [
path("login/", Login.as_view(), name="login"),
path("oauth2/login/", DiscordLoginAction.as_view(), name="discord-login"),
path("oauth2/login/redirect/", DiscordLoginRedirect.as_view(), name="discord-login-redirect")
path("oauth2/login/redirect/", DiscordLoginRedirect.as_view(), name="discord-login-redirect"),
path("logout/", LogoutView.as_view(), name="logout"),
path("guilds/", GuildsView.as_view(), name="guilds"),
path("channels/", GuildChannelsView.as_view(), name="channels")
]

View File

@ -29,6 +29,7 @@ class DiscordLoginRedirect(View):
code = request.GET.get("code")
access_token = self.exchange_code_for_token(code)
raw_user_data = self.get_raw_user_data(access_token)
raw_user_data["access_token"] = access_token
log.debug(raw_user_data)
@ -78,3 +79,33 @@ class DiscordLoginRedirect(View):
class Login(TemplateView):
template_name = "accounts/login.html"
class GuildsView(View):
def get(self, request, *args, **kwargs):
response = requests.get(
url="https://discord.com/api/v6/users/@me/guilds",
headers={"Authorization": f"Bearer {request.user.access_token}"}
)
content = response.json()
return JsonResponse(content, safe=False)
class GuildChannelsView(View):
def get(self, request, *args, **kwargs):
guild_id = request.GET.get("guild")
log.debug("fetching channels from %s using token: %s", guild_id, settings.BOT_TOKEN)
response = requests.get(
url=f"https://discord.com/api/v10/guilds/{guild_id}/channels",
headers={"Authorization": f"Bot {settings.BOT_TOKEN}"}
)
return JsonResponse(response.json(), safe=False)

View File

@ -2,7 +2,7 @@
from django.contrib import admin
from .models import Subscription, SubscriptionChannel, TrackedContent
from .models import Subscription, TrackedContent
@admin.register(Subscription)
@ -13,11 +13,11 @@ class SubscriptionAdmin(admin.ModelAdmin):
]
@admin.register(SubscriptionChannel)
class SubscriptionAdmin(admin.ModelAdmin):
list_display = [
"uuid", "id", "subscription", "creation_datetime"
]
# @admin.register(SubscriptionTarget)
# class SubscriptionTargetAdmin(admin.ModelAdmin):
# list_display = [
# "id", "creation_datetime"
# ]
@admin.register(TrackedContent)

View File

@ -0,0 +1,34 @@
# Generated by Django 5.0.1 on 2024-03-11 15:34
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='SubscriptionTarget',
fields=[
('id', models.PositiveBigIntegerField(help_text='Discord Channel ID', primary_key=True, serialize=False, verbose_name='id')),
('creation_datetime', models.DateTimeField(default=django.utils.timezone.now, editable=False, help_text='when this instance was created.', verbose_name='creation datetime')),
],
options={
'verbose_name': 'subscription target',
'verbose_name_plural': 'subscription targets',
'get_latest_by': '-creation_date',
},
),
migrations.AddField(
model_name='subscription',
name='targets',
field=models.ManyToManyField(help_text='Discord channels where subscription content is delivered to.', to='home.subscriptiontarget', verbose_name='targets'),
),
migrations.DeleteModel(
name='SubscriptionChannel',
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 5.0.1 on 2024-03-11 17:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0002_subscriptiontarget_subscription_targets_and_more'),
]
operations = [
migrations.RemoveField(
model_name='subscription',
name='targets',
),
migrations.DeleteModel(
name='SubscriptionTarget',
),
migrations.AddField(
model_name='subscription',
name='targets',
field=models.CharField(default='', help_text='Discord channels where subscription content is delivered to.', max_length=500, verbose_name='targets'),
preserve_default=False,
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-03-11 18:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0003_remove_subscription_targets_and_more'),
]
operations = [
migrations.AddField(
model_name='subscription',
name='extra_notes',
field=models.CharField(blank=True, help_text='Additional user written notes about this item.', max_length=250, null=True, verbose_name='extra notes'),
),
]

View File

@ -39,6 +39,33 @@ class IconPathGenerator:
return Path(instance.__class__.__name__.lower()) / str(instance.uuid) / "icon.webp"
# class SubscriptionTarget(models.Model):
# """
# Represents a Discord TextChannel that should be subject to the content of a Subscription.
# """
# id = models.PositiveBigIntegerField(
# verbose_name=_("id"),
# primary_key=True,
# help_text=_("Discord Channel ID")
# )
# creation_datetime = models.DateTimeField(
# verbose_name=_("creation datetime"),
# help_text=_("when this instance was created."),
# default=timezone.now,
# editable=False
# )
# class Meta:
# verbose_name = "subscription target"
# verbose_name_plural = "subscription targets"
# get_latest_by = "-creation_date"
# def __str__(self):
# return str(self.id)
class Subscription(models.Model):
"""
Represents a stored RSS Feed.
@ -80,6 +107,12 @@ class Subscription(models.Model):
help_text=_("Identifier for the discord server that owns this subscription.")
)
targets = models.CharField(
verbose_name=_("targets"),
max_length=500,
help_text=_("Discord channels where subscription content is delivered to.")
)
creation_datetime = models.DateTimeField(
verbose_name=_("creation datetime"),
help_text=_("when this instance was created."),
@ -87,6 +120,14 @@ class Subscription(models.Model):
editable=False
)
extra_notes = models.CharField(
verbose_name=_("extra notes"),
max_length=250,
null=True,
blank=True,
help_text=_("Additional user written notes about this item.")
)
class Meta:
verbose_name = "subscription"
verbose_name_plural = "subscriptions"
@ -104,13 +145,13 @@ class Subscription(models.Model):
log.debug("%sSubscription Saved %s", new_text, self.uuid)
super().save(*args, **kwargs)
@property
def channels(self):
"""
Returns all SubscriptionChannel objects linked to this Subscription.
"""
# @property
# def channels(self):
# """
# Returns all SubscriptionChannel objects linked to this Subscription.
# """
return SubscriptionChannel.objects.filter(subscription=self)
# return SubscriptionChannel.objects.filter(subscription=self)
# def save(self, *args, **kwargs):
# if Subscription.objects.filter(server=self.server).count() >= 1:
@ -119,55 +160,55 @@ class Subscription(models.Model):
# super().save(*args, **kwargs)
class SubscriptionChannel(models.Model):
"""
Represents a Discord TextChannel that should be subject to the content of a Subscription.
"""
# class SubscriptionChannel(models.Model):
# """
# Represents a Discord TextChannel that should be subject to the content of a Subscription.
# """
uuid = models.UUIDField(
primary_key=True,
default=uuid4,
editable=False
)
# uuid = models.UUIDField(
# primary_key=True,
# default=uuid4,
# editable=False
# )
# The ID is not auto generated, but rather be obtained from discord.
id = models.PositiveBigIntegerField(
verbose_name=_("id"),
help_text=_("Identifier of the channel, provided by Discord.")
)
# # The ID is not auto generated, but rather be obtained from discord.
# id = models.PositiveBigIntegerField(
# verbose_name=_("id"),
# help_text=_("Identifier of the channel, provided by Discord.")
# )
subscription = models.ForeignKey(
verbose_name=_("subscription"),
help_text=_("The subscription of this instance."),
to=Subscription,
on_delete=models.CASCADE
)
# subscription = models.ForeignKey(
# verbose_name=_("subscription"),
# help_text=_("The subscription of this instance."),
# to=Subscription,
# on_delete=models.CASCADE
# )
creation_datetime = models.DateTimeField(
verbose_name=_("creation datetime"),
help_text=_("when this instance was created."),
default=timezone.now,
editable=False
)
# creation_datetime = models.DateTimeField(
# verbose_name=_("creation datetime"),
# help_text=_("when this instance was created."),
# default=timezone.now,
# editable=False
# )
class Meta:
verbose_name = "subscription channel"
verbose_name_plural = "subscription channels"
get_latest_by = "-creation_date"
constraints = [
models.UniqueConstraint(fields=["id", "subscription"], name="unique id & sub pair")
]
# class Meta:
# verbose_name = "subscription channel"
# verbose_name_plural = "subscription channels"
# get_latest_by = "-creation_date"
# constraints = [
# models.UniqueConstraint(fields=["id", "subscription"], name="unique id & sub pair")
# ]
def __str__(self):
return str(self.id)
# def __str__(self):
# return str(self.id)
def save(self, *args, **kwargs):
if SubscriptionChannel.objects.filter(subscription=self.subscription).count() >= 4:
raise IntegrityError(
f"SubscriptionChannel limit reached for subscription '{self.subscription}'"
)
# def save(self, *args, **kwargs):
# if SubscriptionChannel.objects.filter(subscription=self.subscription).count() >= 4:
# raise IntegrityError(
# f"SubscriptionChannel limit reached for subscription '{self.subscription}'"
# )
super().save(*args, **kwargs)
# super().save(*args, **kwargs)
class TrackedContent(models.Model):

View File

@ -99,15 +99,15 @@
<!-- ### $App Screen Content ### -->
<main class="main-content bg-body-tertiary">
<div id="mainContent">
<!--
<div class="row gap-20 masonry pos-r">
<div class="masonry-sizer col-md-6"></div>
<div class="masonry-item col-12">
<div class="row gap-20">
<div class="row gap-20"> -->
<!-- Total Subscriptions -->
<div class="col-md-3">
<!-- <div class="col-md-3">
<div class="layers bd bg-body p-20">
<div class="layer w-100 mB-10">
<h6 class="lh-1">Total Subscriptions</h6>
@ -125,10 +125,10 @@
</div>
</div>
</div>
</div>
</div> -->
<!-- Subscription Activity (24hrs) -->
<div class="col-md-3">
<!-- <div class="col-md-3">
<div class="layers bd bg-body p-20">
<div class="layer w-100 mB-10">
<h6 class="lh-1">Subscription Activity (24hrs)</h6>
@ -144,15 +144,15 @@
</div>
</div>
</div>
</div>
</div> -->
</div>
<!-- </div>
</div>
<div class="masonry-item col-12">
<hr>
</div>
</div>
</div> -->
<div class="container-fluid mT-10 pX-0">
<div class="lh-1 pX-5 pB-20 peers">
<div class="peer peer-greed">
@ -164,7 +164,6 @@
New Subscription
</button>
</div>
<!-- bd -->
</div>
<div id="subscriptionContainer" class="row gap-20">
@ -211,16 +210,22 @@
</div>
</div>
<div id="navChannelPanel" class="tab-pane fade" role="tabpanel" aria-labelledby="navChannelTab" tabindex="0">
<div>
<div class="mb-3">
<label for="editSubServer" class="form-label">Server</label>
<select name="editSubServer" id="editSubServer" class="form-select bdrs-2 bd" required>
<option value="">Select a server</option>
</select>
<div class="form-help">Ensure that PYRSS Bot is a member of the selected server.</div>
<div class="invalid-feedback">Please Select a Server.</div>
</div>
<div>
<label for="editSubChannels">Channels</label>
<select name="editSubChannels" id="editSubChannels" class="form-select bdrs-2 bd" required multiple>
<option value="1039201459188805692">CodeHub</option>
<option value="753323563381031042">Orange</option>
<option value="1204426362794811453">PYRSS Home</option>
<option value="410208534861447168">Ryujinx</option>
</select>
<div class="invalid-feedback">Please Select a Server.</div>
</div>
</div>
<div id="navExtrasPanel" class="tab-pane fade" role="tabpanel" aria-labelledby="navExtrasTab" tabindex="0">
@ -252,38 +257,78 @@
{% block javascripts %}
<script id="subItemTemplate" type="text/template">
<div class="col-md-4 col-xxl-3">
<div class="sub-item layers bd bg-body p-20 h-100" data-uuid="">
<div class="layer w-100 mb-3">
<h6 class="lh-1 sub-name mb-0"></h6>
<div class="sub-item layers bd bg-body h-100 rounded-3" data-uuid="">
<!-- <div class="layer w-100 bdB pb-4">
<div class="d-flex px-4">
<span class="h5 lh-1 sub-name mb-0"></span>
<img class="sub-img h-100">
</div>
</div>
<div class="layer w-100 mb-3">
<!-- <div class="lh-1 fw-bold">UUID</div> -->
<div class="layer w-100 py-4 px-4">
<p class="sub-desc mb-0"></p>
</div>
<div class="layer w-100 mb-3 px-4">
<span class=sub-uuid></span>
</div>
<div class="layer w-100 mb-3">
<!-- <h6 class="lh-1">RSS URL</h6> -->
<div class="layer w-100 mb-3 px-4">
<a class="sub-rss"></a>
</div>
<div class="layer w-100 mb-3">
<div class="layer w-100 mb-3 px-4">
<img class="sub-img mw-100" height="75">
</div>
<div class="layer d-flex flex-wrap w-100 mt-auto">
<!-- <div class="me-auto">
<button class="btn btn-sm bg-body-tertiary bdrs-2 mT-10 mR-5 cur-p draw-border draw-border-primary">
<i class="bi bi-link-45deg me-1"></i>
Channel Links
</button>
</div> -->
<div class="ms-auto">
<button class="sub-edit btn btn-sm bg-body-tertiary bdrs-2 mT-10 cur-p draw-border draw-border-secondary">
<div class="layer d-flex flex-wrap w-100 mt-auto bdT pb-3 px-4">
<div class="me-auto d-flex ai-c">
<div class="form-check">
<input type="checkbox" class="form-check-input mt-3" style="transform:scale(1.25);">
</div>
</div>
<div>
<button class="sub-edit btn btn-sm bg-body-tertiary bdrs-2 mt-3 cur-p draw-border draw-border-secondary">
<i class="bi bi-pencil me-1`"></i>
Edit
</button>
<button class="sub-delete btn btn-sm bg-body-tertiary bdrs-2 mT-10 cur-p draw-border draw-border-danger">
<button class="sub-delete btn btn-sm bg-body-tertiary bdrs-2 mt-3 cur-p draw-border draw-border-danger">
<i class="bi bi-trash me-1`"></i>
Delete
</button>
</div>
</div> -->
<div class="layer w-100">
<div class="peers p-4 flex-nowrap">
<div class="peer peer-greed me-4">
<h5 class="sub-name"></h5>
<span class="sub-uuid"></span>
</div>
<div class="peer">
<img src="" alt="" class="sub-img">
</div>
</div>
</div>
<div class="w-50 align-self-start bd mx-4"></div>
<div class="layer w-100 p-4">
<p class="sub-desc mb-4"></p>
<a class="sub-rss"></a>
</div>
<div class="layer w-100 bdT p-4 mt-auto">
<div class="peers ai-c">
<div class="peer peer-greed">
<div class="form-check form-switch ms-2">
<input type="checkbox" name="" id="" class="form-check-input" style="transform: scale(1.5);">
</div>
</div>
<div class="peer">
<button class="sub-edit btn bg-body-tertiary rounded-3">
<i class="bi bi-pencil me-1"></i>
Edit
</button>
</div>
<div class="peer ms-3">
<button class="sub-delete btn bg-body-tertiary rounded-3">
<i class="bi bi-trash3 me-1"></i>
Delete
</button>
</div>
</div>
</div>
</div>
</div>
@ -295,17 +340,77 @@
template.find(".sub-item").attr("data-uuid", data.uuid);
template.find(".sub-name").text(data.name);
template.find(".sub-uuid").text(data.uuid);
template.find(".sub-rss").text(data.rss_url);
template.find(".sub-rss").text(data.rss_url).attr("href", data.rss_url);
template.find(".sub-img").attr("src", data.image);
template.find(".sub-desc").text(data.extra_notes);
template.find(".sub-edit").attr("onclick", `subEditModal("${data.uuid}");`);
template.find(".sub-delete").attr("onclick", `unsubscribe("${data.uuid}");`);
return template
}
$(document).ready(function() {
loadGuilds();
loadSubscriptions();
$("#editSubServer").change(function() {
loadChannels($(this).find("option:selected").attr("value"));
});
});
function loadGuilds() {
$.ajax({
url: "/guilds",
type: "GET",
success: function(response) {
for (i = 1; i < response.length; i++) {
var guild = response[i];
$("#editSubServer").append($("<option>", {
value: guild.id,
text: guild.name
}));
}
},
error: function(response) {
alert(JSON.stringify(response, null, 4));
}
});
}
function loadChannels(guildID) {
$("#editSubChannels").empty();
$.ajax({
url: `/channels?guild=${guildID}`,
type: "GET",
success: function(response) {
for (i = 1; i < response.length; i++) {
var channel = response[i];
if (channel.type !== 0) {
continue
}
var selectedChannelIDs = $("#editSubChannels").attr("data-current").split(";");
$("#editSubChannels").append($("<option>", {
value: channel.id,
text: "#" + channel.name,
selected: selectedChannelIDs.includes(channel.id.toString())
}));
}
},
error: function(response) {
console.error(JSON.stringify(response, null, 4));
if (response.code == "50001") {
alert("PYRSS Bot is Missing Access to this Server");
}
else {
alert("unknown error fetching channels " + response.code)
}
}
});
}
function updateSubscriptionCount(difference, overwrite) {
const beforeChange = overwrite ? 0 : Number($(".subs-count").text());
$(".subs-count").text(beforeChange + difference);
@ -340,7 +445,9 @@
var modal = $("#subEditModal");
modal.find("input").val(null);
modal.find("textarea").val(null);
modal.find("select").val("");
$("#editSubChannels").empty();
$("#subEditForm").removeClass("was-validated");
$("#navDetailsTab").click();
@ -355,6 +462,8 @@
$("#editSubName").val(resp.name);
$("#editSubURL").val(resp.rss_url);
$("#editSubServer").val(String(resp.server)).trigger("change");
$("#editSubChannels").attr("data-current", resp.targets);
$("#editSubNotes").val(resp.extra_notes);
});
}
@ -378,7 +487,11 @@
formData.append("name", $("#editSubName").val());
formData.append("rss_url", $("#editSubURL").val());
formData.append("server", $("#editSubServer").val());
formData.append("extra_notes", $("#editSubNotes").val());
var selectedTargets = $("#editSubChannels option:selected").toArray().map(item => item.value).join(';');
formData.append("targets", selectedTargets);
var imageFile = $("#editSubImage")[0].files[0];
if (imageFile) {
formData.append("image", imageFile);

View File

@ -108,10 +108,10 @@
<li class="dropdown">
<a href="" class="dropdown-toggle no-after peers fxw-nw ai-c lh-1" data-bs-toggle="dropdown">
<div class="peer mR-10">
<img class="w-2r h-2r bdrs-50p" src="{{ request.user.icon.url }}" style="object-fit: cover;" alt="">
<img class="w-2r h-2r bdrs-50p" src="{{ request.user.avatar_url }}" style="object-fit: cover;" alt="">
</div>
<div class="peer">
<span class="fsz-sm">{{ request.user.formal_fullname }}</span>
<span class="fsz-sm">{{ request.user }}</span>
</div>
</a>
<ul class="dropdown-menu fsz-sm">

View File

@ -126,6 +126,7 @@ AUTHENTICATION_BACKENDS = [
# Discord Related Settings
BOT_TOKEN = env("BOT_TOKEN")
DISCORD_KEY = env("DISCORD_KEY")
DISCORD_SECRET = env("DISCORD_SECRET")
DISCORD_SCOPES = ["identify", "guilds"]