diff options
Diffstat (limited to 'mastodon')
-rw-r--r-- | mastodon/Mastodon.py | 351 | ||||
-rw-r--r-- | mastodon/streaming.py | 29 |
2 files changed, 215 insertions, 165 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index cd641e5..68f4914 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py | |||
@@ -6,7 +6,6 @@ import mimetypes | |||
6 | import time | 6 | import time |
7 | import random | 7 | import random |
8 | import string | 8 | import string |
9 | import pytz | ||
10 | import datetime | 9 | import datetime |
11 | from contextlib import closing | 10 | from contextlib import closing |
12 | import pytz | 11 | import pytz |
@@ -17,6 +16,7 @@ import dateutil.parser | |||
17 | import re | 16 | import re |
18 | import copy | 17 | import copy |
19 | 18 | ||
19 | |||
20 | class Mastodon: | 20 | class Mastodon: |
21 | """ | 21 | """ |
22 | Super basic but thorough and easy to use Mastodon | 22 | Super basic but thorough and easy to use Mastodon |
@@ -28,12 +28,12 @@ class Mastodon: | |||
28 | __DEFAULT_BASE_URL = 'https://mastodon.social' | 28 | __DEFAULT_BASE_URL = 'https://mastodon.social' |
29 | __DEFAULT_TIMEOUT = 300 | 29 | __DEFAULT_TIMEOUT = 300 |
30 | 30 | ||
31 | |||
32 | ### | 31 | ### |
33 | # Registering apps | 32 | # Registering apps |
34 | ### | 33 | ### |
35 | @staticmethod | 34 | @staticmethod |
36 | def create_app(client_name, scopes = ['read', 'write', 'follow'], redirect_uris = None, website = None, to_file = None, api_base_url = __DEFAULT_BASE_URL, request_timeout = __DEFAULT_TIMEOUT): | 35 | def create_app(client_name, scopes=['read', 'write', 'follow'], redirect_uris=None, website=None, to_file=None, |
36 | api_base_url=__DEFAULT_BASE_URL, request_timeout=__DEFAULT_TIMEOUT): | ||
37 | """ | 37 | """ |
38 | Create a new app with given client_name and scopes (read, write, follow) | 38 | Create a new app with given client_name and scopes (read, write, follow) |
39 | 39 | ||
@@ -47,7 +47,7 @@ class Mastodon: | |||
47 | Returns client_id and client_secret. | 47 | Returns client_id and client_secret. |
48 | """ | 48 | """ |
49 | api_base_url = Mastodon.__protocolize(api_base_url) | 49 | api_base_url = Mastodon.__protocolize(api_base_url) |
50 | 50 | ||
51 | request_data = { | 51 | request_data = { |
52 | 'client_name': client_name, | 52 | 'client_name': client_name, |
53 | 'scopes': " ".join(scopes) | 53 | 'scopes': " ".join(scopes) |
@@ -55,18 +55,18 @@ class Mastodon: | |||
55 | 55 | ||
56 | try: | 56 | try: |
57 | if redirect_uris is not None: | 57 | if redirect_uris is not None: |
58 | request_data['redirect_uris'] = redirect_uris; | 58 | request_data['redirect_uris'] = redirect_uris |
59 | else: | 59 | else: |
60 | request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob'; | 60 | request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob' |
61 | if website is not None: | 61 | if website is not None: |
62 | request_data['website'] = website | 62 | request_data['website'] = website |
63 | 63 | ||
64 | response = requests.post(api_base_url + '/api/v1/apps', data = request_data, timeout = request_timeout) | 64 | response = requests.post(api_base_url + '/api/v1/apps', data=request_data, timeout=request_timeout) |
65 | response = response.json() | 65 | response = response.json() |
66 | except Exception as e: | 66 | except Exception as e: |
67 | raise MastodonNetworkError("Could not complete request: %s" % e) | 67 | raise MastodonNetworkError("Could not complete request: %s" % e) |
68 | 68 | ||
69 | if to_file != None: | 69 | if to_file is not None: |
70 | with open(to_file, 'w') as secret_file: | 70 | with open(to_file, 'w') as secret_file: |
71 | secret_file.write(response['client_id'] + '\n') | 71 | secret_file.write(response['client_id'] + '\n') |
72 | secret_file.write(response['client_secret'] + '\n') | 72 | secret_file.write(response['client_secret'] + '\n') |
@@ -76,7 +76,10 @@ class Mastodon: | |||
76 | ### | 76 | ### |
77 | # Authentication, including constructor | 77 | # Authentication, including constructor |
78 | ### | 78 | ### |
79 | def __init__(self, client_id, client_secret = None, access_token = None, api_base_url = __DEFAULT_BASE_URL, debug_requests = False, ratelimit_method = "wait", ratelimit_pacefactor = 1.1, request_timeout = __DEFAULT_TIMEOUT): | 79 | def __init__(self, client_id, client_secret=None, access_token=None, |
80 | api_base_url=__DEFAULT_BASE_URL, debug_requests=False, | ||
81 | ratelimit_method="wait", ratelimit_pacefactor=1.1, | ||
82 | request_timeout=__DEFAULT_TIMEOUT): | ||
80 | """ | 83 | """ |
81 | Create a new API wrapper instance based on the given client_secret and client_id. If you | 84 | Create a new API wrapper instance based on the given client_secret and client_id. If you |
82 | give a client_id and it is not a file, you must also give a secret. | 85 | give a client_id and it is not a file, you must also give a secret. |
@@ -115,7 +118,7 @@ class Mastodon: | |||
115 | 118 | ||
116 | self.request_timeout = request_timeout | 119 | self.request_timeout = request_timeout |
117 | 120 | ||
118 | if not ratelimit_method in ["throw", "wait", "pace"]: | 121 | if ratelimit_method not in ["throw", "wait", "pace"]: |
119 | raise MastodonIllegalArgumentError("Invalid ratelimit method.") | 122 | raise MastodonIllegalArgumentError("Invalid ratelimit method.") |
120 | 123 | ||
121 | if os.path.isfile(self.client_id): | 124 | if os.path.isfile(self.client_id): |
@@ -123,15 +126,15 @@ class Mastodon: | |||
123 | self.client_id = secret_file.readline().rstrip() | 126 | self.client_id = secret_file.readline().rstrip() |
124 | self.client_secret = secret_file.readline().rstrip() | 127 | self.client_secret = secret_file.readline().rstrip() |
125 | else: | 128 | else: |
126 | if self.client_secret == None: | 129 | if self.client_secret is None: |
127 | raise MastodonIllegalArgumentError('Specified client id directly, but did not supply secret') | 130 | raise MastodonIllegalArgumentError('Specified client id directly, but did not supply secret') |
128 | 131 | ||
129 | if self.access_token != None and os.path.isfile(self.access_token): | 132 | if self.access_token is not None and os.path.isfile(self.access_token): |
130 | with open(self.access_token, 'r') as token_file: | 133 | with open(self.access_token, 'r') as token_file: |
131 | self.access_token = token_file.readline().rstrip() | 134 | self.access_token = token_file.readline().rstrip() |
132 | |||
133 | 135 | ||
134 | def auth_request_url(self, client_id = None, redirect_uris = "urn:ietf:wg:oauth:2.0:oob", scopes = ['read', 'write', 'follow']): | 136 | def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob", |
137 | scopes=['read', 'write', 'follow']): | ||
135 | """Returns the url that a client needs to request the grant from the server. | 138 | """Returns the url that a client needs to request the grant from the server. |
136 | """ | 139 | """ |
137 | if client_id is None: | 140 | if client_id is None: |
@@ -140,8 +143,8 @@ class Mastodon: | |||
140 | if os.path.isfile(client_id): | 143 | if os.path.isfile(client_id): |
141 | with open(client_id, 'r') as secret_file: | 144 | with open(client_id, 'r') as secret_file: |
142 | client_id = secret_file.readline().rstrip() | 145 | client_id = secret_file.readline().rstrip() |
143 | 146 | ||
144 | params = {} | 147 | params = dict() |
145 | params['client_id'] = client_id | 148 | params['client_id'] = client_id |
146 | params['response_type'] = "code" | 149 | params['response_type'] = "code" |
147 | params['redirect_uri'] = redirect_uris | 150 | params['redirect_uri'] = redirect_uris |
@@ -149,22 +152,23 @@ class Mastodon: | |||
149 | formatted_params = urlencode(params) | 152 | formatted_params = urlencode(params) |
150 | return "".join([self.api_base_url, "/oauth/authorize?", formatted_params]) | 153 | return "".join([self.api_base_url, "/oauth/authorize?", formatted_params]) |
151 | 154 | ||
152 | def log_in(self, username = None, password = None,\ | 155 | def log_in(self, username=None, password=None, |
153 | code = None, redirect_uri = "urn:ietf:wg:oauth:2.0:oob", refresh_token = None,\ | 156 | code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None, |
154 | scopes = ['read', 'write', 'follow'], to_file = None): | 157 | scopes=['read', 'write', 'follow'], to_file=None): |
155 | """ | 158 | """ |
156 | Your username is the e-mail you use to log in into mastodon. | 159 | Your username is the e-mail you use to log in into mastodon. |
157 | 160 | ||
158 | Can persist access token to file, to be used in the constructor. | 161 | Can persist access token to file, to be used in the constructor. |
159 | 162 | ||
160 | Supports refresh_token but Mastodon.social doesn't implement it at the moment. | 163 | Supports refresh_token but Mastodon.social doesn't implement it at the moment. |
161 | 164 | ||
162 | Handles password, authorization_code, and refresh_token authentication. | 165 | Handles password, authorization_code, and refresh_token authentication. |
163 | 166 | ||
164 | Will throw a MastodonIllegalArgumentError if username / password | 167 | Will throw a MastodonIllegalArgumentError if username / password |
165 | are wrong, scopes are not valid or granted scopes differ from requested. | 168 | are wrong, scopes are not valid or granted scopes differ from requested. |
166 | 169 | ||
167 | For OAuth2 documentation, compare https://github.com/doorkeeper-gem/doorkeeper/wiki/Interacting-as-an-OAuth-client-with-Doorkeeper | 170 | For OAuth2 documentation, compare |
171 | https://github.com/doorkeeper-gem/doorkeeper/wiki/Interacting-as-an-OAuth-client-with-Doorkeeper | ||
168 | 172 | ||
169 | Returns the access token. | 173 | Returns the access token. |
170 | """ | 174 | """ |
@@ -179,13 +183,13 @@ class Mastodon: | |||
179 | params['grant_type'] = 'refresh_token' | 183 | params['grant_type'] = 'refresh_token' |
180 | else: | 184 | else: |
181 | raise MastodonIllegalArgumentError('Invalid arguments given. username and password or code are required.') | 185 | raise MastodonIllegalArgumentError('Invalid arguments given. username and password or code are required.') |
182 | 186 | ||
183 | params['client_id'] = self.client_id | 187 | params['client_id'] = self.client_id |
184 | params['client_secret'] = self.client_secret | 188 | params['client_secret'] = self.client_secret |
185 | params['scope'] = " ".join(scopes) | 189 | params['scope'] = " ".join(scopes) |
186 | 190 | ||
187 | try: | 191 | try: |
188 | response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting = False) | 192 | response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting=False) |
189 | self.access_token = response['access_token'] | 193 | self.access_token = response['access_token'] |
190 | self.__set_refresh_token(response.get('refresh_token')) | 194 | self.__set_refresh_token(response.get('refresh_token')) |
191 | self.__set_token_expired(int(response.get('expires_in', 0))) | 195 | self.__set_token_expired(int(response.get('expires_in', 0))) |
@@ -201,9 +205,10 @@ class Mastodon: | |||
201 | received_scopes = " ".join(sorted(response["scope"].split(" "))) | 205 | received_scopes = " ".join(sorted(response["scope"].split(" "))) |
202 | 206 | ||
203 | if requested_scopes != received_scopes: | 207 | if requested_scopes != received_scopes: |
204 | raise MastodonAPIError('Granted scopes "' + received_scopes + '" differ from requested scopes "' + requested_scopes + '".') | 208 | raise MastodonAPIError( |
209 | 'Granted scopes "' + received_scopes + '" differ from requested scopes "' + requested_scopes + '".') | ||
205 | 210 | ||
206 | if to_file != None: | 211 | if to_file is not None: |
207 | with open(to_file, 'w') as token_file: | 212 | with open(to_file, 'w') as token_file: |
208 | token_file.write(response['access_token'] + '\n') | 213 | token_file.write(response['access_token'] + '\n') |
209 | 214 | ||
@@ -223,7 +228,7 @@ class Mastodon: | |||
223 | ### | 228 | ### |
224 | # Reading data: Timelines | 229 | # Reading data: Timelines |
225 | ## | 230 | ## |
226 | def timeline(self, timeline = "home", max_id = None, since_id = None, limit = None): | 231 | def timeline(self, timeline="home", max_id=None, since_id=None, limit=None): |
227 | """ | 232 | """ |
228 | Fetch statuses, most recent ones first. Timeline can be home, local, public, | 233 | Fetch statuses, most recent ones first. Timeline can be home, local, public, |
229 | or tag/hashtag. See the following functions documentation for what those do. | 234 | or tag/hashtag. See the following functions documentation for what those do. |
@@ -239,39 +244,44 @@ class Mastodon: | |||
239 | params_initial['local'] = True | 244 | params_initial['local'] = True |
240 | 245 | ||
241 | params = self.__generate_params(params_initial, ['timeline']) | 246 | params = self.__generate_params(params_initial, ['timeline']) |
242 | return self.__api_request('GET', '/api/v1/timelines/' + timeline, params) | 247 | url = '/api/v1/timelines/{0}'.format(timeline) |
248 | return self.__api_request('GET', url, params) | ||
243 | 249 | ||
244 | def timeline_home(self, max_id = None, since_id = None, limit = None): | 250 | def timeline_home(self, max_id=None, since_id=None, limit=None): |
245 | """ | 251 | """ |
246 | Fetch the authenticated users home timeline (i.e. followed users and self). | 252 | Fetch the authenticated users home timeline (i.e. followed users and self). |
247 | 253 | ||
248 | Returns a list of toot dicts. | 254 | Returns a list of toot dicts. |
249 | """ | 255 | """ |
250 | return self.timeline('home', max_id = max_id, since_id = since_id, limit = limit) | 256 | return self.timeline('home', max_id=max_id, since_id=since_id, |
257 | limit=limit) | ||
251 | 258 | ||
252 | def timeline_local(self, max_id = None, since_id = None, limit = None): | 259 | def timeline_local(self, max_id=None, since_id=None, limit=None): |
253 | """ | 260 | """ |
254 | Fetches the local / instance-wide timeline, not including replies. | 261 | Fetches the local / instance-wide timeline, not including replies. |
255 | 262 | ||
256 | Returns a list of toot dicts. | 263 | Returns a list of toot dicts. |
257 | """ | 264 | """ |
258 | return self.timeline('local', max_id = max_id, since_id = since_id, limit = limit) | 265 | return self.timeline('local', max_id=max_id, since_id=since_id, |
266 | limit=limit) | ||
259 | 267 | ||
260 | def timeline_public(self, max_id = None, since_id = None, limit = None): | 268 | def timeline_public(self, max_id=None, since_id=None, limit=None): |
261 | """ | 269 | """ |
262 | Fetches the public / visible-network timeline, not including replies. | 270 | Fetches the public / visible-network timeline, not including replies. |
263 | 271 | ||
264 | Returns a list of toot dicts. | 272 | Returns a list of toot dicts. |
265 | """ | 273 | """ |
266 | return self.timeline('public', max_id = max_id, since_id = since_id, limit = limit) | 274 | return self.timeline('public', max_id=max_id, since_id=since_id, |
275 | limit=limit) | ||
267 | 276 | ||
268 | def timeline_hashtag(self, hashtag, max_id = None, since_id = None, limit = None): | 277 | def timeline_hashtag(self, hashtag, max_id=None, since_id=None, limit=None): |
269 | """ | 278 | """ |
270 | Fetch a timeline of toots with a given hashtag. | 279 | Fetch a timeline of toots with a given hashtag. |
271 | 280 | ||
272 | Returns a list of toot dicts. | 281 | Returns a list of toot dicts. |
273 | """ | 282 | """ |
274 | return self.timeline('tag/' + str(hashtag), max_id = max_id, since_id = since_id, limit = limit) | 283 | url = 'tag/{0}'.format(str(hashtag)) |
284 | return self.timeline(url, max_id=max_id, since_id=since_id, limit=limit) | ||
275 | 285 | ||
276 | ### | 286 | ### |
277 | # Reading data: Statuses | 287 | # Reading data: Statuses |
@@ -282,7 +292,8 @@ class Mastodon: | |||
282 | 292 | ||
283 | Returns a toot dict. | 293 | Returns a toot dict. |
284 | """ | 294 | """ |
285 | return self.__api_request('GET', '/api/v1/statuses/' + str(id)) | 295 | url = '/api/v1/statuses/{0}'.format(str(id)) |
296 | return self.__api_request('GET', url) | ||
286 | 297 | ||
287 | def status_card(self, id): | 298 | def status_card(self, id): |
288 | """ | 299 | """ |
@@ -291,7 +302,8 @@ class Mastodon: | |||
291 | 302 | ||
292 | Returns a card dict. | 303 | Returns a card dict. |
293 | """ | 304 | """ |
294 | return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/card') | 305 | url = '/api/v1/statuses/{0}/card'.format(str(id)) |
306 | return self.__api_request('GET', url) | ||
295 | 307 | ||
296 | def status_context(self, id): | 308 | def status_context(self, id): |
297 | """ | 309 | """ |
@@ -299,7 +311,8 @@ class Mastodon: | |||
299 | 311 | ||
300 | Returns a context dict. | 312 | Returns a context dict. |
301 | """ | 313 | """ |
302 | return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/context') | 314 | url = '/api/v1/statuses/{0}/context'.format(str(id)) |
315 | return self.__api_request('GET', url) | ||
303 | 316 | ||
304 | def status_reblogged_by(self, id): | 317 | def status_reblogged_by(self, id): |
305 | """ | 318 | """ |
@@ -307,7 +320,8 @@ class Mastodon: | |||
307 | 320 | ||
308 | Returns a list of user dicts. | 321 | Returns a list of user dicts. |
309 | """ | 322 | """ |
310 | return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/reblogged_by') | 323 | url = '/api/v1/statuses/{0}/reblogged_by'.format(str(id)) |
324 | return self.__api_request('GET', url) | ||
311 | 325 | ||
312 | def status_favourited_by(self, id): | 326 | def status_favourited_by(self, id): |
313 | """ | 327 | """ |
@@ -315,12 +329,13 @@ class Mastodon: | |||
315 | 329 | ||
316 | Returns a list of user dicts. | 330 | Returns a list of user dicts. |
317 | """ | 331 | """ |
318 | return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/favourited_by') | 332 | url = '/api/v1/statuses/{0}/favourited_by'.format(str(id)) |
333 | return self.__api_request('GET', url) | ||
319 | 334 | ||
320 | ### | 335 | ### |
321 | # Reading data: Notifications | 336 | # Reading data: Notifications |
322 | ### | 337 | ### |
323 | def notifications(self, id = None, max_id = None, since_id = None, limit = None): | 338 | def notifications(self, id=None, max_id=None, since_id=None, limit=None): |
324 | """ | 339 | """ |
325 | Fetch notifications (mentions, favourites, reblogs, follows) for the authenticated | 340 | Fetch notifications (mentions, favourites, reblogs, follows) for the authenticated |
326 | user. | 341 | user. |
@@ -329,11 +344,12 @@ class Mastodon: | |||
329 | 344 | ||
330 | Returns a list of notification dicts. | 345 | Returns a list of notification dicts. |
331 | """ | 346 | """ |
332 | if id == None: | 347 | if id is None: |
333 | params = self.__generate_params(locals(), ['id']) | 348 | params = self.__generate_params(locals(), ['id']) |
334 | return self.__api_request('GET', '/api/v1/notifications', params) | 349 | return self.__api_request('GET', '/api/v1/notifications', params) |
335 | else: | 350 | else: |
336 | return self.__api_request('GET', '/api/v1/notifications/' + str(id)) | 351 | url = '/api/v1/notifications/{0}'.format(str(id)) |
352 | return self.__api_request('GET', url) | ||
337 | 353 | ||
338 | ### | 354 | ### |
339 | # Reading data: Accounts | 355 | # Reading data: Accounts |
@@ -344,7 +360,8 @@ class Mastodon: | |||
344 | 360 | ||
345 | Returns a user dict. | 361 | Returns a user dict. |
346 | """ | 362 | """ |
347 | return self.__api_request('GET', '/api/v1/accounts/' + str(id)) | 363 | url = '/api/v1/accounts/{0}'.format(str(id)) |
364 | return self.__api_request('GET', url) | ||
348 | 365 | ||
349 | def account_verify_credentials(self): | 366 | def account_verify_credentials(self): |
350 | """ | 367 | """ |
@@ -354,32 +371,35 @@ class Mastodon: | |||
354 | """ | 371 | """ |
355 | return self.__api_request('GET', '/api/v1/accounts/verify_credentials') | 372 | return self.__api_request('GET', '/api/v1/accounts/verify_credentials') |
356 | 373 | ||
357 | def account_statuses(self, id, max_id = None, since_id = None, limit = None): | 374 | def account_statuses(self, id, max_id=None, since_id=None, limit=None): |
358 | """ | 375 | """ |
359 | Fetch statuses by user id. Same options as timeline are permitted. | 376 | Fetch statuses by user id. Same options as timeline are permitted. |
360 | 377 | ||
361 | Returns a list of toot dicts. | 378 | Returns a list of toot dicts. |
362 | """ | 379 | """ |
363 | params = self.__generate_params(locals(), ['id']) | 380 | params = self.__generate_params(locals(), ['id']) |
364 | return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/statuses', params) | 381 | url = '/api/v1/accounts/{0}/statuses'.format(str(id)) |
382 | return self.__api_request('GET', url, params) | ||
365 | 383 | ||
366 | def account_following(self, id, max_id = None, since_id = None, limit = None): | 384 | def account_following(self, id, max_id=None, since_id=None, limit=None): |
367 | """ | 385 | """ |
368 | Fetch users the given user is following. | 386 | Fetch users the given user is following. |
369 | 387 | ||
370 | Returns a list of user dicts. | 388 | Returns a list of user dicts. |
371 | """ | 389 | """ |
372 | params = self.__generate_params(locals(), ['id']) | 390 | params = self.__generate_params(locals(), ['id']) |
373 | return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/following', params) | 391 | url = '/api/v1/accounts/{0}/following'.format(str(id)) |
392 | return self.__api_request('GET', url, params) | ||
374 | 393 | ||
375 | def account_followers(self, id, max_id = None, since_id = None, limit = None): | 394 | def account_followers(self, id, max_id=None, since_id=None, limit=None): |
376 | """ | 395 | """ |
377 | Fetch users the given user is followed by. | 396 | Fetch users the given user is followed by. |
378 | 397 | ||
379 | Returns a list of user dicts. | 398 | Returns a list of user dicts. |
380 | """ | 399 | """ |
381 | params = self.__generate_params(locals(), ['id']) | 400 | params = self.__generate_params(locals(), ['id']) |
382 | return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/followers', params) | 401 | url = '/api/v1/accounts/{0}/followers'.format(str(id)) |
402 | return self.__api_request('GET', url, params) | ||
383 | 403 | ||
384 | def account_relationships(self, id): | 404 | def account_relationships(self, id): |
385 | """ | 405 | """ |
@@ -389,9 +409,10 @@ class Mastodon: | |||
389 | Returns a list of relationship dicts. | 409 | Returns a list of relationship dicts. |
390 | """ | 410 | """ |
391 | params = self.__generate_params(locals()) | 411 | params = self.__generate_params(locals()) |
392 | return self.__api_request('GET', '/api/v1/accounts/relationships', params) | 412 | return self.__api_request('GET', '/api/v1/accounts/relationships', |
413 | params) | ||
393 | 414 | ||
394 | def account_search(self, q, limit = None): | 415 | def account_search(self, q, limit=None): |
395 | """ | 416 | """ |
396 | Fetch matching accounts. Will lookup an account remotely if the search term is | 417 | Fetch matching accounts. Will lookup an account remotely if the search term is |
397 | in the username@domain format and not yet in the database. | 418 | in the username@domain format and not yet in the database. |
@@ -404,7 +425,7 @@ class Mastodon: | |||
404 | ### | 425 | ### |
405 | # Reading data: Searching | 426 | # Reading data: Searching |
406 | ### | 427 | ### |
407 | def search(self, q, resolve = False): | 428 | def search(self, q, resolve=False): |
408 | """ | 429 | """ |
409 | Fetch matching hashtags, accounts and statuses. Will search federated | 430 | Fetch matching hashtags, accounts and statuses. Will search federated |
410 | instances if resolve is True. | 431 | instances if resolve is True. |
@@ -417,7 +438,7 @@ class Mastodon: | |||
417 | ### | 438 | ### |
418 | # Reading data: Mutes and Blocks | 439 | # Reading data: Mutes and Blocks |
419 | ### | 440 | ### |
420 | def mutes(self, max_id = None, since_id = None, limit = None): | 441 | def mutes(self, max_id=None, since_id=None, limit=None): |
421 | """ | 442 | """ |
422 | Fetch a list of users muted by the authenticated user. | 443 | Fetch a list of users muted by the authenticated user. |
423 | 444 | ||
@@ -426,7 +447,7 @@ class Mastodon: | |||
426 | params = self.__generate_params(locals()) | 447 | params = self.__generate_params(locals()) |
427 | return self.__api_request('GET', '/api/v1/mutes', params) | 448 | return self.__api_request('GET', '/api/v1/mutes', params) |
428 | 449 | ||
429 | def blocks(self, max_id = None, since_id = None, limit = None): | 450 | def blocks(self, max_id=None, since_id=None, limit=None): |
430 | """ | 451 | """ |
431 | Fetch a list of users blocked by the authenticated user. | 452 | Fetch a list of users blocked by the authenticated user. |
432 | 453 | ||
@@ -449,7 +470,7 @@ class Mastodon: | |||
449 | ### | 470 | ### |
450 | # Reading data: Favourites | 471 | # Reading data: Favourites |
451 | ### | 472 | ### |
452 | def favourites(self, max_id = None, since_id = None, limit = None): | 473 | def favourites(self, max_id=None, since_id=None, limit=None): |
453 | """ | 474 | """ |
454 | Fetch the authenticated user's favourited statuses. | 475 | Fetch the authenticated user's favourited statuses. |
455 | 476 | ||
@@ -461,7 +482,7 @@ class Mastodon: | |||
461 | ### | 482 | ### |
462 | # Reading data: Follow requests | 483 | # Reading data: Follow requests |
463 | ### | 484 | ### |
464 | def follow_requests(self, max_id = None, since_id = None, limit = None): | 485 | def follow_requests(self, max_id=None, since_id=None, limit=None): |
465 | """ | 486 | """ |
466 | Fetch the authenticated user's incoming follow requests. | 487 | Fetch the authenticated user's incoming follow requests. |
467 | 488 | ||
@@ -473,7 +494,7 @@ class Mastodon: | |||
473 | ### | 494 | ### |
474 | # Reading data: Domain blocks | 495 | # Reading data: Domain blocks |
475 | ### | 496 | ### |
476 | def domain_blocks(self, max_id = None, since_id = None, limit = None): | 497 | def domain_blocks(self, max_id=None, since_id=None, limit=None): |
477 | """ | 498 | """ |
478 | Fetch the authenticated user's blocked domains. | 499 | Fetch the authenticated user's blocked domains. |
479 | 500 | ||
@@ -481,11 +502,12 @@ class Mastodon: | |||
481 | """ | 502 | """ |
482 | params = self.__generate_params(locals()) | 503 | params = self.__generate_params(locals()) |
483 | return self.__api_request('GET', '/api/v1/domain_blocks', params) | 504 | return self.__api_request('GET', '/api/v1/domain_blocks', params) |
484 | 505 | ||
485 | ### | 506 | ### |
486 | # Writing data: Statuses | 507 | # Writing data: Statuses |
487 | ### | 508 | ### |
488 | def status_post(self, status, in_reply_to_id = None, media_ids = None, sensitive = False, visibility = '', spoiler_text = None): | 509 | def status_post(self, status, in_reply_to_id=None, media_ids=None, |
510 | sensitive=False, visibility='', spoiler_text=None): | ||
489 | """ | 511 | """ |
490 | Post a status. Can optionally be in reply to another status and contain | 512 | Post a status. Can optionally be in reply to another status and contain |
491 | up to four pieces of media (Uploaded via media_post()). media_ids can | 513 | up to four pieces of media (Uploaded via media_post()). media_ids can |
@@ -517,12 +539,13 @@ class Mastodon: | |||
517 | # Validate visibility parameter | 539 | # Validate visibility parameter |
518 | valid_visibilities = ['private', 'public', 'unlisted', 'direct', ''] | 540 | valid_visibilities = ['private', 'public', 'unlisted', 'direct', ''] |
519 | if params_initial['visibility'].lower() not in valid_visibilities: | 541 | if params_initial['visibility'].lower() not in valid_visibilities: |
520 | raise ValueError('Invalid visibility value! Acceptable values are %s' % valid_visibilities) | 542 | raise ValueError('Invalid visibility value! Acceptable ' |
543 | 'values are %s' % valid_visibilities) | ||
521 | 544 | ||
522 | if params_initial['sensitive'] == False: | 545 | if params_initial['sensitive'] is False: |
523 | del[params_initial['sensitive']] | 546 | del [params_initial['sensitive']] |
524 | 547 | ||
525 | if media_ids != None: | 548 | if media_ids is not None: |
526 | try: | 549 | try: |
527 | media_ids_proper = [] | 550 | media_ids_proper = [] |
528 | for media_id in media_ids: | 551 | for media_id in media_ids: |
@@ -531,7 +554,8 @@ class Mastodon: | |||
531 | else: | 554 | else: |
532 | media_ids_proper.append(media_id) | 555 | media_ids_proper.append(media_id) |
533 | except Exception as e: | 556 | except Exception as e: |
534 | raise MastodonIllegalArgumentError("Invalid media dict: %s" % e) | 557 | raise MastodonIllegalArgumentError("Invalid media " |
558 | "dict: %s" % e) | ||
535 | 559 | ||
536 | params_initial["media_ids"] = media_ids_proper | 560 | params_initial["media_ids"] = media_ids_proper |
537 | 561 | ||
@@ -552,14 +576,16 @@ class Mastodon: | |||
552 | 576 | ||
553 | Returns an empty dict for good measure. | 577 | Returns an empty dict for good measure. |
554 | """ | 578 | """ |
555 | return self.__api_request('DELETE', '/api/v1/statuses/' + str(id)) | 579 | url = '/api/v1/statuses/{0}'.format(str(id)) |
580 | return self.__api_request('DELETE', url) | ||
556 | 581 | ||
557 | def status_reblog(self, id): | 582 | def status_reblog(self, id): |
558 | """Reblog a status. | 583 | """Reblog a status. |
559 | 584 | ||
560 | Returns a toot with with a new status that wraps around the reblogged one. | 585 | Returns a toot with with a new status that wraps around the reblogged one. |
561 | """ | 586 | """ |
562 | return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/reblog") | 587 | url = '/api/v1/statuses/{0}/reblog'.format(str(id)) |
588 | return self.__api_request('POST', url) | ||
563 | 589 | ||
564 | def status_unreblog(self, id): | 590 | def status_unreblog(self, id): |
565 | """ | 591 | """ |
@@ -567,7 +593,8 @@ class Mastodon: | |||
567 | 593 | ||
568 | Returns a toot dict with the status that used to be reblogged. | 594 | Returns a toot dict with the status that used to be reblogged. |
569 | """ | 595 | """ |
570 | return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unreblog") | 596 | url = '/api/v1/statuses/{0}/unreblog'.format(str(id)) |
597 | return self.__api_request('POST', url) | ||
571 | 598 | ||
572 | def status_favourite(self, id): | 599 | def status_favourite(self, id): |
573 | """ | 600 | """ |
@@ -575,7 +602,8 @@ class Mastodon: | |||
575 | 602 | ||
576 | Returns a toot dict with the favourited status. | 603 | Returns a toot dict with the favourited status. |
577 | """ | 604 | """ |
578 | return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/favourite") | 605 | url = '/api/v1/statuses/{0}/favourite'.format(str(id)) |
606 | return self.__api_request('POST', url) | ||
579 | 607 | ||
580 | def status_unfavourite(self, id): | 608 | def status_unfavourite(self, id): |
581 | """ | 609 | """ |
@@ -583,7 +611,8 @@ class Mastodon: | |||
583 | 611 | ||
584 | Returns a toot dict with the un-favourited status. | 612 | Returns a toot dict with the un-favourited status. |
585 | """ | 613 | """ |
586 | return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unfavourite") | 614 | url = '/api/v1/statuses/{0}/unfavourite'.format(str(id)) |
615 | return self.__api_request('POST', url) | ||
587 | 616 | ||
588 | ### | 617 | ### |
589 | # Writing data: Notifications | 618 | # Writing data: Notifications |
@@ -603,7 +632,8 @@ class Mastodon: | |||
603 | 632 | ||
604 | Returns a relationship dict containing the updated relationship to the user. | 633 | Returns a relationship dict containing the updated relationship to the user. |
605 | """ | 634 | """ |
606 | return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/follow") | 635 | url = '/api/v1/accounts/{0}/follow'.format(str(id)) |
636 | return self.__api_request('POST', url) | ||
607 | 637 | ||
608 | def follows(self, uri): | 638 | def follows(self, uri): |
609 | """ | 639 | """ |
@@ -620,7 +650,8 @@ class Mastodon: | |||
620 | 650 | ||
621 | Returns a relationship dict containing the updated relationship to the user. | 651 | Returns a relationship dict containing the updated relationship to the user. |
622 | """ | 652 | """ |
623 | return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unfollow") | 653 | url = '/api/v1/accounts/{0}/unfollow'.format(str(id)) |
654 | return self.__api_request('POST', url) | ||
624 | 655 | ||
625 | def account_block(self, id): | 656 | def account_block(self, id): |
626 | """ | 657 | """ |
@@ -628,7 +659,8 @@ class Mastodon: | |||
628 | 659 | ||
629 | Returns a relationship dict containing the updated relationship to the user. | 660 | Returns a relationship dict containing the updated relationship to the user. |
630 | """ | 661 | """ |
631 | return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/block") | 662 | url = '/api/v1/accounts/{0}/block'.format(str(id)) |
663 | return self.__api_request('POST', url) | ||
632 | 664 | ||
633 | def account_unblock(self, id): | 665 | def account_unblock(self, id): |
634 | """ | 666 | """ |
@@ -636,7 +668,8 @@ class Mastodon: | |||
636 | 668 | ||
637 | Returns a relationship dict containing the updated relationship to the user. | 669 | Returns a relationship dict containing the updated relationship to the user. |
638 | """ | 670 | """ |
639 | return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unblock") | 671 | url = '/api/v1/accounts/{0}/unblock'.format(str(id)) |
672 | return self.__api_request('POST', url) | ||
640 | 673 | ||
641 | def account_mute(self, id): | 674 | def account_mute(self, id): |
642 | """ | 675 | """ |
@@ -644,7 +677,8 @@ class Mastodon: | |||
644 | 677 | ||
645 | Returns a relationship dict containing the updated relationship to the user. | 678 | Returns a relationship dict containing the updated relationship to the user. |
646 | """ | 679 | """ |
647 | return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/mute") | 680 | url = '/api/v1/accounts/{0}/mute'.format(str(id)) |
681 | return self.__api_request('POST', url) | ||
648 | 682 | ||
649 | def account_unmute(self, id): | 683 | def account_unmute(self, id): |
650 | """ | 684 | """ |
@@ -652,9 +686,11 @@ class Mastodon: | |||
652 | 686 | ||
653 | Returns a relationship dict containing the updated relationship to the user. | 687 | Returns a relationship dict containing the updated relationship to the user. |
654 | """ | 688 | """ |
655 | return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unmute") | 689 | url = '/api/v1/accounts/{0}/unmute'.format(str(id)) |
690 | return self.__api_request('POST', url) | ||
656 | 691 | ||
657 | def account_update_credentials(self, display_name = None, note = None, avatar = None, header = None): | 692 | def account_update_credentials(self, display_name=None, note=None, |
693 | avatar=None, header=None): | ||
658 | """ | 694 | """ |
659 | Update the profile for the currently authenticated user. | 695 | Update the profile for the currently authenticated user. |
660 | 696 | ||
@@ -689,7 +725,8 @@ class Mastodon: | |||
689 | 725 | ||
690 | Returns an empty dict. | 726 | Returns an empty dict. |
691 | """ | 727 | """ |
692 | return self.__api_request('POST', '/api/v1/follow_requests/' + str(id) + "/authorize") | 728 | url = '/api/v1/follow_requests/{0}/authorize'.format(str(id)) |
729 | return self.__api_request('POST', url) | ||
693 | 730 | ||
694 | def follow_request_reject(self, id): | 731 | def follow_request_reject(self, id): |
695 | """ | 732 | """ |
@@ -697,12 +734,13 @@ class Mastodon: | |||
697 | 734 | ||
698 | Returns an empty dict. | 735 | Returns an empty dict. |
699 | """ | 736 | """ |
700 | return self.__api_request('POST', '/api/v1/follow_requests/' + str(id) + "/reject") | 737 | url = '/api/v1/follow_requests/{0}/reject'.format(str(id)) |
738 | return self.__api_request('POST', url) | ||
701 | 739 | ||
702 | ### | 740 | ### |
703 | # Writing data: Media | 741 | # Writing data: Media |
704 | ### | 742 | ### |
705 | def media_post(self, media_file, mime_type = None): | 743 | def media_post(self, media_file, mime_type=None): |
706 | """ | 744 | """ |
707 | Post an image. media_file can either be image data or | 745 | Post an image. media_file can either be image data or |
708 | a file name. If image data is passed directly, the mime | 746 | a file name. If image data is passed directly, the mime |
@@ -715,45 +753,49 @@ class Mastodon: | |||
715 | Returns a media dict. This contains the id that can be used in | 753 | Returns a media dict. This contains the id that can be used in |
716 | status_post to attach the media file to a toot. | 754 | status_post to attach the media file to a toot. |
717 | """ | 755 | """ |
718 | if mime_type == None and os.path.isfile(media_file): | 756 | if mime_type is None and os.path.isfile(media_file): |
719 | mime_type = mimetypes.guess_type(media_file)[0] | 757 | mime_type = mimetypes.guess_type(media_file)[0] |
720 | media_file = open(media_file, 'rb') | 758 | media_file = open(media_file, 'rb') |
721 | 759 | ||
722 | if mime_type == None: | 760 | if mime_type is None: |
723 | raise MastodonIllegalArgumentError('Could not determine mime type or data passed directly without mime type.') | 761 | raise MastodonIllegalArgumentError('Could not determine mime type' |
762 | ' or data passed directly ' | ||
763 | 'without mime type.') | ||
724 | 764 | ||
725 | random_suffix = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) | 765 | random_suffix = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) |
726 | file_name = "mastodonpyupload_" + str(time.time()) + "_" + str(random_suffix) + mimetypes.guess_extension(mime_type) | 766 | file_name = "mastodonpyupload_" + str(time.time()) + "_" + str(random_suffix) + mimetypes.guess_extension( |
767 | mime_type) | ||
727 | 768 | ||
728 | media_file_description = (file_name, media_file, mime_type) | 769 | media_file_description = (file_name, media_file, mime_type) |
729 | return self.__api_request('POST', '/api/v1/media', files = {'file': media_file_description}) | 770 | return self.__api_request('POST', '/api/v1/media', |
771 | files={'file': media_file_description}) | ||
730 | 772 | ||
731 | ### | 773 | ### |
732 | # Writing data: Domain blocks | 774 | # Writing data: Domain blocks |
733 | ### | 775 | ### |
734 | def domain_block(self, domain = None): | 776 | def domain_block(self, domain=None): |
735 | """ | 777 | """ |
736 | Add a block for all statuses originating from the specified domain for the logged-in user. | 778 | Add a block for all statuses originating from the specified domain for the logged-in user. |
737 | """ | 779 | """ |
738 | params = self.__generate_params(locals()) | 780 | params = self.__generate_params(locals()) |
739 | return self.__api_request('POST', '/api/v1/domain_blocks', params) | 781 | return self.__api_request('POST', '/api/v1/domain_blocks', params) |
740 | 782 | ||
741 | def domain_unblock(self, domain = None): | 783 | def domain_unblock(self, domain=None): |
742 | """ | 784 | """ |
743 | Remove a domain block for the logged-in user. | 785 | Remove a domain block for the logged-in user. |
744 | """ | 786 | """ |
745 | params = self.__generate_params(locals()) | 787 | params = self.__generate_params(locals()) |
746 | return self.__api_request('DELETE', '/api/v1/domain_blocks', params) | 788 | return self.__api_request('DELETE', '/api/v1/domain_blocks', params) |
747 | 789 | ||
748 | ### | 790 | ### |
749 | # Pagination | 791 | # Pagination |
750 | ### | 792 | ### |
751 | def fetch_next(self, previous_page): | 793 | def fetch_next(self, previous_page): |
752 | """ | 794 | """ |
753 | Fetches the next page of results of a paginated request. Pass in the | 795 | Fetches the next page of results of a paginated request. Pass in the |
754 | previous page in its entirety, or the pagination information dict | 796 | previous page in its entirety, or the pagination information dict |
755 | returned as a part of that pages last status ('_pagination_next'). | 797 | returned as a part of that pages last status ('_pagination_next'). |
756 | 798 | ||
757 | Returns the next page or None if no further data is available. | 799 | Returns the next page or None if no further data is available. |
758 | """ | 800 | """ |
759 | if isinstance(previous_page, list): | 801 | if isinstance(previous_page, list): |
@@ -763,21 +805,21 @@ class Mastodon: | |||
763 | return None | 805 | return None |
764 | else: | 806 | else: |
765 | params = copy.deepcopy(previous_page) | 807 | params = copy.deepcopy(previous_page) |
766 | 808 | ||
767 | method = params['_pagination_method'] | 809 | method = params['_pagination_method'] |
768 | del params['_pagination_method'] | 810 | del params['_pagination_method'] |
769 | 811 | ||
770 | endpoint = params['_pagination_endpoint'] | 812 | endpoint = params['_pagination_endpoint'] |
771 | del params['_pagination_endpoint'] | 813 | del params['_pagination_endpoint'] |
772 | 814 | ||
773 | return self.__api_request(method, endpoint, params) | 815 | return self.__api_request(method, endpoint, params) |
774 | 816 | ||
775 | def fetch_previous(self, next_page): | 817 | def fetch_previous(self, next_page): |
776 | """ | 818 | """ |
777 | Fetches the previous page of results of a paginated request. Pass in the | 819 | Fetches the previous page of results of a paginated request. Pass in the |
778 | previous page in its entirety, or the pagination information dict | 820 | previous page in its entirety, or the pagination information dict |
779 | returned as a part of that pages first status ('_pagination_prev'). | 821 | returned as a part of that pages first status ('_pagination_prev'). |
780 | 822 | ||
781 | Returns the previous page or None if no further data is available. | 823 | Returns the previous page or None if no further data is available. |
782 | """ | 824 | """ |
783 | if isinstance(next_page, list): | 825 | if isinstance(next_page, list): |
@@ -787,34 +829,34 @@ class Mastodon: | |||
787 | return None | 829 | return None |
788 | else: | 830 | else: |
789 | params = copy.deepcopy(next_page) | 831 | params = copy.deepcopy(next_page) |
790 | 832 | ||
791 | method = params['_pagination_method'] | 833 | method = params['_pagination_method'] |
792 | del params['_pagination_method'] | 834 | del params['_pagination_method'] |
793 | 835 | ||
794 | endpoint = params['_pagination_endpoint'] | 836 | endpoint = params['_pagination_endpoint'] |
795 | del params['_pagination_endpoint'] | 837 | del params['_pagination_endpoint'] |
796 | 838 | ||
797 | return self.__api_request(method, endpoint, params) | 839 | return self.__api_request(method, endpoint, params) |
798 | 840 | ||
799 | def fetch_remaining(self, first_page): | 841 | def fetch_remaining(self, first_page): |
800 | """ | 842 | """ |
801 | Fetches all the remaining pages of a paginated request starting from a | 843 | Fetches all the remaining pages of a paginated request starting from a |
802 | first page and returns the entire set of results (including the first page | 844 | first page and returns the entire set of results (including the first page |
803 | that was passed in) as a big list. | 845 | that was passed in) as a big list. |
804 | 846 | ||
805 | Be careful, as this might generate a lot of requests, depending on what you are | 847 | Be careful, as this might generate a lot of requests, depending on what you are |
806 | fetching, and might cause you to run into rate limits very quickly. | 848 | fetching, and might cause you to run into rate limits very quickly. |
807 | """ | 849 | """ |
808 | first_page = copy.deepcopy(first_page) | 850 | first_page = copy.deepcopy(first_page) |
809 | 851 | ||
810 | all_pages = [] | 852 | all_pages = [] |
811 | current_page = first_page | 853 | current_page = first_page |
812 | while current_page != None and len(current_page) > 0: | 854 | while current_page is not None and len(current_page) > 0: |
813 | all_pages.extend(current_page) | 855 | all_pages.extend(current_page) |
814 | current_page = self.fetch_next(current_page) | 856 | current_page = self.fetch_next(current_page) |
815 | 857 | ||
816 | return all_pages | 858 | return all_pages |
817 | 859 | ||
818 | ### | 860 | ### |
819 | # Streaming | 861 | # Streaming |
820 | ### | 862 | ### |
@@ -858,7 +900,7 @@ class Mastodon: | |||
858 | incoming events. | 900 | incoming events. |
859 | """ | 901 | """ |
860 | return self.__stream("/api/v1/streaming/hashtag?tag={}".format(tag), listener) | 902 | return self.__stream("/api/v1/streaming/hashtag?tag={}".format(tag), listener) |
861 | 903 | ||
862 | ### | 904 | ### |
863 | # Internal helpers, dragons probably | 905 | # Internal helpers, dragons probably |
864 | ### | 906 | ### |
@@ -870,22 +912,22 @@ class Mastodon: | |||
870 | Assumes UTC if timezone is not given. | 912 | Assumes UTC if timezone is not given. |
871 | """ | 913 | """ |
872 | date_time_utc = None | 914 | date_time_utc = None |
873 | if date_time.tzinfo == None: | 915 | if date_time.tzinfo is None: |
874 | date_time_utc = date_time.replace(tzinfo = pytz.utc) | 916 | date_time_utc = date_time.replace(tzinfo=pytz.utc) |
875 | else: | 917 | else: |
876 | date_time_utc = date_time.astimezone(pytz.utc) | 918 | date_time_utc = date_time.astimezone(pytz.utc) |
877 | 919 | ||
878 | epoch_utc = datetime.datetime.utcfromtimestamp(0).replace(tzinfo = pytz.utc) | 920 | epoch_utc = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc) |
879 | 921 | ||
880 | return (date_time_utc - epoch_utc).total_seconds() | 922 | return (date_time_utc - epoch_utc).total_seconds() |
881 | 923 | ||
882 | def __api_request(self, method, endpoint, params = {}, files = {}, do_ratelimiting = True): | 924 | def __api_request(self, method, endpoint, params={}, files={}, do_ratelimiting=True): |
883 | """ | 925 | """ |
884 | Internal API request helper. | 926 | Internal API request helper. |
885 | """ | 927 | """ |
886 | response = None | 928 | response = None |
887 | headers = None | 929 | headers = None |
888 | 930 | remaining_wait = 0 | |
889 | # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it | 931 | # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it |
890 | # would take to not hit the rate limit at that request rate. | 932 | # would take to not hit the rate limit at that request rate. |
891 | if do_ratelimiting and self.ratelimit_method == "pace": | 933 | if do_ratelimiting and self.ratelimit_method == "pace": |
@@ -906,10 +948,10 @@ class Mastodon: | |||
906 | time.sleep(to_next) | 948 | time.sleep(to_next) |
907 | 949 | ||
908 | # Generate request headers | 950 | # Generate request headers |
909 | if self.access_token != None: | 951 | if self.access_token is not None: |
910 | headers = {'Authorization': 'Bearer ' + self.access_token} | 952 | headers = {'Authorization': 'Bearer ' + self.access_token} |
911 | 953 | ||
912 | if self.debug_requests == True: | 954 | if self.debug_requests: |
913 | print('Mastodon: Request to endpoint "' + endpoint + '" using method "' + method + '".') | 955 | print('Mastodon: Request to endpoint "' + endpoint + '" using method "' + method + '".') |
914 | print('Parameters: ' + str(params)) | 956 | print('Parameters: ' + str(params)) |
915 | print('Headers: ' + str(headers)) | 957 | print('Headers: ' + str(headers)) |
@@ -923,24 +965,28 @@ class Mastodon: | |||
923 | response_object = None | 965 | response_object = None |
924 | try: | 966 | try: |
925 | if method == 'GET': | 967 | if method == 'GET': |
926 | response_object = requests.get(self.api_base_url + endpoint, params = params, headers = headers, files = files, timeout = self.request_timeout) | 968 | response_object = requests.get(self.api_base_url + endpoint, params=params, |
927 | 969 | headers=headers, files=files, | |
970 | timeout=self.request_timeout) | ||
928 | if method == 'POST': | 971 | if method == 'POST': |
929 | response_object = requests.post(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) | 972 | response_object = requests.post(self.api_base_url + endpoint, data=params, headers=headers, |
973 | files=files, timeout=self.request_timeout) | ||
930 | 974 | ||
931 | if method == 'PATCH': | 975 | if method == 'PATCH': |
932 | response_object = requests.patch(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) | 976 | response_object = requests.patch(self.api_base_url + endpoint, data=params, headers=headers, |
977 | files=files, timeout=self.request_timeout) | ||
933 | 978 | ||
934 | if method == 'DELETE': | 979 | if method == 'DELETE': |
935 | response_object = requests.delete(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) | 980 | response_object = requests.delete(self.api_base_url + endpoint, data=params, headers=headers, |
981 | files=files, timeout=self.request_timeout) | ||
936 | except Exception as e: | 982 | except Exception as e: |
937 | raise MastodonNetworkError("Could not complete request: %s" % e) | 983 | raise MastodonNetworkError("Could not complete request: %s" % e) |
938 | 984 | ||
939 | if response_object == None: | 985 | if response_object is None: |
940 | raise MastodonIllegalArgumentError("Illegal request.") | 986 | raise MastodonIllegalArgumentError("Illegal request.") |
941 | 987 | ||
942 | # Handle response | 988 | # Handle response |
943 | if self.debug_requests == True: | 989 | if self.debug_requests: |
944 | print('Mastodon: Response received with code ' + str(response_object.status_code) + '.') | 990 | print('Mastodon: Response received with code ' + str(response_object.status_code) + '.') |
945 | print('response headers: ' + str(response_object.headers)) | 991 | print('response headers: ' + str(response_object.headers)) |
946 | print('Response text content: ' + str(response_object.text)) | 992 | print('Response text content: ' + str(response_object.text)) |
@@ -954,20 +1000,26 @@ class Mastodon: | |||
954 | try: | 1000 | try: |
955 | response = response_object.json() | 1001 | response = response_object.json() |
956 | except: | 1002 | except: |
957 | raise MastodonAPIError("Could not parse response as JSON, response code was %s, bad json content was '%s'" % (response_object.status_code, response_object.content)) | 1003 | raise MastodonAPIError( |
1004 | "Could not parse response as JSON, response code was %s, " | ||
1005 | "bad json content was '%s'" % (response_object.status_code, | ||
1006 | response_object.content)) | ||
958 | 1007 | ||
959 | # Parse link headers | 1008 | # Parse link headers |
960 | if isinstance(response, list) and 'Link' in response_object.headers and response_object.headers['Link'] != "": | 1009 | if isinstance(response, list) and \ |
961 | tmp_urls = requests.utils.parse_header_links(response_object.headers['Link'].rstrip('>').replace('>,<', ',<')) | 1010 | 'Link' in response_object.headers and \ |
1011 | response_object.headers['Link'] != "": | ||
1012 | tmp_urls = requests.utils.parse_header_links( | ||
1013 | response_object.headers['Link'].rstrip('>').replace('>,<', ',<')) | ||
962 | for url in tmp_urls: | 1014 | for url in tmp_urls: |
963 | if not 'rel' in url: | 1015 | if 'rel' not in url: |
964 | continue | 1016 | continue |
965 | 1017 | ||
966 | if url['rel'] == 'next': | 1018 | if url['rel'] == 'next': |
967 | # Be paranoid and extract max_id specifically | 1019 | # Be paranoid and extract max_id specifically |
968 | next_url = url['url'] | 1020 | next_url = url['url'] |
969 | matchgroups = re.search(r"max_id=([0-9]*)", next_url) | 1021 | matchgroups = re.search(r"max_id=([0-9]*)", next_url) |
970 | 1022 | ||
971 | if matchgroups: | 1023 | if matchgroups: |
972 | next_params = copy.deepcopy(params) | 1024 | next_params = copy.deepcopy(params) |
973 | next_params['_pagination_method'] = method | 1025 | next_params['_pagination_method'] = method |
@@ -976,12 +1028,12 @@ class Mastodon: | |||
976 | if "since_id" in next_params: | 1028 | if "since_id" in next_params: |
977 | del next_params['since_id'] | 1029 | del next_params['since_id'] |
978 | response[-1]['_pagination_next'] = next_params | 1030 | response[-1]['_pagination_next'] = next_params |
979 | 1031 | ||
980 | if url['rel'] == 'prev': | 1032 | if url['rel'] == 'prev': |
981 | # Be paranoid and extract since_id specifically | 1033 | # Be paranoid and extract since_id specifically |
982 | prev_url = url['url'] | 1034 | prev_url = url['url'] |
983 | matchgroups = re.search(r"since_id=([0-9]*)", prev_url) | 1035 | matchgroups = re.search(r"since_id=([0-9]*)", prev_url) |
984 | 1036 | ||
985 | if matchgroups: | 1037 | if matchgroups: |
986 | prev_params = copy.deepcopy(params) | 1038 | prev_params = copy.deepcopy(params) |
987 | prev_params['_pagination_method'] = method | 1039 | prev_params['_pagination_method'] = method |
@@ -990,7 +1042,7 @@ class Mastodon: | |||
990 | if "max_id" in prev_params: | 1042 | if "max_id" in prev_params: |
991 | del prev_params['max_id'] | 1043 | del prev_params['max_id'] |
992 | response[0]['_pagination_prev'] = prev_params | 1044 | response[0]['_pagination_prev'] = prev_params |
993 | 1045 | ||
994 | # Handle rate limiting | 1046 | # Handle rate limiting |
995 | if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting: | 1047 | if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting: |
996 | self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining']) | 1048 | self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining']) |
@@ -1024,21 +1076,20 @@ class Mastodon: | |||
1024 | 1076 | ||
1025 | return response | 1077 | return response |
1026 | 1078 | ||
1027 | def __stream(self, endpoint, listener, params = {}): | 1079 | def __stream(self, endpoint, listener, params={}): |
1028 | """ | 1080 | """ |
1029 | Internal streaming API helper. | 1081 | Internal streaming API helper. |
1030 | """ | 1082 | """ |
1031 | 1083 | ||
1032 | headers = {} | 1084 | headers = {} |
1033 | if self.access_token != None: | 1085 | if self.access_token is not None: |
1034 | headers = {'Authorization': 'Bearer ' + self.access_token} | 1086 | headers = {'Authorization': 'Bearer ' + self.access_token} |
1035 | 1087 | ||
1036 | url = self.api_base_url + endpoint | 1088 | url = self.api_base_url + endpoint |
1037 | with closing(requests.get(url, headers = headers, data = params, stream = True)) as r: | 1089 | with closing(requests.get(url, headers=headers, data=params, stream=True)) as r: |
1038 | listener.handle_stream(r.iter_lines()) | 1090 | listener.handle_stream(r.iter_lines()) |
1039 | 1091 | ||
1040 | 1092 | def __generate_params(self, params, exclude=[]): | |
1041 | def __generate_params(self, params, exclude = []): | ||
1042 | """ | 1093 | """ |
1043 | Internal named-parameters-to-dict helper. | 1094 | Internal named-parameters-to-dict helper. |
1044 | 1095 | ||
@@ -1052,7 +1103,7 @@ class Mastodon: | |||
1052 | del params['self'] | 1103 | del params['self'] |
1053 | param_keys = list(params.keys()) | 1104 | param_keys = list(params.keys()) |
1054 | for key in param_keys: | 1105 | for key in param_keys: |
1055 | if params[key] == None or key in exclude: | 1106 | if params[key] is None or key in exclude: |
1056 | del params[key] | 1107 | del params[key] |
1057 | 1108 | ||
1058 | param_keys = list(params.keys()) | 1109 | param_keys = list(params.keys()) |
@@ -1063,28 +1114,24 @@ class Mastodon: | |||
1063 | 1114 | ||
1064 | return params | 1115 | return params |
1065 | 1116 | ||
1066 | |||
1067 | def __get_token_expired(self): | 1117 | def __get_token_expired(self): |
1068 | """Internal helper for oauth code""" | 1118 | """Internal helper for oauth code""" |
1069 | if self._token_expired < datetime.datetime.now(): | 1119 | return self._token_expired < datetime.datetime.now() |
1070 | return True | ||
1071 | else: | ||
1072 | return False | ||
1073 | 1120 | ||
1074 | def __set_token_expired(self, value): | 1121 | def __set_token_expired(self, value): |
1075 | """Internal helper for oauth code""" | 1122 | """Internal helper for oauth code""" |
1076 | self._token_expired = datetime.datetime.now() + datetime.timedelta(seconds=value) | 1123 | self._token_expired = datetime.datetime.now() + datetime.timedelta(seconds=value) |
1077 | return | 1124 | return |
1078 | 1125 | ||
1079 | def __get_refresh_token(self): | 1126 | def __get_refresh_token(self): |
1080 | """Internal helper for oauth code""" | 1127 | """Internal helper for oauth code""" |
1081 | return self._refresh_token | 1128 | return self._refresh_token |
1082 | 1129 | ||
1083 | def __set_refresh_token(self, value): | 1130 | def __set_refresh_token(self, value): |
1084 | """Internal helper for oauth code""" | 1131 | """Internal helper for oauth code""" |
1085 | self._refresh_token = value | 1132 | self._refresh_token = value |
1086 | return | 1133 | return |
1087 | 1134 | ||
1088 | @staticmethod | 1135 | @staticmethod |
1089 | def __protocolize(base_url): | 1136 | def __protocolize(base_url): |
1090 | """Internal add-protocol-to-url helper""" | 1137 | """Internal add-protocol-to-url helper""" |
@@ -1095,21 +1142,25 @@ class Mastodon: | |||
1095 | base_url = base_url.rstrip("/") | 1142 | base_url = base_url.rstrip("/") |
1096 | return base_url | 1143 | return base_url |
1097 | 1144 | ||
1145 | |||
1098 | ## | 1146 | ## |
1099 | # Exceptions | 1147 | # Exceptions |
1100 | ## | 1148 | ## |
1101 | class MastodonIllegalArgumentError(ValueError): | 1149 | class MastodonIllegalArgumentError(ValueError): |
1102 | pass | 1150 | pass |
1103 | 1151 | ||
1152 | |||
1104 | class MastodonFileNotFoundError(IOError): | 1153 | class MastodonFileNotFoundError(IOError): |
1105 | pass | 1154 | pass |
1106 | 1155 | ||
1156 | |||
1107 | class MastodonNetworkError(IOError): | 1157 | class MastodonNetworkError(IOError): |
1108 | pass | 1158 | pass |
1109 | 1159 | ||
1160 | |||
1110 | class MastodonAPIError(Exception): | 1161 | class MastodonAPIError(Exception): |
1111 | pass | 1162 | pass |
1112 | 1163 | ||
1164 | |||
1113 | class MastodonRatelimitError(Exception): | 1165 | class MastodonRatelimitError(Exception): |
1114 | pass | 1166 | pass |
1115 | |||
diff --git a/mastodon/streaming.py b/mastodon/streaming.py index 3212848..290ed44 100644 --- a/mastodon/streaming.py +++ b/mastodon/streaming.py | |||
@@ -1,7 +1,7 @@ | |||
1 | ''' | 1 | """ |
2 | Handlers for the Streaming API: | 2 | Handlers for the Streaming API: |
3 | https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/Streaming-API.md | 3 | https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/Streaming-API.md |
4 | ''' | 4 | """ |
5 | 5 | ||
6 | import json | 6 | import json |
7 | import logging | 7 | import logging |
@@ -12,43 +12,43 @@ log = logging.getLogger(__name__) | |||
12 | 12 | ||
13 | 13 | ||
14 | class MalformedEventError(Exception): | 14 | class MalformedEventError(Exception): |
15 | '''Raised when the server-sent event stream is malformed.''' | 15 | """Raised when the server-sent event stream is malformed.""" |
16 | pass | 16 | pass |
17 | 17 | ||
18 | 18 | ||
19 | class StreamListener(object): | 19 | class StreamListener(object): |
20 | '''Callbacks for the streaming API. Create a subclass, override the on_xxx | 20 | """Callbacks for the streaming API. Create a subclass, override the on_xxx |
21 | methods for the kinds of events you're interested in, then pass an instance | 21 | methods for the kinds of events you're interested in, then pass an instance |
22 | of your subclass to Mastodon.user_stream(), Mastodon.public_stream(), or | 22 | of your subclass to Mastodon.user_stream(), Mastodon.public_stream(), or |
23 | Mastodon.hashtag_stream().''' | 23 | Mastodon.hashtag_stream().""" |
24 | 24 | ||
25 | def on_update(self, status): | 25 | def on_update(self, status): |
26 | '''A new status has appeared! 'status' is the parsed JSON dictionary | 26 | """A new status has appeared! 'status' is the parsed JSON dictionary |
27 | describing the status.''' | 27 | describing the status.""" |
28 | pass | 28 | pass |
29 | 29 | ||
30 | def on_notification(self, notification): | 30 | def on_notification(self, notification): |
31 | '''A new notification. 'notification' is the parsed JSON dictionary | 31 | """A new notification. 'notification' is the parsed JSON dictionary |
32 | describing the notification.''' | 32 | describing the notification.""" |
33 | pass | 33 | pass |
34 | 34 | ||
35 | def on_delete(self, status_id): | 35 | def on_delete(self, status_id): |
36 | '''A status has been deleted. status_id is the status' integer ID.''' | 36 | """A status has been deleted. status_id is the status' integer ID.""" |
37 | pass | 37 | pass |
38 | 38 | ||
39 | def handle_heartbeat(self): | 39 | def handle_heartbeat(self): |
40 | '''The server has sent us a keep-alive message. This callback may be | 40 | """The server has sent us a keep-alive message. This callback may be |
41 | useful to carry out periodic housekeeping tasks, or just to confirm | 41 | useful to carry out periodic housekeeping tasks, or just to confirm |
42 | that the connection is still open.''' | 42 | that the connection is still open.""" |
43 | 43 | ||
44 | def handle_stream(self, lines): | 44 | def handle_stream(self, lines): |
45 | ''' | 45 | """ |
46 | Handles a stream of events from the Mastodon server. When each event | 46 | Handles a stream of events from the Mastodon server. When each event |
47 | is received, the corresponding .on_[name]() method is called. | 47 | is received, the corresponding .on_[name]() method is called. |
48 | 48 | ||
49 | lines: an iterable of lines of bytes sent by the Mastodon server, as | 49 | lines: an iterable of lines of bytes sent by the Mastodon server, as |
50 | returned by requests.Response.iter_lines(). | 50 | returned by requests.Response.iter_lines(). |
51 | ''' | 51 | """ |
52 | event = {} | 52 | event = {} |
53 | for raw_line in lines: | 53 | for raw_line in lines: |
54 | try: | 54 | try: |
@@ -104,4 +104,3 @@ class StreamListener(object): | |||
104 | else: | 104 | else: |
105 | # TODO: allow handlers to return/raise to stop streaming cleanly | 105 | # TODO: allow handlers to return/raise to stop streaming cleanly |
106 | handler(payload) | 106 | handler(payload) |
107 | |||