3 * @copyright Copyright (C) 2010-2022, the Friendica project
5 * @license GNU AGPL version 3 or any later version
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 namespace Friendica\Module\Admin;
24 use Friendica\Core\Protocol;
25 use Friendica\Core\Renderer;
26 use Friendica\Database\DBA;
28 use Friendica\Model\GServer;
29 use Friendica\Module\BaseAdmin;
31 class Federation extends BaseAdmin
33 protected function content(array $request = []): string
37 // get counts on active federation systems this node is knowing
38 // We list the more common systems by name. The rest is counted as "other"
40 'friendica' => ['name' => 'Friendica', 'color' => '#ffc018'], // orange from the logo
41 'birdsitelive' => ['name' => 'BirdsiteLIVE', 'color' => '#1b6ec2'], // Color from the page
42 'bookwyrm' => ['name' => 'BookWyrm', 'color' => '#00d1b2'], // Color from the page
43 'diaspora' => ['name' => 'Diaspora', 'color' => '#a1a1a1'], // logo is black and white, makes a gray
44 'funkwhale' => ['name' => 'Funkwhale', 'color' => '#4082B4'], // From the homepage
45 'gnusocial' => ['name' => 'GNU Social/Statusnet', 'color' => '#a22430'], // dark red from the logo
46 'gotosocial' => ['name' => 'GoToSocial', 'color' => '#df8958'], // Some color from their mascot
47 'hometown' => ['name' => 'Hometown', 'color' => '#1f70c1'], // Color from the Patreon page
48 'hubzilla' => ['name' => 'Hubzilla/Red Matrix', 'color' => '#43488a'], // blue from the logo
49 'lemmy' => ['name' => 'Lemmy', 'color' => '#00c853'], // Green from the page
50 'mastodon' => ['name' => 'Mastodon', 'color' => '#1a9df9'], // blue from the Mastodon logo
51 'misskey' => ['name' => 'Misskey', 'color' => '#ccfefd'], // Font color of the homepage
52 'mobilizon' => ['name' => 'Mobilizon', 'color' => '#ffd599'], // Background color of parts of the homepage
53 'nextcloud' => ['name' => 'Nextcloud', 'color' => '#1cafff'], // Logo color
54 'mistpark' => ['name' => 'Nomad projects (Mistpark, Osada, Roadhouse, Zap)', 'color' => '#348a4a'], // Green like the Mistpark green
55 'owncast' => ['name' => 'Owncast', 'color' => '#007bff'], // Font color of the homepage
56 'peertube' => ['name' => 'Peertube', 'color' => '#ffad5c'], // One of the logo colors
57 'pixelfed' => ['name' => 'Pixelfed', 'color' => '#11da47'], // One of the logo colors
58 'pleroma' => ['name' => 'Pleroma', 'color' => '#E46F0F'], // Orange from the text that is used on Pleroma instances
59 'plume' => ['name' => 'Plume', 'color' => '#7765e3'], // From the homepage
60 'relay' => ['name' => 'ActivityPub Relay', 'color' => '#888888'], // Grey like the second color of the ActivityPub logo
61 'socialhome' => ['name' => 'SocialHome', 'color' => '#52056b'], // lilac from the Django Image used at the Socialhome homepage
62 'wordpress' => ['name' => 'WordPress', 'color' => '#016087'], // Background color of the homepage
63 'write.as' => ['name' => 'Write.as', 'color' => '#00ace3'], // Border color of the homepage
64 'writefreely' => ['name' => 'WriteFreely', 'color' => '#292929'], // Font color of the homepage
65 'other' => ['name' => DI::l10n()->t('Other'), 'color' => '#F1007E'], // ActivityPub main color
68 $platforms = array_keys($systems);
71 foreach ($platforms as $platform) {
72 $counts[$platform] = [];
81 $gservers = DBA::p("SELECT COUNT(*) AS `total`, SUM(`registered-users`) AS `users`,
82 SUM(IFNULL(`local-posts`, 0) + IFNULL(`local-comments`, 0)) AS `posts`,
83 SUM(IFNULL(`active-month-users`, `active-week-users`)) AS `month`,
84 SUM(IFNULL(`active-halfyear-users`, `active-week-users`)) AS `halfyear`, `platform`,
85 ANY_VALUE(`network`) AS `network`, MAX(`version`) AS `version`
86 FROM `gserver` WHERE NOT `failed` AND `detection-method` != ? AND NOT `network` IN (?, ?) GROUP BY `platform`", GServer::DETECT_MANUAL, Protocol::PHANTOM, Protocol::FEED);
87 while ($gserver = DBA::fetch($gservers)) {
88 $total += $gserver['total'];
89 $users += $gserver['users'];
90 $month += $gserver['month'];
91 $halfyear += $gserver['halfyear'];
92 $posts += $gserver['posts'];
95 $versions = DBA::p("SELECT COUNT(*) AS `total`, `version` FROM `gserver`
96 WHERE NOT `failed` AND `platform` = ? AND `detection-method` != ? AND NOT `network` IN (?, ?)
97 GROUP BY `version` ORDER BY `version`", $gserver['platform'], GServer::DETECT_MANUAL, Protocol::PHANTOM, Protocol::FEED);
98 while ($version = DBA::fetch($versions)) {
99 $version['version'] = str_replace(["\n", "\r", "\t"], " ", $version['version']);
101 if (in_array($gserver['platform'], ['Red Matrix', 'redmatrix', 'red'])) {
102 $version['version'] = 'Red ' . $version['version'];
103 } elseif (in_array($gserver['platform'], ['osada', 'mistpark', 'roadhouse', 'zap'])) {
104 $version['version'] = $gserver['platform'] . ' ' . $version['version'];
105 } elseif (in_array($gserver['platform'], ['activityrelay', 'pub-relay', 'selective-relay', 'aoderelay'])) {
106 $version['version'] = $gserver['platform'] . '-' . $version['version'];
109 $versionCounts[] = $version;
111 DBA::close($versions);
113 $platform = $gserver['platform'] = strtolower($gserver['platform']);
115 if ($platform == 'friendika') {
116 $platform = 'friendica';
117 } elseif (in_array($platform, ['red matrix', 'redmatrix', 'red'])) {
118 $platform = 'hubzilla';
119 } elseif (in_array($platform, ['mistpark', 'osada', 'roadhouse', 'zap'])) {
120 $platform = 'mistpark';
121 } elseif(stristr($platform, 'pleroma')) {
122 $platform = 'pleroma';
123 } elseif(stristr($platform, 'statusnet')) {
124 $platform = 'gnusocial';
125 } elseif(stristr($platform, 'wordpress')) {
126 $platform = 'wordpress';
127 } elseif (in_array($platform, ['activityrelay', 'pub-relay', 'selective-relay', 'aoderelay'])) {
129 } elseif (!in_array($platform, $platforms)) {
133 if ($platform != $gserver['platform']) {
134 if ($platform == 'other') {
135 $versionCounts = $counts[$platform][1] ?? [];
136 $versionCounts[] = ['version' => $gserver['platform'] ?: DI::l10n()->t('unknown'), 'total' => $gserver['total']];
137 $gserver['version'] = '';
139 $versionCounts = array_merge($versionCounts, $counts[$platform][1] ?? []);
142 $gserver['platform'] = $platform;
143 $gserver['total'] += $counts[$platform][0]['total'] ?? 0;
144 $gserver['users'] += $counts[$platform][0]['users'] ?? 0;
145 $gserver['month'] += $counts[$platform][0]['month'] ?? 0;
146 $gserver['halfyear'] += $counts[$platform][0]['halfyear'] ?? 0;
147 $gserver['posts'] += $counts[$platform][0]['posts'] ?? 0;
150 if ($platform == 'friendica') {
151 $versionCounts = self::reformaFriendicaVersions($versionCounts);
152 } elseif ($platform == 'pleroma') {
153 $versionCounts = self::reformaPleromaVersions($versionCounts);
154 } elseif ($platform == 'diaspora') {
155 $versionCounts = self::reformaDiasporaVersions($versionCounts);
156 } elseif ($platform == 'relay') {
157 $versionCounts = self::reformatRelayVersions($versionCounts);
158 } elseif (in_array($platform, ['funkwhale', 'mastodon', 'mobilizon', 'misskey', 'gotosocial'])) {
159 $versionCounts = self::removeVersionSuffixes($versionCounts);
162 if (!in_array($platform, ['other', 'relay', 'mistpark'])) {
163 $versionCounts = self::sortVersion($versionCounts);
165 ksort($versionCounts);
168 $gserver['platform'] = $systems[$platform]['name'];
169 $gserver['totallbl'] = DI::l10n()->t('%s total systems', number_format($gserver['total']));
170 $gserver['monthlbl'] = DI::l10n()->t('%s active users last month', number_format($gserver['month']));
171 $gserver['halfyearlbl'] = DI::l10n()->t('%s active users last six months', number_format($gserver['halfyear']));
172 $gserver['userslbl'] = DI::l10n()->t('%s registered users', number_format($gserver['users']));
173 $gserver['postslbl'] = DI::l10n()->t('%s locally created posts and comments', number_format($gserver['posts']));
175 if (($gserver['users'] > 0) && ($gserver['posts'] > 0)) {
176 $gserver['postsuserlbl'] = DI::l10n()->t('%s posts per user', number_format($gserver['posts'] / $gserver['users'], 1));
178 $gserver['postsuserlbl'] = '';
180 if (($gserver['users'] > 0) && ($gserver['total'] > 0)) {
181 $gserver['userssystemlbl'] = DI::l10n()->t('%s users per system', number_format($gserver['users'] / $gserver['total'], 1));
183 $gserver['userssystemlbl'] = '';
186 $counts[$platform] = [$gserver, $versionCounts, str_replace([' ', '%', '.'], '', $platform), $systems[$platform]['color']];
188 DBA::close($gserver);
191 $intro = DI::l10n()->t('This page offers you some numbers to the known part of the federated social network your Friendica node is part of. These numbers are not complete but only reflect the part of the network your node is aware of.');
193 // load the template, replace the macros and return the page content
194 $t = Renderer::getMarkupTemplate('admin/federation.tpl');
195 return Renderer::replaceMacros($t, [
196 '$title' => DI::l10n()->t('Administration'),
197 '$page' => DI::l10n()->t('Federation Statistics'),
199 '$counts' => $counts,
200 '$version' => FRIENDICA_VERSION,
201 '$legendtext' => DI::l10n()->t('Currently this node is aware of %s nodes (%s active users last month, %s active users last six months, %s registered users in total) from the following platforms:', number_format($total), number_format($month), number_format($halfyear), number_format($users)),
206 * early friendica versions have the format x.x.xxxx where xxxx is the
207 * DB version stamp; those should be operated out and versions be combined
209 * @param array $versionCounts list of version numbers
210 * @return array with cleaned version numbers
212 private static function reformaFriendicaVersions(array $versionCounts)
216 foreach ($versionCounts as $vv) {
217 $newVC = $vv['total'];
218 $newVV = $vv['version'];
219 $lastDot = strrpos($newVV, '.');
220 $firstDash = strpos($newVV, '-');
221 $len = strlen($newVV) - 1;
222 if (($lastDot == $len - 4) && (!strrpos($newVV, '-rc') == $len - 3) && (!$firstDash == $len - 1)) {
223 $newVV = substr($newVV, 0, $lastDot);
225 if (isset($newV[$newVV])) {
226 $newV[$newVV] += $newVC;
228 $newV[$newVV] = $newVC;
231 foreach ($newV as $key => $value) {
232 array_push($newVv, ['total' => $value, 'version' => $key]);
234 $versionCounts = $newVv;
236 return $versionCounts;
240 * in the DB the Diaspora versions have the format x.x.x.x-xx the last
241 * part (-xx) should be removed to clean up the versions from the "head
242 * commit" information and combined into a single entry for x.x.x.x
244 * @param array $versionCounts list of version numbers
245 * @return array with cleaned version numbers
247 private static function reformaDiasporaVersions(array $versionCounts)
251 foreach ($versionCounts as $vv) {
252 $newVC = $vv['total'];
253 $newVV = $vv['version'];
254 $posDash = strpos($newVV, '-');
256 $newVV = substr($newVV, 0, $posDash);
258 if (isset($newV[$newVV])) {
259 $newV[$newVV] += $newVC;
261 $newV[$newVV] = $newVC;
264 foreach ($newV as $key => $value) {
265 array_push($newVv, ['total' => $value, 'version' => $key]);
267 $versionCounts = $newVv;
269 return $versionCounts;
273 * Clean up Pleroma version numbers
275 * @param array $versionCounts list of version numbers
276 * @return array with cleaned version numbers
278 private static function reformaPleromaVersions(array $versionCounts)
281 foreach ($versionCounts as $key => $value) {
282 $version = $versionCounts[$key]['version'];
283 $parts = explode(' ', trim($version));
285 $part = array_pop($parts);
286 } while (!empty($parts) && ((strlen($part) >= 40) || (strlen($part) <= 3)));
287 // only take the x.x.x part of the version, not the "release" after the dash
288 if (!empty($part) && strpos($part, '-')) {
289 $part = explode('-', $part)[0];
292 if (empty($compacted[$part])) {
293 $compacted[$part] = $versionCounts[$key]['total'];
295 $compacted[$part] += $versionCounts[$key]['total'];
301 foreach ($compacted as $version => $pl_total) {
302 $versionCounts[] = ['version' => $version, 'total' => $pl_total];
305 return $versionCounts;
309 * Clean up version numbers
311 * @param array $versionCounts list of version numbers
312 * @return array with cleaned version numbers
314 private static function removeVersionSuffixes(array $versionCounts)
317 foreach ($versionCounts as $key => $value) {
318 $version = $versionCounts[$key]['version'];
320 foreach ([' ', '+', '-', '#', '_', '~'] as $delimiter) {
321 $parts = explode($delimiter, trim($version));
322 $version = array_shift($parts);
325 if (empty($compacted[$version])) {
326 $compacted[$version] = $versionCounts[$key]['total'];
328 $compacted[$version] += $versionCounts[$key]['total'];
333 foreach ($compacted as $version => $pl_total) {
334 $versionCounts[] = ['version' => $version, 'total' => $pl_total];
337 return $versionCounts;
341 * Clean up relay version numbers
343 * @param array $versionCounts list of version numbers
344 * @return array with cleaned version numbers
346 private static function reformatRelayVersions(array $versionCounts)
349 foreach ($versionCounts as $key => $value) {
350 $version = $versionCounts[$key]['version'];
352 $parts = explode(' ', trim($version));
353 $version = array_shift($parts);
355 if (empty($compacted[$version])) {
356 $compacted[$version] = $versionCounts[$key]['total'];
358 $compacted[$version] += $versionCounts[$key]['total'];
363 foreach ($compacted as $version => $pl_total) {
364 $versionCounts[] = ['version' => $version, 'total' => $pl_total];
367 return $versionCounts;
371 * Reformat, sort and compact version numbers
373 * @param array $versionCounts list of version numbers
374 * @return array with reformatted version numbers
376 private static function sortVersion(array $versionCounts)
379 // clean up version numbers
381 // some platforms do not provide version information, add a unkown there
382 // to the version string for the displayed list.
383 foreach ($versionCounts as $key => $value) {
384 if ($versionCounts[$key]['version'] == '') {
385 $versionCounts[$key] = ['total' => $versionCounts[$key]['total'], 'version' => DI::l10n()->t('unknown')];
389 // Assure that the versions are sorted correctly
392 foreach ($versionCounts as $vv) {
393 $version = trim(strip_tags($vv["version"]));
395 $versions[] = $version;
398 usort($versions, 'version_compare');
401 foreach ($versions as $version) {
402 $versionCounts[] = $v2[$version];
405 return $versionCounts;