rewrite generating and loading servers

This commit is contained in:
Corban-Lee Jones 2024-09-24 14:07:47 +01:00
parent 9bcb99dd30
commit edf047f148
11 changed files with 225 additions and 401 deletions

View File

@ -2,22 +2,19 @@
from rest_framework.permissions import BasePermission
from apps.home.models import r_Server
from apps.authentication.models import ServerMember
class UserHasDiscordPermissions(BasePermission):
class HasServerAccess(BasePermission):
"""
Permission to ensure that the user is permitted to make
changes on behalf of the server they are representing.
An object permission class, the object must have a 'server' attribute.
"""
message = "You lack administrator access to this server"
def has_object_permission(self, request, view, obj):
if not hasattr(obj, "server"):
raise Exception(f"obj '{obj}' must have attr 'server'")
# class SubscriptionServerMember(BasePermission):
# """
# Permission for each subscription that omits the sub if
# the request user isn't a member of it's server.
# """
# def has_object_permission(self, request, view, obj):
# return obj.server in request.user.servers
return ServerMember.objects.filter(user=request.user, server=obj.server).exists()

View File

@ -259,14 +259,7 @@ class TrackedContentSerializer_POST(DynamicModelSerializer):
fields = ("id", "guid", "title", "url", "subscription", "channel_id", "message_id", "blocked", "creation_datetime")
# rewrite
class DiscordServerIdSerializer(serializers.Serializer):
server_id = serializers.IntegerField()
#region rewrite
class r_ServerSerializer(DynamicModelSerializer):
class Meta:

View File

@ -23,7 +23,6 @@ from .views import (
UniqueContentRule_DetailView,
#rewrite
CreateDiscordServerView,
r_Server_ListView,
r_Server_DetailView,
r_ContentFilter_ListView,
@ -90,8 +89,6 @@ urlpatterns = [
#region rewrite
path("discord-servers/", CreateDiscordServerView.as_view()),
path("r_servers/", include([
path("", r_Server_ListView.as_view()),
path("<int:pk>/", r_Server_DetailView.as_view())

View File

@ -47,7 +47,6 @@ from .serializers import (
UniqueContentRuleSerializer,
#rewrite
DiscordServerIdSerializer,
r_ServerSerializer,
r_ContentFilterSerializer,
r_MessageMutatorSerializer,
@ -56,6 +55,7 @@ from .serializers import (
r_ContentSerializer,
r_UniqueContentRuleSerializer
)
from .permissions import HasServerAccess
from .errors import NotAMemberError
log = logging.getLogger(__name__)
@ -712,74 +712,7 @@ class DeletableDetailView(generics.RetrieveDestroyAPIView):
parser_classes = [MultiPartParser, FormParser]
class CreateDiscordServerView(generics.CreateAPIView):
serializer_class = DiscordServerIdSerializer
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
if not serializer.is_valid():
return Response()
server_id = serializer.validated_data["server_id"]
response = requests.get(
url=f"{settings.DISCORD_API_URL}/guilds/{server_id}",
headers={"Authorization": f"Bot {settings.BOT_TOKEN}"}
)
raw = response.json()
if not response.status_code == 200:
return Response(
status=response.status_code,
data=raw
)
server = r_Server.objects.filter(id=server_id)
if server.exists():
return self.create_member_for_server(server.first(), request.user)
else:
return self.create_server(raw, request.user)
def create_member_for_server(self, server: r_Server, user: DiscordUser) -> Response:
response = requests.get(
url=f"{settings.DISCORD_API_URL}/users/@me/guilds/{server.id}/member", # TODO: continue here
headers={"Authorization": f"Bearer {user.access_token}"} # the scope of the token doesnt cover membership, so
) # this needs to be updated against the bot from the
raw = response.json() # discord developers dashboard.
if response.status_code != 200:
if raw.get("code") == 0:
log.warning("Failed to get member data, does the oauth url contain the correct permissions?")
return Response(
status=response.status_code,
data=raw
)
# TODO: might need to a case where this member already exists
ServerMember.objects.get_or_create(
server=server,
user=user,
nick=raw.get("nick"),
permissions=raw["permissions"]
)
return Response(
status=200,
data={"message": "success"}
)
def create_server(self, raw: dict, user: DiscordUser) -> Response:
server = r_Server.objects.create(
id=raw["id"],
name=raw["name"],
icon_hash=raw["icon"],
owner_id=raw["owner_id"]
)
return self.create_member_for_server(server, user)
class r_Server_ListView(ListCreateView): # maybe change to ListView only later, and create through secure backend means?
class r_Server_ListView(ListView):
filterset_fields = []
search_fields = []
ordering_fields = []
@ -789,7 +722,7 @@ class r_Server_ListView(ListCreateView): # maybe change to ListView only later,
return r_Server.objects.all()
class r_Server_DetailView(ChangableDetailView): # maybe change to ListView only later, and create through secure backend means?
class r_Server_DetailView(DetailView):
serializer_class = r_ServerSerializer
def get_queryset(self):

View File

@ -75,34 +75,79 @@ class GuildSettingsAdmin(admin.ModelAdmin):
@admin.register(r_Server)
class r_ServerAdmin(admin.ModelAdmin):
pass
list_display = ["id", "name", "icon_hash", "active"]
@admin.register(r_ContentFilter)
class r_ContentFilterAdmin(admin.ModelAdmin):
pass
list_display = [
"id",
"server",
"name",
"match",
"matching_algorithm",
"is_insensitive",
"is_whitelist"
]
@admin.register(r_MessageMutator)
class r_MessageMutatorAdmin(admin.ModelAdmin):
pass
list_display = [
"id",
"name",
"value"
]
@admin.register(r_MessageStyle)
class r_MessageStyleAdmin(admin.ModelAdmin):
pass
list_display = [
"id",
"server",
"is_embed",
"is_hyperlinked",
"show_author",
"show_timestamp",
"show_images",
"fetch_images",
"title_mutator",
"description_mutator"
]
@admin.register(r_Subscription)
class r_Subscription(admin.ModelAdmin):
pass
list_display = [
"id",
"server",
"name",
"url",
"created_at",
"updated_at",
"extra_notes",
"active",
"message_style"
]
@admin.register(r_Content)
class r_ContentAdmin(admin.ModelAdmin):
pass
list_display = [
"id",
"subscription",
"item_id",
"item_guid",
"item_url",
"item_title",
"item_content_hash"
]
@admin.register(r_UniqueContentRule)
class r_UniqueContentRule(admin.ModelAdmin):
pass
list_display = [
"id",
"name",
"value"
]

View File

@ -7,6 +7,5 @@ from .views import IndexView, GuildsView
urlpatterns = [
path("", login_required(IndexView.as_view()), name="index"),
path("user-guilds", GuildsView.as_view(), name="user-guilds")
path("generate-servers/", GuildsView.as_view(), name="generate-servers")
]

View File

@ -183,3 +183,13 @@ async function getUniqueContentRule(ruleId) {
return await ajaxRequest(`/api/unique-content-rule/${ruleId}/`, "GET");
}
//#region rewrite
async function generateServers() {
return ajaxRequest("/generate-servers/", "GET");
}
async function getServers() {
return ajaxRequest("/api/r_servers/", "GET");
}

View File

@ -5,8 +5,7 @@ $(document).ready(async function() {
$("#subscriptionsTab").click();
await loadSavedGuilds();
await loadServerOptions();
await loadServers();
});
$(document).on("selectedServerChange", function() {
@ -189,4 +188,25 @@ function arrayToHtmlList(array, bold=false) {
});
return $ul;
}
function logError(error) {
if (error instanceof Error) {
// Logs typical error properties like message and stack
console.error({
message: error.message,
stack: error.stack,
name: error.name,
});
} else if (typeof error === 'object' && error !== null) {
// Try to stringify if it's an object
try {
console.error(JSON.stringify(error, null, 2));
} catch (stringifyError) {
console.error('Could not stringify the error:', error);
}
} else {
// Fallback for any other types (string, number, etc.)
console.error('Error:', error);
}
}

View File

@ -1,263 +1,178 @@
// #region Loaded Servers
var loadedServers = {};
var _loadedServers = []
var selectedServer = null;
// Returns the currently active server, or null if none are active.
function getCurrentlyActiveServer() {
const activeServerAndId = Object.entries(loadedServers).find(([id, server]) => server.currentlyActive);
if (activeServerAndId === undefined)
return null;
function getLoadedServer(options) {
let servers = _loadedServers.filter(item => {
var [id, activeServer] = activeServerAndId;
activeServer.id = id;
return activeServer;
}
// Returns the requested server from the provided snowflake id
function getServerFromSnowflake(guildId) {
const serverAndId = Object.entries(loadedServers).find(([id, server]) => server.guild_id == guildId);
if (serverAndId === undefined)
return null;
var [id, server] = serverAndId;
server.id = id;
return server;
}
function addToLoadedServers(server, selectNew=true) {
// Remove the 'id' property and add the 'currentlyActive' property
({id, ...rest} = server, server = {...rest, currentlyActive: false})
// Save the server as loaded
loadedServers[id] = server;
// Display the loaded server
addServerTemplate(id, sanitise(server.guild_id), sanitise(server.name), sanitise(server.icon), sanitise(server.permissions), sanitise(server.owner));
// Select the newly added server
if (selectNew) {
selectServer(id);
}
}
function removeFromLoadedServers(serverPrimaryKey) {
delete loadedServers[serverPrimaryKey];
removeServerTemplate(serverPrimaryKey);
$("#backToSelectServer").click();
}
// #endregion
// #region Server Back Btn
$("#backToSelectServer").on("click", function() {
$("#noSelectedServer").show();
$("#selectedServerContainer").hide();
});
// #endregion
// #region Server Modal
$("#serverOptionsRefreshBtn").on("click", async function() {
await loadServerOptions();
});
// Load server options into the 'Add Server' dropdown
async function loadServerOptions() {
// Disable controls while loading
$("#serverOptions").prop("disabled", true);
$("#serverOptionsRefreshBtn").prop("disabled", true).find("i.bi").addClass("spinning-360");
// Remove existing options
$("#serverOptions option").each(function() {
if ($(this).val()) {
$(this).remove();
for (let key in options) {
if (item[key] !== options[key]) {
return false
}
}
return true;
});
// Deselect any selected option
$("#serverOptions").val(null).trigger("change");
return servers || [];
}
// Fetch and append the server options
try {
const servers = await loadGuilds();
servers.forEach(server => {
$("#serverOptions").append($("<option>", {
value: server.id,
text: sanitise(server.name),
"data-icon": sanitise(server.icon),
"data-permissions": sanitise(server.permissions),
"data-isowner": sanitise(server.owner)
}));
});
}
catch (error) {
console.error(JSON.stringify(error, null, 4));
showToast("danger", `Error Loading Guilds: HTTP ${error.status}`, error.responseJSON.message, 15000);
}
finally {
// Re-enable controls
$("#serverOptions").prop("disabled", false);
$("#serverOptionsRefreshBtn").prop("disabled", false).find("i.bi").removeClass("spinning-360");
function getServerFromSnowflake(id) {
server = getLoadedServer({id: id});
if (!server.length) {
throw new Error("No Server with that ID");
}
return server[0];
}
function addToLoadedServers(serverData, autoSelect=false) {
_loadedServers.push(serverData);
createSelectButton(serverData);
if (autoSelect) {
selectServer(serverData["id"]);
}
}
function removeFromLoadedServers(id) {
_loadedServers = _loadedServers.filter(item => item.id !== id);
removeSelectButton(id)
if (selectedServer.id === id) {
selectedServer(null);
}
}
// #endregion
// #region Server Sidebar
// #region UI Buttons
// Load any existing 'saved guilds' from the database
async function loadSavedGuilds() {
try {
const response = await getSavedGuilds(currentUserId);
function createSelectButton(serverData) {
// server details
let id = serverData["id"];
let name = serverData["name"];
let iconHash = serverData["icon"];
response.forEach(server => {
// 'Register' the server, by storing it for later and
// displaying it on the server list sidebar
addToLoadedServers(server, false);
});
}
catch (error) {
alert("Error loading saved guilds: " + error);
}
}
// Create an element for the added server and show it
function addServerTemplate(serverPrimaryKey, serverGuildId, serverName, serverIconHash, serverPermissions, serverIsOwner) {
let template = $($("#serverItemTemplate").html());
let imageUrl = `https://cdn.discordapp.com/icons/${serverGuildId}/${serverIconHash}.webp?size=80`;
let imageUrl = `https://cdn.discordapp.com/icons/${id}/${iconHash}.webp?size=80`;
let altText = name.split(' ').map(word => word.charAt(0)).join(''); // initials of server name, used if iconUrl is 404
template.find("img").attr("src", imageUrl);
template.find(".js-guildId").text(serverGuildId);
template.find(".js-guildName").text(serverName);
template.attr("data-id", serverPrimaryKey);
template.attr("data-guild-id", serverGuildId);
template.find("img").attr("src", imageUrl).attr("alt", altText);
template.find(".js-guildName").text(name);
template.find(".js-guildId").text(id);
template.attr("data-id", id);
// Bind the button for selecting this server
template.find(".server-item-selector").off("click").on("click", function() {
$(".server-item-selector").removeClass("active");
$(this).addClass("active");
selectServer(serverPrimaryKey);
selectServer(id);
});
$("#serverList").prepend(template);
}
function removeServerTemplate(serverPrimaryKey) {
$(`#serverList .server-item[data-id=${serverPrimaryKey}]`).remove();
function removeSelectButton(id) {
$(`#serverList .server-item[data-id=${id}]`).remove();
}
// Open 'Add Server' Form Modal
$("#newServerBtn").on("click", function() {
newServerModal();
$("#backToSelectServer").on("click", function() {
$("#noSelectedServer").show();
$("#selectedServerContainer").hide();
selectServer = null;
});
function newServerModal() {
$("#serverFormModal").modal("show");
}
// #endregion
// #region New Server
// #region Server Selection
// Submit 'Add Server' Form
$("#serverForm").on("submit", async function(event) {
event.preventDefault();
var selectedOption = $("#serverOptions option:selected");
serverName = selectedOption.text();
serverGuildId = selectedOption.val();
serverIconHash = selectedOption.attr("data-icon");
serverPermissions = selectedOption.attr("data-permissions");
serverIsOwner = selectedOption.attr("data-isowner");
var serverPrimaryKey = await registerNewServer(serverName, serverGuildId, serverIconHash, serverPermissions, serverIsOwner);
if (serverPrimaryKey)
addToLoadedServers(await getSavedGuild(serverPrimaryKey));
$("#serverFormModal").modal("hide");
});
// Add a new 'saved guild' based on the info provided
// returns `response.id` if successful, else false
async function registerNewServer(serverName, serverGuildId, serverIconHash, serverPermissions, serverIsOwner) {
var formData = new FormData();
formData.append("name", serverName);
formData.append("guild_id", serverGuildId);
formData.append("icon", serverIconHash);
formData.append("added_by", currentUserId);
formData.append("permissions", serverPermissions);
formData.append("owner", serverIsOwner === "true");
try { response = await newSavedGuild(formData); }
catch (err) {
if (err.status === 409)
showToast("warning", "Server Conflict", `Can't add ${sanitise(serverName)} because it already exists.`, 10000);
else
console.error(JSON.stringify(err, null, 4));
return false;
function selectServer(id) {
let server = getServerFromSnowflake(id);
if (!server) {
$("#noSelectedServer").show();
$("#selectedServerContainer").hide();
selectServer = null;
return;
}
debugger
return response.id;
}
// #endregion
// #region Select Server
function selectServer(primaryKey) {
var server = loadedServers[primaryKey];
// Change appearance of selected vs none-selected items
$("#serverList .server-item").removeClass("active")
$(`#serverList .server-item[data-id=${primaryKey}]`).addClass("active")
$("#serverList .server-item").removeClass("active");
$(`#serverList .server-item[data-id=${id}]`).addClass("active");
// Display details of the selected server
$("#selectedServerContainer .selected-server-name").text(sanitise(server.name));
$("#selectedServerContainer .selected-server-id").text(sanitise(server.guild_id));
$("#selectedServerContainer .selected-server-icon").attr("src", `https://cdn.discordapp.com/icons/${server.guild_id}/${server.icon}.webp?size=80`);
// Disable all loaded servers
$.each(loadedServers, function(serverPrimaryKey, server) {
server.currentlyActive = false;
});
// Activate current selected server
loadedServers[primaryKey].currentlyActive = true;
// Global variable
selectedServer = server;
// Update UI
$("#noSelectedServer").hide();
$("#selectedServerContainer").show().css("display", "flex");
// Announce change to any listeners
$(document).trigger("selectedServerChange");
}
// #endregion
// #region Delete Server Btn
// #region Resolve Strings
function resolveServerStrings() {
// Server icon
$(".resolve-to-server-icon").attr(
"src",
`https://cdn.discordapp.com/icons/${selectedServer.id}/${selectedServer.icon}.webp?size=80`
);
// Server names
$(".resolve-to-server-name").text(selectedServer.name);
// Server Guild Ids
$(".resolve-to-server-id").text(selectedServer.id);
// Bot Invite links
$(".resolve-to-invite-link").attr("href", `https://discord.com/oauth2/authorize
?client_id=${discordClientId}
&permissions=2147534848
&scope=bot+applications.commands
&guild_id=${selectedServer.id}
&disable_guild_select=true`);
}
// #endregion
// #region Change Listener
$(document).on("selectedServerChange", function() {
resolveServerStrings();
$("#serverJoinAlert").hide();
});
// #endregion
// #region Load Servers
async function loadServers() {
try {
let response = await generateServers();
response.forEach(server => {
addToLoadedServers(server, false);
});
}
catch (error) {
logError(error);
}
}
// #endregion
// #region Server Deletion
$("#deleteSelectedServerBtn").on("click", async function() {
const notes = [
"No Subscriptions, Filters or Tracked Content will be deleted.",
"No data will be deleted for other users.",
"The server will no longer appear on your sidebar.",
"You can re-add the server",
"All Subscriptions, Filters and Tracked Content will be available when/if you re-add the server."
];
const notesString = arrayToHtmlList(notes).prop("outerHTML");
await confirmationModal(
"Close this server?",
`This is a safe, non-permanent action:<br><br>${notesString}`,
"Delete Server Data?",
"All related items will be erased, are you sure? (Only the owner can confirm)",
"warning",
deleteSelectedServer,
null
@ -265,51 +180,7 @@ $("#deleteSelectedServerBtn").on("click", async function() {
});
async function deleteSelectedServer() {
var activeServer = getCurrentlyActiveServer();
if (!activeServer) {
showToast("danger", "Error Deleting Server", "You must select a server to delete.");
return;
}
console.debug(`Deleting ${activeServer.id}: ${JSON.stringify(activeServer, null, 4)}`)
try {
await deleteSavedGuild(activeServer.id);
removeFromLoadedServers(activeServer.id);
}
catch (error) {
alert(error)
alert(JSON.stringify(error, null, 4))
}
};
// #endregion
$(document).on("selectedServerChange", function() {
resolveServerStrings();
$("#serverJoinAlert").hide();
})
// #region Resolve Strings
function resolveServerStrings() {
const server = getCurrentlyActiveServer();
// Server names
$(".resolve-to-server-name").text(sanitise(server.name));
// Server Guild Ids
$(".resolve-to-server-id").text(sanitise(server.guild_id))
// Bot Invite links
$(".resolve-to-invite-link").attr("href", `https://discord.com/oauth2/authorize
?client_id=${discordClientId}
&permissions=2147534848
&scope=bot+applications.commands
&guild_id=${sanitise(server.guild_id)}
&disable_guild_select=true`);
alert("not implemented");
}
// #endregion
// #endregion

View File

@ -1,34 +0,0 @@
<div id="serverFormModal" class="modal fade" data-bs-backdrop="static" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content rounded-1">
<form id="serverForm" class="mb-0" novalidate>
<div class="modal-header">
<h5 class="modal-title ms-2">
Add Server
</h5>
</div>
<div class="modal-body p-4">
<div class="d-flex flex-nowrap mb-3">
<div class="flex-fill">
<select name="serverOptions" id="serverOptions" class="select-2 rounded-1" data-dropdownparent="#serverFormModal">
<option value="">-- Select a Server --</option>
</select>
</div>
<button type="button" id="serverOptionsRefreshBtn" class="btn btn-secondary rounded-1 ms-3">
<i class="bi bi-arrow-clockwise d-block"></i>
</button>
</div>
<p class="mb-0 form-text">
<b>Not seeing your server?</b>
Ensure that you are authenticated as either the owner or an administrator of the server you wish to add.
</p>
</div>
<div class="modal-footer px-4">
<button type="submit" class="btn btn-primary rounded-1 me-0">Submit</button>
<button type="button" class="btn btn-secondary rounded-1 ms-3" data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -14,12 +14,6 @@
<div class="d-flex flex-nowrap h-100">
<div class="server-sidebar d-flex flex-column bg-body-tertiary py-3 border-end">
<ul id="serverList" class="nav nav-pills nav-flush flex-column mb-auto text-center px-lg-2 px-1 flex-nowrap overflow-y-auto">
<li class="nav-item">
<button type="button" id="newServerBtn" class="btn btn-outline-primary rounded-1 mt-2 w-100 d-flex justify-content-center align-items-center">
<i class="bi bi-plus-lg fs-5 d-lg-none d-inline"></i>
<span class="small d-lg-inline d-none">Add Server</span>
</button>
</li>
</ul>
</div>
<div class="flex-grow-1 container-fluid bg-body overflow-y-auto" style="min-width: 0;">
@ -45,7 +39,7 @@
<div class="col-12 bg-body-tertiary border-bottom p-3 py-sm-4">
<div class="row">
<div class="col-sm-7 col-md-6 d-flex align-items-center">
<img alt="Server Icon" class="rounded-3 selected-server-icon d-none d-sm-block">
<img alt="Server Icon" class="resolve-to-server-icon rounded-3 d-none d-sm-block">
<div class="ms-sm-3" style="min-width: 0">
<h3 class="mb-0 resolve-to-server-name text-truncate"></h3>
<h5 class="mb-0 resolve-to-server-id text-truncate text-body-secondary text-monospace"></h5>
@ -112,7 +106,6 @@
</div>
</div>
</div>
{% include "home/includes/servermodal.html" %}
{% include "home/includes/submodal.html" %}
{% include "home/includes/filtermodal.html" %}
{% include "home/includes/deletemodal.html" %}