diff options
Diffstat (limited to 'mastodon')
-rw-r--r-- | mastodon/Mastodon.py | 206 | ||||
-rw-r--r-- | mastodon/__init__.py | 4 | ||||
-rw-r--r-- | mastodon/streaming.py | 91 |
3 files changed, 217 insertions, 84 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index 079fc85..91e6b11 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py | |||
@@ -17,10 +17,12 @@ import re | |||
17 | import copy | 17 | import copy |
18 | import threading | 18 | import threading |
19 | import sys | 19 | import sys |
20 | import six | ||
21 | |||
20 | try: | 22 | try: |
21 | from urllib.parse import urlparse | 23 | from urllib.parse import urlparse |
22 | except ImportError: | 24 | except ImportError: |
23 | from urlparse import urlparse | 25 | from urlparse import urlparse |
24 | 26 | ||
25 | 27 | ||
26 | class Mastodon: | 28 | class Mastodon: |
@@ -244,6 +246,12 @@ class Mastodon: | |||
244 | 246 | ||
245 | Returns a list of toot dicts. | 247 | Returns a list of toot dicts. |
246 | """ | 248 | """ |
249 | if max_id != None: | ||
250 | max_id = self.__unpack_id(max_id) | ||
251 | |||
252 | if since_id != None: | ||
253 | since_id = self.__unpack_id(since_id) | ||
254 | |||
247 | params_initial = locals() | 255 | params_initial = locals() |
248 | 256 | ||
249 | if timeline == "local": | 257 | if timeline == "local": |
@@ -289,6 +297,12 @@ class Mastodon: | |||
289 | 297 | ||
290 | Returns a list of toot dicts. | 298 | Returns a list of toot dicts. |
291 | """ | 299 | """ |
300 | if max_id != None: | ||
301 | max_id = self.__unpack_id(max_id) | ||
302 | |||
303 | if since_id != None: | ||
304 | since_id = self.__unpack_id(since_id) | ||
305 | |||
292 | params_initial = locals() | 306 | params_initial = locals() |
293 | 307 | ||
294 | if local == False: | 308 | if local == False: |
@@ -308,6 +322,7 @@ class Mastodon: | |||
308 | 322 | ||
309 | Returns a toot dict. | 323 | Returns a toot dict. |
310 | """ | 324 | """ |
325 | id = self.__unpack_id(id) | ||
311 | url = '/api/v1/statuses/{0}'.format(str(id)) | 326 | url = '/api/v1/statuses/{0}'.format(str(id)) |
312 | return self.__api_request('GET', url) | 327 | return self.__api_request('GET', url) |
313 | 328 | ||
@@ -318,6 +333,7 @@ class Mastodon: | |||
318 | 333 | ||
319 | Returns a card dict. | 334 | Returns a card dict. |
320 | """ | 335 | """ |
336 | id = self.__unpack_id(id) | ||
321 | url = '/api/v1/statuses/{0}/card'.format(str(id)) | 337 | url = '/api/v1/statuses/{0}/card'.format(str(id)) |
322 | return self.__api_request('GET', url) | 338 | return self.__api_request('GET', url) |
323 | 339 | ||
@@ -327,6 +343,7 @@ class Mastodon: | |||
327 | 343 | ||
328 | Returns a context dict. | 344 | Returns a context dict. |
329 | """ | 345 | """ |
346 | id = self.__unpack_id(id) | ||
330 | url = '/api/v1/statuses/{0}/context'.format(str(id)) | 347 | url = '/api/v1/statuses/{0}/context'.format(str(id)) |
331 | return self.__api_request('GET', url) | 348 | return self.__api_request('GET', url) |
332 | 349 | ||
@@ -336,6 +353,7 @@ class Mastodon: | |||
336 | 353 | ||
337 | Returns a list of user dicts. | 354 | Returns a list of user dicts. |
338 | """ | 355 | """ |
356 | id = self.__unpack_id(id) | ||
339 | url = '/api/v1/statuses/{0}/reblogged_by'.format(str(id)) | 357 | url = '/api/v1/statuses/{0}/reblogged_by'.format(str(id)) |
340 | return self.__api_request('GET', url) | 358 | return self.__api_request('GET', url) |
341 | 359 | ||
@@ -345,6 +363,7 @@ class Mastodon: | |||
345 | 363 | ||
346 | Returns a list of user dicts. | 364 | Returns a list of user dicts. |
347 | """ | 365 | """ |
366 | id = self.__unpack_id(id) | ||
348 | url = '/api/v1/statuses/{0}/favourited_by'.format(str(id)) | 367 | url = '/api/v1/statuses/{0}/favourited_by'.format(str(id)) |
349 | return self.__api_request('GET', url) | 368 | return self.__api_request('GET', url) |
350 | 369 | ||
@@ -360,10 +379,17 @@ class Mastodon: | |||
360 | 379 | ||
361 | Returns a list of notification dicts. | 380 | Returns a list of notification dicts. |
362 | """ | 381 | """ |
382 | if max_id != None: | ||
383 | max_id = self.__unpack_id(max_id) | ||
384 | |||
385 | if since_id != None: | ||
386 | since_id = self.__unpack_id(since_id) | ||
387 | |||
363 | if id is None: | 388 | if id is None: |
364 | params = self.__generate_params(locals(), ['id']) | 389 | params = self.__generate_params(locals(), ['id']) |
365 | return self.__api_request('GET', '/api/v1/notifications', params) | 390 | return self.__api_request('GET', '/api/v1/notifications', params) |
366 | else: | 391 | else: |
392 | id = self.__unpack_id(id) | ||
367 | url = '/api/v1/notifications/{0}'.format(str(id)) | 393 | url = '/api/v1/notifications/{0}'.format(str(id)) |
368 | return self.__api_request('GET', url) | 394 | return self.__api_request('GET', url) |
369 | 395 | ||
@@ -376,6 +402,7 @@ class Mastodon: | |||
376 | 402 | ||
377 | Returns a user dict. | 403 | Returns a user dict. |
378 | """ | 404 | """ |
405 | id = self.__unpack_id(id) | ||
379 | url = '/api/v1/accounts/{0}'.format(str(id)) | 406 | url = '/api/v1/accounts/{0}'.format(str(id)) |
380 | return self.__api_request('GET', url) | 407 | return self.__api_request('GET', url) |
381 | 408 | ||
@@ -393,6 +420,13 @@ class Mastodon: | |||
393 | 420 | ||
394 | Returns a list of toot dicts. | 421 | Returns a list of toot dicts. |
395 | """ | 422 | """ |
423 | id = self.__unpack_id(id) | ||
424 | if max_id != None: | ||
425 | max_id = self.__unpack_id(max_id) | ||
426 | |||
427 | if since_id != None: | ||
428 | since_id = self.__unpack_id(since_id) | ||
429 | |||
396 | params = self.__generate_params(locals(), ['id']) | 430 | params = self.__generate_params(locals(), ['id']) |
397 | url = '/api/v1/accounts/{0}/statuses'.format(str(id)) | 431 | url = '/api/v1/accounts/{0}/statuses'.format(str(id)) |
398 | return self.__api_request('GET', url, params) | 432 | return self.__api_request('GET', url, params) |
@@ -403,6 +437,13 @@ class Mastodon: | |||
403 | 437 | ||
404 | Returns a list of user dicts. | 438 | Returns a list of user dicts. |
405 | """ | 439 | """ |
440 | id = self.__unpack_id(id) | ||
441 | if max_id != None: | ||
442 | max_id = self.__unpack_id(max_id) | ||
443 | |||
444 | if since_id != None: | ||
445 | since_id = self.__unpack_id(since_id) | ||
446 | |||
406 | params = self.__generate_params(locals(), ['id']) | 447 | params = self.__generate_params(locals(), ['id']) |
407 | url = '/api/v1/accounts/{0}/following'.format(str(id)) | 448 | url = '/api/v1/accounts/{0}/following'.format(str(id)) |
408 | return self.__api_request('GET', url, params) | 449 | return self.__api_request('GET', url, params) |
@@ -413,6 +454,13 @@ class Mastodon: | |||
413 | 454 | ||
414 | Returns a list of user dicts. | 455 | Returns a list of user dicts. |
415 | """ | 456 | """ |
457 | id = self.__unpack_id(id) | ||
458 | if max_id != None: | ||
459 | max_id = self.__unpack_id(max_id) | ||
460 | |||
461 | if since_id != None: | ||
462 | since_id = self.__unpack_id(since_id) | ||
463 | |||
416 | params = self.__generate_params(locals(), ['id']) | 464 | params = self.__generate_params(locals(), ['id']) |
417 | url = '/api/v1/accounts/{0}/followers'.format(str(id)) | 465 | url = '/api/v1/accounts/{0}/followers'.format(str(id)) |
418 | return self.__api_request('GET', url, params) | 466 | return self.__api_request('GET', url, params) |
@@ -424,6 +472,7 @@ class Mastodon: | |||
424 | 472 | ||
425 | Returns a list of relationship dicts. | 473 | Returns a list of relationship dicts. |
426 | """ | 474 | """ |
475 | id = self.__unpack_id(id) | ||
427 | params = self.__generate_params(locals()) | 476 | params = self.__generate_params(locals()) |
428 | return self.__api_request('GET', '/api/v1/accounts/relationships', | 477 | return self.__api_request('GET', '/api/v1/accounts/relationships', |
429 | params) | 478 | params) |
@@ -460,6 +509,12 @@ class Mastodon: | |||
460 | 509 | ||
461 | Returns a list of user dicts. | 510 | Returns a list of user dicts. |
462 | """ | 511 | """ |
512 | if max_id != None: | ||
513 | max_id = self.__unpack_id(max_id) | ||
514 | |||
515 | if since_id != None: | ||
516 | since_id = self.__unpack_id(since_id) | ||
517 | |||
463 | params = self.__generate_params(locals()) | 518 | params = self.__generate_params(locals()) |
464 | return self.__api_request('GET', '/api/v1/mutes', params) | 519 | return self.__api_request('GET', '/api/v1/mutes', params) |
465 | 520 | ||
@@ -469,6 +524,12 @@ class Mastodon: | |||
469 | 524 | ||
470 | Returns a list of user dicts. | 525 | Returns a list of user dicts. |
471 | """ | 526 | """ |
527 | if max_id != None: | ||
528 | max_id = self.__unpack_id(max_id) | ||
529 | |||
530 | if since_id != None: | ||
531 | since_id = self.__unpack_id(since_id) | ||
532 | |||
472 | params = self.__generate_params(locals()) | 533 | params = self.__generate_params(locals()) |
473 | return self.__api_request('GET', '/api/v1/blocks', params) | 534 | return self.__api_request('GET', '/api/v1/blocks', params) |
474 | 535 | ||
@@ -495,6 +556,12 @@ class Mastodon: | |||
495 | 556 | ||
496 | Returns a list of toot dicts. | 557 | Returns a list of toot dicts. |
497 | """ | 558 | """ |
559 | if max_id != None: | ||
560 | max_id = self.__unpack_id(max_id) | ||
561 | |||
562 | if since_id != None: | ||
563 | since_id = self.__unpack_id(since_id) | ||
564 | |||
498 | params = self.__generate_params(locals()) | 565 | params = self.__generate_params(locals()) |
499 | return self.__api_request('GET', '/api/v1/favourites', params) | 566 | return self.__api_request('GET', '/api/v1/favourites', params) |
500 | 567 | ||
@@ -507,6 +574,12 @@ class Mastodon: | |||
507 | 574 | ||
508 | Returns a list of user dicts. | 575 | Returns a list of user dicts. |
509 | """ | 576 | """ |
577 | if max_id != None: | ||
578 | max_id = self.__unpack_id(max_id) | ||
579 | |||
580 | if since_id != None: | ||
581 | since_id = self.__unpack_id(since_id) | ||
582 | |||
510 | params = self.__generate_params(locals()) | 583 | params = self.__generate_params(locals()) |
511 | return self.__api_request('GET', '/api/v1/follow_requests', params) | 584 | return self.__api_request('GET', '/api/v1/follow_requests', params) |
512 | 585 | ||
@@ -519,6 +592,12 @@ class Mastodon: | |||
519 | 592 | ||
520 | Returns a list of blocked domain URLs (as strings, without protocol specifier). | 593 | Returns a list of blocked domain URLs (as strings, without protocol specifier). |
521 | """ | 594 | """ |
595 | if max_id != None: | ||
596 | max_id = self.__unpack_id(max_id) | ||
597 | |||
598 | if since_id != None: | ||
599 | since_id = self.__unpack_id(since_id) | ||
600 | |||
522 | params = self.__generate_params(locals()) | 601 | params = self.__generate_params(locals()) |
523 | return self.__api_request('GET', '/api/v1/domain_blocks', params) | 602 | return self.__api_request('GET', '/api/v1/domain_blocks', params) |
524 | 603 | ||
@@ -555,6 +634,9 @@ class Mastodon: | |||
555 | 634 | ||
556 | Returns a toot dict with the new status. | 635 | Returns a toot dict with the new status. |
557 | """ | 636 | """ |
637 | if in_reply_to_id != None: | ||
638 | in_reply_to_id = self.__unpack_id(in_reply_to_id) | ||
639 | |||
558 | params_initial = locals() | 640 | params_initial = locals() |
559 | 641 | ||
560 | # Validate visibility parameter | 642 | # Validate visibility parameter |
@@ -599,6 +681,7 @@ class Mastodon: | |||
599 | 681 | ||
600 | Returns an empty dict for good measure. | 682 | Returns an empty dict for good measure. |
601 | """ | 683 | """ |
684 | id = self.__unpack_id(id) | ||
602 | url = '/api/v1/statuses/{0}'.format(str(id)) | 685 | url = '/api/v1/statuses/{0}'.format(str(id)) |
603 | return self.__api_request('DELETE', url) | 686 | return self.__api_request('DELETE', url) |
604 | 687 | ||
@@ -608,6 +691,7 @@ class Mastodon: | |||
608 | 691 | ||
609 | Returns a toot dict with a new status that wraps around the reblogged one. | 692 | Returns a toot dict with a new status that wraps around the reblogged one. |
610 | """ | 693 | """ |
694 | id = self.__unpack_id(id) | ||
611 | url = '/api/v1/statuses/{0}/reblog'.format(str(id)) | 695 | url = '/api/v1/statuses/{0}/reblog'.format(str(id)) |
612 | return self.__api_request('POST', url) | 696 | return self.__api_request('POST', url) |
613 | 697 | ||
@@ -617,6 +701,7 @@ class Mastodon: | |||
617 | 701 | ||
618 | Returns a toot dict with the status that used to be reblogged. | 702 | Returns a toot dict with the status that used to be reblogged. |
619 | """ | 703 | """ |
704 | id = self.__unpack_id(id) | ||
620 | url = '/api/v1/statuses/{0}/unreblog'.format(str(id)) | 705 | url = '/api/v1/statuses/{0}/unreblog'.format(str(id)) |
621 | return self.__api_request('POST', url) | 706 | return self.__api_request('POST', url) |
622 | 707 | ||
@@ -626,6 +711,7 @@ class Mastodon: | |||
626 | 711 | ||
627 | Returns a toot dict with the favourited status. | 712 | Returns a toot dict with the favourited status. |
628 | """ | 713 | """ |
714 | id = self.__unpack_id(id) | ||
629 | url = '/api/v1/statuses/{0}/favourite'.format(str(id)) | 715 | url = '/api/v1/statuses/{0}/favourite'.format(str(id)) |
630 | return self.__api_request('POST', url) | 716 | return self.__api_request('POST', url) |
631 | 717 | ||
@@ -635,6 +721,7 @@ class Mastodon: | |||
635 | 721 | ||
636 | Returns a toot dict with the un-favourited status. | 722 | Returns a toot dict with the un-favourited status. |
637 | """ | 723 | """ |
724 | id = self.__unpack_id(id) | ||
638 | url = '/api/v1/statuses/{0}/unfavourite'.format(str(id)) | 725 | url = '/api/v1/statuses/{0}/unfavourite'.format(str(id)) |
639 | return self.__api_request('POST', url) | 726 | return self.__api_request('POST', url) |
640 | 727 | ||
@@ -644,6 +731,7 @@ class Mastodon: | |||
644 | 731 | ||
645 | Returns a toot dict with the now muted status | 732 | Returns a toot dict with the now muted status |
646 | """ | 733 | """ |
734 | id = self.__unpack_id(id) | ||
647 | url = '/api/v1/statuses/{0}/mute'.format(str(id)) | 735 | url = '/api/v1/statuses/{0}/mute'.format(str(id)) |
648 | return self.__api_request('POST', url) | 736 | return self.__api_request('POST', url) |
649 | 737 | ||
@@ -653,6 +741,7 @@ class Mastodon: | |||
653 | 741 | ||
654 | Returns a toot dict with the status that used to be muted. | 742 | Returns a toot dict with the status that used to be muted. |
655 | """ | 743 | """ |
744 | id = self.__unpack_id(id) | ||
656 | url = '/api/v1/statuses/{0}/unmute'.format(str(id)) | 745 | url = '/api/v1/statuses/{0}/unmute'.format(str(id)) |
657 | return self.__api_request('POST', url) | 746 | return self.__api_request('POST', url) |
658 | 747 | ||
@@ -670,6 +759,7 @@ class Mastodon: | |||
670 | """ | 759 | """ |
671 | Deletes a single notification | 760 | Deletes a single notification |
672 | """ | 761 | """ |
762 | id = self.__unpack_id(id) | ||
673 | params = self.__generate_params(locals()) | 763 | params = self.__generate_params(locals()) |
674 | return self.__api_request('POST', '/api/v1/notifications/dismiss', params) | 764 | return self.__api_request('POST', '/api/v1/notifications/dismiss', params) |
675 | 765 | ||
@@ -682,6 +772,7 @@ class Mastodon: | |||
682 | 772 | ||
683 | Returns a relationship dict containing the updated relationship to the user. | 773 | Returns a relationship dict containing the updated relationship to the user. |
684 | """ | 774 | """ |
775 | id = self.__unpack_id(id) | ||
685 | url = '/api/v1/accounts/{0}/follow'.format(str(id)) | 776 | url = '/api/v1/accounts/{0}/follow'.format(str(id)) |
686 | return self.__api_request('POST', url) | 777 | return self.__api_request('POST', url) |
687 | 778 | ||
@@ -700,6 +791,7 @@ class Mastodon: | |||
700 | 791 | ||
701 | Returns a relationship dict containing the updated relationship to the user. | 792 | Returns a relationship dict containing the updated relationship to the user. |
702 | """ | 793 | """ |
794 | id = self.__unpack_id(id) | ||
703 | url = '/api/v1/accounts/{0}/unfollow'.format(str(id)) | 795 | url = '/api/v1/accounts/{0}/unfollow'.format(str(id)) |
704 | return self.__api_request('POST', url) | 796 | return self.__api_request('POST', url) |
705 | 797 | ||
@@ -709,6 +801,7 @@ class Mastodon: | |||
709 | 801 | ||
710 | Returns a relationship dict containing the updated relationship to the user. | 802 | Returns a relationship dict containing the updated relationship to the user. |
711 | """ | 803 | """ |
804 | id = self.__unpack_id(id) | ||
712 | url = '/api/v1/accounts/{0}/block'.format(str(id)) | 805 | url = '/api/v1/accounts/{0}/block'.format(str(id)) |
713 | return self.__api_request('POST', url) | 806 | return self.__api_request('POST', url) |
714 | 807 | ||
@@ -718,6 +811,7 @@ class Mastodon: | |||
718 | 811 | ||
719 | Returns a relationship dict containing the updated relationship to the user. | 812 | Returns a relationship dict containing the updated relationship to the user. |
720 | """ | 813 | """ |
814 | id = self.__unpack_id(id) | ||
721 | url = '/api/v1/accounts/{0}/unblock'.format(str(id)) | 815 | url = '/api/v1/accounts/{0}/unblock'.format(str(id)) |
722 | return self.__api_request('POST', url) | 816 | return self.__api_request('POST', url) |
723 | 817 | ||
@@ -727,6 +821,7 @@ class Mastodon: | |||
727 | 821 | ||
728 | Returns a relationship dict containing the updated relationship to the user. | 822 | Returns a relationship dict containing the updated relationship to the user. |
729 | """ | 823 | """ |
824 | id = self.__unpack_id(id) | ||
730 | url = '/api/v1/accounts/{0}/mute'.format(str(id)) | 825 | url = '/api/v1/accounts/{0}/mute'.format(str(id)) |
731 | return self.__api_request('POST', url) | 826 | return self.__api_request('POST', url) |
732 | 827 | ||
@@ -736,6 +831,7 @@ class Mastodon: | |||
736 | 831 | ||
737 | Returns a relationship dict containing the updated relationship to the user. | 832 | Returns a relationship dict containing the updated relationship to the user. |
738 | """ | 833 | """ |
834 | id = self.__unpack_id(id) | ||
739 | url = '/api/v1/accounts/{0}/unmute'.format(str(id)) | 835 | url = '/api/v1/accounts/{0}/unmute'.format(str(id)) |
740 | return self.__api_request('POST', url) | 836 | return self.__api_request('POST', url) |
741 | 837 | ||
@@ -763,6 +859,8 @@ class Mastodon: | |||
763 | 859 | ||
764 | Returns a report dict. | 860 | Returns a report dict. |
765 | """ | 861 | """ |
862 | account_id = self.__unpack_id(account_id) | ||
863 | status_ids = map(lambda x: self.__unpack_id(x), status_ids) | ||
766 | params = self.__generate_params(locals()) | 864 | params = self.__generate_params(locals()) |
767 | return self.__api_request('POST', '/api/v1/reports/', params) | 865 | return self.__api_request('POST', '/api/v1/reports/', params) |
768 | 866 | ||
@@ -775,6 +873,7 @@ class Mastodon: | |||
775 | 873 | ||
776 | Returns an empty dict. | 874 | Returns an empty dict. |
777 | """ | 875 | """ |
876 | id = self.__unpack_id(id) | ||
778 | url = '/api/v1/follow_requests/{0}/authorize'.format(str(id)) | 877 | url = '/api/v1/follow_requests/{0}/authorize'.format(str(id)) |
779 | return self.__api_request('POST', url) | 878 | return self.__api_request('POST', url) |
780 | 879 | ||
@@ -784,6 +883,7 @@ class Mastodon: | |||
784 | 883 | ||
785 | Returns an empty dict. | 884 | Returns an empty dict. |
786 | """ | 885 | """ |
886 | id = self.__unpack_id(id) | ||
787 | url = '/api/v1/follow_requests/{0}/reject'.format(str(id)) | 887 | url = '/api/v1/follow_requests/{0}/reject'.format(str(id)) |
788 | return self.__api_request('POST', url) | 888 | return self.__api_request('POST', url) |
789 | 889 | ||
@@ -911,57 +1011,34 @@ class Mastodon: | |||
911 | ### | 1011 | ### |
912 | # Streaming | 1012 | # Streaming |
913 | ### | 1013 | ### |
914 | def user_stream(self, listener, async=False): | 1014 | def stream_user(self, listener, async=False): |
915 | """ | 1015 | """ |
916 | Streams events that are relevant to the authorized user, i.e. home | 1016 | Streams events that are relevant to the authorized user, i.e. home |
917 | timeline and notifications. 'listener' should be a subclass of | 1017 | timeline and notifications. 'listener' should be a subclass of |
918 | StreamListener which will receive callbacks for incoming events. | 1018 | StreamListener which will receive callbacks for incoming events. |
919 | |||
920 | If async is False, this method blocks forever. | ||
921 | |||
922 | If async is True, 'listener' will listen on another thread and this method | ||
923 | will return a handle corresponding to the open connection. The | ||
924 | connection may be closed at any time by calling its close() method. | ||
925 | """ | 1019 | """ |
926 | return self.__stream('/api/v1/streaming/user', listener, async=async) | 1020 | return self.__stream('/api/v1/streaming/user', listener, async=async) |
927 | 1021 | ||
928 | def public_stream(self, listener, async=False): | 1022 | def stream_public(self, listener, async=False): |
929 | """ | 1023 | """ |
930 | Streams public events. 'listener' should be a subclass of StreamListener | 1024 | Streams public events. 'listener' should be a subclass of StreamListener |
931 | which will receive callbacks for incoming events. | 1025 | which will receive callbacks for incoming events. |
932 | |||
933 | If async is False, this method blocks forever. | ||
934 | |||
935 | If async is True, 'listener' will listen on another thread and this method | ||
936 | will return a handle corresponding to the open connection. The | ||
937 | connection may be closed at any time by calling its close() method. | ||
938 | """ | 1026 | """ |
939 | return self.__stream('/api/v1/streaming/public', listener, async=async) | 1027 | return self.__stream('/api/v1/streaming/public', listener, async=async) |
940 | 1028 | ||
941 | def local_stream(self, listener, async=False): | 1029 | def stream_local(self, listener, async=False): |
942 | """ | 1030 | """ |
943 | Streams local events. 'listener' should be a subclass of StreamListener | 1031 | Streams local events. 'listener' should be a subclass of StreamListener |
944 | which will receive callbacks for incoming events. | 1032 | which will receive callbacks for incoming events. |
945 | 1033 | ||
946 | If async is False, this method blocks forever. | ||
947 | |||
948 | If async is True, 'listener' will listen on another thread and this method | ||
949 | will return a handle corresponding to the open connection. The | ||
950 | connection may be closed at any time by calling its close() method. | ||
951 | """ | 1034 | """ |
952 | return self.__stream('/api/v1/streaming/public/local', listener, async=async) | 1035 | return self.__stream('/api/v1/streaming/public/local', listener, async=async) |
953 | 1036 | ||
954 | def hashtag_stream(self, tag, listener, async=False): | 1037 | def stream_hashtag(self, tag, listener, async=False): |
955 | """ | 1038 | """ |
956 | Returns all public statuses for the hashtag 'tag'. 'listener' should be | 1039 | Returns all public statuses for the hashtag 'tag'. 'listener' should be |
957 | a subclass of StreamListener which will receive callbacks for incoming | 1040 | a subclass of StreamListener which will receive callbacks for incoming |
958 | events. | 1041 | events. |
959 | |||
960 | If async is False, this method blocks forever. | ||
961 | |||
962 | If async is True, 'listener' will listen on another thread and this method | ||
963 | will return a handle corresponding to the open connection. The | ||
964 | connection may be closed at any time by calling its close() method. | ||
965 | """ | 1042 | """ |
966 | return self.__stream("/api/v1/streaming/hashtag?tag={}".format(tag), listener) | 1043 | return self.__stream("/api/v1/streaming/hashtag?tag={}".format(tag), listener) |
967 | 1044 | ||
@@ -985,8 +1062,8 @@ class Mastodon: | |||
985 | 1062 | ||
986 | return (date_time_utc - epoch_utc).total_seconds() | 1063 | return (date_time_utc - epoch_utc).total_seconds() |
987 | 1064 | ||
988 | 1065 | @staticmethod | |
989 | def __json_date_parse(self, json_object): | 1066 | def __json_date_parse(json_object): |
990 | """ | 1067 | """ |
991 | Parse dates in certain known json fields, if possible. | 1068 | Parse dates in certain known json fields, if possible. |
992 | """ | 1069 | """ |
@@ -1002,27 +1079,25 @@ class Mastodon: | |||
1002 | raise MastodonAPIError('Encountered invalid date.') | 1079 | raise MastodonAPIError('Encountered invalid date.') |
1003 | return json_object | 1080 | return json_object |
1004 | 1081 | ||
1005 | def __json_id_to_bignum(self, json_object): | 1082 | @staticmethod |
1083 | def __json_id_to_bignum(json_object): | ||
1006 | """ | 1084 | """ |
1007 | Converts json string IDs to native python bignums. | 1085 | Converts json string IDs to native python bignums. |
1008 | """ | 1086 | """ |
1009 | if sys.version_info.major >= 3: | 1087 | for key in ('id', 'in_reply_to_id', 'in_reply_to_account_id'): |
1010 | str_type = str | 1088 | if (key in json_object and |
1011 | else: | 1089 | isinstance(json_object[key], six.text_type)): |
1012 | str_type = unicode | 1090 | try: |
1013 | 1091 | json_object[key] = int(json_object[key]) | |
1014 | if ('id' in json_object and | 1092 | except ValueError: |
1015 | isinstance(json_object['id'], str_type)): | 1093 | pass |
1016 | try: | ||
1017 | json_object['id'] = int(json_object['id']) | ||
1018 | except ValueError: | ||
1019 | pass | ||
1020 | 1094 | ||
1021 | return json_object | 1095 | return json_object |
1022 | 1096 | ||
1023 | def __json_hooks(self, json_object): | 1097 | @staticmethod |
1024 | json_object = self.__json_date_parse(json_object) | 1098 | def __json_hooks(json_object): |
1025 | json_object = self.__json_id_to_bignum(json_object) | 1099 | json_object = Mastodon.__json_date_parse(json_object) |
1100 | json_object = Mastodon.__json_id_to_bignum(json_object) | ||
1026 | return json_object | 1101 | return json_object |
1027 | 1102 | ||
1028 | def __api_request(self, method, endpoint, params={}, files={}, do_ratelimiting=True): | 1103 | def __api_request(self, method, endpoint, params={}, files={}, do_ratelimiting=True): |
@@ -1201,9 +1276,16 @@ class Mastodon: | |||
1201 | instance = self.instance() | 1276 | instance = self.instance() |
1202 | if "streaming_api" in instance["urls"] and instance["urls"]["streaming_api"] != self.api_base_url: | 1277 | if "streaming_api" in instance["urls"] and instance["urls"]["streaming_api"] != self.api_base_url: |
1203 | # This is probably a websockets URL, which is really for the browser, but requests can't handle it | 1278 | # This is probably a websockets URL, which is really for the browser, but requests can't handle it |
1204 | # So we do this below to turn it into an HTTPS URL | 1279 | # So we do this below to turn it into an HTTPS or HTTP URL |
1205 | parse = urlparse(instance["urls"]["streaming_api"]) | 1280 | parse = urlparse(instance["urls"]["streaming_api"]) |
1206 | url = "https://" + parse.netloc | 1281 | if parse.scheme == 'wss': |
1282 | url = "https://" + parse.netloc | ||
1283 | elif parse.scheme == 'ws': | ||
1284 | url = "http://" + parse.netloc | ||
1285 | else: | ||
1286 | raise MastodonAPIError( | ||
1287 | "Could not parse streaming api location returned from server: {}.".format( | ||
1288 | instance["urls"]["streaming_api"])) | ||
1207 | else: | 1289 | else: |
1208 | url = self.api_base_url | 1290 | url = self.api_base_url |
1209 | 1291 | ||
@@ -1224,7 +1306,11 @@ class Mastodon: | |||
1224 | def close(self): | 1306 | def close(self): |
1225 | self.connection.close() | 1307 | self.connection.close() |
1226 | 1308 | ||
1309 | def is_alive(self): | ||
1310 | return self._thread.is_alive() | ||
1311 | |||
1227 | def _threadproc(self): | 1312 | def _threadproc(self): |
1313 | self._thread = threading.current_thread() | ||
1228 | with closing(connection) as r: | 1314 | with closing(connection) as r: |
1229 | try: | 1315 | try: |
1230 | listener.handle_stream(r.iter_lines()) | 1316 | listener.handle_stream(r.iter_lines()) |
@@ -1268,7 +1354,20 @@ class Mastodon: | |||
1268 | del params[key] | 1354 | del params[key] |
1269 | 1355 | ||
1270 | return params | 1356 | return params |
1271 | 1357 | ||
1358 | def __unpack_id(self, id): | ||
1359 | """ | ||
1360 | Internal object-to-id converter | ||
1361 | |||
1362 | Checks if id is a dict that contains id and | ||
1363 | returns the id inside, otherwise just returns | ||
1364 | the id straight. | ||
1365 | """ | ||
1366 | if isinstance(id, dict) and "id" in id: | ||
1367 | return id["id"] | ||
1368 | else: | ||
1369 | return id | ||
1370 | |||
1272 | def __get_token_expired(self): | 1371 | def __get_token_expired(self): |
1273 | """Internal helper for oauth code""" | 1372 | """Internal helper for oauth code""" |
1274 | return self._token_expired < datetime.datetime.now() | 1373 | return self._token_expired < datetime.datetime.now() |
@@ -1306,6 +1405,7 @@ class MastodonError(Exception): | |||
1306 | 1405 | ||
1307 | 1406 | ||
1308 | class MastodonIllegalArgumentError(ValueError, MastodonError): | 1407 | class MastodonIllegalArgumentError(ValueError, MastodonError): |
1408 | """Raised when an incorrect parameter is passed to a function""" | ||
1309 | pass | 1409 | pass |
1310 | 1410 | ||
1311 | 1411 | ||
@@ -1314,16 +1414,24 @@ class MastodonIOError(IOError, MastodonError): | |||
1314 | 1414 | ||
1315 | 1415 | ||
1316 | class MastodonFileNotFoundError(MastodonIOError): | 1416 | class MastodonFileNotFoundError(MastodonIOError): |
1417 | """Raised when a file requested to be loaded can not be opened""" | ||
1317 | pass | 1418 | pass |
1318 | 1419 | ||
1319 | 1420 | ||
1320 | class MastodonNetworkError(MastodonIOError): | 1421 | class MastodonNetworkError(MastodonIOError): |
1422 | """Raised when network communication with the server fails""" | ||
1321 | pass | 1423 | pass |
1322 | 1424 | ||
1323 | 1425 | ||
1324 | class MastodonAPIError(MastodonError): | 1426 | class MastodonAPIError(MastodonError): |
1427 | """Raised when the mastodon API generates a response that cannot be handled""" | ||
1325 | pass | 1428 | pass |
1326 | 1429 | ||
1327 | 1430 | ||
1328 | class MastodonRatelimitError(MastodonError): | 1431 | class MastodonRatelimitError(MastodonError): |
1432 | """Raised when rate limiting is set to manual mode and the rate limit is exceeded""" | ||
1433 | pass | ||
1434 | |||
1435 | class MastodonMalformedEventError(MastodonError): | ||
1436 | """Raised when the server-sent event stream is malformed""" | ||
1329 | pass | 1437 | pass |
diff --git a/mastodon/__init__.py b/mastodon/__init__.py index 9c8e39b..fdf776d 100644 --- a/mastodon/__init__.py +++ b/mastodon/__init__.py | |||
@@ -1,4 +1,4 @@ | |||
1 | from mastodon.Mastodon import Mastodon | 1 | from mastodon.Mastodon import Mastodon |
2 | from mastodon.streaming import StreamListener, MalformedEventError | 2 | from mastodon.streaming import StreamListener, CallbackStreamListener |
3 | 3 | ||
4 | __all__ = ['Mastodon', 'StreamListener', 'MalformedEventError'] | 4 | __all__ = ['Mastodon', 'StreamListener', 'CallbackStreamListener'] |
diff --git a/mastodon/streaming.py b/mastodon/streaming.py index 290ed44..92a02dc 100644 --- a/mastodon/streaming.py +++ b/mastodon/streaming.py | |||
@@ -4,17 +4,9 @@ https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/Streaming-A | |||
4 | """ | 4 | """ |
5 | 5 | ||
6 | import json | 6 | import json |
7 | import logging | ||
8 | import six | 7 | import six |
9 | 8 | from mastodon import Mastodon | |
10 | 9 | from mastodon.Mastodon import MastodonMalformedEventError | |
11 | log = logging.getLogger(__name__) | ||
12 | |||
13 | |||
14 | class MalformedEventError(Exception): | ||
15 | """Raised when the server-sent event stream is malformed.""" | ||
16 | pass | ||
17 | |||
18 | 10 | ||
19 | class StreamListener(object): | 11 | class StreamListener(object): |
20 | """Callbacks for the streaming API. Create a subclass, override the on_xxx | 12 | """Callbacks for the streaming API. Create a subclass, override the on_xxx |
@@ -24,7 +16,7 @@ class StreamListener(object): | |||
24 | 16 | ||
25 | def on_update(self, status): | 17 | def on_update(self, status): |
26 | """A new status has appeared! 'status' is the parsed JSON dictionary | 18 | """A new status has appeared! 'status' is the parsed JSON dictionary |
27 | describing the status.""" | 19 | describing the status.""" |
28 | pass | 20 | pass |
29 | 21 | ||
30 | def on_notification(self, notification): | 22 | def on_notification(self, notification): |
@@ -40,7 +32,8 @@ describing the status.""" | |||
40 | """The server has sent us a keep-alive message. This callback may be | 32 | """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 | 33 | useful to carry out periodic housekeeping tasks, or just to confirm |
42 | that the connection is still open.""" | 34 | that the connection is still open.""" |
43 | 35 | pass | |
36 | |||
44 | def handle_stream(self, lines): | 37 | def handle_stream(self, lines): |
45 | """ | 38 | """ |
46 | Handles a stream of events from the Mastodon server. When each event | 39 | Handles a stream of events from the Mastodon server. When each event |
@@ -55,7 +48,7 @@ describing the status.""" | |||
55 | line = raw_line.decode('utf-8') | 48 | line = raw_line.decode('utf-8') |
56 | except UnicodeDecodeError as err: | 49 | except UnicodeDecodeError as err: |
57 | six.raise_from( | 50 | six.raise_from( |
58 | MalformedEventError("Malformed UTF-8", line), | 51 | MastodonMalformedEventError("Malformed UTF-8", line), |
59 | err | 52 | err |
60 | ) | 53 | ) |
61 | 54 | ||
@@ -63,7 +56,7 @@ describing the status.""" | |||
63 | self.handle_heartbeat() | 56 | self.handle_heartbeat() |
64 | elif line == '': | 57 | elif line == '': |
65 | # end of event | 58 | # end of event |
66 | self._despatch(event) | 59 | self._dispatch(event) |
67 | event = {} | 60 | event = {} |
68 | else: | 61 | else: |
69 | key, value = line.split(': ', 1) | 62 | key, value = line.split(': ', 1) |
@@ -74,33 +67,65 @@ describing the status.""" | |||
74 | else: | 67 | else: |
75 | event[key] = value | 68 | event[key] = value |
76 | 69 | ||
77 | # end of stream | 70 | def _dispatch(self, event): |
78 | if event: | ||
79 | log.warn("outstanding partial event at end of stream: %s", event) | ||
80 | |||
81 | def _despatch(self, event): | ||
82 | try: | 71 | try: |
83 | name = event['event'] | 72 | name = event['event'] |
84 | data = event['data'] | 73 | data = event['data'] |
85 | payload = json.loads(data) | 74 | payload = json.loads(data, object_hook = Mastodon._Mastodon__json_hooks) |
86 | except KeyError as err: | 75 | except KeyError as err: |
87 | six.raise_from( | 76 | six.raise_from( |
88 | MalformedEventError('Missing field', err.args[0], event), | 77 | MastodonMalformedEventError('Missing field', err.args[0], event), |
89 | err | 78 | err |
90 | ) | 79 | ) |
91 | except ValueError as err: | 80 | except ValueError as err: |
92 | # py2: plain ValueError | 81 | # py2: plain ValueError |
93 | # py3: json.JSONDecodeError, a subclass of ValueError | 82 | # py3: json.JSONDecodeError, a subclass of ValueError |
94 | six.raise_from( | 83 | six.raise_from( |
95 | MalformedEventError('Bad JSON', data), | 84 | MastodonMalformedEventError('Bad JSON', data), |
96 | err | 85 | err |
97 | ) | 86 | ) |
98 | 87 | ||
99 | handler_name = 'on_' + name | 88 | handler_name = 'on_' + name |
100 | try: | 89 | try: |
101 | handler = getattr(self, handler_name) | 90 | handler = getattr(self, handler_name) |
102 | except AttributeError: | 91 | except AttributeError as err: |
103 | log.warn("Unhandled event '%s'", name) | 92 | six.raise_from( |
93 | MastodonMalformedEventError('Bad event type', name), | ||
94 | err | ||
95 | ) | ||
104 | else: | 96 | else: |
105 | # TODO: allow handlers to return/raise to stop streaming cleanly | 97 | # TODO: allow handlers to return/raise to stop streaming cleanly |
106 | handler(payload) | 98 | handler(payload) |
99 | |||
100 | class CallbackStreamListener(StreamListener): | ||
101 | """ | ||
102 | Simple callback stream handler class. | ||
103 | Can optionally additionally send local update events to a separate handler. | ||
104 | """ | ||
105 | def __init__(self, update_handler = None, local_update_handler = None, delete_handler = None, notification_handler = None): | ||
106 | super(CallbackStreamListener, self).__init__() | ||
107 | self.update_handler = update_handler | ||
108 | self.local_update_handler = local_update_handler | ||
109 | self.delete_handler = delete_handler | ||
110 | self.notification_handler = notification_handler | ||
111 | |||
112 | def on_update(self, status): | ||
113 | if self.update_handler != None: | ||
114 | self.update_handler(status) | ||
115 | |||
116 | try: | ||
117 | if self.local_update_handler != None and not "@" in status["account"]["acct"]: | ||
118 | self.local_update_handler(status) | ||
119 | except Exception as err: | ||
120 | six.raise_from( | ||
121 | MastodonMalformedEventError('received bad update', status), | ||
122 | err | ||
123 | ) | ||
124 | |||
125 | def on_delete(self, deleted_id): | ||
126 | if self.delete_handler != None: | ||
127 | self.delete_handler(deleted_id) | ||
128 | |||
129 | def on_notification(self, notification): | ||
130 | if self.notification_handler != None: | ||
131 | self.notification_handler(notification) \ No newline at end of file | ||