diff --git a/public/language/en-GB/admin/menu.json b/public/language/en-GB/admin/menu.json index d55f62045e..8074988d6e 100644 --- a/public/language/en-GB/admin/menu.json +++ b/public/language/en-GB/admin/menu.json @@ -55,6 +55,7 @@ "federation/relays": "Relays", "federation/pruning": "Storage", "federation/safety": "Trust & Safety", + "federation/analytics": "Analytics", "section-appearance": "Appearance", "appearance/themes": "Themes", diff --git a/public/language/en-GB/admin/settings/activitypub.json b/public/language/en-GB/admin/settings/activitypub.json index 6c65323ce5..901ea81c8b 100644 --- a/public/language/en-GB/admin/settings/activitypub.json +++ b/public/language/en-GB/admin/settings/activitypub.json @@ -63,5 +63,8 @@ "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", - "content.world-default-cid": "Default category ID for "World" page composer" + "content.world-default-cid": "Default category ID for "World" page composer", + + "analytics.intro": "From this page you can view the state of your instance's federation with other servers", + "analytics.activities": "Received Activities" } \ No newline at end of file diff --git a/public/src/admin/federation/analytics.js b/public/src/admin/federation/analytics.js new file mode 100644 index 0000000000..dc6c4ca0b3 --- /dev/null +++ b/public/src/admin/federation/analytics.js @@ -0,0 +1,158 @@ +import { + Chart, + LineController, + CategoryScale, + LinearScale, + LineElement, + PointElement, + Tooltip, + Filler, +} from 'chart.js'; + +import { get } from 'api'; + +Chart.register( + LineController, + CategoryScale, + LinearScale, + LineElement, + PointElement, + Tooltip, + Filler +); + +export function init() { + const charts = initializeCharts(); + + const hostFilterEl = document.getElementById('hostFilter'); + if (hostFilterEl) { + hostFilterEl.addEventListener('change', async function () { + const { activities } = await get(`/api${ajaxify.data.url}?host=${this.value}`); + const chart = charts.get('activities'); + chart.data.datasets[0].data = activities; + chart.update(); + }); + } +} + +function initializeCharts() { + const activitiesCanvas = document.getElementById('activities'); + // const dailyCanvas = document.getElementById('pageviews:daily'); + // const topicsCanvas = document.getElementById('topics:daily'); + // const postsCanvas = document.getElementById('posts:daily'); + const hourlyLabels = utils.getHoursArray().map(function (text, idx) { + return idx % 3 ? '' : text; + }); + // const dailyLabels = utils.getDaysArray().map(function (text, idx) { + // return idx % 3 ? '' : text; + // }); + + if (utils.isMobile()) { + Chart.defaults.plugins.tooltip.enabled = false; + } + + const commonDataSetOpts = { + label: '', + fill: true, + tension: 0.25, + pointHoverBackgroundColor: '#fff', + pointBorderColor: '#fff', + }; + + const data = { + 'activities': { + labels: hourlyLabels, + datasets: [ + { + ...commonDataSetOpts, + backgroundColor: 'rgba(186,139,175,0.2)', + borderColor: 'rgba(186,139,175,1)', + pointBackgroundColor: 'rgba(186,139,175,1)', + pointHoverBorderColor: 'rgba(186,139,175,1)', + data: ajaxify.data.activities, + }, + ], + }, + // 'pageviews:daily': { + // labels: dailyLabels, + // datasets: [ + // { + // ...commonDataSetOpts, + // backgroundColor: 'rgba(151,187,205,0.2)', + // borderColor: 'rgba(151,187,205,1)', + // pointBackgroundColor: 'rgba(151,187,205,1)', + // pointHoverBorderColor: 'rgba(151,187,205,1)', + // data: ajaxify.data.analytics['pageviews:daily'], + // }, + // ], + // }, + // 'topics:daily': { + // labels: dailyLabels.slice(-7), + // datasets: [ + // { + // ...commonDataSetOpts, + // backgroundColor: 'rgba(171,70,66,0.2)', + // borderColor: 'rgba(171,70,66,1)', + // pointBackgroundColor: 'rgba(171,70,66,1)', + // pointHoverBorderColor: 'rgba(171,70,66,1)', + // data: ajaxify.data.analytics['topics:daily'], + // }, + // ], + // }, + // 'posts:daily': { + // labels: dailyLabels.slice(-7), + // datasets: [ + // { + // ...commonDataSetOpts, + // backgroundColor: 'rgba(161,181,108,0.2)', + // borderColor: 'rgba(161,181,108,1)', + // pointBackgroundColor: 'rgba(161,181,108,1)', + // pointHoverBorderColor: 'rgba(161,181,108,1)', + // data: ajaxify.data.analytics['posts:daily'], + // }, + // ], + // }, + }; + + activitiesCanvas.width = $(activitiesCanvas).parent().width(); + // dailyCanvas.width = $(dailyCanvas).parent().width(); + // topicsCanvas.width = $(topicsCanvas).parent().width(); + // postsCanvas.width = $(postsCanvas).parent().width(); + + const chartOpts = { + responsive: true, + animation: false, + scales: { + y: { + beginAtZero: true, + }, + }, + }; + + return new Map([ + ['activities', new Chart(activitiesCanvas.getContext('2d'), { + type: 'line', + data: data.activities, + options: chartOpts, + })], + ]); + + // new Chart(dailyCanvas.getContext('2d'), { + // type: 'line', + // data: data['pageviews:daily'], + // options: chartOpts, + // }); + + // new Chart(topicsCanvas.getContext('2d'), { + // type: 'line', + // data: data['topics:daily'], + // options: chartOpts, + // }); + + // new Chart(postsCanvas.getContext('2d'), { + // type: 'line', + // data: data['posts:daily'], + // options: chartOpts, + // }); +} + diff --git a/src/activitypub/instances.js b/src/activitypub/instances.js index 4302c17f48..07135c9dfc 100644 --- a/src/activitypub/instances.js +++ b/src/activitypub/instances.js @@ -12,6 +12,8 @@ Instances.log = async (domain) => { Instances.getCount = async () => db.sortedSetCard('instances:lastSeen'); +Instances.list = async () => db.getSortedSetMembers('instances:lastSeen'); + Instances.isAllowed = async (domain) => { const allowed = await activitypub.blocklists.check(domain); let { activitypubFilter: type, activitypubFilterList: list } = meta.config; diff --git a/src/controllers/admin/federation.js b/src/controllers/admin/federation.js index d089d6d509..d18ef06176 100644 --- a/src/controllers/admin/federation.js +++ b/src/controllers/admin/federation.js @@ -1,6 +1,7 @@ 'use strict'; const activitypub = require('../../activitypub'); +const analytics = require('../../analytics'); const federationController = module.exports; @@ -52,3 +53,19 @@ federationController.safety = async function (req, res) { instanceCount, }); }; + +federationController.analytics = async function (req, res) { + const instances = await activitypub.instances.list(); + let { host } = req.query; + if (!instances.includes(host)) { + host = undefined; + } + const set = host ? `activities:byHost:${host}` : 'activities'; + const activities = await analytics.getHourlyStatsForSet(set, Date.now(), 24); + + res.render('admin/federation/analytics', { + title: '[[admin/menu:federation/analytics]]', + instances, + activities, + }); +}; diff --git a/src/routes/admin.js b/src/routes/admin.js index 768e457409..de6e493d7d 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -57,6 +57,7 @@ module.exports = function (app, name, middleware, controllers) { helpers.setupAdminPageRoute(app, `/${name}/federation/relays`, middlewares, controllers.admin.federation.relays); helpers.setupAdminPageRoute(app, `/${name}/federation/pruning`, middlewares, controllers.admin.federation.pruning); helpers.setupAdminPageRoute(app, `/${name}/federation/safety`, middlewares, controllers.admin.federation.safety); + helpers.setupAdminPageRoute(app, `/${name}/federation/analytics`, middlewares, controllers.admin.federation.analytics); helpers.setupAdminPageRoute(app, `/${name}/appearance/themes`, middlewares, controllers.admin.appearance.themes); helpers.setupAdminPageRoute(app, `/${name}/appearance/skins`, middlewares, controllers.admin.appearance.skins); diff --git a/src/views/admin/federation/analytics.tpl b/src/views/admin/federation/analytics.tpl new file mode 100644 index 0000000000..5271e48b5a --- /dev/null +++ b/src/views/admin/federation/analytics.tpl @@ -0,0 +1,33 @@ +
+ + +
+
+
+

[[admin/settings/activitypub:analytics.intro]]

+
+ +
+ +
+ +
+ +
+
+
+ +
+
+
+
+
+ + +
+
diff --git a/src/views/admin/partials/navigation.tpl b/src/views/admin/partials/navigation.tpl index 14ceedcbae..728c86d293 100644 --- a/src/views/admin/partials/navigation.tpl +++ b/src/views/admin/partials/navigation.tpl @@ -109,6 +109,7 @@ [[admin/menu:federation/relays]] [[admin/menu:federation/pruning]] [[admin/menu:federation/safety]] + [[admin/menu:federation/analytics]]