Improved server detection / new servers added to federation statistics (#13793)
[friendica.git/.git] / src / Module / Admin / Federation.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2023, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
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.
11  *
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.
16  *
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/>.
19  *
20  */
21
22 namespace Friendica\Module\Admin;
23
24 use Friendica\App;
25 use Friendica\Core\Protocol;
26 use Friendica\Core\Renderer;
27 use Friendica\Database\DBA;
28 use Friendica\DI;
29 use Friendica\Model\GServer;
30 use Friendica\Module\BaseAdmin;
31
32 class Federation extends BaseAdmin
33 {
34         protected function content(array $request = []): string
35         {
36                 parent::content();
37
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"
40                 $systems = [
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
81                 ];
82
83                 $platforms = array_keys($systems);
84
85                 $counts = [];
86                 foreach ($platforms as $platform) {
87                         $counts[$platform] = [];
88                 }
89
90                 $total    = 0;
91                 $users    = 0;
92                 $month    = 0;
93                 $halfyear = 0;
94                 $posts    = 0;
95
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                         ANY_VALUE(`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'];
109
110                         $versionCounts = [];
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']);
116
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'];
125                                 }
126
127                                 $versionCounts[] = $version;
128                         }
129                         DBA::close($versions);
130
131                         $platform = $gserver['platform'] = strtolower($gserver['platform']);
132
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'])) {
140                                 $platform = 'nomad';
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'])) {
150                                 $platform = 'relay';
151                         } elseif (!in_array($platform, $platforms)) {
152                                 $platform = 'other';
153                         }
154
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'] = '';
160                                 } else {
161                                         $versionCounts = array_merge($versionCounts, $counts[$platform][1] ?? []);
162                                 }
163
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;
170                         }
171
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);
182                         }
183
184                         if (!in_array($platform, ['other', 'relay', 'mistpark'])) {
185                                 $versionCounts = self::sortVersion($versionCounts);
186                         } else {
187                                 ksort($versionCounts);
188                         }
189
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']));
196
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));
199                         } else {
200                                 $gserver['postsuserlbl'] = '';
201                         }
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));
204                         } else {
205                                 $gserver['userssystemlbl'] = '';
206                         }
207
208                         $counts[$platform] = [$gserver, $versionCounts, str_replace([' ', '%', '.'], '', $platform), $systems[$platform]['color']];
209                 }
210                 DBA::close($gservers);
211
212                 // some helpful text
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.');
214
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'),
220                         '$intro' => $intro,
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)),
224                 ]);
225         }
226
227         /**
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
230          *
231          * @param array $versionCounts list of version numbers
232          * @return array with cleaned version numbers
233          */
234         private static function reformaFriendicaVersions(array $versionCounts)
235         {
236                 $newV = [];
237                 $newVv = [];
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);
246                         }
247                         if (isset($newV[$newVV])) {
248                                 $newV[$newVV] += $newVC;
249                         } else {
250                                 $newV[$newVV] = $newVC;
251                         }
252                 }
253                 foreach ($newV as $key => $value) {
254                         array_push($newVv, ['total' => $value, 'version' => $key]);
255                 }
256                 $versionCounts = $newVv;
257
258                 return $versionCounts;
259         }
260
261         /**
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
265          *
266          * @param array $versionCounts list of version numbers
267          * @return array with cleaned version numbers
268          */
269         private static function reformaDiasporaVersions(array $versionCounts)
270         {
271                 $newV = [];
272                 $newVv = [];
273                 foreach ($versionCounts as $vv) {
274                         $newVC = $vv['total'];
275                         $newVV = $vv['version'];
276                         $posDash = strpos($newVV, '-');
277                         if ($posDash) {
278                                 $newVV = substr($newVV, 0, $posDash);
279                         }
280                         if (isset($newV[$newVV])) {
281                                 $newV[$newVV] += $newVC;
282                         } else {
283                                 $newV[$newVV] = $newVC;
284                         }
285                 }
286                 foreach ($newV as $key => $value) {
287                         array_push($newVv, ['total' => $value, 'version' => $key]);
288                 }
289                 $versionCounts = $newVv;
290
291                 return $versionCounts;
292         }
293
294         /**
295          * Clean up Pleroma version numbers
296          *
297          * @param array $versionCounts list of version numbers
298          * @return array with cleaned version numbers
299          */
300         private static function reformaPleromaVersions(array $versionCounts)
301         {
302                 $compacted = [];
303                 foreach ($versionCounts as $key => $value) {
304                         $version = $versionCounts[$key]['version'];
305                         $parts = explode(' ', trim($version));
306                         do {
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];
312                         }
313                         if (!empty($part)) {
314                                 if (empty($compacted[$part])) {
315                                         $compacted[$part] = $versionCounts[$key]['total'];
316                                 } else {
317                                         $compacted[$part] += $versionCounts[$key]['total'];
318                                 }
319                         }
320                 }
321
322                 $versionCounts = [];
323                 foreach ($compacted as $version => $pl_total) {
324                         $versionCounts[] = ['version' => $version, 'total' => $pl_total];
325                 }
326
327                 return $versionCounts;
328         }
329
330         /**
331          * Clean up version numbers
332          *
333          * @param array $versionCounts list of version numbers
334          * @return array with cleaned version numbers
335          */
336         private static function removeVersionSuffixes(array $versionCounts)
337         {
338                 $compacted = [];
339                 foreach ($versionCounts as $key => $value) {
340                         $version = $versionCounts[$key]['version'];
341
342                         foreach ([' ', '+', '-', '#', '_', '~'] as $delimiter) {
343                                 $parts = explode($delimiter, trim($version));
344                                 $version = array_shift($parts);
345                         }
346
347                         if (empty($compacted[$version])) {
348                                 $compacted[$version] = $versionCounts[$key]['total'];
349                         } else {
350                                 $compacted[$version] += $versionCounts[$key]['total'];
351                         }
352                 }
353
354                 $versionCounts = [];
355                 foreach ($compacted as $version => $pl_total) {
356                         $versionCounts[] = ['version' => $version, 'total' => $pl_total];
357                 }
358
359                 return $versionCounts;
360         }
361
362         /**
363          * Clean up relay version numbers
364          *
365          * @param array $versionCounts list of version numbers
366          * @return array with cleaned version numbers
367          */
368         private static function reformatRelayVersions(array $versionCounts)
369         {
370                 $compacted = [];
371                 foreach ($versionCounts as $key => $value) {
372                         $version = $versionCounts[$key]['version'];
373
374                         $parts = explode(' ', trim($version));
375                         $version = array_shift($parts);
376
377                         if (empty($compacted[$version])) {
378                                 $compacted[$version] = $versionCounts[$key]['total'];
379                         } else {
380                                 $compacted[$version] += $versionCounts[$key]['total'];
381                         }
382                 }
383
384                 $versionCounts = [];
385                 foreach ($compacted as $version => $pl_total) {
386                         $versionCounts[] = ['version' => $version, 'total' => $pl_total];
387                 }
388
389                 return $versionCounts;
390         }
391
392         /**
393          * Reformat, sort and compact version numbers
394          *
395          * @param array $versionCounts list of version numbers
396          * @return array with reformatted version numbers
397          */
398         private static function sortVersion(array $versionCounts)
399         {
400                 //
401                 // clean up version numbers
402                 //
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')];
408                         }
409                 }
410
411                 // Assure that the versions are sorted correctly
412                 $v2 = [];
413                 $versions = [];
414                 foreach ($versionCounts as $vv) {
415                         $version = trim(strip_tags($vv["version"]));
416                         $v2[$version] = $vv;
417                         $versions[] = $version;
418                 }
419
420                 usort($versions, 'version_compare');
421
422                 $versionCounts = [];
423                 foreach ($versions as $version) {
424                         $versionCounts[] = $v2[$version];
425                 }
426
427                 return $versionCounts;
428         }
429 }