var subTable = null; discordChannels = []; subSearchTimeout = null; subOptions = null; // Create subscription table async function initSubscriptionTable() { subOptions = await getSubscriptionOptions(); await initTable("#subscriptionsTabPane", "subTable", loadSubscriptions, showEditSubModal, deleteSelectedSubscriptions, subOptions); subTable = $("#subTable").DataTable({ info: false, paging: false, ordering: false, searching: false, autoWidth: false, order: [], select: { style: "multi+shift", selector: 'th:first-child input[type="checkbox"]' }, columnDefs: [ { orderable: false, targets: "no-sort" }, { targets: 0, checkboxes: { selectRow: true } }, ], columns: [ { // Select row checkbox column title: '', data: null, orderable: false, className: "text-center col-switch-width", render: function() { return '' } }, { title: "ID", data: "id", visible: false }, { title: "Name", data: "name", className: "text-truncate", render: function(data, type, row) { const name = sanitise(data); return ``; } }, { title: "URL", data: "url", className: "text-truncate", render: function(data, type) { const url = sanitise(data); return `${url}`; } }, { title: "Channels", data: "channels_count", className: "text-center", render: function(data) { const channelsCount = sanitise(data); return `${channelsCount}`; } }, { title: "Created", data: "creation_datetime", render: function(data, type) { let dateTime = new Date(data); return $(` %H:%M:%S")}"> ${formatStringDate(dateTime, "%D, %b %Y")} `).popover()[0]; } }, { title: "Notes", data: "extra_notes", orderable: false, className: "text-center", render: function(data, type) { if (!data) { return "" } const extraNotes = sanitise(data); return $(` `).popover()[0]; } }, { title: "Active", data: "active", orderable: false, className: "text-center form-switch", render: function(data, type) { return `` } }, { orderable: false, className: "p-0", render: function(data, type, row) { const embedColour = sanitise(row.embed_colour); return `
 
` } } ] }); bindTableCheckboxes("#subTable", subTable, "#subscriptionsTabPane .table-del-btn"); } async function updateSubFromObject(sub, handleErrorMsg=true) { let data = { "name": sub.name, "url": sub.url, "guild_id": sub.guild_id, "extra_notes": sub.extra_notes, "embed_colour": sub.embed_colour, "article_fetch_image": sub.article_fetch_image, "published_threshold": sub.published_threshold, "active": sub.active }; let formData = new FormData(); for (key in data) { formData.append(key, data[key]); } sub.article_title_mutators.forEach(mutator => formData.append("article_title_mutators", mutator.id)); sub.article_desc_mutators.forEach(mutator => formData.append("article_desc_mutators", mutator.id)); sub.filters.forEach(filter => formData.append("filters", filter)); return await saveSubscription(sub.id, formData, handleErrorMsg=handleErrorMsg); } $("#subscriptionsTabPane").on("change", ".sub-toggle-active", async function () { /* Lock all toggles to soft-prevent spam. There is a rate limit, but allowing the user to reach it from this toggle would be bad. */ $(".sub-toggle-active").prop("disabled", true); try { const active = $(this).prop("checked"); const sub = subTable.row($(this).closest("tr")).data(); // Update the table row sub.active = active; subTable.data(sub).draw(); // Update the database const subId = await updateSubFromObject(sub, handleErrorMsg=false); if (!subId) { throw Error("This subscription no longer exists."); } showToast( active ? "success" : "danger", "Subscription " + (active ? "Activated" : "Deactivated"), "Subscription ID: " + subId ); } catch (error) { console.error(error); showToast( "danger", "Error Updating Subscription", `Tried to toggle activeness, but encountered a problem.
${error}` ); } finally { // Re-enable toggles after 500ms setTimeout(() => { $(".sub-toggle-active").prop("disabled", false); }, 500 ); } }); // Open new subscription modal $("#addSubscriptionBtn").on("click", async function() { await showEditSubModal(-1); }); async function showEditSubModal(subId) { if (subId === -1) { $("#subFormModal .form-create, #subAdvancedModal .form-create").show(); $("#subFormModal .form-edit, #subAdvancedModal .form-edit").hide(); $("#subFormModal input, #subFormModal textarea").val(""); $("#subChannels").val("").change(); $("#subFilters").val("").change(); $("#subTitleMutators").val("").change(); $("#subDescMutators").val("").change(); $("#subActive").prop("checked", true); $("#subEmbedColour .colour-reset").click(); $("#subArticleFetchImage").prop("checked", true); $("#subPubThreshold").val(getCurrentDateTime()); } else { $("#subFormModal .form-create, #subAdvancedModal .form-create").hide(); $("#subFormModal .form-edit, #subAdvancedModal .form-edit").show(); const subscription = subTable.row(function(idx, data, node) { return data.id === subId; }).data(); $("#subName").val(subscription.name); $("#subUrl").val(subscription.url); $("#subExtraNotes").val(subscription.extra_notes); $("#subActive").prop("checked", subscription.active); $("#subTitleMutators").val("").change(); $("#subTitleMutators").val(subscription.article_title_mutators.map(mutator => mutator.id)).change(); $("#subDescMutators").val("").change(); $("#subDescMutators").val(subscription.article_desc_mutators.map(mutator => mutator.id)).change(); const channels = await getSubChannels(subscription.id); $("#subChannels").val("").change(); $("#subChannels").val(channels.results.map(channel => channel.channel_id)).change(); $("#subFilters").val("").change(); $("#subFilters").val(subscription.filters).change(); updateColourInput("subEmbedColour", `#${subscription.embed_colour}`); $("#subArticleFetchImage").prop("checked", subscription.article_fetch_image); $("#subPubThreshold").val(subscription.published_threshold.split('+')[0]); } $("#subId").val(subId); $("#subFormModal").modal("show"); } function getValueFromField(elem) { const tagName = elem.tagName.toLowerCase(); const $elem = $(elem); if (tagName) { return $elem.val() } switch ($elem.attr("type")) { case "checkbox": return $elem.prop("checked"); default: return $elem.val(); } } $("#subForm").on("submit", async function(event) { event.preventDefault(); let subId = $("#subId").val(); let guildId = getCurrentlyActiveServer().guild_id; // TODO: move this into a function, so I can fix the active toggle switches which are broken due to this change let formData = new FormData(); formData.append("guild_id", guildId); // Populate formdata with [data-field] control values $('#subForm [data-field], #subAdvancedModal [data-field]').each(function() { const value = getValueFromField(this); formData.append($(this).data("field"), value); }); // Add title mutators to formdata $("#subTitleMutators option:selected").toArray().map(mutator => parseInt(mutator.value)).forEach( mutator => formData.append("article_title_mutators", mutator) ); // Add description mutator to formdata $("#subDescMutators option:selected").toArray().map(mutator => parseInt(mutator.value)).forEach( mutator => formData.append("article_desc_mutators", mutator) ); // Add Filters to formdata $("#subFilters option:selected").toArray().forEach( filter => formData.append("filters", parseInt(filter.value)) ); // This field is constructed differently, so needs to be specifically added formData.append("embed_colour", getColourInputVal("subEmbedColour", false)); subId = await saveSubscription(subId, formData); if (subId) { showToast("success", "Subscription Saved", `Subscription ID ${subId}`); } else { showToast("danger", "Error Saving Subscription", ""); return; } await deleteSubChannels(subId); $("#subChannels option:selected").each(async function() { let $channel = $(this); let channelFormData = new FormData(); channelFormData.append("channel_id", $channel.val()); channelFormData.append("channel_name", $channel.data("name")); channelFormData.append("subscription", subId); await newSubChannel(channelFormData); }); await loadSubscriptions(guildId); $("#subFormModal").modal("hide"); }); async function saveSubscription(id, formData, handleErrorMsg=true) { let response try { response = id === "-1" ? await newSubscription(formData) : await editSubscription(id, formData); } catch (err) { console.error(err); if (handleErrorMsg) { showToast("danger", "Subscription Error", err.responseText, 18000); } return false; } return response.id; } async function saveSubChannel(formData) { var response try { response = await newSubChannel(formData); } catch (error) { console.log(error); showToast("danger", "Failed to save subchannel", error, 18000); return false } return response.id } function clearExistingSubRows() { $("#subTable thead .table-select-all").prop("checked", false).prop("indeterminate", false); subTable.clear().draw(false); } $("#subscriptionsTabPane").on("click", ".table-refresh-btn", async function() { loadSubscriptions(getCurrentlyActiveServer().guild_id); }); async function loadSubscriptions(guildId) { if (!guildId) return; setTableFilter("subTable", "guild_id", guildId); ensureTablePagination("subTable"); $("#subscriptionsTabPane .table-del-btn").prop("disabled", true); clearExistingSubRows(); try { var subs = await getSubscriptions(tableFilters["subTable"], tableSorts["subTable"]); subTable.rows.add(subs.results).draw(false); } catch (err) { console.error(err) showToast("danger", `Error Loading Subscriptions: HTTP ${err.status}`, err, 15000); return; } updateTableContainer( "subscriptionsTabPane", tableFilters["subTable"]["page"], tableFilters["subTable"]["page_size"], subs.results.length, subs.count, subs.next, subs.previous ); $("#subTable thead .table-select-all").prop("disabled", subs.results.length === 0); console.debug("loading subs, " + subs.results.length + " found"); } // #region Server Change Event Handler $(document).on("selectedServerChange", async function() { let server = getCurrentlyActiveServer(); guildId = server.guild_id; await updateDefaultSubEmbedColour(); await loadSubscriptions(guildId); await loadChannelOptions(guildId); await loadFilterOptions(guildId); await loadMutatorOptions(); }) async function updateDefaultSubEmbedColour(settings=null) { if (!settings){ settings = (await getGuildSettings(guildId)).results[0] } $("#subEmbedColour .colour-reset").attr("data-defaultcolour", "#" + settings.default_embed_colour); } // #endregion // #region Delete Subscriptions // Delete button on the 'edit subscription' modal $("#deleteEditSub").on("click", async function() { const subId = parseInt($("#subId").val()); const sub = subTable.row(function(idx, row) { return row.id === subId }).data(); const subName = sanitise(sub.name); $("#subFormModal").modal("hide"); await confirmationModal( "Delete a Subscription", `Do you wish to permanently delete ${subName}?`, "danger", async () => { await deleteSubscription(subId); await loadSubscriptions(getCurrentlyActiveServer().guild_id); showToast( "danger", "Deleted a Subscription", subName, 12000 ); }, async () => { $("#subFormModal").modal("show"); } ); }); async function deleteSelectedSubscriptions() { const rows = subTable.rows(".selected").data().toArray(); const names = rows.map(row => row.name); const namesString = arrayToHtmlList(names, true).prop("outerHTML"); const isMany = names.length > 1; await confirmationModal( `Delete ${isMany ? "Many Subscriptions" : "a Subscription"}`, `Do you wish to permanently delete ${isMany ? "these" : "this"} ${names.length} subscription${isMany ? "s" : ""}?

${namesString}`, "danger", async () => { rows.forEach(async row => { await deleteSubscription(row.id) }); showToast( "danger", `Deleted ${names.length} Subscription${isMany ? "s" : ""}`, `${arrayToHtmlList(names, false).prop("outerHTML")}`, 12000 ); // Multi-deletion can take time, this timeout ensures the refresh is accurate setTimeout(async () => { await loadSubscriptions(getCurrentlyActiveServer().guild_id); }, 600); }, null ); } // #endregion // #region Load Modal Options async function loadChannelOptions(guildId) { // Disable input while options are loading $("#subChannels").prop("disabled", true); // Delete existing options $("#subChannels option").each(function() { if ($(this).val()) $(this).remove(); }); // Clear select2 input $("#subChannels").val("").change(); try { const channels = await loadChannels(guildId); // If we have reached the discord API rate limit if (channels.message && channels.message.includes("rate limit")) { throw new Error( `${channels.message} Retry after ${channels.retry_after} seconds.` ) } // If we can't fetch channels due to error if (channels.code === 50001) { // Also check that the user hasn't changed the currently active guild, otherwise // the alert will show under the wrong server. if (getCurrentlyActiveServer().guild_id === guildId) $("#serverJoinAlert").show(); const guildName = sanitise(getServerFromSnowflake(guildId).name); throw new Error( `Unable to retrieve channels from Guild ${guildName}. Ensure that @PYRSS is a member with permissions to view channels.` ); } // Sort by the specified position of each channel object channels.sort((a, b) => a.position - b.position); discordChannels = []; channels.forEach(channel => { // We only want TextChannels, which have a type of 0 if (channel.type !== 0) return; let channelObj = {text: `#${channel.name}`, value: channel.id, "data-name": channel.name} $("#subChannels").append($("