536 lines
19 KiB
JavaScript
536 lines
19 KiB
JavaScript
var subTable = null;
|
|
|
|
// Create subscription table
|
|
function initSubscriptionTable() {
|
|
subTable = $("#subTable").DataTable({
|
|
info: false,
|
|
paging: 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: '<input type="checkbox" class="form-check-input table-select-all" />',
|
|
data: null,
|
|
orderable: false,
|
|
className: "text-center",
|
|
render: function() {
|
|
return '<input type="checkbox" class="form-check-input table-select-row" />'
|
|
}
|
|
},
|
|
{ title: "ID", data: "id", visible: false },
|
|
{
|
|
title: "Name",
|
|
data: "name",
|
|
render: function(data, type, row) {
|
|
return `<a href="#" onclick="showEditSubModal(${row.id})" class="text-decoration-none">${data}</a>`
|
|
}
|
|
},
|
|
{
|
|
title: "URL",
|
|
data: "url",
|
|
render: function(data, type) {
|
|
return `<a href="${data}" class="text-decoration-none" target="_blank">${data}</a>`
|
|
}
|
|
},
|
|
{ title: "Channels", data: "channels_count" },
|
|
// {
|
|
// title: "Content",
|
|
// data: "channels_count",
|
|
// render: function(data, type) {
|
|
|
|
// }
|
|
// },
|
|
{
|
|
title: "Created",
|
|
data: "creation_datetime",
|
|
render: function(data, type) {
|
|
return new Date(data).toISOString().split("T")[0];
|
|
}
|
|
},
|
|
{
|
|
title: "Notes",
|
|
data: "extra_notes",
|
|
orderable: false,
|
|
className: "text-center",
|
|
render: function(data, type) {
|
|
if (!data) return "-";
|
|
return $(`<i class="bi bi-chat-left-text" data-bs-trigger="hover focus" data-bs-toggle="popover" data-bs-title="Extra Notes" data-bs-content="${data}"></i>`).popover()[0];
|
|
}
|
|
},
|
|
{
|
|
title: "Active",
|
|
data: "active",
|
|
orderable: false,
|
|
className: "text-center form-switch",
|
|
render: function(data, type) {
|
|
return `<input type="checkbox" class="sub-toggle-active form-check-input ms-0" ${data ? "checked" : ""} />`
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
subTable.on('page.dt length.dt', function() {
|
|
loadSubscriptions(getCurrentlyActiveServer().guild_id);
|
|
});
|
|
}
|
|
|
|
$("#subTable").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 subPrimaryKey = await saveSubscription(
|
|
sub.id,
|
|
sub.name,
|
|
sub.url,
|
|
sub.guild_id,
|
|
sub.extra_notes,
|
|
sub.filters,
|
|
active,
|
|
handleErrorMsg=false
|
|
);
|
|
if (!subPrimaryKey)
|
|
throw Error("This subscription no longer exists.");
|
|
|
|
showToast(
|
|
active ? "success" : "danger",
|
|
"Subscription " + (active ? "Activated" : "Deactivated"),
|
|
"Subscription ID: " + subPrimaryKey
|
|
);
|
|
}
|
|
catch (error) {
|
|
console.error(error);
|
|
showToast(
|
|
"danger",
|
|
"Error Updating Subscription",
|
|
`Tried to toggle activeness, but encountered a problem. <br><code>${error}</code>`
|
|
);
|
|
}
|
|
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 input, #subFormModal textarea").val("");
|
|
$("#subFormModal .form-create").show();
|
|
$("#subFormModal .form-edit").hide();
|
|
$("#subChannels").val("").change();
|
|
$("#subFilters").val("").change();
|
|
$("#subActive").prop("checked", true);
|
|
$("#subImagePreview img").attr("src", "").hide();
|
|
$("#subImagePreview small").show();
|
|
}
|
|
else {
|
|
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);
|
|
$("#subFormModal .form-create").hide();
|
|
$("#subFormModal .form-edit").show();
|
|
|
|
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();
|
|
}
|
|
|
|
$("#subId").val(subId);
|
|
$("#subFormModal").modal("show");
|
|
}
|
|
|
|
$("#subForm").on("submit", async function(event) {
|
|
event.preventDefault();
|
|
|
|
var id = $("#subId").val();
|
|
name = $("#subName").val();
|
|
url = $("#subUrl").val();
|
|
guildId = getCurrentlyActiveServer().guild_id;
|
|
extraNotes = $("#subExtraNotes").val();
|
|
subChannels = $("#subChannels option:selected").toArray().map(channel => channel.value);
|
|
subFilters = $("#subFilters option:selected").toArray().map(filter => parseInt(filter.value));
|
|
active = $("#subActive").prop("checked");
|
|
|
|
// alert(JSON.stringify(subFilters, null, 4));
|
|
|
|
var subPrimaryKey = await saveSubscription(id, name, url, guildId, extraNotes, subFilters, active);
|
|
|
|
await deleteSubChannels(subPrimaryKey);
|
|
subChannels.forEach(async channelId => {
|
|
await saveSubChannel(channelId, subPrimaryKey);
|
|
});
|
|
|
|
if (subPrimaryKey)
|
|
showToast("success", "Subscription Saved", "Subscription ID: " + subPrimaryKey);
|
|
await loadSubscriptions(guildId);
|
|
|
|
$("#subFormModal").modal("hide");
|
|
});
|
|
|
|
async function saveSubscription(id, name, url, guildId, extraNotes, filters, active, handleErrorMsg=true) {
|
|
var formData = new FormData();
|
|
formData.append("name", name);
|
|
formData.append("url", url);
|
|
formData.append("guild_id", guildId);
|
|
formData.append("extra_notes", extraNotes);
|
|
filters.forEach(filter => formData.append("filters", filter));
|
|
formData.append("active", active);
|
|
|
|
var response;
|
|
|
|
try {
|
|
if (id === "-1") response = await newSubscription(formData);
|
|
else response = await editSubscription(id, formData);
|
|
}
|
|
catch (err) {
|
|
if (handleErrorMsg)
|
|
showToast("danger", "Subscription Error", err.responseText, 18000);
|
|
|
|
return false;
|
|
}
|
|
|
|
return response.id;
|
|
}
|
|
|
|
async function saveSubChannel(channelId, subscriptionId) {
|
|
var formData = new FormData();
|
|
formData.append("channel_id", channelId);
|
|
formData.append("subscription", subscriptionId);
|
|
|
|
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);
|
|
}
|
|
|
|
$("#refreshSubscriptionBtn").on("click", async function() {
|
|
loadSubscriptions(getCurrentlyActiveServer().guild_id);
|
|
});
|
|
|
|
async function loadSubscriptions(guildId, page=1, pageSize=null) {
|
|
|
|
if (!guildId)
|
|
return;
|
|
|
|
if (!pageSize)
|
|
pageSize = $("#subTablePageSize").val();
|
|
|
|
$("#deleteSelectedSubscriptionsBtn").prop("disabled", true);
|
|
clearExistingSubRows();
|
|
|
|
try {
|
|
const subscriptions = await getSubscriptions(guildId, page, pageSize);
|
|
subTable.rows.add(subscriptions.results).draw(false);
|
|
handleSubPagination(page, pageSize, subscriptions.count, subscriptions.next, subscriptions.previous);
|
|
$("#subTable thead .table-select-all").prop("disabled", subscriptions.results.length === 0);
|
|
console.debug("loading subs, " + subscriptions.results.length + " found");
|
|
}
|
|
catch (err) {
|
|
console.error(JSON.stringify(err, null, 4));
|
|
showToast("danger", `Error Loading Subscriptions: HTTP ${err.status}`, err.responseJSON.message, 15000);
|
|
}
|
|
}
|
|
|
|
$("#subTablePageSize").on("change", async function() {
|
|
const page = 1; // reset to page 1 to ensure the page exists.
|
|
const pageSize = $(this).val();
|
|
|
|
loadSubscriptions(getCurrentlyActiveServer().guild_id, page, pageSize);
|
|
});
|
|
|
|
// Update the UI pagination options according to the passed data.
|
|
function handleSubPagination(currentPage, pageSize, totalItems, nextExists, prevExists) {
|
|
|
|
$("#subPagination").attr("data-page", currentPage);
|
|
|
|
// Remove existing page-specific buttons
|
|
$("#subPagination .page-pick").remove();
|
|
|
|
// Determine states of 'previous page' and 'next page' buttons
|
|
$("#subPagination .page-prev").toggleClass("disabled", !prevExists).attr("tabindex", prevExists ? "" : "-1");
|
|
$("#subPagination .page-next").toggleClass("disabled", !nextExists).attr("tabindex", nextExists ? "" : "-1");
|
|
|
|
// Calculate amount of pages to account for
|
|
const pages = Math.max(Math.ceil(totalItems / pageSize), 1);
|
|
|
|
// Create a button for each page
|
|
for (let i = 1; i < pages + 1; i++) {
|
|
let pageItem = $("<li>").addClass("page-item");
|
|
let pageLink = $("<button>")
|
|
.attr("type", "button")
|
|
.attr("data-page", i)
|
|
.addClass("page-link page-pick")
|
|
.text(i)
|
|
|
|
pageItem.append(pageLink);
|
|
|
|
// Insert the new page button before the 'next page' button
|
|
$("#subPagination .pagination .page-next").parent().before(pageItem);
|
|
}
|
|
|
|
// Disable the button for the current page
|
|
$(`#subPagination .pagination .page-pick[data-page="${currentPage}"]`).addClass("disabled").attr("tabindex", -1);
|
|
}
|
|
|
|
$("#subPagination .pagination").on("click", ".page-link", async function() {
|
|
|
|
let wantedPage;
|
|
let currentPage = parseInt($("#subPagination").attr("data-page"));
|
|
|
|
if ($(this).hasClass("page-prev"))
|
|
wantedPage = currentPage - 1
|
|
else if ($(this).hasClass("page-next"))
|
|
wantedPage = currentPage + 1;
|
|
else
|
|
wantedPage = $(this).attr("data-page");
|
|
|
|
console.debug("moving to page: " + wantedPage);
|
|
|
|
await loadSubscriptions(getCurrentlyActiveServer().guild_id, wantedPage)
|
|
});
|
|
|
|
$(document).on("selectedServerChange", async function() {
|
|
|
|
// Hide alerts
|
|
$("#serverJoinAlert").attr("style", "display: none !important");
|
|
|
|
const activeServer = getCurrentlyActiveServer();
|
|
await loadSubscriptions(activeServer.guild_id);
|
|
await loadChannelOptions(activeServer.guild_id);
|
|
await loadFilterOptions(activeServer.guild_id);
|
|
})
|
|
|
|
// Dev button (to be removed)
|
|
// auto fills dummy data into form fields to quickly create test subs
|
|
$("#devGenerateSub").on("click", function() {
|
|
const items = [
|
|
["BBC News · Top Stories", "http://feeds.bbci.co.uk/news/rss.xml"],
|
|
["BBC News · World", "http://feeds.bbci.co.uk/news/world/rss.xml"],
|
|
["BBC News · Entertainment", "http://feeds.bbci.co.uk/news/entertainment_and_arts/rss.xml"],
|
|
["BBC News · UK", "http://feeds.bbci.co.uk/news/uk/rss.xml"],
|
|
["BBC News · Business", "http://feeds.bbci.co.uk/news/business/rss.xml"],
|
|
["BBC News · Politics", "http://feeds.bbci.co.uk/news/politics/rss.xml"],
|
|
["BBC News · Health", "http://feeds.bbci.co.uk/news/health/rss.xml"],
|
|
["BBC News · Science", "http://feeds.bbci.co.uk/news/science_and_environment/rss.xml"],
|
|
["BBC News · Technology", "http://feeds.bbci.co.uk/news/technology/rss.xml"],
|
|
["BBC News · Education", "http://feeds.bbci.co.uk/news/education/rss.xml"],
|
|
["BBC News · England", "http://feeds.bbci.co.uk/news/england/rss.xml"],
|
|
["BBC News · Scotland", "http://feeds.bbci.co.uk/news/scotland/rss.xml"],
|
|
["BBC News · Wales", "http://feeds.bbci.co.uk/news/wales/rss.xml"],
|
|
["BBC News · Northern Ireland", "http://feeds.bbci.co.uk/news/northern_ireland/rss.xml"],
|
|
["BBC News · Europe", "http://feeds.bbci.co.uk/news/europe/rss.xml"],
|
|
["BBC News · Latin America", "http://feeds.bbci.co.uk/news/latin_america/rss.xml"],
|
|
["BBC News · US & Canada", "http://feeds.bbci.co.uk/news/us_and_canada/rss.xml"],
|
|
["BBC News · Middle East", "http://feeds.bbci.co.uk/news/middle_east/rss.xml"],
|
|
["BBC News · Asia", "http://feeds.bbci.co.uk/news/asia/rss.xml"],
|
|
["BBC News · Africa", "http://feeds.bbci.co.uk/news/africa/rss.xml"],
|
|
["BBC Sports", "https://news.bbc.co.uk/sport1/hi/help/rss/default.stm"],
|
|
["This Week in Self-Hosted", "https://selfh.st/rss/"],
|
|
["The Babylon Bee", "https://babylonbee.com/feed"],
|
|
["Reddit · r/memes", "https://reddit.com/r/memes/.rss"],
|
|
["Reddit · r/selfhosted", "https://reddit.com/r/selfhosted/.rss"],
|
|
["Sky News", "http://feeds.skynews.com/feeds/rss/home.xml"],
|
|
["Sky News · UK", "http://feeds.skynews.com/feeds/rss/uk.xml"],
|
|
["Sky News · Entertainment", "http://feeds.skynews.com/feeds/rss/entertainment.xml"],
|
|
["Sky News · Strange News", "http://feeds.skynews.com/feeds/rss/strange.xml"]
|
|
];
|
|
|
|
const selected = items[Math.floor(Math.random()*items.length)]
|
|
|
|
$("#subName").val(selected[0]);
|
|
$("#subUrl").val(selected[1]);
|
|
|
|
showToast("info", "Generated", "Picked random sub data");
|
|
});
|
|
|
|
// Delete button on the 'edit subscription' modal
|
|
$("#deleteEditSub").on("click", async function() {
|
|
const subId = $("#subId").val();
|
|
await deleteSubscription(subId);
|
|
await loadSubscriptions(getCurrentlyActiveServer().guild_id);
|
|
$("#subFormModal").modal("hide");
|
|
showToast("danger", "Deleted Subscription", "Subscription ID: " + subId);
|
|
});
|
|
|
|
$("#deleteSelectedSubscriptionsBtn").on("click", async function() {
|
|
// showToast("danger", "Not Implemented", "This feature isn't implemented");
|
|
|
|
var rows = subTable.rows(".selected").data();
|
|
$.each(rows, async function() {
|
|
// alert(JSON.stringify(this, null, 4));
|
|
await deleteSubscription(this.id);
|
|
showToast("danger", "Deleted Subscription", "Subscription ID: " + this.id);
|
|
});
|
|
|
|
await loadSubscriptions(getCurrentlyActiveServer().guild_id);
|
|
})
|
|
|
|
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);
|
|
console.debug(JSON.stringify(channels));
|
|
|
|
// 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)
|
|
showServerJoinAlert();
|
|
|
|
const guildName = getServerFromSnowflake(guildId).name;
|
|
|
|
throw new Error(
|
|
`Unable to retrieve channels from Guild <b>${guildName}</b>.
|
|
Ensure that @PYRSS is a member with permissions
|
|
to view channels.`
|
|
);
|
|
}
|
|
|
|
channels.forEach(channel => {
|
|
|
|
// We only want TextChannels, which have a type of 0
|
|
if (channel.type !== 0)
|
|
return;
|
|
|
|
$("#subChannels").append($("<option>", {
|
|
text: "#" + channel.name,
|
|
value: channel.id
|
|
}));
|
|
});
|
|
}
|
|
catch(error) {
|
|
console.error(error);
|
|
showToast("danger", "Error loading channels", error, 18000);
|
|
}
|
|
finally {
|
|
// Re-enable the input
|
|
$("#subChannels").prop("disabled", false);
|
|
}
|
|
}
|
|
|
|
async function loadFilterOptions(guildId) {
|
|
|
|
// Disable input while options are loading
|
|
$("#subFilters").prop("disabled", true);
|
|
|
|
// Delete existing options
|
|
$("#subFilters option").each(function() {
|
|
if ($(this).val())
|
|
$(this).remove();
|
|
});
|
|
|
|
// Clear select2 input
|
|
$("#subFilters").val("").change();
|
|
|
|
try {
|
|
const filters = await getFilters(guildId);
|
|
console.log(JSON.stringify(filters));
|
|
|
|
filters.results.forEach(filter => {
|
|
$("#subFilters").append($("<option>", {
|
|
text: filter.name,
|
|
value: filter.id
|
|
}));
|
|
});
|
|
}
|
|
catch(error) {
|
|
console.error(error);
|
|
showToast("danger", "Error loading sub filters", error, 18000);
|
|
}
|
|
finally {
|
|
// Re-enable the input
|
|
$("#subFilters").prop("disabled", false);
|
|
}
|
|
|
|
}
|
|
|
|
function showServerJoinAlert() {
|
|
const guildId = getCurrentlyActiveServer().guild_id;
|
|
const inviteUrl = `https://discord.com/oauth2/authorize
|
|
?client_id=1129345991758336020
|
|
&permissions=2147534848&scope=bot+applications.commands
|
|
&guild_id=${guildId}
|
|
&disable_guild_select=true`
|
|
|
|
$("#serverJoinAlert a.alert-link").attr("href", inviteUrl);
|
|
$("#serverJoinAlert").show();
|
|
}
|
|
|
|
$("#subImage").on("change", function () {
|
|
const [file] = $("#subImage")[0].files;
|
|
if (file) {
|
|
$("#subImagePreview small").hide();
|
|
$("#subImagePreview img").attr("src", URL.createObjectURL(file)).show();
|
|
}
|
|
}); |