longwei 1.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- longwei/__init__.py +74 -0
- longwei/_mixin_auth.py +218 -0
- longwei/_mixin_credentials.py +103 -0
- longwei/_mixin_request_helpers.py +115 -0
- longwei/_mixin_statuses.py +386 -0
- longwei/_mixin_timelines.py +265 -0
- longwei/client_2_server.py +117 -0
- longwei/constants.py +36 -0
- longwei/exceptions.py +176 -0
- longwei/models.py +296 -0
- longwei/types.py +9 -0
- longwei/utils.py +139 -0
- longwei-1.0.0.dev0.dist-info/METADATA +121 -0
- longwei-1.0.0.dev0.dist-info/RECORD +15 -0
- longwei-1.0.0.dev0.dist-info/WHEEL +4 -0
longwei/__init__.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Package 'longwei' level definitions."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
from longwei.constants import INSTANCE_TYPE_MASTODON
|
|
9
|
+
from longwei.constants import INSTANCE_TYPE_PLEROMA
|
|
10
|
+
from longwei.constants import INSTANCE_TYPE_TAKAHE
|
|
11
|
+
from longwei.exceptions import ActivityPubError
|
|
12
|
+
from longwei.exceptions import ApiError
|
|
13
|
+
from longwei.exceptions import ClientError
|
|
14
|
+
from longwei.exceptions import ConflictError
|
|
15
|
+
from longwei.exceptions import ForbiddenError
|
|
16
|
+
from longwei.exceptions import GoneError
|
|
17
|
+
from longwei.exceptions import NetworkError
|
|
18
|
+
from longwei.exceptions import NotFoundError
|
|
19
|
+
from longwei.exceptions import RatelimitError
|
|
20
|
+
from longwei.exceptions import ServerError
|
|
21
|
+
from longwei.exceptions import UnauthorizedError
|
|
22
|
+
from longwei.exceptions import UnprocessedError
|
|
23
|
+
from longwei.models import Account
|
|
24
|
+
from longwei.types import ActivityPubClass
|
|
25
|
+
from longwei.types import Status
|
|
26
|
+
|
|
27
|
+
__display_name__: Final[str] = "Longwei"
|
|
28
|
+
__version__: Final[str] = version(str(__package__))
|
|
29
|
+
|
|
30
|
+
USER_AGENT: Final[str] = f"{__display_name__}_v{__version__}_Python_{sys.version.split()[0]}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Visibility(str, Enum):
|
|
34
|
+
"""Enumerating possible Visibility values for statuses."""
|
|
35
|
+
|
|
36
|
+
PUBLIC = "public"
|
|
37
|
+
UNLISTED = "unlisted"
|
|
38
|
+
PRIVATE = "private"
|
|
39
|
+
DIRECT = "direct"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SearchType(str, Enum):
|
|
43
|
+
"""Enumerating possible type values for status searches."""
|
|
44
|
+
|
|
45
|
+
ACCOUNTS = "accounts"
|
|
46
|
+
HASHTAGS = "hashtags"
|
|
47
|
+
STATUSES = "statuses"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = [ # noqa: RUF022
|
|
51
|
+
"Account",
|
|
52
|
+
"ActivityPubClass",
|
|
53
|
+
"ActivityPubError",
|
|
54
|
+
"ApiError",
|
|
55
|
+
"ClientError",
|
|
56
|
+
"ConflictError",
|
|
57
|
+
"__display_name__",
|
|
58
|
+
"ForbiddenError",
|
|
59
|
+
"GoneError",
|
|
60
|
+
"INSTANCE_TYPE_MASTODON",
|
|
61
|
+
"INSTANCE_TYPE_PLEROMA",
|
|
62
|
+
"INSTANCE_TYPE_TAKAHE",
|
|
63
|
+
"NetworkError",
|
|
64
|
+
"NotFoundError",
|
|
65
|
+
"RatelimitError",
|
|
66
|
+
"SearchType",
|
|
67
|
+
"ServerError",
|
|
68
|
+
"Status",
|
|
69
|
+
"UnauthorizedError",
|
|
70
|
+
"UnprocessedError",
|
|
71
|
+
"USER_AGENT",
|
|
72
|
+
"__version__",
|
|
73
|
+
"Visibility",
|
|
74
|
+
]
|
longwei/_mixin_auth.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Auth mixin: OAuth app creation, token acquisition, authorization URL generation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from urllib.parse import urlencode
|
|
6
|
+
|
|
7
|
+
from httpx import AsyncClient
|
|
8
|
+
from httpx import HTTPError
|
|
9
|
+
|
|
10
|
+
from longwei import USER_AGENT
|
|
11
|
+
from longwei import __display_name__
|
|
12
|
+
from longwei.constants import REDIRECT_URI
|
|
13
|
+
from longwei.exceptions import NetworkError
|
|
14
|
+
from longwei.utils import _check_exception
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__display_name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _AuthMixin:
|
|
20
|
+
"""Mixin providing static OAuth/authentication methods."""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
async def generate_authorization_url(
|
|
24
|
+
instance_url: str,
|
|
25
|
+
client_id: str,
|
|
26
|
+
user_agent: str = USER_AGENT,
|
|
27
|
+
) -> str:
|
|
28
|
+
"""Create URL to get access token interactively from website.
|
|
29
|
+
|
|
30
|
+
:param instance_url: The URL of the Mastodon instance you want to connect to
|
|
31
|
+
:param client_id: Client id of app as generated by create_app method
|
|
32
|
+
:param user_agent: User agent identifier to use. Defaults to longwei related one.
|
|
33
|
+
|
|
34
|
+
:returns: String containing URL to visit to get access token interactively from instance.
|
|
35
|
+
"""
|
|
36
|
+
logger.debug(
|
|
37
|
+
"ActivityPub.get_auth_token_interactive(instance_url=%s, client=...,client_id=%s, user_agent=%s)",
|
|
38
|
+
instance_url,
|
|
39
|
+
client_id,
|
|
40
|
+
user_agent,
|
|
41
|
+
)
|
|
42
|
+
if "http" not in instance_url:
|
|
43
|
+
instance_url = f"https://{instance_url}"
|
|
44
|
+
|
|
45
|
+
url_params = urlencode(
|
|
46
|
+
{
|
|
47
|
+
"response_type": "code",
|
|
48
|
+
"client_id": client_id,
|
|
49
|
+
"redirect_uri": REDIRECT_URI,
|
|
50
|
+
"scope": "read write",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
auth_url = f"{instance_url}/oauth/authorize?{url_params}"
|
|
54
|
+
logger.debug(
|
|
55
|
+
"ActivityPub.get_auth_token_interactive(...) -> %s",
|
|
56
|
+
auth_url,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return auth_url
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
async def validate_authorization_code(
|
|
63
|
+
client: AsyncClient,
|
|
64
|
+
instance_url: str,
|
|
65
|
+
authorization_code: str,
|
|
66
|
+
client_id: str,
|
|
67
|
+
client_secret: str,
|
|
68
|
+
) -> str:
|
|
69
|
+
"""Validate an authorization code and get access token needed for API access.
|
|
70
|
+
|
|
71
|
+
:param client: httpx.AsyncClient
|
|
72
|
+
:param instance_url: The URL of the Mastodon instance you want to connect to
|
|
73
|
+
:param authorization_code: authorization code
|
|
74
|
+
:param client_id: client id as returned by create_app method
|
|
75
|
+
:param client_secret: client secret as returned by create_app method
|
|
76
|
+
|
|
77
|
+
:returns: access token
|
|
78
|
+
"""
|
|
79
|
+
logger.debug(
|
|
80
|
+
"ActivityPub.validate_authorization_code(authorization_code=%s, client_id=%s, client_secret=<redacted>)",
|
|
81
|
+
authorization_code,
|
|
82
|
+
client_id,
|
|
83
|
+
)
|
|
84
|
+
if "http" not in instance_url:
|
|
85
|
+
instance_url = f"https://{instance_url}"
|
|
86
|
+
|
|
87
|
+
data = {
|
|
88
|
+
"client_id": client_id,
|
|
89
|
+
"client_secret": client_secret,
|
|
90
|
+
"scope": "read write",
|
|
91
|
+
"redirect_uri": REDIRECT_URI,
|
|
92
|
+
"grant_type": "authorization_code",
|
|
93
|
+
"code": authorization_code,
|
|
94
|
+
}
|
|
95
|
+
try:
|
|
96
|
+
response = await client.post(url=f"{instance_url}/oauth/token", data=data)
|
|
97
|
+
logger.debug(
|
|
98
|
+
"ActivityPub.validate_authorization_code - response:\n%s",
|
|
99
|
+
response,
|
|
100
|
+
)
|
|
101
|
+
await _check_exception(response)
|
|
102
|
+
response_dict = response.json()
|
|
103
|
+
except HTTPError as error:
|
|
104
|
+
raise NetworkError from error
|
|
105
|
+
|
|
106
|
+
logger.debug(
|
|
107
|
+
"ActivityPub.validate_authorization_code - response.json: \n%s",
|
|
108
|
+
json.dumps(response_dict, indent=4),
|
|
109
|
+
)
|
|
110
|
+
return str(response_dict["access_token"])
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
async def get_auth_token( # noqa: PLR0913 - No way around needing all this parameters
|
|
114
|
+
instance_url: str,
|
|
115
|
+
username: str,
|
|
116
|
+
password: str,
|
|
117
|
+
client: AsyncClient,
|
|
118
|
+
user_agent: str = USER_AGENT,
|
|
119
|
+
client_website: str = "https://pypi.org/project/longwei/",
|
|
120
|
+
) -> str:
|
|
121
|
+
"""Create an app and use it to get an access token.
|
|
122
|
+
|
|
123
|
+
:param instance_url: The URL of the Mastodon instance you want to connect to
|
|
124
|
+
:param username: The username of the account you want to get an auth_token for
|
|
125
|
+
:param password: The password of the account you want to get an auth_token for
|
|
126
|
+
:param client: httpx.AsyncClient
|
|
127
|
+
:param user_agent: User agent identifier to use. Defaults to longwei related one.
|
|
128
|
+
:param client_website: Link to site for user_agent. Defaults to link to longwei on Pypi.org
|
|
129
|
+
|
|
130
|
+
:returns: The access token is being returned.
|
|
131
|
+
"""
|
|
132
|
+
logger.debug(
|
|
133
|
+
"ActivityPub.get_auth_token(instance_url=%s, username=%s, password=<redacted>, client=..., "
|
|
134
|
+
"user_agent=%s, client_website=%s)",
|
|
135
|
+
instance_url,
|
|
136
|
+
username,
|
|
137
|
+
user_agent,
|
|
138
|
+
client_website,
|
|
139
|
+
)
|
|
140
|
+
if "http" not in instance_url:
|
|
141
|
+
instance_url = f"https://{instance_url}"
|
|
142
|
+
|
|
143
|
+
client_id, client_secret = await _AuthMixin.create_app(
|
|
144
|
+
client_website=client_website,
|
|
145
|
+
instance_url=instance_url,
|
|
146
|
+
client=client,
|
|
147
|
+
user_agent=user_agent,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
data = {
|
|
151
|
+
"client_id": client_id,
|
|
152
|
+
"client_secret": client_secret,
|
|
153
|
+
"scope": "read write",
|
|
154
|
+
"redirect_uris": REDIRECT_URI,
|
|
155
|
+
"grant_type": "password",
|
|
156
|
+
"username": username,
|
|
157
|
+
"password": password,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
response = await client.post(url=f"{instance_url}/oauth/token", data=data)
|
|
162
|
+
logger.debug("ActivityPub.get_auth_token - response:\n%s", response)
|
|
163
|
+
await _check_exception(response)
|
|
164
|
+
response_dict = response.json()
|
|
165
|
+
except HTTPError as error:
|
|
166
|
+
raise NetworkError from error
|
|
167
|
+
|
|
168
|
+
logger.debug(
|
|
169
|
+
"ActivityPub.get_auth_token - response.json: \n%s",
|
|
170
|
+
json.dumps(response_dict, indent=4),
|
|
171
|
+
)
|
|
172
|
+
return str(response_dict["access_token"])
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
async def create_app(
|
|
176
|
+
instance_url: str,
|
|
177
|
+
client: AsyncClient,
|
|
178
|
+
user_agent: str = USER_AGENT,
|
|
179
|
+
client_website: str = "https://pypi.org/project/longwei/",
|
|
180
|
+
) -> tuple[str, str]:
|
|
181
|
+
"""Create an app.
|
|
182
|
+
|
|
183
|
+
:param instance_url: The URL of the Mastodon instance you want to connect to
|
|
184
|
+
:param client: httpx.AsyncClient
|
|
185
|
+
:param user_agent: User agent identifier to use. Defaults to longwei related one.
|
|
186
|
+
:param client_website: Link to site for user_agent. Defaults to link to longwei on Pypi.org
|
|
187
|
+
|
|
188
|
+
:returns: tuple(client_id, client_secret)
|
|
189
|
+
"""
|
|
190
|
+
logger.debug(
|
|
191
|
+
"ActivityPub.create_app(instance_url=%s, client=..., user_agent=%s, client_website=%s)",
|
|
192
|
+
instance_url,
|
|
193
|
+
user_agent,
|
|
194
|
+
client_website,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if "http" not in instance_url:
|
|
198
|
+
instance_url = f"https://{instance_url}"
|
|
199
|
+
|
|
200
|
+
data = {
|
|
201
|
+
"client_name": user_agent,
|
|
202
|
+
"client_website": client_website,
|
|
203
|
+
"scopes": "read write",
|
|
204
|
+
"redirect_uris": REDIRECT_URI,
|
|
205
|
+
}
|
|
206
|
+
try:
|
|
207
|
+
response = await client.post(url=f"{instance_url}/api/v1/apps", data=data)
|
|
208
|
+
logger.debug("ActivityPub.create_app response: \n%s", response)
|
|
209
|
+
await _check_exception(response)
|
|
210
|
+
response_dict = response.json()
|
|
211
|
+
except HTTPError as error:
|
|
212
|
+
raise NetworkError from error
|
|
213
|
+
|
|
214
|
+
logger.debug(
|
|
215
|
+
"ActivityPub.create_app response.json: \n%s",
|
|
216
|
+
json.dumps(response_dict, indent=4),
|
|
217
|
+
)
|
|
218
|
+
return (response_dict["client_id"]), (response_dict["client_secret"])
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Credentials mixin: verify_credentials, determine_instance_type, _set_instance_parameters."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from httpx import HTTPError
|
|
8
|
+
|
|
9
|
+
from longwei import __display_name__
|
|
10
|
+
from longwei.constants import INSTANCE_TYPE_PLEROMA
|
|
11
|
+
from longwei.constants import INSTANCE_TYPE_TAKAHE
|
|
12
|
+
from longwei.constants import MAX_ATTACHMENTS_PLEROMA
|
|
13
|
+
from longwei.constants import MAX_ATTACHMENTS_TAKAHE
|
|
14
|
+
from longwei.exceptions import NetworkError
|
|
15
|
+
from longwei.models import Account
|
|
16
|
+
from longwei.types import ActivityPubClass
|
|
17
|
+
from longwei.utils import _check_exception
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__display_name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _CredentialsMixin:
|
|
23
|
+
"""Mixin providing credential verification and instance type detection."""
|
|
24
|
+
|
|
25
|
+
async def verify_credentials(self: ActivityPubClass) -> Account:
|
|
26
|
+
"""Verify the credentials of the user.
|
|
27
|
+
|
|
28
|
+
:returns: The account information for the authenticated user.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
url = f"{self.instance}/api/v1/accounts/verify_credentials"
|
|
32
|
+
logger.debug("ActivityPub.verify_credentials() - url=%s", url)
|
|
33
|
+
try:
|
|
34
|
+
response = await self.client.get(url=url, headers=self.headers, timeout=self.timeout)
|
|
35
|
+
logger.debug(
|
|
36
|
+
"ActivityPub.verify_credentials - response: \n%s",
|
|
37
|
+
response,
|
|
38
|
+
)
|
|
39
|
+
self._update_ratelimit(response.headers)
|
|
40
|
+
await _check_exception(response=response)
|
|
41
|
+
self._parse_next_prev(links=response.headers.get("Link"))
|
|
42
|
+
account = Account.model_validate(response.json())
|
|
43
|
+
except HTTPError as error:
|
|
44
|
+
raise NetworkError from error
|
|
45
|
+
|
|
46
|
+
return account
|
|
47
|
+
|
|
48
|
+
async def determine_instance_type(self: ActivityPubClass) -> None:
|
|
49
|
+
"""Check if the instance is a Pleroma instance or not."""
|
|
50
|
+
instance = self.instance
|
|
51
|
+
if "http" not in self.instance:
|
|
52
|
+
instance = f"https://{self.instance}"
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
response = await self.client.get(url=f"{instance}/api/v1/instance", timeout=self.timeout)
|
|
56
|
+
await _check_exception(response=response)
|
|
57
|
+
response_dict = response.json()
|
|
58
|
+
except HTTPError as error:
|
|
59
|
+
raise NetworkError from error
|
|
60
|
+
|
|
61
|
+
logger.debug(
|
|
62
|
+
"ActivityPub.determine_instance_type -> response.dict:\n%s",
|
|
63
|
+
json.dumps(response_dict, indent=4),
|
|
64
|
+
)
|
|
65
|
+
self.instance = instance
|
|
66
|
+
|
|
67
|
+
self._set_instance_parameters(response_dict)
|
|
68
|
+
|
|
69
|
+
logger.debug(
|
|
70
|
+
"ActivityPub.determine_instance_type() ... instance_type=%s",
|
|
71
|
+
self.instance_type,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _set_instance_parameters(self, response_dict: dict[str, Any]) -> None:
|
|
75
|
+
"""Set instance parameters from the /api/v1/instance response dict.
|
|
76
|
+
|
|
77
|
+
:param response_dict: Dictionary returned by call to .../api/v1/instance
|
|
78
|
+
"""
|
|
79
|
+
version = response_dict.get("version", "")
|
|
80
|
+
if INSTANCE_TYPE_TAKAHE in version:
|
|
81
|
+
self.instance_type = INSTANCE_TYPE_TAKAHE
|
|
82
|
+
self.max_attachments = MAX_ATTACHMENTS_TAKAHE
|
|
83
|
+
elif INSTANCE_TYPE_PLEROMA in version:
|
|
84
|
+
self.instance_type = INSTANCE_TYPE_PLEROMA
|
|
85
|
+
self.max_attachments = MAX_ATTACHMENTS_PLEROMA
|
|
86
|
+
|
|
87
|
+
instance_config = response_dict.get("configuration", {})
|
|
88
|
+
|
|
89
|
+
if (max_status_len := instance_config.get("statuses", {}).get("max_characters")) or (
|
|
90
|
+
max_status_len := response_dict.get("max_characters")
|
|
91
|
+
):
|
|
92
|
+
self.max_status_len = max_status_len
|
|
93
|
+
|
|
94
|
+
if (max_attachments := instance_config.get("statuses", {}).get("max_media_attachments")) or (
|
|
95
|
+
max_attachments := response_dict.get("max_media_attachments")
|
|
96
|
+
):
|
|
97
|
+
self.max_attachments = max_attachments
|
|
98
|
+
|
|
99
|
+
if max_att_size := instance_config.get("media_attachments", {}).get("image_size_limit"):
|
|
100
|
+
self.max_att_size = max_att_size
|
|
101
|
+
|
|
102
|
+
if mime_types := instance_config.get("media_attachments", {}).get("supported_mime_types"):
|
|
103
|
+
self.supported_mime_types = mime_types
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Request helper mixin: rate-limit checking, pagination parsing, rate-limit updating."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import parse_qs
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
from whenever import Instant
|
|
9
|
+
|
|
10
|
+
from longwei import __display_name__
|
|
11
|
+
from longwei.constants import INSTANCE_TYPE_PLEROMA
|
|
12
|
+
from longwei.constants import INSTANCE_TYPE_TAKAHE
|
|
13
|
+
from longwei.constants import PAGINATION_UNDEFINED
|
|
14
|
+
from longwei.exceptions import RatelimitError
|
|
15
|
+
from longwei.types import ActivityPubClass
|
|
16
|
+
from longwei.utils import parse_rate_limit_reset
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__display_name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _RequestHelpersMixin:
|
|
22
|
+
"""Mixin providing _pre_call_checks, _parse_next_prev, and _update_ratelimit."""
|
|
23
|
+
|
|
24
|
+
async def _pre_call_checks(self: ActivityPubClass) -> None:
|
|
25
|
+
"""Do checks before contacting the instance server.
|
|
26
|
+
|
|
27
|
+
Raises RatelimitError if rate limit is exhausted and reset is in the future.
|
|
28
|
+
"""
|
|
29
|
+
logger.debug(
|
|
30
|
+
"ActivityPub.__pre_call_checks - Limit remaining: %s - Limit resetting at %s",
|
|
31
|
+
self.ratelimit_remaining,
|
|
32
|
+
self.ratelimit_reset,
|
|
33
|
+
)
|
|
34
|
+
if self.ratelimit_remaining == 0 and self.ratelimit_reset > Instant.now().py_datetime():
|
|
35
|
+
raise RatelimitError(
|
|
36
|
+
429,
|
|
37
|
+
None,
|
|
38
|
+
"Rate limited",
|
|
39
|
+
ratelimit_reset=self.ratelimit_reset,
|
|
40
|
+
ratelimit_limit=self.ratelimit_limit,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def _parse_next_prev(self: ActivityPubClass, links: str | None) -> None:
|
|
44
|
+
"""Extract min_id and max_id from a Link header and store in self.pagination.
|
|
45
|
+
|
|
46
|
+
:param links: The links header from the response
|
|
47
|
+
"""
|
|
48
|
+
logger.debug("ActivityPub.__parse_next_prev - links = %s", links)
|
|
49
|
+
|
|
50
|
+
if not links:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
self.pagination = PAGINATION_UNDEFINED
|
|
54
|
+
|
|
55
|
+
for comma_links in links.split(sep=", "):
|
|
56
|
+
pagination_rel: str | None = None
|
|
57
|
+
if 'rel="next"' in comma_links:
|
|
58
|
+
pagination_rel = "next"
|
|
59
|
+
elif 'rel="prev"' in comma_links:
|
|
60
|
+
pagination_rel = "prev"
|
|
61
|
+
else:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
urls = comma_links.split(sep="; ")
|
|
65
|
+
|
|
66
|
+
logger.debug("ActivityPub.__parse_next_prev - rel = %s - urls = %s", pagination_rel, urls)
|
|
67
|
+
|
|
68
|
+
for url in urls:
|
|
69
|
+
parsed_url = urlparse(url=url.lstrip("<").rstrip(">"))
|
|
70
|
+
queries_dict = parse_qs(str(parsed_url.query))
|
|
71
|
+
logger.debug("ActivityPub.__parse_next_prev - queries_dict = %s", queries_dict)
|
|
72
|
+
min_id = queries_dict.get("min_id")
|
|
73
|
+
max_id = queries_dict.get("max_id")
|
|
74
|
+
if min_id:
|
|
75
|
+
self.pagination[pagination_rel]["min_id"] = min_id[0]
|
|
76
|
+
if max_id:
|
|
77
|
+
self.pagination[pagination_rel]["max_id"] = max_id[0]
|
|
78
|
+
|
|
79
|
+
logger.debug("ActivityPub.__parse_next_prev - pagination = %s", self.pagination)
|
|
80
|
+
|
|
81
|
+
def _update_ratelimit(self: ActivityPubClass, headers: Any) -> None:
|
|
82
|
+
"""Update rate limit tracking from response headers.
|
|
83
|
+
|
|
84
|
+
:param headers: The headers of the response
|
|
85
|
+
"""
|
|
86
|
+
temp_ratelimit_limit = temp_ratelimit_remaining = temp_ratelimit_reset = None
|
|
87
|
+
|
|
88
|
+
if self.instance_type in (INSTANCE_TYPE_TAKAHE, INSTANCE_TYPE_PLEROMA):
|
|
89
|
+
# Takahe and Pleroma do not return rate limit headers.
|
|
90
|
+
# Default to 5 minute rate limit reset time
|
|
91
|
+
temp_ratelimit_reset = (Instant.now().add(minutes=5)).format_iso()
|
|
92
|
+
else:
|
|
93
|
+
temp_ratelimit_limit = headers.get("X-RateLimit-Limit")
|
|
94
|
+
temp_ratelimit_remaining = headers.get("X-RateLimit-Remaining")
|
|
95
|
+
temp_ratelimit_reset = headers.get("X-RateLimit-Reset")
|
|
96
|
+
|
|
97
|
+
if temp_ratelimit_limit:
|
|
98
|
+
self.ratelimit_limit = int(temp_ratelimit_limit)
|
|
99
|
+
if temp_ratelimit_remaining:
|
|
100
|
+
self.ratelimit_remaining = int(temp_ratelimit_remaining)
|
|
101
|
+
if temp_ratelimit_reset:
|
|
102
|
+
parsed_reset = parse_rate_limit_reset(temp_ratelimit_reset)
|
|
103
|
+
if parsed_reset:
|
|
104
|
+
self.ratelimit_reset = parsed_reset.py_datetime()
|
|
105
|
+
|
|
106
|
+
logger.debug(
|
|
107
|
+
"ActivityPub.__update_ratelimit - Pleroma Instance: %s - RateLimit Limit %s",
|
|
108
|
+
self.instance_type == INSTANCE_TYPE_PLEROMA,
|
|
109
|
+
self.ratelimit_limit,
|
|
110
|
+
)
|
|
111
|
+
logger.debug(
|
|
112
|
+
"ActivityPub.__update_ratelimit - Limit remaining: %s - Limit resetting at %s",
|
|
113
|
+
self.ratelimit_remaining,
|
|
114
|
+
self.ratelimit_reset,
|
|
115
|
+
)
|