3 * @copyright Copyright (C) 2010-2024, 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;
25 use Friendica\Core\Protocol;
26 use Friendica\Core\Renderer;
27 use Friendica\Database\DBA;
29 use Friendica\Model\GServer;
30 use Friendica\Module\BaseAdmin;
32 class Federation extends BaseAdmin
34 protected function content(array $request = []): string
38 // get counts on active federation systems this node is knowing
39 // We list the more common systems by name. The rest is counted as "other"
41 'friendica' => ['name' => 'Friendica', 'color' => '#ffc018'], // orange from the logo
42 'akkoma' => ['name' => 'Akkoma', 'color' => '#9574cd'], // Color from the page
43 'birdsitelive' => ['name' => 'BirdsiteLIVE', 'color' => '#1b6ec2'], // Color from the page
44 'bookwyrm' => ['name' => 'BookWyrm', 'color' => '#00d1b2'], // Color from the page
45 'castopod' => ['name' => 'Castopod', 'color' => '#00564a'], // Background color from the page
46 'diaspora' => ['name' => 'Diaspora', 'color' => '#a1a1a1'], // logo is black and white, makes a gray
47 'calckey' => ['name' => 'firefish (Calckey)', 'color' => '#1c4a5c'], // Color from the page
48 'sharkey' => ['name' => 'Sharkey', 'color' => 'lightpink'], // Font color from the homepage
49 'foundkey' => ['name' => 'Foundkey', 'color' => '#609926'], // Some random color from the repository
50 'funkwhale' => ['name' => 'Funkwhale', 'color' => '#4082B4'], // From the homepage
51 'gancio' => ['name' => 'Gancio', 'color' => '#7253ed'], // Fontcolor from the page
52 'gnusocial' => ['name' => 'GNU Social/Statusnet', 'color' => '#a22430'], // dark red from the logo
53 'gotosocial' => ['name' => 'GoToSocial', 'color' => '#df8958'], // Some color from their mascot
54 'hometown' => ['name' => 'Hometown', 'color' => '#1f70c1'], // Color from the Patreon page
55 'honk' => ['name' => 'Honk', 'color' => '#0d0d0d'], // Background color from the page
56 'hubzilla' => ['name' => 'Hubzilla/Red Matrix', 'color' => '#43488a'], // blue from the logo
57 'iceshrimp' => ['name' => 'iceshrimp', 'color' => 'mediumslateblue'], // Color that is used in their software
58 'kbin' => ['name' => 'kbin', 'color' => '#61366b'], // Color from their main instance
59 'lemmy' => ['name' => 'Lemmy', 'color' => '#00c853'], // Green from the page
60 'mastodon' => ['name' => 'Mastodon', 'color' => '#1a9df9'], // blue from the Mastodon logo
61 'microblog' => ['name' => 'Microblog', 'color' => '#fdb52b'], // Color from the page
62 'misskey' => ['name' => 'Misskey', 'color' => '#ccfefd'], // Font color of the homepage
63 'mobilizon' => ['name' => 'Mobilizon', 'color' => '#ffd599'], // Background color of parts of the homepage
64 'nextcloud' => ['name' => 'Nextcloud', 'color' => '#1cafff'], // Logo color
65 'nomad' => ['name' => 'Nomad projects (Mistpark, Osada, Roadhouse, Streams. Zap)', 'color' => '#348a4a'], // Green like the Mistpark green
66 'owncast' => ['name' => 'Owncast', 'color' => '#007bff'], // Font color of the homepage
67 'peertube' => ['name' => 'Peertube', 'color' => '#ffad5c'], // One of the logo colors
68 'pixelfed' => ['name' => 'Pixelfed', 'color' => '#11da47'], // One of the logo colors
69 'pleroma' => ['name' => 'Pleroma', 'color' => '#E46F0F'], // Orange from the text that is used on Pleroma instances
70 'plume' => ['name' => 'Plume', 'color' => '#7765e3'], // From the homepage
71 'postmarks' => ['name' => 'Postmarks', 'color' => 'darkblue'], // Header color from the homepage
72 'relay' => ['name' => 'ActivityPub Relay', 'color' => '#888888'], // Grey like the second color of the ActivityPub logo
73 'socialhome' => ['name' => 'SocialHome', 'color' => '#52056b'], // lilac from the Django Image used at the Socialhome homepage
74 'snac' => ['name' => 'Snac', 'color' => '#2966a8'], // Color from one of their themes
75 'takahe' => ['name' => 'Takahē', 'color' => '#26323c'], // Background color of the homepage
76 'wildebeest' => ['name' => 'Wildebeest', 'color' => '#0055dc'], // Color of the mascot
77 'wordpress' => ['name' => 'WordPress', 'color' => '#016087'], // Background color of the homepage
78 'write.as' => ['name' => 'Write.as', 'color' => '#00ace3'], // Border color of the homepage
79 'writefreely' => ['name' => 'WriteFreely', 'color' => '#292929'], // Font color of the homepage
80 'other' => ['name' => DI::l10n()->t('Other'), 'color' => '#F1007E'], // ActivityPub main color
83 $platforms = array_keys($systems);
86 foreach ($platforms as $platform) {
87 $counts[$platform] = [];
96 $gservers = DBA::p("SELECT COUNT(*) AS `total`, SUM(`registered-users`) AS `users`,
97 SUM(IFNULL(`local-posts`, 0) + IFNULL(`local-comments`, 0)) AS `posts`,
98 SUM(IFNULL(`active-month-users`, `active-week-users`)) AS `month`,
99 SUM(IFNULL(`active-halfyear-users`, `active-week-users`)) AS `halfyear`, `platform`,
100 MIN(`network`) AS `network`, MAX(`version`) AS `version`
101 FROM `gserver` WHERE NOT `failed` AND `platform` != ? AND `detection-method` != ? AND NOT `network` IN (?, ?) GROUP BY `platform`",
102 '', GServer::DETECT_MANUAL, Protocol::PHANTOM, Protocol::FEED);
103 while ($gserver = DBA::fetch($gservers)) {
104 $total += $gserver['total'];
105 $users += $gserver['users'];
106 $month += $gserver['month'];
107 $halfyear += $gserver['halfyear'];
108 $posts += $gserver['posts'];
111 $versions = DBA::p("SELECT COUNT(*) AS `total`, `version` FROM `gserver`
112 WHERE NOT `failed` AND `platform` = ? AND `detection-method` != ? AND NOT `network` IN (?, ?)
113 GROUP BY `version` ORDER BY `version`", $gserver['platform'], GServer::DETECT_MANUAL, Protocol::PHANTOM, Protocol::FEED);
114 while ($version = DBA::fetch($versions)) {
115 $version['version'] = str_replace(["\n", "\r", "\t"], " ", $version['version']);
117 if (in_array($gserver['platform'], ['Red Matrix', 'redmatrix', 'red'])) {
118 $version['version'] = 'Red ' . $version['version'];
119 } elseif (in_array($gserver['platform'], ['osada', 'mistpark', 'roadhouse', 'streams', 'zap'])) {
120 $version['version'] = $gserver['platform'] . ' ' . $version['version'];
121 } elseif (in_array($gserver['platform'], ['activityrelay', 'pub-relay', 'selective-relay', 'aoderelay'])) {
122 $version['version'] = $gserver['platform'] . '-' . $version['version'];
123 } elseif (in_array($gserver['platform'], ['calckey', 'firefish'])) {
124 $version['version'] = $gserver['platform'] . '-' . $version['version'];
127 $versionCounts[] = $version;
129 DBA::close($versions);
131 $platform = $gserver['platform'] = strtolower($gserver['platform']);
133 if ($platform == 'friendika') {
134 $platform = 'friendica';
135 } elseif (in_array($platform, ['calckey', 'firefish'])) {
136 $platform = 'calckey';
137 } elseif (in_array($platform, ['red matrix', 'redmatrix', 'red'])) {
138 $platform = 'hubzilla';
139 } elseif (in_array($platform, ['osada', 'mistpark', 'roadhouse', 'streams', 'zap'])) {
141 } elseif(stristr($platform, 'pleroma')) {
142 $platform = 'pleroma';
143 } elseif(stristr($platform, 'statusnet')) {
144 $platform = 'gnusocial';
145 } elseif(stristr($platform, 'nextcloud')) {
146 $platform = 'nextcloud';
147 } elseif(stristr($platform, 'wordpress')) {
148 $platform = 'wordpress';
149 } elseif (in_array($platform, ['activityrelay', 'pub-relay', 'selective-relay', 'aoderelay'])) {
151 } elseif (!in_array($platform, $platforms)) {
155 if ($platform != $gserver['platform']) {
156 if ($platform == 'other') {
157 $versionCounts = $counts[$platform][1] ?? [];
158 $versionCounts[] = ['version' => $gserver['platform'] ?: DI::l10n()->t('unknown'), 'total' => $gserver['total']];
159 $gserver['version'] = '';
161 $versionCounts = array_merge($versionCounts, $counts[$platform][1] ?? []);
164 $gserver['platform'] = $platform;
165 $gserver['total'] += $counts[$platform][0]['total'] ?? 0;
166 $gserver['users'] += $counts[$platform][0]['users'] ?? 0;
167 $gserver['month'] += $counts[$platform][0]['month'] ?? 0;
168 $gserver['halfyear'] += $counts[$platform][0]['halfyear'] ?? 0;
169 $gserver['posts'] += $counts[$platform][0]['posts'] ?? 0;
172 if ($platform == 'friendica') {
173 $versionCounts = self::reformaFriendicaVersions($versionCounts);
174 } elseif (in_array($platform, ['pleroma', 'akkoma'])) {
175 $versionCounts = self::reformaPleromaVersions($versionCounts);
176 } elseif ($platform == 'diaspora') {
177 $versionCounts = self::reformaDiasporaVersions($versionCounts);
178 } elseif ($platform == 'relay') {
179 $versionCounts = self::reformatRelayVersions($versionCounts);
180 } elseif (in_array($platform, ['funkwhale', 'mastodon', 'mobilizon', 'misskey', 'gotosocial'])) {
181 $versionCounts = self::removeVersionSuffixes($versionCounts);
184 if (!in_array($platform, ['other', 'relay', 'mistpark'])) {
185 $versionCounts = self::sortVersion($versionCounts);
187 ksort($versionCounts);
190 $gserver['platform'] = $systems[$platform]['name'];
191 $gserver['totallbl'] = DI::l10n()->tt('%2$s total system' , '%2$s total systems' , $gserver['total'], number_format($gserver['total']));
192 $gserver['monthlbl'] = DI::l10n()->tt('%2$s active user last month' , '%2$s active users last month' , $gserver['month'] ?? 0, number_format($gserver['month'] ?? 0));
193 $gserver['halfyearlbl'] = DI::l10n()->tt('%2$s active user last six months' , '%2$s active users last six months' , $gserver['halfyear'] ?? 0, number_format($gserver['halfyear'] ?? 0));
194 $gserver['userslbl'] = DI::l10n()->tt('%2$s registered user' , '%2$s registered users' , $gserver['users'], number_format($gserver['users']));
195 $gserver['postslbl'] = DI::l10n()->tt('%2$s locally created post or comment', '%2$s locally created posts and comments', $gserver['posts'], number_format($gserver['posts']));
197 if (($gserver['users'] > 0) && ($gserver['posts'] > 0)) {
198 $gserver['postsuserlbl'] = DI::l10n()->tt('%2$s post per user', '%2$s posts per user', $gserver['posts'] / $gserver['users'], number_format($gserver['posts'] / $gserver['users'], 1));
200 $gserver['postsuserlbl'] = '';
202 if (($gserver['users'] > 0) && ($gserver['total'] > 0)) {
203 $gserver['userssystemlbl'] = DI::l10n()->tt('%2$s user per system', '%2$s users per system', $gserver['users'] / $gserver['total'], number_format($gserver['users'] / $gserver['total'], 1));
205 $gserver['userssystemlbl'] = '';
208 $counts[$platform] = [$gserver, $versionCounts, str_replace([' ', '%', '.'], '', $platform), $systems[$platform]['color']];
210 DBA::close($gservers);
213 $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.');
215 // load the template, replace the macros and return the page content
216 $t = Renderer::getMarkupTemplate('admin/federation.tpl');
217 return Renderer::replaceMacros($t, [
218 '$title' => DI::l10n()->t('Administration'),
219 '$page' => DI::l10n()->t('Federation Statistics'),
221 '$counts' => $counts,
222 '$version' => App::VERSION,
223 '$legendtext' => DI::l10n()->tt('Currently this node is aware of %2$s node (%3$s active users last month, %4$s active users last six months, %5$s registered users in total) from the following platforms:', 'Currently this node is aware of %2$s nodes (%3$s active users last month, %4$s active users last six months, %5$s registered users in total) from the following platforms:', $total, number_format($total), number_format($month), number_format($halfyear), number_format($users)),
228 * early friendica versions have the format x.x.xxxx where xxxx is the
229 * DB version stamp; those should be operated out and versions be combined
231 * @param array $versionCounts list of version numbers
232 * @return array with cleaned version numbers
234 private static function reformaFriendicaVersions(array $versionCounts)
238 foreach ($versionCounts as $vv) {
239 $newVC = $vv['total'];
240 $newVV = $vv['version'];
241 $lastDot = strrpos($newVV, '.');
242 $firstDash = strpos($newVV, '-');
243 $len = strlen($newVV) - 1;
244 if (($lastDot == $len - 4) && (!strrpos($newVV, '-rc') == $len - 3) && (!$firstDash == $len - 1)) {
245 $newVV = substr($newVV, 0, $lastDot);
247 if (isset($newV[$newVV])) {
248 $newV[$newVV] += $newVC;
250 $newV[$newVV] = $newVC;
253 foreach ($newV as $key => $value) {
254 array_push($newVv, ['total' => $value, 'version' => $key]);
256 $versionCounts = $newVv;
258 return $versionCounts;
262 * in the DB the Diaspora versions have the format x.x.x.x-xx the last
263 * part (-xx) should be removed to clean up the versions from the "head
264 * commit" information and combined into a single entry for x.x.x.x
266 * @param array $versionCounts list of version numbers
267 * @return array with cleaned version numbers
269 private static function reformaDiasporaVersions(array $versionCounts)
273 foreach ($versionCounts as $vv) {
274 $newVC = $vv['total'];
275 $newVV = $vv['version'];
276 $posDash = strpos($newVV, '-');
278 $newVV = substr($newVV, 0, $posDash);
280 if (isset($newV[$newVV])) {
281 $newV[$newVV] += $newVC;
283 $newV[$newVV] = $newVC;
286 foreach ($newV as $key => $value) {
287 array_push($newVv, ['total' => $value, 'version' => $key]);
289 $versionCounts = $newVv;
291 return $versionCounts;
295 * Clean up Pleroma version numbers
297 * @param array $versionCounts list of version numbers
298 * @return array with cleaned version numbers
300 private static function reformaPleromaVersions(array $versionCounts)
303 foreach ($versionCounts as $key => $value) {
304 $version = $versionCounts[$key]['version'];
305 $parts = explode(' ', trim($version));
307 $part = array_pop($parts);
308 } while (!empty($parts) && ((strlen($part) >= 40) || (strlen($part) <= 3)));
309 // only take the x.x.x part of the version, not the "release" after the dash
310 if (!empty($part) && strpos($part, '-')) {
311 $part = explode('-', $part)[0];
314 if (empty($compacted[$part])) {
315 $compacted[$part] = $versionCounts[$key]['total'];
317 $compacted[$part] += $versionCounts[$key]['total'];
323 foreach ($compacted as $version => $pl_total) {
324 $versionCounts[] = ['version' => $version, 'total' => $pl_total];
327 return $versionCounts;
331 * Clean up version numbers
333 * @param array $versionCounts list of version numbers
334 * @return array with cleaned version numbers
336 private static function removeVersionSuffixes(array $versionCounts)
339 foreach ($versionCounts as $key => $value) {
340 $version = $versionCounts[$key]['version'];
342 foreach ([' ', '+', '-', '#', '_', '~'] as $delimiter) {
343 $parts = explode($delimiter, trim($version));
344 $version = array_shift($parts);
347 if (empty($compacted[$version])) {
348 $compacted[$version] = $versionCounts[$key]['total'];
350 $compacted[$version] += $versionCounts[$key]['total'];
355 foreach ($compacted as $version => $pl_total) {
356 $versionCounts[] = ['version' => $version, 'total' => $pl_total];
359 return $versionCounts;
363 * Clean up relay version numbers
365 * @param array $versionCounts list of version numbers
366 * @return array with cleaned version numbers
368 private static function reformatRelayVersions(array $versionCounts)
371 foreach ($versionCounts as $key => $value) {
372 $version = $versionCounts[$key]['version'];
374 $parts = explode(' ', trim($version));
375 $version = array_shift($parts);
377 if (empty($compacted[$version])) {
378 $compacted[$version] = $versionCounts[$key]['total'];
380 $compacted[$version] += $versionCounts[$key]['total'];
385 foreach ($compacted as $version => $pl_total) {
386 $versionCounts[] = ['version' => $version, 'total' => $pl_total];
389 return $versionCounts;
393 * Reformat, sort and compact version numbers
395 * @param array $versionCounts list of version numbers
396 * @return array with reformatted version numbers
398 private static function sortVersion(array $versionCounts)
401 // clean up version numbers
403 // some platforms do not provide version information, add a unknown there
404 // to the version string for the displayed list.
405 foreach ($versionCounts as $key => $value) {
406 if ($versionCounts[$key]['version'] == '') {
407 $versionCounts[$key] = ['total' => $versionCounts[$key]['total'], 'version' => DI::l10n()->t('unknown')];
411 // Assure that the versions are sorted correctly
414 foreach ($versionCounts as $vv) {
415 $version = trim(strip_tags($vv["version"]));
417 $versions[] = $version;
420 usort($versions, 'version_compare');
423 foreach ($versions as $version) {
424 $versionCounts[] = $v2[$version];
427 return $versionCounts;