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;
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 'funkwhale' => ['name' => 'Funkwhale', 'color' => '#4082B4'], // From the homepage
48 'gnusocial' => ['name' => 'GNU Social/Statusnet', 'color' => '#a22430'], // dark red from the logo
49 'gotosocial' => ['name' => 'GoToSocial', 'color' => '#df8958'], // Some color from their mascot
50 'hometown' => ['name' => 'Hometown', 'color' => '#1f70c1'], // Color from the Patreon page
51 'honk' => ['name' => 'Honk', 'color' => '##0d0d0d'], // Background color from the page
52 'hubzilla' => ['name' => 'Hubzilla/Red Matrix', 'color' => '#43488a'], // blue from the logo
53 'lemmy' => ['name' => 'Lemmy', 'color' => '#00c853'], // Green from the page
54 'mastodon' => ['name' => 'Mastodon', 'color' => '#1a9df9'], // blue from the Mastodon logo
55 'microblog' => ['name' => 'Microblog', 'color' => '#fdb52b'], // Color from the page
56 'misskey' => ['name' => 'Misskey', 'color' => '#ccfefd'], // Font color of the homepage
57 'mobilizon' => ['name' => 'Mobilizon', 'color' => '#ffd599'], // Background color of parts of the homepage
58 'nextcloud' => ['name' => 'Nextcloud', 'color' => '#1cafff'], // Logo color
59 'mistpark' => ['name' => 'Nomad projects (Mistpark, Osada, Roadhouse, Zap)', 'color' => '#348a4a'], // Green like the Mistpark green
60 'owncast' => ['name' => 'Owncast', 'color' => '#007bff'], // Font color of the homepage
61 'peertube' => ['name' => 'Peertube', 'color' => '#ffad5c'], // One of the logo colors
62 'pixelfed' => ['name' => 'Pixelfed', 'color' => '#11da47'], // One of the logo colors
63 'pleroma' => ['name' => 'Pleroma', 'color' => '#E46F0F'], // Orange from the text that is used on Pleroma instances
64 'plume' => ['name' => 'Plume', 'color' => '#7765e3'], // From the homepage
65 'relay' => ['name' => 'ActivityPub Relay', 'color' => '#888888'], // Grey like the second color of the ActivityPub logo
66 'socialhome' => ['name' => 'SocialHome', 'color' => '#52056b'], // lilac from the Django Image used at the Socialhome homepage
67 'wordpress' => ['name' => 'WordPress', 'color' => '#016087'], // Background color of the homepage
68 'write.as' => ['name' => 'Write.as', 'color' => '#00ace3'], // Border color of the homepage
69 'writefreely' => ['name' => 'WriteFreely', 'color' => '#292929'], // Font color of the homepage
70 'other' => ['name' => DI::l10n()->t('Other'), 'color' => '#F1007E'], // ActivityPub main color
73 $platforms = array_keys($systems);
76 foreach ($platforms as $platform) {
77 $counts[$platform] = [];
86 $gservers = DBA::p("SELECT COUNT(*) AS `total`, SUM(`registered-users`) AS `users`,
87 SUM(IFNULL(`local-posts`, 0) + IFNULL(`local-comments`, 0)) AS `posts`,
88 SUM(IFNULL(`active-month-users`, `active-week-users`)) AS `month`,
89 SUM(IFNULL(`active-halfyear-users`, `active-week-users`)) AS `halfyear`, `platform`,
90 ANY_VALUE(`network`) AS `network`, MAX(`version`) AS `version`
91 FROM `gserver` WHERE NOT `failed` AND `platform` != ? AND `detection-method` != ? AND NOT `network` IN (?, ?) GROUP BY `platform`",
92 '', GServer::DETECT_MANUAL, Protocol::PHANTOM, Protocol::FEED);
93 while ($gserver = DBA::fetch($gservers)) {
94 $total += $gserver['total'];
95 $users += $gserver['users'];
96 $month += $gserver['month'];
97 $halfyear += $gserver['halfyear'];
98 $posts += $gserver['posts'];
101 $versions = DBA::p("SELECT COUNT(*) AS `total`, `version` FROM `gserver`
102 WHERE NOT `failed` AND `platform` = ? AND `detection-method` != ? AND NOT `network` IN (?, ?)
103 GROUP BY `version` ORDER BY `version`", $gserver['platform'], GServer::DETECT_MANUAL, Protocol::PHANTOM, Protocol::FEED);
104 while ($version = DBA::fetch($versions)) {
105 $version['version'] = str_replace(["\n", "\r", "\t"], " ", $version['version']);
107 if (in_array($gserver['platform'], ['Red Matrix', 'redmatrix', 'red'])) {
108 $version['version'] = 'Red ' . $version['version'];
109 } elseif (in_array($gserver['platform'], ['osada', 'mistpark', 'roadhouse', 'zap', 'macgirvin', 'mkultra'])) {
110 $version['version'] = $gserver['platform'] . ' ' . $version['version'];
111 } elseif (in_array($gserver['platform'], ['activityrelay', 'pub-relay', 'selective-relay', 'aoderelay'])) {
112 $version['version'] = $gserver['platform'] . '-' . $version['version'];
115 $versionCounts[] = $version;
117 DBA::close($versions);
119 $platform = $gserver['platform'] = strtolower($gserver['platform']);
121 if ($platform == 'friendika') {
122 $platform = 'friendica';
123 } elseif (in_array($platform, ['red matrix', 'redmatrix', 'red'])) {
124 $platform = 'hubzilla';
125 } elseif (in_array($platform, ['osada', 'mistpark', 'roadhouse', 'zap', 'macgirvin', 'mkultra'])) {
126 $platform = 'mistpark';
127 } elseif(stristr($platform, 'pleroma')) {
128 $platform = 'pleroma';
129 } elseif(stristr($platform, 'statusnet')) {
130 $platform = 'gnusocial';
131 } elseif(stristr($platform, 'wordpress')) {
132 $platform = 'wordpress';
133 } elseif (in_array($platform, ['activityrelay', 'pub-relay', 'selective-relay', 'aoderelay'])) {
135 } elseif (!in_array($platform, $platforms)) {
139 if ($platform != $gserver['platform']) {
140 if ($platform == 'other') {
141 $versionCounts = $counts[$platform][1] ?? [];
142 $versionCounts[] = ['version' => $gserver['platform'] ?: DI::l10n()->t('unknown'), 'total' => $gserver['total']];
143 $gserver['version'] = '';
145 $versionCounts = array_merge($versionCounts, $counts[$platform][1] ?? []);
148 $gserver['platform'] = $platform;
149 $gserver['total'] += $counts[$platform][0]['total'] ?? 0;
150 $gserver['users'] += $counts[$platform][0]['users'] ?? 0;
151 $gserver['month'] += $counts[$platform][0]['month'] ?? 0;
152 $gserver['halfyear'] += $counts[$platform][0]['halfyear'] ?? 0;
153 $gserver['posts'] += $counts[$platform][0]['posts'] ?? 0;
156 if ($platform == 'friendica') {
157 $versionCounts = self::reformaFriendicaVersions($versionCounts);
158 } elseif (in_array($platform, ['pleroma', 'akkoma'])) {
159 $versionCounts = self::reformaPleromaVersions($versionCounts);
160 } elseif ($platform == 'diaspora') {
161 $versionCounts = self::reformaDiasporaVersions($versionCounts);
162 } elseif ($platform == 'relay') {
163 $versionCounts = self::reformatRelayVersions($versionCounts);
164 } elseif (in_array($platform, ['funkwhale', 'mastodon', 'mobilizon', 'misskey', 'gotosocial'])) {
165 $versionCounts = self::removeVersionSuffixes($versionCounts);
168 if (!in_array($platform, ['other', 'relay', 'mistpark'])) {
169 $versionCounts = self::sortVersion($versionCounts);
171 ksort($versionCounts);
174 $gserver['platform'] = $systems[$platform]['name'];
175 $gserver['totallbl'] = DI::l10n()->tt('%2$s total system' , '%2$s total systems' , $gserver['total'], number_format($gserver['total']));
176 $gserver['monthlbl'] = DI::l10n()->tt('%2$s active user last month' , '%2$s active users last month' , $gserver['month'] ?? 0, number_format($gserver['month']));
177 $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']));
178 $gserver['userslbl'] = DI::l10n()->tt('%2$s registered user' , '%2$s registered users' , $gserver['users'], number_format($gserver['users']));
179 $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']));
181 if (($gserver['users'] > 0) && ($gserver['posts'] > 0)) {
182 $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));
184 $gserver['postsuserlbl'] = '';
186 if (($gserver['users'] > 0) && ($gserver['total'] > 0)) {
187 $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));
189 $gserver['userssystemlbl'] = '';
192 $counts[$platform] = [$gserver, $versionCounts, str_replace([' ', '%', '.'], '', $platform), $systems[$platform]['color']];
194 DBA::close($gservers);
197 $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.');
199 // load the template, replace the macros and return the page content
200 $t = Renderer::getMarkupTemplate('admin/federation.tpl');
201 return Renderer::replaceMacros($t, [
202 '$title' => DI::l10n()->t('Administration'),
203 '$page' => DI::l10n()->t('Federation Statistics'),
205 '$counts' => $counts,
206 '$version' => App::VERSION,
207 '$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)),
212 * early friendica versions have the format x.x.xxxx where xxxx is the
213 * DB version stamp; those should be operated out and versions be combined
215 * @param array $versionCounts list of version numbers
216 * @return array with cleaned version numbers
218 private static function reformaFriendicaVersions(array $versionCounts)
222 foreach ($versionCounts as $vv) {
223 $newVC = $vv['total'];
224 $newVV = $vv['version'];
225 $lastDot = strrpos($newVV, '.');
226 $firstDash = strpos($newVV, '-');
227 $len = strlen($newVV) - 1;
228 if (($lastDot == $len - 4) && (!strrpos($newVV, '-rc') == $len - 3) && (!$firstDash == $len - 1)) {
229 $newVV = substr($newVV, 0, $lastDot);
231 if (isset($newV[$newVV])) {
232 $newV[$newVV] += $newVC;
234 $newV[$newVV] = $newVC;
237 foreach ($newV as $key => $value) {
238 array_push($newVv, ['total' => $value, 'version' => $key]);
240 $versionCounts = $newVv;
242 return $versionCounts;
246 * in the DB the Diaspora versions have the format x.x.x.x-xx the last
247 * part (-xx) should be removed to clean up the versions from the "head
248 * commit" information and combined into a single entry for x.x.x.x
250 * @param array $versionCounts list of version numbers
251 * @return array with cleaned version numbers
253 private static function reformaDiasporaVersions(array $versionCounts)
257 foreach ($versionCounts as $vv) {
258 $newVC = $vv['total'];
259 $newVV = $vv['version'];
260 $posDash = strpos($newVV, '-');
262 $newVV = substr($newVV, 0, $posDash);
264 if (isset($newV[$newVV])) {
265 $newV[$newVV] += $newVC;
267 $newV[$newVV] = $newVC;
270 foreach ($newV as $key => $value) {
271 array_push($newVv, ['total' => $value, 'version' => $key]);
273 $versionCounts = $newVv;
275 return $versionCounts;
279 * Clean up Pleroma version numbers
281 * @param array $versionCounts list of version numbers
282 * @return array with cleaned version numbers
284 private static function reformaPleromaVersions(array $versionCounts)
287 foreach ($versionCounts as $key => $value) {
288 $version = $versionCounts[$key]['version'];
289 $parts = explode(' ', trim($version));
291 $part = array_pop($parts);
292 } while (!empty($parts) && ((strlen($part) >= 40) || (strlen($part) <= 3)));
293 // only take the x.x.x part of the version, not the "release" after the dash
294 if (!empty($part) && strpos($part, '-')) {
295 $part = explode('-', $part)[0];
298 if (empty($compacted[$part])) {
299 $compacted[$part] = $versionCounts[$key]['total'];
301 $compacted[$part] += $versionCounts[$key]['total'];
307 foreach ($compacted as $version => $pl_total) {
308 $versionCounts[] = ['version' => $version, 'total' => $pl_total];
311 return $versionCounts;
315 * Clean up version numbers
317 * @param array $versionCounts list of version numbers
318 * @return array with cleaned version numbers
320 private static function removeVersionSuffixes(array $versionCounts)
323 foreach ($versionCounts as $key => $value) {
324 $version = $versionCounts[$key]['version'];
326 foreach ([' ', '+', '-', '#', '_', '~'] as $delimiter) {
327 $parts = explode($delimiter, trim($version));
328 $version = array_shift($parts);
331 if (empty($compacted[$version])) {
332 $compacted[$version] = $versionCounts[$key]['total'];
334 $compacted[$version] += $versionCounts[$key]['total'];
339 foreach ($compacted as $version => $pl_total) {
340 $versionCounts[] = ['version' => $version, 'total' => $pl_total];
343 return $versionCounts;
347 * Clean up relay version numbers
349 * @param array $versionCounts list of version numbers
350 * @return array with cleaned version numbers
352 private static function reformatRelayVersions(array $versionCounts)
355 foreach ($versionCounts as $key => $value) {
356 $version = $versionCounts[$key]['version'];
358 $parts = explode(' ', trim($version));
359 $version = array_shift($parts);
361 if (empty($compacted[$version])) {
362 $compacted[$version] = $versionCounts[$key]['total'];
364 $compacted[$version] += $versionCounts[$key]['total'];
369 foreach ($compacted as $version => $pl_total) {
370 $versionCounts[] = ['version' => $version, 'total' => $pl_total];
373 return $versionCounts;
377 * Reformat, sort and compact version numbers
379 * @param array $versionCounts list of version numbers
380 * @return array with reformatted version numbers
382 private static function sortVersion(array $versionCounts)
385 // clean up version numbers
387 // some platforms do not provide version information, add a unkown there
388 // to the version string for the displayed list.
389 foreach ($versionCounts as $key => $value) {
390 if ($versionCounts[$key]['version'] == '') {
391 $versionCounts[$key] = ['total' => $versionCounts[$key]['total'], 'version' => DI::l10n()->t('unknown')];
395 // Assure that the versions are sorted correctly
398 foreach ($versionCounts as $vv) {
399 $version = trim(strip_tags($vv["version"]));
401 $versions[] = $version;
404 usort($versions, 'version_compare');
407 foreach ($versions as $version) {
408 $versionCounts[] = $v2[$version];
411 return $versionCounts;