From 5cf0fa27a95f8c56280ea429dd1d57529fc490ee Mon Sep 17 00:00:00 2001 From: halcy Date: Sun, 27 Nov 2022 02:43:22 +0200 Subject: [PATCH] add admin stats APIs --- TODO.md | 4 +- docs/index.rst | 49 ++++++- mastodon/Mastodon.py | 139 +++++++++++++++++- tests/cassettes/test_admin_stats.yaml | 201 ++++++++++++++++++++++++++ tests/setup.sql | 5 + tests/test_admin.py | 48 ++++++ 6 files changed, 436 insertions(+), 10 deletions(-) create mode 100644 tests/cassettes/test_admin_stats.yaml diff --git a/TODO.md b/TODO.md index 8e5dad4..b61cee2 100644 --- a/TODO.md +++ b/TODO.md @@ -44,7 +44,7 @@ Refer to mastodon changelog and API docs for details when implementing, add or m * [x] Add support for incoming edited posts * [x] Add notifications for posts deleted by moderators <- by email. not actually API relevant. * [x] Add explore page with trending posts and links -* [ ] Add graphs and retention metrics to admin dashboard +* [x] Add graphs and retention metrics to admin dashboard * [ ] Add GET /api/v1/accounts/familiar_followers to REST API * [ ] Add POST /api/v1/accounts/:id/remove_from_followers to REST API * [x] Add category and rule_ids params to POST /api/v1/reports IN REST API @@ -55,7 +55,7 @@ Refer to mastodon changelog and API docs for details when implementing, add or m 3.5.3 ----- -* [ ] Add limited attribute to accounts in REST API +* [later with tool to update dicts] Add limited attribute to accounts in REST API 4.0.0 and beyond ---------------- diff --git a/docs/index.rst b/docs/index.rst index 95ae0aa..e7d52dc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -345,6 +345,18 @@ Toot dicts 'poll': # A poll dict if a poll is attached to this status. } +Status edit dicts +~~~~~~~~~~~~~~~~~ +.. _status edit dict: + +.. code-block:: python + + mastodonstatus_history(id)[0] + # Returns the following dictionary + { + TODO + } + Mention dicts ~~~~~~~~~~~~~ .. _mention dict: @@ -902,13 +914,37 @@ Admin domain block dicts 'obfuscate': #Boolean. True if domain name is obfuscated when listing. } -Status edit dicts -~~~~~~~~~~~~~~~~~ -.. _status edit dict: +Admin measure dicts +~~~~~~~~~~~~~~~~~~~ +.. _admin measure dict: .. code-block:: python - mastodonstatus_history(id)[0] + api.admin_measures(datetime.now() - timedelta(hours=24*5), datetime.now(), active_users=True) + # Returns the following dictionary + { + TODO + } + +Admin dimension dicts +~~~~~~~~~~~~~~~~~~~~~ +.. _admin dimension dict: + +.. code-block:: python + + api.admin_dimensions(datetime.now() - timedelta(hours=24*5), datetime.now(), languages=True) + # Returns the following dictionary + { + TODO + } + +Admin retention dicts +~~~~~~~~~~~~~~~~~~~~~ +.. _admin retention dict: + +.. code-block:: python + + api.admin_retention(datetime.now() - timedelta(hours=24*5), datetime.now()) # Returns the following dictionary { TODO @@ -1471,6 +1507,11 @@ have admin: scopes attached with a lot of care, but be extra careful with those .. automethod:: Mastodon.admin_update_domain_block .. automethod:: Mastodon.admin_delete_domain_block +.. automethod:: Mastodon.admin_measures +.. automethod:: Mastodon.admin_dimensions +.. automethod:: Mastodon.admin_retention + + Acknowledgements ---------------- Mastodon.py contains work by a large number of contributors, many of which have diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index 82e058c..03aa413 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py @@ -108,7 +108,6 @@ def api_version(created_ver, last_changed_ver, return_value_ver): raise MastodonVersionError( "Version check failed (Need version " + version + ")") elif major == self.mastodon_major and minor > self.mastodon_minor: - print(self.mastodon_minor) raise MastodonVersionError( "Version check failed (Need version " + version + ")") elif major == self.mastodon_major and minor == self.mastodon_minor and patch > self.mastodon_patch: @@ -264,6 +263,9 @@ class Mastodon: __DICT_VERSION_ANNOUNCEMENT = bigger_version("3.1.0", __DICT_VERSION_REACTION) __DICT_VERSION_STATUS_EDIT = "3.5.0" __DICT_VERSION_ADMIN_DOMAIN_BLOCK = "4.0.0" + __DICT_VERSION_ADMIN_MEASURE = "3.5.0" + __DICT_VERSION_ADMIN_DIMENSION = "3.5.0" + __DICT_VERSION_ADMIN_RETENTION = "3.5.0" ### # Registering apps @@ -432,7 +434,6 @@ class Mastodon: try_base_url = secret_file.readline().rstrip() if try_base_url is not None and len(try_base_url) != 0: try_base_url = Mastodon.__protocolize(try_base_url) - print(self.api_base_url, try_base_url) if not (self.api_base_url is None or try_base_url == self.api_base_url): raise MastodonIllegalArgumentError('Mismatch in base URLs between files and/or specified') self.api_base_url = try_base_url @@ -544,7 +545,6 @@ class Mastodon: We parse this from the hopefully present "Date" header, but make no effort to compensate for latency. """ response = self.__api_request("HEAD", "/", return_response_object=True) - print(response.headers) if 'Date' in response.headers: server_time_datetime = dateutil.parser.parse(response.headers['Date']) @@ -3456,6 +3456,130 @@ class Mastodon: else: raise AttributeError("You must provide an id of an existing domain block to remove it.") + @api_version("3.5.0", "3.5.0", __DICT_VERSION_ADMIN_MEASURE) + def admin_measures(self, start_at, end_at, active_users=False, new_users=False, interactions=False, opened_reports = False, resolved_reports=False, + tag_accounts=None, tag_uses=None, tag_servers=None, instance_accounts=None, instance_media_attachments=None, instance_reports=None, + instance_statuses=None, instance_follows=None, instance_followers=None): + """ + Retrieves numerical instance information for the time period (at day granularity) between `start_at` and `end_at`. + + * `active_users`: Pass true to retrieve the number of active users on your instance within the time period + * `new_users`: Pass true to retrieve the number of users who joined your instance within the time period + * `interactions`: Pass true to retrieve the number of interactions (favourites, boosts, replies) on local statuses within the time period + * `opened_reports`: Pass true to retrieve the number of reports filed within the time period + * `resolved_reports` = Pass true to retrieve the number of reports resolved within the time period + * `tag_accounts`: Pass a tag ID to get the number of accounts which used that tag in at least one status within the time period + * `tag_uses`: Pass a tag ID to get the number of statuses which used that tag within the time period + * `tag_servers`: Pass a tag ID to to get the number of remote origin servers for statuses which used that tag within the time period + * `instance_accounts`: Pass a domain to get the number of accounts originating from that remote domain within the time period + * `instance_media_attachments`: Pass a domain to get the amount of space used by media attachments from that remote domain within the time period + * `instance_reports`: Pass a domain to get the number of reports filed against accounts from that remote domain within the time period + * `instance_statuses`: Pass a domain to get the number of statuses originating from that remote domain within the time period + * `instance_follows`: Pass a domain to get the number of accounts from a remote domain followed by that local user within the time period + * `instance_followers`: Pass a domain to get the number of local accounts followed by accounts from that remote domain within the time period + + This API call is relatively expensive - watch your servers load if you want to get a lot of statistical data. Especially the instance_statuses stats + might take a long time to compute and, in fact, time out. + + There is currently no way to get tag IDs implemented in Mastodon.py, because the Mastodon public API does not implement one. This will be fixed in a future + release. + + Returns a list of `admin measure dicts`_. + """ + params_init = locals() + keys = [] + for key in ["active_users", "new_users", "interactions", "opened_reports", "resolved_reports"]: + if params_init[key] == True: + keys.append(key) + + params = {} + for key in ["tag_accounts", "tag_uses", "tag_servers"]: + if params_init[key] is not None: + keys.append(key) + params[key] = {"id": self.__unpack_id(params_init[key])} + for key in ["instance_accounts", "instance_media_attachments", "instance_reports", "instance_statuses", "instance_follows", "instance_followers"]: + if params_init[key] is not None: + keys.append(key) + params[key] = {"domain": Mastodon.__deprotocolize(params_init[key]).split("/")[0]} + + if len(keys) == 0: + raise MastodonIllegalArgumentError("Must request at least one metric.") + + params["keys"] = keys + params["start_at"] = self.__consistent_isoformat_utc(start_at) + params["end_at"] = self.__consistent_isoformat_utc(end_at) + + return self.__api_request('POST', '/api/v1/admin/measures', params, use_json=True) + + @api_version("3.5.0", "3.5.0", __DICT_VERSION_ADMIN_DIMENSION) + def admin_dimensions(self, start_at, end_at, limit=None, languages=False, sources=False, servers=False, space_usage=False, software_versions=False, + tag_servers=None, tag_languages=None, instance_accounts=None, instance_languages=None): + """ + Retrieves primarily categorical instance information for the time period (at day granularity) between `start_at` and `end_at`. + + * `languages`: Pass true to get the most-used languages on this server + * `sources`: Pass true to get the most-used client apps on this server + * `servers`: Pass true to get the remote servers with the most statuses + * `space_usage`: Pass true to get the how much space is used by different components your software stack + * `software_versions`: Pass true to get the version numbers for your software stack + * `tag_servers`: Pass a tag ID to get the most-common servers for statuses including a trending tag + * `tag_languages`: Pass a tag ID to get the most-used languages for statuses including a trending tag + * `instance_accounts`: Pass a domain to get the most-followed accounts from a remote server + * `instance_languages`: Pass a domain to get the most-used languages from a remote server + + Pass `limit` to set how many results you want on queries where that makes sense. + + This API call is relatively expensive - watch your servers load if you want to get a lot of statistical data. + + There is currently no way to get tag IDs implemented in Mastodon.py, because the Mastodon public API does not implement one. This will be fixed in a future + release. + + Returns a list of `admin dimensions dicts`_. + """ + params_init = locals() + keys = [] + for key in ["languages", "sources", "servers", "space_usage", "software_versions"]: + if params_init[key] == True: + keys.append(key) + + params = {} + for key in ["tag_servers", "tag_languages"]: + if params_init[key] is not None: + keys.append(key) + params[key] = {"id": self.__unpack_id(params_init[key])} + for key in ["instance_accounts", "instance_languages"]: + if params_init[key] is not None: + keys.append(key) + params[key] = {"domain": Mastodon.__deprotocolize(params_init[key]).split("/")[0]} + + if len(keys) == 0: + raise MastodonIllegalArgumentError("Must request at least one dimension.") + + params["keys"] = keys + if limit is not None: + params["limit"] = limit + params["start_at"] = self.__consistent_isoformat_utc(start_at) + params["end_at"] = self.__consistent_isoformat_utc(end_at) + + return self.__api_request('POST', '/api/v1/admin/dimensions', params, use_json=True) + + @api_version("3.5.0", "3.5.0", __DICT_VERSION_ADMIN_RETENTION) + def admin_retention(self, start_at, end_at, frequency="day"): + """ + Gets user retention statistics (at `frequency` - "day" or "month" - granularity) between `start_at` and `end_at`. + + Returns a list of `admin retention dicts`_ + """ + if not frequency in ["day", "month"]: + raise MastodonIllegalArgumentError("Frequency must be day or month") + + params = { + "start_at": self.__consistent_isoformat_utc(start_at), + "end_at": self.__consistent_isoformat_utc(end_at), + "frequency": frequency + } + return self.__api_request('POST', '/api/v1/admin/retention', params) + ### # Push subscription crypto utilities ### @@ -3942,7 +4066,6 @@ class Mastodon: if not response_object.ok: try: response = response_object.json(object_hook=self.__json_hooks) - print(response) if isinstance(response, dict) and 'error' in response: error_msg = response['error'] elif isinstance(response, str): @@ -4348,6 +4471,14 @@ class Mastodon: base_url = base_url.rstrip("/") return base_url + @staticmethod + def __deprotocolize(base_url): + """Internal helper to strip http and https from a URL""" + if base_url.startswith("http://"): + base_url = base_url[7:] + elif base_url.startswith("https://") or base_url.startswith("onion://"): + base_url = base_url[8:] + return base_url ## # Exceptions diff --git a/tests/cassettes/test_admin_stats.yaml b/tests/cassettes/test_admin_stats.yaml new file mode 100644 index 0000000..dc88b9b --- /dev/null +++ b/tests/cassettes/test_admin_stats.yaml @@ -0,0 +1,201 @@ +interactions: +- request: + body: '{"instance_accounts": {"domain": "chitter.xyz"}, "instance_media_attachments": + {"domain": "chitter.xyz"}, "instance_reports": {"domain": "chitter.xyz"}, "instance_statuses": + {"domain": "chitter.xyz"}, "instance_follows": {"domain": "chitter.xyz"}, "instance_followers": + {"domain": "chitter.xyz"}, "keys": ["active_users", "new_users", "opened_reports", + "resolved_reports", "instance_accounts", "instance_media_attachments", "instance_reports", + "instance_statuses", "instance_follows", "instance_followers"], "start_at": + "2022-11-22T00:42:51+00:00", "end_at": "2022-11-27T00:42:51+00:00"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer __MASTODON_PY_TEST_ACCESS_TOKEN_2 + Connection: + - keep-alive + Content-Length: + - '587' + Content-Type: + - application/json + User-Agent: + - tests/v311 + method: POST + uri: http://localhost:3000/api/v1/admin/measures + response: + body: + string: '[{"key":"active_users","unit":null,"total":"2","previous_total":"0","data":[{"date":"2022-11-22T00:00:00Z","value":"0"},{"date":"2022-11-23T00:00:00Z","value":"0"},{"date":"2022-11-24T00:00:00Z","value":"0"},{"date":"2022-11-25T00:00:00Z","value":"0"},{"date":"2022-11-26T00:00:00Z","value":"2"}]},{"key":"new_users","unit":null,"total":"4","previous_total":"0","data":[{"date":"2022-11-22T00:00:00.000+00:00","value":"0"},{"date":"2022-11-23T00:00:00.000+00:00","value":"0"},{"date":"2022-11-24T00:00:00.000+00:00","value":"0"},{"date":"2022-11-25T00:00:00.000+00:00","value":"0"},{"date":"2022-11-26T00:00:00.000+00:00","value":"4"},{"date":"2022-11-27T00:00:00.000+00:00","value":"0"}]},{"key":"opened_reports","unit":null,"total":"0","previous_total":"0","data":[{"date":"2022-11-22T00:00:00.000+00:00","value":"0"},{"date":"2022-11-23T00:00:00.000+00:00","value":"0"},{"date":"2022-11-24T00:00:00.000+00:00","value":"0"},{"date":"2022-11-25T00:00:00.000+00:00","value":"0"},{"date":"2022-11-26T00:00:00.000+00:00","value":"0"},{"date":"2022-11-27T00:00:00.000+00:00","value":"0"}]},{"key":"resolved_reports","unit":null,"total":"0","previous_total":"0","data":[{"date":"2022-11-22T00:00:00.000+00:00","value":"0"},{"date":"2022-11-23T00:00:00.000+00:00","value":"0"},{"date":"2022-11-24T00:00:00.000+00:00","value":"0"},{"date":"2022-11-25T00:00:00.000+00:00","value":"0"},{"date":"2022-11-26T00:00:00.000+00:00","value":"0"},{"date":"2022-11-27T00:00:00.000+00:00","value":"0"}]},{"key":"instance_accounts","unit":null,"total":"0","data":[{"date":"2022-11-22T00:00:00.000+00:00","value":"0"},{"date":"2022-11-23T00:00:00.000+00:00","value":"0"},{"date":"2022-11-24T00:00:00.000+00:00","value":"0"},{"date":"2022-11-25T00:00:00.000+00:00","value":"0"},{"date":"2022-11-26T00:00:00.000+00:00","value":"0"},{"date":"2022-11-27T00:00:00.000+00:00","value":"0"}]},{"key":"instance_media_attachments","unit":"bytes","total":"0","human_value":"0 + Bytes","data":[{"date":"2022-11-22T00:00:00.000+00:00","value":""},{"date":"2022-11-23T00:00:00.000+00:00","value":""},{"date":"2022-11-24T00:00:00.000+00:00","value":""},{"date":"2022-11-25T00:00:00.000+00:00","value":""},{"date":"2022-11-26T00:00:00.000+00:00","value":""},{"date":"2022-11-27T00:00:00.000+00:00","value":""}]},{"key":"instance_reports","unit":null,"total":"0","data":[{"date":"2022-11-22T00:00:00.000+00:00","value":"0"},{"date":"2022-11-23T00:00:00.000+00:00","value":"0"},{"date":"2022-11-24T00:00:00.000+00:00","value":"0"},{"date":"2022-11-25T00:00:00.000+00:00","value":"0"},{"date":"2022-11-26T00:00:00.000+00:00","value":"0"},{"date":"2022-11-27T00:00:00.000+00:00","value":"0"}]},{"key":"instance_statuses","unit":null,"total":"0","data":[{"date":"2022-11-22T00:00:00.000+00:00","value":"0"},{"date":"2022-11-23T00:00:00.000+00:00","value":"0"},{"date":"2022-11-24T00:00:00.000+00:00","value":"0"},{"date":"2022-11-25T00:00:00.000+00:00","value":"0"},{"date":"2022-11-26T00:00:00.000+00:00","value":"0"},{"date":"2022-11-27T00:00:00.000+00:00","value":"0"}]},{"key":"instance_follows","unit":null,"total":"0","data":[{"date":"2022-11-22T00:00:00.000+00:00","value":"0"},{"date":"2022-11-23T00:00:00.000+00:00","value":"0"},{"date":"2022-11-24T00:00:00.000+00:00","value":"0"},{"date":"2022-11-25T00:00:00.000+00:00","value":"0"},{"date":"2022-11-26T00:00:00.000+00:00","value":"0"},{"date":"2022-11-27T00:00:00.000+00:00","value":"0"}]},{"key":"instance_followers","unit":null,"total":"0","data":[{"date":"2022-11-22T00:00:00.000+00:00","value":"0"},{"date":"2022-11-23T00:00:00.000+00:00","value":"0"},{"date":"2022-11-24T00:00:00.000+00:00","value":"0"},{"date":"2022-11-25T00:00:00.000+00:00","value":"0"},{"date":"2022-11-26T00:00:00.000+00:00","value":"0"},{"date":"2022-11-27T00:00:00.000+00:00","value":"0"}]}]' + headers: + Cache-Control: + - no-store + Content-Security-Policy: + - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src + ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; + style-src ''self'' http://localhost:3000 ''nonce-drS6KPeE5pwtqRFGPVh3ww==''; + media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' + https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' + data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 + ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' + ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; + worker-src ''self'' blob: http://localhost:3000' + Content-Type: + - application/json; charset=utf-8 + ETag: + - W/"bb40e02b66cfdf5be1ff5a980c8242af" + Referrer-Policy: + - strict-origin-when-cross-origin + Transfer-Encoding: + - chunked + Vary: + - Accept, Origin + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 11b2cb9c-d3c4-41ba-802d-888e1ee62c9e + X-Runtime: + - '0.540102' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: '{"instance_accounts": {"domain": "chitter.xyz"}, "instance_languages": + {"domain": "chitter.xyz"}, "keys": ["languages", "sources", "servers", "space_usage", + "instance_accounts", "instance_languages"], "limit": 3, "start_at": "2022-11-22T00:42:52+00:00", + "end_at": "2022-11-27T00:42:52+00:00"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer __MASTODON_PY_TEST_ACCESS_TOKEN_2 + Connection: + - keep-alive + Content-Length: + - '292' + Content-Type: + - application/json + User-Agent: + - tests/v311 + method: POST + uri: http://localhost:3000/api/v1/admin/dimensions + response: + body: + string: '[{"key":"languages","data":[{"key":"de","human_key":"German","value":"1"}]},{"key":"sources","data":[{"key":"web","human_key":"Website","value":"4"}]},{"key":"servers","data":[]},{"key":"space_usage","data":[{"key":"postgresql","human_key":"PostgreSQL","value":"16610095","unit":"bytes","human_value":"15.8 + MB"},{"key":"redis","human_key":"Redis","value":"1560216","unit":"bytes","human_value":"1.49 + MB"},{"key":"media","human_key":"Media storage","value":"0","unit":"bytes","human_value":"0 + Bytes"}]},{"key":"instance_accounts","data":[]},{"key":"instance_languages","data":[]}]' + headers: + Cache-Control: + - no-store + Content-Security-Policy: + - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src + ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; + style-src ''self'' http://localhost:3000 ''nonce-tTPpzIcGAZb7y2EXyfEsFg==''; + media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' + https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' + data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 + ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' + ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; + worker-src ''self'' blob: http://localhost:3000' + Content-Type: + - application/json; charset=utf-8 + ETag: + - W/"b5c3e0d37fd2fdab9859f566f5b2fa2e" + Referrer-Policy: + - strict-origin-when-cross-origin + Transfer-Encoding: + - chunked + Vary: + - Accept, Origin + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 7332d9ca-00cb-4eaf-9d95-ee3baf8414f9 + X-Runtime: + - '0.066790' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: start_at=2022-11-17T00%3A42%3A52%2B00%3A00&end_at=2022-11-27T00%3A42%3A52%2B00%3A00&frequency=day + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer __MASTODON_PY_TEST_ACCESS_TOKEN_2 + Connection: + - keep-alive + Content-Length: + - '97' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - tests/v311 + method: POST + uri: http://localhost:3000/api/v1/admin/retention + response: + body: + string: '[{"period":"2022-11-17T00:00:00+00:00","frequency":"day","data":[{"date":"2022-11-17T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-18T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-19T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-20T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-21T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-22T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-23T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-24T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-25T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-26T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-27T00:00:00+00:00","rate":0.0,"value":"0"}]},{"period":"2022-11-18T00:00:00+00:00","frequency":"day","data":[{"date":"2022-11-18T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-19T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-20T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-21T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-22T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-23T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-24T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-25T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-26T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-27T00:00:00+00:00","rate":0.0,"value":"0"}]},{"period":"2022-11-19T00:00:00+00:00","frequency":"day","data":[{"date":"2022-11-19T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-20T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-21T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-22T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-23T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-24T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-25T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-26T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-27T00:00:00+00:00","rate":0.0,"value":"0"}]},{"period":"2022-11-20T00:00:00+00:00","frequency":"day","data":[{"date":"2022-11-20T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-21T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-22T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-23T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-24T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-25T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-26T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-27T00:00:00+00:00","rate":0.0,"value":"0"}]},{"period":"2022-11-21T00:00:00+00:00","frequency":"day","data":[{"date":"2022-11-21T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-22T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-23T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-24T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-25T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-26T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-27T00:00:00+00:00","rate":0.0,"value":"0"}]},{"period":"2022-11-22T00:00:00+00:00","frequency":"day","data":[{"date":"2022-11-22T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-23T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-24T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-25T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-26T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-27T00:00:00+00:00","rate":0.0,"value":"0"}]},{"period":"2022-11-23T00:00:00+00:00","frequency":"day","data":[{"date":"2022-11-23T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-24T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-25T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-26T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-27T00:00:00+00:00","rate":0.0,"value":"0"}]},{"period":"2022-11-24T00:00:00+00:00","frequency":"day","data":[{"date":"2022-11-24T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-25T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-26T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-27T00:00:00+00:00","rate":0.0,"value":"0"}]},{"period":"2022-11-25T00:00:00+00:00","frequency":"day","data":[{"date":"2022-11-25T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-26T00:00:00+00:00","rate":0.0,"value":"0"},{"date":"2022-11-27T00:00:00+00:00","rate":0.0,"value":"0"}]},{"period":"2022-11-26T00:00:00+00:00","frequency":"day","data":[{"date":"2022-11-26T00:00:00+00:00","rate":0.5,"value":"2"},{"date":"2022-11-27T00:00:00+00:00","rate":0.0,"value":"0"}]},{"period":"2022-11-27T00:00:00+00:00","frequency":"day","data":[{"date":"2022-11-27T00:00:00+00:00","rate":0.0,"value":"0"}]}]' + headers: + Cache-Control: + - no-store + Content-Security-Policy: + - 'base-uri ''none''; default-src ''none''; frame-ancestors ''none''; font-src + ''self'' http://localhost:3000; img-src ''self'' https: data: blob: http://localhost:3000; + style-src ''self'' http://localhost:3000 ''nonce-AkyM5KkEra/OBSMZSu3SqQ==''; + media-src ''self'' https: data: http://localhost:3000; frame-src ''self'' + https:; manifest-src ''self'' http://localhost:3000; connect-src ''self'' + data: blob: http://localhost:3000 http://localhost:3000 ws://localhost:4000 + ws://localhost:3035 http://localhost:3035; script-src ''self'' ''unsafe-inline'' + ''unsafe-eval'' http://localhost:3000; child-src ''self'' blob: http://localhost:3000; + worker-src ''self'' blob: http://localhost:3000' + Content-Type: + - application/json; charset=utf-8 + ETag: + - W/"c607f49eb27c19d561dab0434594a06e" + Referrer-Policy: + - strict-origin-when-cross-origin + Transfer-Encoding: + - chunked + Vary: + - Accept, Origin + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 67a94c6b-e1fe-4c60-90c5-478c98e3e0f7 + X-Runtime: + - '0.435749' + X-XSS-Protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/setup.sql b/tests/setup.sql index c9e908d..1d19bc8 100644 --- a/tests/setup.sql +++ b/tests/setup.sql @@ -27,6 +27,11 @@ UPDATE users SET encrypted_password = '$2a$10$8eAdhF69RiZiV0puZ.8iOOgMqBACmwJu8Z9X4CiN91iwRXbeC2jvi' WHERE email = 'mastodonpy_test_2@localhost:3000'; +UPDATE users SET + locale = 'de', + encrypted_password = '$2a$10$8eAdhF69RiZiV0puZ.8iOOgMqBACmwJu8Z9X4CiN91iwRXbeC2jvi' +WHERE email = 'zerocool@example.com'; + INSERT INTO oauth_applications ( id, name, diff --git a/tests/test_admin.py b/tests/test_admin.py index 887ed14..49d9876 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,5 +1,7 @@ import pytest import time +from datetime import datetime, timedelta +from mastodon import MastodonIllegalArgumentError @pytest.mark.vcr() def test_admin_accounts(api2): @@ -134,3 +136,49 @@ def test_admin_domain_blocks(api2): assert block3.private_comment == "jk ilu <3" api2.admin_delete_domain_block(block2) assert not block3.id in map(lambda x: x.id, api2.admin_domain_blocks()) + +@pytest.mark.vcr() +def test_admin_stats(api2): + assert api2.admin_measures( + datetime.now() - timedelta(hours=24*5), + datetime.now(), + active_users=True, + new_users=True, + opened_reports=True, + resolved_reports=True, + instance_accounts="chitter.xyz", + instance_media_attachments="chitter.xyz", + instance_reports="http://chitter.xyz/", + instance_statuses="chitter.xyz", + instance_follows="http://chitter.xyz", + instance_followers="chitter.xyz", + #tag_accounts=0, + #tag_uses=0, + #tag_servers=0, + ) + + assert api2.admin_dimensions( + datetime.now() - timedelta(hours=24*5), + datetime.now(), + limit=3, + languages=True, + sources=True, + servers=True, + space_usage=True, + #tag_servers=0, + #tag_languages=0, + instance_accounts="chitter.xyz", + instance_languages="https://chitter.xyz" + ) + + api2.admin_retention( + datetime.now() - timedelta(days=10), + datetime.now() + ) + + with pytest.raises(MastodonIllegalArgumentError): + api2.admin_retention( + datetime.now() - timedelta(days=10), + datetime.now(), + frequency="dayz" + ) \ No newline at end of file