12
12
import logging
13
13
import asyncio
14
14
15
+ from yarl import URL
15
16
from aiohttp import ClientSession
16
17
from aiohttp .client_exceptions import ContentTypeError , ClientError
17
18
18
19
from mautrix .errors import make_request_error , MatrixConnectionError
20
+ from mautrix .util .logging import TraceLogger
19
21
20
22
if TYPE_CHECKING :
21
23
from mautrix .types import JSON
22
24
23
25
24
26
class APIPath (Enum ):
25
- """The known Matrix API path prefixes."""
26
- CLIENT = "/_matrix/client/r0"
27
- CLIENT_UNSTABLE = "/_matrix/client/unstable"
28
- MEDIA = "/_matrix/media/r0"
29
- IDENTITY = "/_matrix/identity/r0"
27
+ """
28
+ The known Matrix API path prefixes.
29
+ These don't start with a slash so they can be used nicely with yarl.
30
+ """
31
+ CLIENT = "_matrix/client/r0"
32
+ CLIENT_UNSTABLE = "_matrix/client/unstable"
33
+ MEDIA = "_matrix/media/r0"
30
34
31
35
def __repr__ (self ):
32
36
return self .value
@@ -60,7 +64,7 @@ class PathBuilder:
60
64
>>> room_id = "!foo:example.com"
61
65
>>> event_id = "$bar:example.com"
62
66
>>> str(Path.rooms[room_id].event[event_id])
63
- "/ _matrix/client/r0/rooms/%21foo%3Aexample.com/event/%24bar%3Aexample.com"
67
+ "_matrix/client/r0/rooms/%21foo%3Aexample.com/event/%24bar%3Aexample.com"
64
68
"""
65
69
66
70
def __init__ (self , path : Union [str , APIPath ] = "" ) -> None :
@@ -105,14 +109,21 @@ def __getitem__(self, append: Union[str, int]) -> 'PathBuilder':
105
109
ClientPath = Path
106
110
UnstableClientPath = PathBuilder (APIPath .CLIENT_UNSTABLE )
107
111
MediaPath = PathBuilder (APIPath .MEDIA )
108
- IdentityPath = PathBuilder (APIPath .IDENTITY )
109
112
110
113
111
114
class HTTPAPI :
112
115
"""HTTPAPI is a simple asyncio Matrix API request sender."""
113
116
114
- def __init__ (self , base_url : str , token : str = "" , * , client_session : ClientSession = None ,
115
- txn_id : int = 0 , log : Optional [logging .Logger ] = None ,
117
+ base_url : URL
118
+ token : str
119
+ log : TraceLogger
120
+ loop : asyncio .AbstractEventLoop
121
+ session : ClientSession
122
+ txn_id : Optional [int ]
123
+
124
+ def __init__ (self , base_url : Union [URL , str ], token : str = "" , * ,
125
+ client_session : ClientSession = None ,
126
+ txn_id : int = 0 , log : Optional [TraceLogger ] = None ,
116
127
loop : Optional [asyncio .AbstractEventLoop ] = None ) -> None :
117
128
"""
118
129
Args:
@@ -122,18 +133,18 @@ def __init__(self, base_url: str, token: str = "", *, client_session: ClientSess
122
133
txn_id: The outgoing transaction ID to start with.
123
134
log: The logging.Logger instance to log requests with.
124
135
"""
125
- self .base_url : str = base_url
126
- self .token : str = token
127
- self .log : Optional [ logging . Logger ] = log or logging .getLogger ("mau.http" )
136
+ self .base_url = URL ( base_url )
137
+ self .token = token
138
+ self .log = log or logging .getLogger ("mau.http" )
128
139
self .loop = loop or asyncio .get_event_loop ()
129
- self .session : ClientSession = client_session or ClientSession (loop = self .loop )
140
+ self .session = client_session or ClientSession (loop = self .loop )
130
141
if txn_id is not None :
131
- self .txn_id : int = txn_id
142
+ self .txn_id = txn_id
132
143
133
- async def _send (self , method : Method , endpoint : str , content : Union [bytes , str ],
144
+ async def _send (self , method : Method , url : URL , content : Union [bytes , str ],
134
145
query_params : Dict [str , str ], headers : Dict [str , str ]) -> 'JSON' :
135
146
while True :
136
- request = self .session .request (str (method ), endpoint , data = content ,
147
+ request = self .session .request (str (method ), url , data = content ,
137
148
params = query_params , headers = headers )
138
149
async with request as response :
139
150
if response .status < 200 or response .status >= 300 :
@@ -150,7 +161,10 @@ async def _send(self, method: Method, endpoint: str, content: Union[bytes, str],
150
161
151
162
if response .status == 429 :
152
163
resp = await response .json ()
153
- await asyncio .sleep (resp ["retry_after_ms" ] / 1000 , loop = self .loop )
164
+ seconds = resp ["retry_after_ms" ] / 1000
165
+ self .log .debug (f"Request to { url } returned 429, "
166
+ f"waiting { seconds } seconds and retrying" )
167
+ await asyncio .sleep (seconds , loop = self .loop )
154
168
else :
155
169
return await response .json ()
156
170
@@ -161,7 +175,7 @@ def _log_request(self, method: Method, path: PathBuilder, content: Union[str, by
161
175
log_content = content if not isinstance (content , bytes ) else f"<{ len (content )} bytes>"
162
176
as_user = query_params .get ("user_id" , None )
163
177
level = 1 if path == Path .sync else 5
164
- self .log .log (level , f"{ method } { path } { log_content } " .strip (" " ),
178
+ self .log .log (level , f"{ method } / { path } { log_content } " .strip (" " ),
165
179
extra = {"matrix_http_request" : {
166
180
"method" : str (method ),
167
181
"path" : str (path ),
@@ -170,23 +184,25 @@ def _log_request(self, method: Method, path: PathBuilder, content: Union[str, by
170
184
"user" : as_user ,
171
185
}})
172
186
173
- async def request (self , method : Method , path : PathBuilder ,
174
- content : Optional [Union ['JSON' , bytes , str ]] = None ,
187
+ async def request (self , method : Method , path : Union [ PathBuilder , str ] ,
188
+ content : Optional [Union [dict , list , bytes , str ]] = None ,
175
189
headers : Optional [Dict [str , str ]] = None ,
176
190
query_params : Optional [Dict [str , str ]] = None ) -> 'JSON' :
177
191
"""
178
- Make a raw HTTP request.
192
+ Make a raw Matrix API request.
179
193
180
194
Args:
181
195
method: The HTTP method to use.
182
- path: The API endpoint to call.
183
- Does not include the base path (e.g. /_matrix/client/r0).
184
- content: The content to post as a dict (json) or bytes/str (raw).
185
- headers: The dict of HTTP headers to send.
186
- query_params: The dict of query parameters to send.
196
+ path: The full API endpoint to call (including the _matrix/... prefix)
197
+ content: The content to post as a dict/list (will be serialized as JSON)
198
+ or bytes/str (will be sent as-is).
199
+ headers: A dict of HTTP headers to send.
200
+ If the headers don't contain ``Content-Type``, it'll be set to ``application/json``.
201
+ The ``Authorization`` header is always overridden if :attr:`token` is set.
202
+ query_params: A dict of query parameters to send.
187
203
188
204
Returns:
189
- The response as a dict .
205
+ The parsed response JSON .
190
206
"""
191
207
content = content or {}
192
208
headers = headers or {}
@@ -203,18 +219,22 @@ async def request(self, method: Method, path: PathBuilder,
203
219
204
220
self ._log_request (method , path , content , orig_content , query_params )
205
221
206
- endpoint = self .base_url + str (path )
222
+ path = str (path )
223
+ if path and path [0 ] == "/" :
224
+ path = path [1 :]
225
+
207
226
try :
208
- return await self ._send (method , endpoint , content , query_params , headers or {})
227
+ return await self ._send (method , self .base_url / path ,
228
+ content , query_params , headers or {})
209
229
except ClientError as e :
210
230
raise MatrixConnectionError (str (e )) from e
211
231
212
232
def get_txn_id (self ) -> str :
213
233
"""Get a new unique transaction ID."""
214
234
self .txn_id += 1
215
- return str ( self .txn_id ) + str ( int (time () * 1000 ))
235
+ return f"mautrix-python_R { self .txn_id } @T { int (time () * 1000 )} "
216
236
217
- def get_download_url (self , mxc_uri : str , download_type : str = "download" ) -> str :
237
+ def get_download_url (self , mxc_uri : str , download_type : str = "download" ) -> URL :
218
238
"""
219
239
Get the full HTTP URL to download a mxc:// URI.
220
240
@@ -234,6 +254,6 @@ def get_download_url(self, mxc_uri: str, download_type: str = "download") -> str
234
254
"https://matrix.org/_matrix/media/r0/download/matrix.org/pqjkOuKZ1ZKRULWXgz2IVZV6"
235
255
"""
236
256
if mxc_uri .startswith ("mxc://" ):
237
- return f" { self .base_url } { APIPath .MEDIA } / { download_type } / { mxc_uri [6 :]} "
257
+ return self .base_url / str ( APIPath .MEDIA ) / download_type / mxc_uri [6 :]
238
258
else :
239
259
raise ValueError ("MXC URI did not begin with `mxc://`" )
0 commit comments