aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'mastodon')
-rw-r--r--mastodon/Mastodon.py206
-rw-r--r--mastodon/__init__.py4
-rw-r--r--mastodon/streaming.py91
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
17import copy 17import copy
18import threading 18import threading
19import sys 19import sys
20import six
21
20try: 22try:
21 from urllib.parse import urlparse 23 from urllib.parse import urlparse
22except ImportError: 24except ImportError:
23 from urlparse import urlparse 25 from urlparse import urlparse
24 26
25 27
26class Mastodon: 28class 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
1308class MastodonIllegalArgumentError(ValueError, MastodonError): 1407class 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
1316class MastodonFileNotFoundError(MastodonIOError): 1416class MastodonFileNotFoundError(MastodonIOError):
1417 """Raised when a file requested to be loaded can not be opened"""
1317 pass 1418 pass
1318 1419
1319 1420
1320class MastodonNetworkError(MastodonIOError): 1421class MastodonNetworkError(MastodonIOError):
1422 """Raised when network communication with the server fails"""
1321 pass 1423 pass
1322 1424
1323 1425
1324class MastodonAPIError(MastodonError): 1426class MastodonAPIError(MastodonError):
1427 """Raised when the mastodon API generates a response that cannot be handled"""
1325 pass 1428 pass
1326 1429
1327 1430
1328class MastodonRatelimitError(MastodonError): 1431class MastodonRatelimitError(MastodonError):
1432 """Raised when rate limiting is set to manual mode and the rate limit is exceeded"""
1433 pass
1434
1435class 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 @@
1from mastodon.Mastodon import Mastodon 1from mastodon.Mastodon import Mastodon
2from mastodon.streaming import StreamListener, MalformedEventError 2from 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
6import json 6import json
7import logging
8import six 7import six
9 8from mastodon import Mastodon
10 9from mastodon.Mastodon import MastodonMalformedEventError
11log = logging.getLogger(__name__)
12
13
14class MalformedEventError(Exception):
15 """Raised when the server-sent event stream is malformed."""
16 pass
17
18 10
19class StreamListener(object): 11class 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
27describing 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
100class 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
Powered by cgit v1.2.3 (git 2.41.0)