From 6c9725133c24a1c4bcc293333344997e864c3d53 Mon Sep 17 00:00:00 2001 From: Alexander Batischev Date: Tue, 27 Jun 2023 00:21:28 +0300 Subject: [PATCH] Add script for updating source/_data/numbers.json The JSON it produces is slightly different from what we had before: 1. indentation is a bit different; 2. it uses non-breaking space as thousands separator. I discussed this with @lostinlight and persuaded her that NBSP is less confusing than commas, because different cultures use different separators; 3. `lastUpdate` uses a format like "27 Jun 2023" rather than "27/06/23". Again, I discussed this with @lostinlight and she agreed that the new format is less confusing. Fixes #152. --- source/_data/numbers.json | 250 +++++++++++++++++--------------------- update-numbers.py | 93 ++++++++++++++ 2 files changed, 206 insertions(+), 137 deletions(-) create mode 100755 update-numbers.py diff --git a/source/_data/numbers.json b/source/_data/numbers.json index 2b578b7..4434a4c 100644 --- a/source/_data/numbers.json +++ b/source/_data/numbers.json @@ -1,139 +1,115 @@ - { - "list": - { - "lastUpdate": "05/01/23", - "diaspora": - { - "population": "638.079", - "activeUsers": "33.328", - "podsNum": "134" - }, - "friendica": - { - "population": "20.923", - "activeUsers": "6.990", - "podsNum": "423" - }, - "hubzilla": - { - "population": "4.917", - "activeUsers": "2.197", - "podsNum": "99" - }, - "mastodon": - { - "population": "6.466.240", - "activeUsers": "4.213.494", - "podsNum": "12.573" - }, - "gnusocial": - { - "population": "2.786", - "activeUsers": "910", - "podsNum": "24" - }, - "pleroma": - { - "population": "125.595", - "activeUsers": "53.669", - "podsNum": "1.661" - }, - "misskey": - { - "population": "37.056", - "activeUsers": "19.771", - "podsNum": "500" - }, - "peertube": - { - "population": "309.723", - "activeUsers": "90.525", - "podsNum": "1.194" - }, - "pixelfed": - { - "population": "124.005", - "activeUsers": "53.107", - "podsNum": "450" - }, - "funkwhale": - { - "population": "5.253", - "activeUsers": "2.112", - "podsNum": "139" - }, - "writefreely": - { - "population": "61.058", - "activeUsers": "8.151", - "podsNum": "567" - }, - "plume": - { - "population": "23.557", - "activeUsers": "", - "podsNum": "63" - }, - "mobilizon": - { - "population": "12.960", - "activeUsers": "", - "podsNum": "116" - }, - "lemmy": - { - "population": "39.610", - "activeUsers": "3.922", - "podsNum": "87" - }, - "gotosocial": - { - "population": "497", - "activeUsers": "", - "podsNum": "497" - }, - "owncast": - { - "population": "158", - "activeUsers": "158", - "podsNum": "186" - }, - "bookwyrm": - { - "population": "10.791", - "activeUsers": "8.014", - "podsNum": "65" - }, - "microblogpub": - { - "population": "84", - "activeUsers": "53", - "podsNum": "100" - }, - "akkoma": - { - "population": "9.425", - "activeUsers": "1.076", - "podsNum": "446" - }, - "calckey": - { - "population": "2.700", - "activeUsers": "2.265", - "podsNum": "108" - }, - "hometown": - { - "population": "10.304", - "activeUsers": "8.537", - "podsNum": "144" - }, - "fediverse": - { - "population": "7.905.721", - "activeUsers": "4.508.331", - "podsNum": "19.576" - } + "list": { + "lastUpdate": "27 Jun 2023", + "diaspora": { + "population": "640\u00a0909", + "activeUsers": "22\u00a0827", + "podsNum": "103" + }, + "friendica": { + "population": "20\u00a0553", + "activeUsers": "5\u00a0285", + "podsNum": "366" + }, + "hubzilla": { + "population": "5\u00a0163", + "activeUsers": "2\u00a0138", + "podsNum": "78" + }, + "mastodon": { + "population": "7\u00a0523\u00a0173", + "activeUsers": "3\u00a0012\u00a0024", + "podsNum": "11\u00a0580" + }, + "gnusocial": { + "population": "314", + "activeUsers": "266", + "podsNum": "21" + }, + "pleroma": { + "population": "104\u00a0287", + "activeUsers": "34\u00a0135", + "podsNum": "1\u00a0184" + }, + "misskey": { + "population": "288\u00a0957", + "activeUsers": "27\u00a0181", + "podsNum": "747" + }, + "peertube": { + "population": "280\u00a0741", + "activeUsers": "61\u00a0665", + "podsNum": "1\u00a0133" + }, + "pixelfed": { + "population": "154\u00a0117", + "activeUsers": "50\u00a0221", + "podsNum": "447" + }, + "funkwhale": { + "population": "8\u00a0547", + "activeUsers": "1\u00a0697", + "podsNum": "118" + }, + "writefreely": { + "population": "84\u00a0940", + "activeUsers": "15\u00a0065", + "podsNum": "548" + }, + "plume": { + "population": "23\u00a0810", + "activeUsers": "1", + "podsNum": "47" + }, + "mobilizon": { + "population": "17\u00a0141", + "activeUsers": "17", + "podsNum": "112" + }, + "lemmy": { + "population": "2\u00a0502\u00a0391", + "activeUsers": "47\u00a0729", + "podsNum": "980" + }, + "gotosocial": { + "population": "912", + "activeUsers": "38", + "podsNum": "595" + }, + "owncast": { + "population": "206", + "activeUsers": "206", + "podsNum": "206" + }, + "bookwyrm": { + "population": "15\u00a0126", + "activeUsers": "8\u00a0161", + "podsNum": "98" + }, + "microblogpub": { + "population": "80", + "activeUsers": "63", + "podsNum": "80" + }, + "akkoma": { + "population": "14\u00a0262", + "activeUsers": "13\u00a0789", + "podsNum": "498" + }, + "calckey": { + "population": "8\u00a0015", + "activeUsers": "6\u00a0217", + "podsNum": "339" + }, + "hometown": { + "population": "14\u00a0458", + "activeUsers": "13\u00a0043", + "podsNum": "160" + }, + "fediverse": { + "population": "11\u00a0708\u00a0102", + "activeUsers": "3\u00a0321\u00a0768", + "podsNum": "19\u00a0440" } -} + } +} \ No newline at end of file diff --git a/update-numbers.py b/update-numbers.py new file mode 100755 index 0000000..7e712f1 --- /dev/null +++ b/update-numbers.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +import datetime +import json +import requests + +SOFTWARES = ['diaspora', 'friendica', 'hubzilla', 'mastodon', 'gnusocial', + 'pleroma', 'misskey', 'peertube', 'pixelfed', 'funkwhale', + 'writefreely', 'plume', 'mobilizon', 'lemmy', 'gotosocial', + 'owncast', 'bookwyrm', 'microblogpub', 'akkoma', 'calckey', + 'hometown'] + +def number_to_string(number): + """ + Format number as string, using non-breaking space as a thousands separator. + """ + return f'{number:,}'.replace(',', '\u00a0') + +class SoftwareNumbers: + """ + Contains numbers for a given software, or for Fediverse overall. + """ + def __init__(self, softwarename): + self.softwarename = softwarename + self.population = 0 + self.active_users = 0 + self.pods_num = 0 + + def __add__(self, other): + result = SoftwareNumbers(self.softwarename) + result.population = self.population + other.population + result.active_users = self.active_users + other.active_users + result.pods_num = self.pods_num + other.pods_num + return result + + def as_object(self): + result = {} + result[self.softwarename] = {} + result[self.softwarename]['population'] = number_to_string(self.population) + result[self.softwarename]['activeUsers'] = number_to_string(self.active_users) + result[self.softwarename]['podsNum'] = number_to_string(self.pods_num) + return result + +class StatsFetcher: + """ + Fetches Fediverse numbers and produces a report. + """ + def __init__(self): + self.session = requests.Session() + + def __get_node_stats_for_software(self, softwarename): + query = '{ nodes(status: "UP", softwarename: "' \ + + softwarename \ + + '") { total_users, active_users_halfyear } }' + response = self.session.post('https://api.fediverse.observer/', data={'query': query}) + return response.json().get('data', {}).get('nodes', []) + + def __calculate_stats_for_software(self, softwarename): + node_stats = self.__get_node_stats_for_software(softwarename) + + result = SoftwareNumbers(softwarename) + result.pods_num = len(node_stats) + for node in node_stats: + result.population += node.get('total_users', 0) + result.active_users += node.get('active_users_halfyear', 0) + + return result + + def get_numbers(self): + """ + Obtain an object with numbers for each of the supported Fediverse + platforms, and also a "fediverse" entry with totals. + """ + numbers = {} + numbers['lastUpdate'] = datetime.date.today().strftime('%d %b %Y') + + fediverse = SoftwareNumbers('fediverse') + for i, software in enumerate(SOFTWARES): + print(f'({i+1:2}/{len(SOFTWARES):2}) Fetching stats for {software}...') + stats = self.__calculate_stats_for_software(software) + fediverse += stats + numbers |= stats.as_object() + + numbers |= fediverse.as_object() + return { 'list': numbers } + +if __name__ == '__main__': + fetcher = StatsFetcher() + numbers = fetcher.get_numbers() + FILEPATH = 'source/_data/numbers.json' + with open(FILEPATH, 'w') as f: + f.write(json.dumps(numbers, indent=2)) + print(f'{FILEPATH} updated.')