1#!/usr/bin/env python 2 3# 4# 5# Copyright 2007-2016, 2018 The Python-Twitter Developers 6# 7# Licensed under the Apache License, Version 2.0 (the "License"); 8# you may not use this file except in compliance with the License. 9# You may obtain a copy of the License at 10# 11# http://www.apache.org/licenses/LICENSE-2.0 12# 13# Unless required by applicable law or agreed to in writing, software 14# distributed under the License is distributed on an "AS IS" BASIS, 15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16# See the License for the specific language governing permissions and 17# limitations under the License. 18 19"""A library that provides a Python interface to the Twitter API""" 20from __future__ import division 21from __future__ import print_function 22 23import json 24import sys 25import gzip 26import time 27import base64 28import re 29import logging 30import requests 31from requests_oauthlib import OAuth1, OAuth2 32import io 33import warnings 34from uuid import uuid4 35import os 36 37try: 38 # python 3 39 from urllib.parse import urlparse, urlunparse, urlencode, quote_plus 40 from urllib.request import __version__ as urllib_version 41except ImportError: 42 from urlparse import urlparse, urlunparse 43 from urllib import urlencode, quote_plus 44 from urllib import __version__ as urllib_version 45 46from twitter import ( 47 __version__, 48 _FileCache, 49 Category, 50 DirectMessage, 51 List, 52 Status, 53 Trend, 54 User, 55 UserStatus, 56) 57 58from twitter.ratelimit import RateLimit 59 60from twitter.twitter_utils import ( 61 calc_expected_status_length, 62 is_url, 63 parse_media_file, 64 enf_type, 65 parse_arg_list) 66 67from twitter.error import ( 68 TwitterError, 69 PythonTwitterDeprecationWarning330, 70) 71 72if sys.version_info > (3,): 73 long = int # pylint: disable=invalid-name,redefined-builtin 74 75CHARACTER_LIMIT = 280 76 77# A singleton representing a lazily instantiated FileCache. 78DEFAULT_CACHE = object() 79 80logger = logging.getLogger(__name__) 81 82 83class Api(object): 84 """A python interface into the Twitter API 85 86 By default, the Api caches results for 1 minute. 87 88 Example usage: 89 90 To create an instance of the twitter.Api class, with no authentication: 91 92 >>> import twitter 93 >>> api = twitter.Api() 94 95 To fetch a single user's public status messages, where "user" is either 96 a Twitter "short name" or their user id. 97 98 >>> statuses = api.GetUserTimeline(user) 99 >>> print([s.text for s in statuses]) 100 101 To use authentication, instantiate the twitter.Api class with a 102 consumer key and secret; and the oAuth key and secret: 103 104 >>> api = twitter.Api(consumer_key='twitter consumer key', 105 consumer_secret='twitter consumer secret', 106 access_token_key='the_key_given', 107 access_token_secret='the_key_secret') 108 109 To fetch your friends (after being authenticated): 110 111 >>> users = api.GetFriends() 112 >>> print([u.name for u in users]) 113 114 To post a twitter status message (after being authenticated): 115 116 >>> status = api.PostUpdate('I love python-twitter!') 117 >>> print(status.text) 118 I love python-twitter! 119 120 There are many other methods, including: 121 122 >>> api.PostUpdates(status) 123 >>> api.PostDirectMessage(user, text) 124 >>> api.GetUser(user) 125 >>> api.GetReplies() 126 >>> api.GetUserTimeline(user) 127 >>> api.GetHomeTimeline() 128 >>> api.GetStatus(status_id) 129 >>> api.GetStatuses(status_ids) 130 >>> api.DestroyStatus(status_id) 131 >>> api.GetFriends(user) 132 >>> api.GetFollowers() 133 >>> api.GetFeatured() 134 >>> api.GetDirectMessages() 135 >>> api.GetSentDirectMessages() 136 >>> api.PostDirectMessage(user, text) 137 >>> api.DestroyDirectMessage(message_id) 138 >>> api.DestroyFriendship(user) 139 >>> api.CreateFriendship(user) 140 >>> api.LookupFriendship(user) 141 >>> api.VerifyCredentials() 142 """ 143 144 DEFAULT_CACHE_TIMEOUT = 60 # cache for 1 minute 145 _API_REALM = 'Twitter API' 146 147 def __init__(self, 148 consumer_key=None, 149 consumer_secret=None, 150 access_token_key=None, 151 access_token_secret=None, 152 application_only_auth=False, 153 input_encoding=None, 154 request_headers=None, 155 cache=DEFAULT_CACHE, 156 base_url=None, 157 stream_url=None, 158 upload_url=None, 159 chunk_size=1024 * 1024, 160 use_gzip_compression=False, 161 debugHTTP=False, 162 timeout=None, 163 sleep_on_rate_limit=False, 164 tweet_mode='compat', 165 proxies=None): 166 """Instantiate a new twitter.Api object. 167 168 Args: 169 consumer_key (str): 170 Your Twitter user's consumer_key. 171 consumer_secret (str): 172 Your Twitter user's consumer_secret. 173 access_token_key (str): 174 The oAuth access token key value you retrieved 175 from running get_access_token.py. 176 access_token_secret (str): 177 The oAuth access token's secret, also retrieved 178 from the get_access_token.py run. 179 application_only_auth: 180 Use Application-Only Auth instead of User Auth. 181 Defaults to False [Optional] 182 input_encoding (str, optional): 183 The encoding used to encode input strings. 184 request_header (dict, optional): 185 A dictionary of additional HTTP request headers. 186 cache (object, optional): 187 The cache instance to use. Defaults to DEFAULT_CACHE. 188 Use None to disable caching. 189 base_url (str, optional): 190 The base URL to use to contact the Twitter API. 191 Defaults to https://api.twitter.com. 192 stream_url (str, optional): 193 The base URL to use for streaming endpoints. 194 Defaults to 'https://stream.twitter.com/1.1'. 195 upload_url (str, optional): 196 The base URL to use for uploads. Defaults to 'https://upload.twitter.com/1.1'. 197 chunk_size (int, optional): 198 Chunk size to use for chunked (multi-part) uploads of images/videos/gifs. 199 Defaults to 1MB. Anything under 16KB and you run the risk of erroring out 200 on 15MB files. 201 use_gzip_compression (bool, optional): 202 Set to True to tell enable gzip compression for any call 203 made to Twitter. Defaults to False. 204 debugHTTP (bool, optional): 205 Set to True to enable debug output from urllib2 when performing 206 any HTTP requests. Defaults to False. 207 timeout (int, optional): 208 Set timeout (in seconds) of the http/https requests. If None the 209 requests lib default will be used. Defaults to None. 210 sleep_on_rate_limit (bool, optional): 211 Whether to sleep an appropriate amount of time if a rate limit is hit for 212 an endpoint. 213 tweet_mode (str, optional): 214 Whether to use the new (as of Sept. 2016) extended tweet mode. See docs for 215 details. Choices are ['compatibility', 'extended']. 216 proxies (dict, optional): 217 A dictionary of proxies for the request to pass through, if not specified 218 allows requests lib to use environmental variables for proxy if any. 219 """ 220 221 # check to see if the library is running on a Google App Engine instance 222 # see GAE.rst for more information 223 if os.environ: 224 if 'APPENGINE_RUNTIME' in os.environ.keys(): 225 # Adapter ensures requests use app engine's urlfetch 226 import requests_toolbelt.adapters.appengine 227 requests_toolbelt.adapters.appengine.monkeypatch() 228 # App Engine does not like this caching strategy, disable caching 229 cache = None 230 231 self.SetCache(cache) 232 self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT 233 self._input_encoding = input_encoding 234 self._use_gzip = use_gzip_compression 235 self._debugHTTP = debugHTTP 236 self._shortlink_size = 19 237 if timeout and timeout < 30: 238 warnings.warn("Warning: The Twitter streaming API sends 30s keepalives, the given timeout is shorter!") 239 self._timeout = timeout 240 self.__auth = None 241 242 self._InitializeRequestHeaders(request_headers) 243 self._InitializeUserAgent() 244 self._InitializeDefaultParameters() 245 246 self.rate_limit = RateLimit() 247 self.sleep_on_rate_limit = sleep_on_rate_limit 248 self.tweet_mode = tweet_mode 249 self.proxies = proxies 250 251 if base_url is None: 252 self.base_url = 'https://api.twitter.com/1.1' 253 else: 254 self.base_url = base_url 255 256 if stream_url is None: 257 self.stream_url = 'https://stream.twitter.com/1.1' 258 else: 259 self.stream_url = stream_url 260 261 if upload_url is None: 262 self.upload_url = 'https://upload.twitter.com/1.1' 263 else: 264 self.upload_url = upload_url 265 266 self.chunk_size = chunk_size 267 268 if self.chunk_size < 1024 * 16: 269 warnings.warn(( 270 "A chunk size lower than 16384 may result in too many " 271 "requests to the Twitter API when uploading videos. You are " 272 "strongly advised to increase it above 16384")) 273 274 if (consumer_key and not 275 (application_only_auth or all([access_token_key, access_token_secret]))): 276 raise TwitterError({'message': "Missing oAuth Consumer Key or Access Token"}) 277 278 self.SetCredentials(consumer_key, consumer_secret, access_token_key, access_token_secret, 279 application_only_auth) 280 281 if debugHTTP: 282 try: 283 import http.client as http_client # python3 284 except ImportError: 285 import httplib as http_client # python2 286 287 http_client.HTTPConnection.debuglevel = 1 288 289 logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from requests 290 logging.getLogger().setLevel(logging.DEBUG) 291 requests_log = logging.getLogger("requests.packages.urllib3") 292 requests_log.setLevel(logging.DEBUG) 293 requests_log.propagate = True 294 295 self._session = requests.Session() 296 297 @staticmethod 298 def GetAppOnlyAuthToken(consumer_key, consumer_secret): 299 """ 300 Generate a Bearer Token from consumer_key and consumer_secret 301 """ 302 key = quote_plus(consumer_key) 303 secret = quote_plus(consumer_secret) 304 bearer_token = base64.b64encode('{}:{}'.format(key, secret).encode('utf8')) 305 306 post_headers = { 307 'Authorization': 'Basic {0}'.format(bearer_token.decode('utf8')), 308 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' 309 } 310 311 res = requests.post(url='https://api.twitter.com/oauth2/token', 312 data={'grant_type': 'client_credentials'}, 313 headers=post_headers) 314 bearer_creds = res.json() 315 return bearer_creds 316 317 def SetCredentials(self, 318 consumer_key, 319 consumer_secret, 320 access_token_key=None, 321 access_token_secret=None, 322 application_only_auth=False): 323 """Set the consumer_key and consumer_secret for this instance 324 325 Args: 326 consumer_key: 327 The consumer_key of the twitter account. 328 consumer_secret: 329 The consumer_secret for the twitter account. 330 access_token_key: 331 The oAuth access token key value you retrieved 332 from running get_access_token.py. 333 access_token_secret: 334 The oAuth access token's secret, also retrieved 335 from the get_access_token.py run. 336 application_only_auth: 337 Whether to generate a bearer token and use Application-Only Auth 338 """ 339 self._consumer_key = consumer_key 340 self._consumer_secret = consumer_secret 341 self._access_token_key = access_token_key 342 self._access_token_secret = access_token_secret 343 344 if application_only_auth: 345 self._bearer_token = self.GetAppOnlyAuthToken(consumer_key, consumer_secret) 346 self.__auth = OAuth2(token=self._bearer_token) 347 else: 348 auth_list = [consumer_key, consumer_secret, 349 access_token_key, access_token_secret] 350 if all(auth_list): 351 self.__auth = OAuth1(consumer_key, consumer_secret, 352 access_token_key, access_token_secret) 353 354 self._config = None 355 356 def GetHelpConfiguration(self): 357 """Get basic help configuration details from Twitter. 358 359 Args: 360 None 361 362 Returns: 363 dict: Sets self._config and returns dict of help config values. 364 """ 365 if self._config is None: 366 url = '%s/help/configuration.json' % self.base_url 367 resp = self._RequestUrl(url, 'GET') 368 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 369 self._config = data 370 return self._config 371 372 def GetShortUrlLength(self, https=False): 373 """Returns number of characters reserved per URL included in a tweet. 374 375 Args: 376 https (bool, optional): 377 If True, return number of characters reserved for https urls 378 or, if False, return number of character reserved for http urls. 379 Returns: 380 (int): Number of characters reserved per URL. 381 """ 382 config = self.GetHelpConfiguration() 383 if https: 384 return config['short_url_length_https'] 385 else: 386 return config['short_url_length'] 387 388 def ClearCredentials(self): 389 """Clear any credentials for this instance 390 """ 391 self._consumer_key = None 392 self._consumer_secret = None 393 self._access_token_key = None 394 self._access_token_secret = None 395 self._bearer_token = None 396 self.__auth = None # for request upgrade 397 398 def GetSearch(self, 399 term=None, 400 raw_query=None, 401 geocode=None, 402 since_id=None, 403 max_id=None, 404 until=None, 405 since=None, 406 count=15, 407 lang=None, 408 locale=None, 409 result_type="mixed", 410 include_entities=None, 411 return_json=False): 412 """Return twitter search results for a given term. You must specify one 413 of term, geocode, or raw_query. 414 415 Args: 416 term (str, optional): 417 Term to search by. Optional if you include geocode. 418 raw_query (str, optional): 419 A raw query as a string. This should be everything after the "?" in 420 the URL (i.e., the query parameters). You are responsible for all 421 type checking and ensuring that the query string is properly 422 formatted, as it will only be URL-encoded before be passed directly 423 to Twitter with no other checks performed. For advanced usage only. 424 *This will override any other parameters passed* 425 since_id (int, optional): 426 Returns results with an ID greater than (that is, more recent 427 than) the specified ID. There are limits to the number of 428 Tweets which can be accessed through the API. If the limit of 429 Tweets has occurred since the since_id, the since_id will be 430 forced to the oldest ID available. 431 max_id (int, optional): 432 Returns only statuses with an ID less than (that is, older 433 than) or equal to the specified ID. 434 until (str, optional): 435 Returns tweets generated before the given date. Date should be 436 formatted as YYYY-MM-DD. 437 since (str, optional): 438 Returns tweets generated since the given date. Date should be 439 formatted as YYYY-MM-DD. 440 geocode (str or list or tuple, optional): 441 Geolocation within which to search for tweets. Can be either a 442 string in the form of "latitude,longitude,radius" where latitude 443 and longitude are floats and radius is a string such as "1mi" or 444 "1km" ("mi" or "km" are the only units allowed). For example: 445 >>> api.GetSearch(geocode="37.781157,-122.398720,1mi"). 446 Otherwise, you can pass a list of either floats or strings for 447 lat/long and a string for radius: 448 >>> api.GetSearch(geocode=[37.781157, -122.398720, "1mi"]) 449 >>> # or: 450 >>> api.GetSearch(geocode=(37.781157, -122.398720, "1mi")) 451 >>> # or: 452 >>> api.GetSearch(geocode=("37.781157", "-122.398720", "1mi")) 453 count (int, optional): 454 Number of results to return. Default is 15 and maxmimum that 455 Twitter returns is 100 irrespective of what you type in. 456 lang (str, optional): 457 Language for results as ISO 639-1 code. Default is None 458 (all languages). 459 locale (str, optional): 460 Language of the search query. Currently only 'ja' is effective. 461 This is intended for language-specific consumers and the default 462 should work in the majority of cases. 463 result_type (str, optional): 464 Type of result which should be returned. Default is "mixed". 465 Valid options are "mixed, "recent", and "popular". 466 include_entities (bool, optional): 467 If True, each tweet will include a node called "entities". 468 This node offers a variety of metadata about the tweet in a 469 discrete structure, including: user_mentions, urls, and 470 hashtags. 471 return_json (bool, optional): 472 If True JSON data will be returned, instead of twitter.Userret 473 Returns: 474 list: A sequence of twitter.Status instances, one for each message 475 containing the term, within the bounds of the geocoded area, or 476 given by the raw_query. 477 """ 478 479 url = '%s/search/tweets.json' % self.base_url 480 parameters = {} 481 482 if since_id: 483 parameters['since_id'] = enf_type('since_id', int, since_id) 484 485 if max_id: 486 parameters['max_id'] = enf_type('max_id', int, max_id) 487 488 if until: 489 parameters['until'] = enf_type('until', str, until) 490 491 if since: 492 parameters['since'] = enf_type('since', str, since) 493 494 if lang: 495 parameters['lang'] = enf_type('lang', str, lang) 496 497 if locale: 498 parameters['locale'] = enf_type('locale', str, locale) 499 500 if term is None and geocode is None and raw_query is None: 501 return [] 502 503 if term is not None: 504 parameters['q'] = term 505 506 if geocode is not None: 507 if isinstance(geocode, list) or isinstance(geocode, tuple): 508 parameters['geocode'] = ','.join([str(geo) for geo in geocode]) 509 else: 510 parameters['geocode'] = enf_type('geocode', str, geocode) 511 512 if include_entities: 513 parameters['include_entities'] = enf_type('include_entities', 514 bool, 515 include_entities) 516 517 parameters['count'] = enf_type('count', int, count) 518 519 if result_type in ["mixed", "popular", "recent"]: 520 parameters['result_type'] = result_type 521 522 if raw_query is not None: 523 url = "{url}?{raw_query}".format( 524 url=url, 525 raw_query=raw_query) 526 resp = self._RequestUrl(url, 'GET') 527 else: 528 resp = self._RequestUrl(url, 'GET', data=parameters) 529 530 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 531 if return_json: 532 return data 533 else: 534 return [Status.NewFromJsonDict(x) for x in data.get('statuses', '')] 535 536 def GetUsersSearch(self, 537 term=None, 538 page=1, 539 count=20, 540 include_entities=None): 541 """Return twitter user search results for a given term. 542 543 Args: 544 term: 545 Term to search by. 546 page: 547 Page of results to return. Default is 1 548 [Optional] 549 count: 550 Number of results to return. Default is 20 551 [Optional] 552 include_entities: 553 If True, each tweet will include a node called "entities,". 554 This node offers a variety of metadata about the tweet in a 555 discrete structure, including: user_mentions, urls, and hashtags. 556 [Optional] 557 558 Returns: 559 A sequence of twitter.User instances, one for each message containing 560 the term 561 """ 562 # Build request parameters 563 parameters = {} 564 565 if term is not None: 566 parameters['q'] = term 567 568 if page != 1: 569 parameters['page'] = page 570 571 if include_entities: 572 parameters['include_entities'] = 1 573 574 try: 575 parameters['count'] = int(count) 576 except ValueError: 577 raise TwitterError({'message': "count must be an integer"}) 578 579 # Make and send requests 580 url = '%s/users/search.json' % self.base_url 581 resp = self._RequestUrl(url, 'GET', data=parameters) 582 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 583 return [User.NewFromJsonDict(x) for x in data] 584 585 def GetTrendsCurrent(self, exclude=None): 586 """Get the current top trending topics (global) 587 588 Args: 589 exclude: 590 Appends the exclude parameter as a request parameter. 591 Currently only exclude=hashtags is supported. [Optional] 592 593 Returns: 594 A list with 10 entries. Each entry contains a trend. 595 """ 596 return self.GetTrendsWoeid(woeid=1, exclude=exclude) 597 598 def GetTrendsWoeid(self, woeid, exclude=None): 599 """Return the top 10 trending topics for a specific WOEID, if trending 600 information is available for it. 601 602 Args: 603 woeid: 604 the Yahoo! Where On Earth ID for a location. 605 exclude: 606 Appends the exclude parameter as a request parameter. 607 Currently only exclude=hashtags is supported. [Optional] 608 609 Returns: 610 A list with 10 entries. Each entry contains a trend. 611 """ 612 url = '%s/trends/place.json' % (self.base_url) 613 parameters = {'id': woeid} 614 615 if exclude: 616 parameters['exclude'] = exclude 617 618 resp = self._RequestUrl(url, verb='GET', data=parameters) 619 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 620 trends = [] 621 timestamp = data[0]['as_of'] 622 623 for trend in data[0]['trends']: 624 trends.append(Trend.NewFromJsonDict(trend, timestamp=timestamp)) 625 return trends 626 627 def GetUserSuggestionCategories(self): 628 """ Return the list of suggested user categories, this can be used in 629 GetUserSuggestion function 630 Returns: 631 A list of categories 632 """ 633 url = '%s/users/suggestions.json' % (self.base_url) 634 resp = self._RequestUrl(url, verb='GET') 635 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 636 637 categories = [] 638 639 for category in data: 640 categories.append(Category.NewFromJsonDict(category)) 641 return categories 642 643 def GetUserSuggestion(self, category): 644 """ Returns a list of users in a category 645 Args: 646 category: 647 The Category object to limit the search by 648 Returns: 649 A list of users in that category 650 """ 651 url = '%s/users/suggestions/%s.json' % (self.base_url, category.slug) 652 653 resp = self._RequestUrl(url, verb='GET') 654 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 655 656 users = [] 657 for user in data['users']: 658 users.append(User.NewFromJsonDict(user)) 659 return users 660 661 def GetHomeTimeline(self, 662 count=None, 663 since_id=None, 664 max_id=None, 665 trim_user=False, 666 exclude_replies=False, 667 contributor_details=False, 668 include_entities=True): 669 """Fetch a collection of the most recent Tweets and retweets posted 670 by the authenticating user and the users they follow. 671 672 The home timeline is central to how most users interact with Twitter. 673 674 Args: 675 count: 676 Specifies the number of statuses to retrieve. May not be 677 greater than 200. Defaults to 20. [Optional] 678 since_id: 679 Returns results with an ID greater than (that is, more recent 680 than) the specified ID. There are limits to the number of 681 Tweets which can be accessed through the API. If the limit of 682 Tweets has occurred since the since_id, the since_id will be 683 forced to the oldest ID available. [Optional] 684 max_id: 685 Returns results with an ID less than (that is, older than) or 686 equal to the specified ID. [Optional] 687 trim_user: 688 When True, each tweet returned in a timeline will include a user 689 object including only the status authors numerical ID. Omit this 690 parameter to receive the complete user object. [Optional] 691 exclude_replies: 692 This parameter will prevent replies from appearing in the 693 returned timeline. Using exclude_replies with the count 694 parameter will mean you will receive up-to count tweets - 695 this is because the count parameter retrieves that many 696 tweets before filtering out retweets and replies. [Optional] 697 contributor_details: 698 This parameter enhances the contributors element of the 699 status response to include the screen_name of the contributor. 700 By default only the user_id of the contributor is included. [Optional] 701 include_entities: 702 The entities node will be disincluded when set to false. 703 This node offers a variety of metadata about the tweet in a 704 discreet structure, including: user_mentions, urls, and 705 hashtags. [Optional] 706 707 Returns: 708 A sequence of twitter.Status instances, one for each message 709 """ 710 url = '%s/statuses/home_timeline.json' % self.base_url 711 712 parameters = {} 713 if count is not None: 714 try: 715 if int(count) > 200: 716 raise TwitterError({'message': "'count' may not be greater than 200"}) 717 except ValueError: 718 raise TwitterError({'message': "'count' must be an integer"}) 719 parameters['count'] = count 720 if since_id: 721 try: 722 parameters['since_id'] = int(since_id) 723 except ValueError: 724 raise TwitterError({'message': "'since_id' must be an integer"}) 725 if max_id: 726 try: 727 parameters['max_id'] = int(max_id) 728 except ValueError: 729 raise TwitterError({'message': "'max_id' must be an integer"}) 730 if trim_user: 731 parameters['trim_user'] = 1 732 if exclude_replies: 733 parameters['exclude_replies'] = 1 734 if contributor_details: 735 parameters['contributor_details'] = 1 736 if not include_entities: 737 parameters['include_entities'] = 'false' 738 resp = self._RequestUrl(url, 'GET', data=parameters) 739 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 740 741 return [Status.NewFromJsonDict(x) for x in data] 742 743 def GetUserTimeline(self, 744 user_id=None, 745 screen_name=None, 746 since_id=None, 747 max_id=None, 748 count=None, 749 include_rts=True, 750 trim_user=False, 751 exclude_replies=False): 752 """Fetch the sequence of public Status messages for a single user. 753 754 The twitter.Api instance must be authenticated if the user is private. 755 756 Args: 757 user_id (int, optional): 758 Specifies the ID of the user for whom to return the 759 user_timeline. Helpful for disambiguating when a valid user ID 760 is also a valid screen name. 761 screen_name (str, optional): 762 Specifies the screen name of the user for whom to return the 763 user_timeline. Helpful for disambiguating when a valid screen 764 name is also a user ID. 765 since_id (int, optional): 766 Returns results with an ID greater than (that is, more recent 767 than) the specified ID. There are limits to the number of 768 Tweets which can be accessed through the API. If the limit of 769 Tweets has occurred since the since_id, the since_id will be 770 forced to the oldest ID available. 771 max_id (int, optional): 772 Returns only statuses with an ID less than (that is, older 773 than) or equal to the specified ID. 774 count (int, optional): 775 Specifies the number of statuses to retrieve. May not be 776 greater than 200. 777 include_rts (bool, optional): 778 If True, the timeline will contain native retweets (if they 779 exist) in addition to the standard stream of tweets. 780 trim_user (bool, optional): 781 If True, statuses will only contain the numerical user ID only. 782 Otherwise a full user object will be returned for each status. 783 exclude_replies (bool, optional) 784 If True, this will prevent replies from appearing in the returned 785 timeline. Using exclude_replies with the count parameter will mean you 786 will receive up-to count tweets - this is because the count parameter 787 retrieves that many tweets before filtering out retweets and replies. 788 This parameter is only supported for JSON and XML responses. 789 790 Returns: 791 A sequence of Status instances, one for each message up to count 792 """ 793 url = '%s/statuses/user_timeline.json' % (self.base_url) 794 parameters = {} 795 796 if user_id: 797 parameters['user_id'] = enf_type('user_id', int, user_id) 798 elif screen_name: 799 parameters['screen_name'] = screen_name 800 if since_id: 801 parameters['since_id'] = enf_type('since_id', int, since_id) 802 if max_id: 803 parameters['max_id'] = enf_type('max_id', int, max_id) 804 if count: 805 parameters['count'] = enf_type('count', int, count) 806 807 parameters['include_rts'] = enf_type('include_rts', bool, include_rts) 808 parameters['trim_user'] = enf_type('trim_user', bool, trim_user) 809 parameters['exclude_replies'] = enf_type('exclude_replies', bool, exclude_replies) 810 811 resp = self._RequestUrl(url, 'GET', data=parameters) 812 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 813 814 return [Status.NewFromJsonDict(x) for x in data] 815 816 def GetStatus(self, 817 status_id, 818 trim_user=False, 819 include_my_retweet=True, 820 include_entities=True, 821 include_ext_alt_text=True): 822 """Returns a single status message, specified by the status_id parameter. 823 824 Args: 825 status_id: 826 The numeric ID of the status you are trying to retrieve. 827 trim_user: 828 When set to True, each tweet returned in a timeline will include 829 a user object including only the status authors numerical ID. 830 Omit this parameter to receive the complete user object. [Optional] 831 include_my_retweet: 832 When set to True, any Tweets returned that have been retweeted by 833 the authenticating user will include an additional 834 current_user_retweet node, containing the ID of the source status 835 for the retweet. [Optional] 836 include_entities: 837 If False, the entities node will be disincluded. 838 This node offers a variety of metadata about the tweet in a 839 discreet structure, including: user_mentions, urls, and 840 hashtags. [Optional] 841 Returns: 842 A twitter.Status instance representing that status message 843 """ 844 url = '%s/statuses/show.json' % (self.base_url) 845 846 parameters = { 847 'id': enf_type('status_id', int, status_id), 848 'trim_user': enf_type('trim_user', bool, trim_user), 849 'include_my_retweet': enf_type('include_my_retweet', bool, include_my_retweet), 850 'include_entities': enf_type('include_entities', bool, include_entities), 851 'include_ext_alt_text': enf_type('include_ext_alt_text', bool, include_ext_alt_text) 852 } 853 854 resp = self._RequestUrl(url, 'GET', data=parameters) 855 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 856 857 return Status.NewFromJsonDict(data) 858 859 def GetStatuses(self, 860 status_ids, 861 trim_user=False, 862 include_entities=True, 863 map=False): 864 """Returns a list of status messages, specified by the status_ids parameter. 865 866 Args: 867 status_ids: 868 A list of the numeric ID of the statuses you are trying to retrieve. 869 trim_user: 870 When set to True, each tweet returned in a timeline will include 871 a user object including only the status authors numerical ID. 872 Omit this parameter to receive the complete user object. [Optional] 873 include_entities: 874 If False, the entities node will be disincluded. 875 This node offers a variety of metadata about the tweet in a 876 discreet structure, including: user_mentions, urls, and 877 hashtags. [Optional] 878 map: 879 If True, returns a dictionary with status id as key and returned 880 status data (or None if tweet does not exist or is inaccessible) 881 as value. Otherwise returns an unordered list of successfully 882 retrieved Tweets. [Optional] 883 Returns: 884 A dictionary or unordered list (depending on the parameter 'map') of 885 twitter Status instances representing the status messages. 886 """ 887 url = '%s/statuses/lookup.json' % (self.base_url) 888 889 map = enf_type('map', bool, map) 890 891 if map: 892 result = {} 893 else: 894 result = [] 895 offset = 0 896 parameters = { 897 'trim_user': enf_type('trim_user', bool, trim_user), 898 'include_entities': enf_type('include_entities', bool, include_entities), 899 'map': map 900 } 901 while offset < len(status_ids): 902 parameters['id'] = ','.join([str(enf_type('status_id', int, status_id)) for status_id in status_ids[offset:offset + 100]]) 903 904 resp = self._RequestUrl(url, 'GET', data=parameters) 905 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 906 if map: 907 result.update({int(key): (Status.NewFromJsonDict(value) if value else None) for key, value in data['id'].items()}) 908 else: 909 result += [Status.NewFromJsonDict(dataitem) for dataitem in data] 910 911 offset += 100 912 913 return result 914 915 def GetStatusOembed(self, 916 status_id=None, 917 url=None, 918 maxwidth=None, 919 hide_media=False, 920 hide_thread=False, 921 omit_script=False, 922 align=None, 923 related=None, 924 lang=None): 925 """Returns information allowing the creation of an embedded representation of a 926 Tweet on third party sites. 927 928 Specify tweet by the id or url parameter. 929 930 Args: 931 status_id: 932 The numeric ID of the status you are trying to embed. 933 url: 934 The url of the status you are trying to embed. 935 maxwidth: 936 The maximum width in pixels that the embed should be rendered at. 937 This value is constrained to be between 250 and 550 pixels. [Optional] 938 hide_media: 939 Specifies whether the embedded Tweet should automatically expand images. [Optional] 940 hide_thread: 941 Specifies whether the embedded Tweet should automatically show the original 942 message in the case that the embedded Tweet is a reply. [Optional] 943 omit_script: 944 Specifies whether the embedded Tweet HTML should include a <script> 945 element pointing to widgets.js. [Optional] 946 align: 947 Specifies whether the embedded Tweet should be left aligned, right aligned, 948 or centered in the page. [Optional] 949 related: 950 A comma sperated string of related screen names. [Optional] 951 lang: 952 Language code for the rendered embed. [Optional] 953 954 Returns: 955 A dictionary with the response. 956 """ 957 request_url = '%s/statuses/oembed.json' % (self.base_url) 958 959 parameters = {} 960 961 if status_id is not None: 962 try: 963 parameters['id'] = int(status_id) 964 except ValueError: 965 raise TwitterError({'message': "'status_id' must be an integer."}) 966 elif url is not None: 967 parameters['url'] = url 968 else: 969 raise TwitterError({'message': "Must specify either 'status_id' or 'url'"}) 970 971 if maxwidth is not None: 972 parameters['maxwidth'] = maxwidth 973 if hide_media is True: 974 parameters['hide_media'] = 'true' 975 if hide_thread is True: 976 parameters['hide_thread'] = 'true' 977 if omit_script is True: 978 parameters['omit_script'] = 'true' 979 if align is not None: 980 if align not in ('left', 'center', 'right', 'none'): 981 raise TwitterError({'message': "'align' must be 'left', 'center', 'right', or 'none'"}) 982 parameters['align'] = align 983 if related: 984 if not isinstance(related, str): 985 raise TwitterError({'message': "'related' should be a string of comma separated screen names"}) 986 parameters['related'] = related 987 if lang is not None: 988 if not isinstance(lang, str): 989 raise TwitterError({'message': "'lang' should be string instance"}) 990 parameters['lang'] = lang 991 992 resp = self._RequestUrl(request_url, 'GET', data=parameters, enforce_auth=False) 993 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 994 995 return data 996 997 def DestroyStatus(self, status_id, trim_user=False): 998 """Destroys the status specified by the required ID parameter. 999 1000 The authenticating user must be the author of the specified 1001 status. 1002 1003 Args: 1004 status_id (int): 1005 The numerical ID of the status you're trying to destroy. 1006 trim_user (bool, optional): 1007 When set to True, each tweet returned in a timeline will include 1008 a user object including only the status authors numerical ID. 1009 1010 Returns: 1011 A twitter.Status instance representing the destroyed status message 1012 """ 1013 url = '%s/statuses/destroy/%s.json' % (self.base_url, status_id) 1014 1015 post_data = { 1016 'id': enf_type('status_id', int, status_id), 1017 'trim_user': enf_type('trim_user', bool, trim_user) 1018 } 1019 1020 resp = self._RequestUrl(url, 'POST', data=post_data) 1021 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 1022 1023 return Status.NewFromJsonDict(data) 1024 1025 def PostUpdate(self, 1026 status, 1027 media=None, 1028 media_additional_owners=None, 1029 media_category=None, 1030 in_reply_to_status_id=None, 1031 auto_populate_reply_metadata=False, 1032 exclude_reply_user_ids=None, 1033 latitude=None, 1034 longitude=None, 1035 place_id=None, 1036 display_coordinates=False, 1037 trim_user=False, 1038 verify_status_length=True, 1039 attachment_url=None): 1040 """Post a twitter status message from the authenticated user. 1041 1042 https://dev.twitter.com/docs/api/1.1/post/statuses/update 1043 1044 Args: 1045 status (str): 1046 The message text to be posted. Must be less than or equal to 1047 CHARACTER_LIMIT characters. 1048 media (int, str, fp, optional): 1049 A URL, a local file, or a file-like object (something with a 1050 read() method), or a list of any combination of the above. 1051 media_additional_owners (list, optional): 1052 A list of user ids representing Twitter users that should be able 1053 to use the uploaded media in their tweets. If you pass a list of 1054 media, then additional_owners will apply to each object. If you 1055 need more granular control, please use the UploadMedia* methods. 1056 media_category (str, optional): 1057 Only for use with the AdsAPI. See 1058 https://dev.twitter.com/ads/creative/promoted-video-overview if 1059 this applies to your application. 1060 in_reply_to_status_id (int, optional): 1061 The ID of an existing status that the status to be posted is 1062 in reply to. This implicitly sets the in_reply_to_user_id 1063 attribute of the resulting status to the user ID of the 1064 message being replied to. Invalid/missing status IDs will be 1065 ignored. 1066 auto_populate_reply_metadata (bool, optional): 1067 Automatically include the @usernames of the users mentioned or 1068 participating in the tweet to which this tweet is in reply. 1069 exclude_reply_user_ids (list, optional): 1070 Remove given user_ids (*not* @usernames) from the tweet's 1071 automatically generated reply metadata. 1072 attachment_url (str, optional): 1073 URL to an attachment resource: one to four photos, a GIF, 1074 video, Quote Tweet, or DM deep link. If not specified and 1075 media parameter is not None, we will attach the first media 1076 object as the attachment URL. If a bad URL is passed, Twitter 1077 will raise an error. 1078 latitude (float, optional): 1079 Latitude coordinate of the tweet in degrees. Will only work 1080 in conjunction with longitude argument. Both longitude and 1081 latitude will be ignored by twitter if the user has a false 1082 geo_enabled setting. 1083 longitude (float, optional): 1084 Longitude coordinate of the tweet in degrees. Will only work 1085 in conjunction with latitude argument. Both longitude and 1086 latitude will be ignored by twitter if the user has a false 1087 geo_enabled setting. 1088 place_id (int, optional): 1089 A place in the world. These IDs can be retrieved from 1090 GET geo/reverse_geocode. 1091 display_coordinates (bool, optional): 1092 Whether or not to put a pin on the exact coordinates a tweet 1093 has been sent from. 1094 trim_user (bool, optional): 1095 If True the returned payload will only contain the user IDs, 1096 otherwise the payload will contain the full user data item. 1097 verify_status_length (bool, optional): 1098 If True, api throws a hard error that the status is over 1099 CHARACTER_LIMIT characters. If False, Api will attempt to post 1100 the status. 1101 Returns: 1102 (twitter.Status) A twitter.Status instance representing the 1103 message posted. 1104 """ 1105 url = '%s/statuses/update.json' % self.base_url 1106 1107 if isinstance(status, str) or self._input_encoding is None: 1108 u_status = status 1109 else: 1110 u_status = str(status, self._input_encoding) 1111 1112 if verify_status_length and calc_expected_status_length(u_status) > CHARACTER_LIMIT: 1113 raise TwitterError("Text must be less than or equal to CHARACTER_LIMIT characters.") 1114 1115 if auto_populate_reply_metadata and not in_reply_to_status_id: 1116 raise TwitterError("If auto_populate_reply_metadata is True, you must set in_reply_to_status_id") 1117 1118 parameters = { 1119 'status': u_status, 1120 'in_reply_to_status_id': in_reply_to_status_id, 1121 'auto_populate_reply_metadata': auto_populate_reply_metadata, 1122 'place_id': place_id, 1123 'display_coordinates': display_coordinates, 1124 'trim_user': trim_user, 1125 'exclude_reply_user_ids': ','.join([str(u) for u in exclude_reply_user_ids or []]), 1126 } 1127 1128 if attachment_url: 1129 parameters['attachment_url'] = attachment_url 1130 1131 if media: 1132 chunked_types = ['video/mp4', 'video/quicktime', 'image/gif'] 1133 media_ids = [] 1134 if isinstance(media, (int, long)): 1135 media_ids.append(media) 1136 1137 elif isinstance(media, list): 1138 for media_file in media: 1139 1140 # If you want to pass just a media ID, it should be an int 1141 if isinstance(media_file, (int, long)): 1142 media_ids.append(media_file) 1143 continue 1144 1145 _, _, file_size, media_type = parse_media_file(media_file) 1146 if (media_type == 'image/gif' or media_type == 'video/mp4') and len(media) > 1: 1147 raise TwitterError( 1148 'You cannot post more than 1 GIF or 1 video in a single status.') 1149 if file_size > self.chunk_size or media_type in chunked_types: 1150 media_id = self.UploadMediaChunked( 1151 media=media_file, 1152 additional_owners=media_additional_owners, 1153 media_category=media_category) 1154 else: 1155 media_id = self.UploadMediaSimple( 1156 media=media_file, 1157 additional_owners=media_additional_owners, 1158 media_category=media_category) 1159 media_ids.append(media_id) 1160 else: 1161 _, _, file_size, media_type = parse_media_file(media) 1162 if file_size > self.chunk_size or media_type in chunked_types: 1163 media_ids.append(self.UploadMediaChunked( 1164 media, media_additional_owners, media_category=media_category 1165 )) 1166 else: 1167 media_ids.append(self.UploadMediaSimple( 1168 media, media_additional_owners, media_category=media_category 1169 )) 1170 parameters['media_ids'] = ','.join([str(mid) for mid in media_ids]) 1171 1172 if latitude is not None and longitude is not None: 1173 parameters['lat'] = str(latitude) 1174 parameters['long'] = str(longitude) 1175 1176 resp = self._RequestUrl(url, 'POST', data=parameters) 1177 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 1178 1179 return Status.NewFromJsonDict(data) 1180 1181 def UploadMediaSimple(self, 1182 media, 1183 additional_owners=None, 1184 media_category=None): 1185 1186 """ Upload a media file to Twitter in one request. Used for small file 1187 uploads that do not require chunked uploads. 1188 1189 Args: 1190 media: 1191 File-like object to upload. 1192 additional_owners: additional Twitter users that are allowed to use 1193 The uploaded media. Should be a list of integers. Maximum 1194 number of additional owners is capped at 100 by Twitter. 1195 media_category: 1196 Category with which to identify media upload. Only use with Ads 1197 API & video files. 1198 1199 Returns: 1200 media_id: 1201 ID of the uploaded media returned by the Twitter API or 0. 1202 1203 """ 1204 url = '%s/media/upload.json' % self.upload_url 1205 parameters = {} 1206 1207 media_fp, _, _, _ = parse_media_file(media) 1208 1209 parameters['media'] = media_fp.read() 1210 1211 if additional_owners and len(additional_owners) > 100: 1212 raise TwitterError({'message': 'Maximum of 100 additional owners may be specified for a Media object'}) 1213 if additional_owners: 1214 parameters['additional_owners'] = additional_owners 1215 if media_category: 1216 parameters['media_category'] = media_category 1217 1218 resp = self._RequestUrl(url, 'POST', data=parameters) 1219 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 1220 1221 try: 1222 return data['media_id'] 1223 except KeyError: 1224 raise TwitterError({'message': 'Media could not be uploaded.'}) 1225 1226 def PostMediaMetadata(self, 1227 media_id, 1228 alt_text=None): 1229 """Provide addtional data for uploaded media. 1230 1231 Args: 1232 media_id: 1233 ID of a previously uploaded media item. 1234 alt_text: 1235 Image Alternate Text. 1236 """ 1237 url = '%s/media/metadata/create.json' % self.upload_url 1238 parameters = {} 1239 1240 parameters['media_id'] = media_id 1241 1242 if alt_text: 1243 parameters['alt_text'] = {"text": alt_text} 1244 1245 resp = self._RequestUrl(url, 'POST', json=parameters) 1246 1247 return resp 1248 1249 def _UploadMediaChunkedInit(self, 1250 media, 1251 additional_owners=None, 1252 media_category=None): 1253 """Start a chunked upload to Twitter. 1254 1255 Args: 1256 media: 1257 File-like object to upload. 1258 additional_owners: additional Twitter users that are allowed to use 1259 The uploaded media. Should be a list of integers. Maximum 1260 number of additional owners is capped at 100 by Twitter. 1261 media_category: 1262 Category with which to identify media upload. Only use with Ads 1263 API & video files. 1264 1265 Returns: 1266 tuple: media_id (returned from Twitter), file-handler object (i.e., has .read() 1267 method), filename media file. 1268 """ 1269 url = '%s/media/upload.json' % self.upload_url 1270 1271 media_fp, filename, file_size, media_type = parse_media_file(media, async_upload=True) 1272 1273 if not all([media_fp, filename, file_size, media_type]): 1274 raise TwitterError({'message': 'Could not process media file'}) 1275 1276 parameters = {} 1277 1278 if additional_owners and len(additional_owners) > 100: 1279 raise TwitterError({'message': 'Maximum of 100 additional owners may be specified for a Media object'}) 1280 if additional_owners: 1281 parameters['additional_owners'] = additional_owners 1282 if media_category: 1283 parameters['media_category'] = media_category 1284 1285 # INIT doesn't read in any data. It's purpose is to prepare Twitter to 1286 # receive the content in APPEND requests. 1287 parameters['command'] = 'INIT' 1288 parameters['media_type'] = media_type 1289 parameters['total_bytes'] = file_size 1290 1291 resp = self._RequestUrl(url, 'POST', data=parameters) 1292 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 1293 1294 try: 1295 media_id = data['media_id'] 1296 except KeyError: 1297 raise TwitterError({'message': 'Media could not be uploaded'}) 1298 1299 return (media_id, media_fp, filename) 1300 1301 def _UploadMediaChunkedAppend(self, 1302 media_id, 1303 media_fp, 1304 filename): 1305 """Appends (i.e., actually uploads) media file to Twitter. 1306 1307 Args: 1308 media_id (int): 1309 ID of the media file received from Init method. 1310 media_fp (file): 1311 File-like object representing media file (must have .read() method) 1312 filename (str): 1313 Filename of the media file being uploaded. 1314 1315 Returns: 1316 True if successful. Raises otherwise. 1317 """ 1318 url = '%s/media/upload.json' % self.upload_url 1319 1320 boundary = "--{0}".format(uuid4().hex).encode('utf-8') 1321 media_id_bytes = str(media_id).encode('utf-8') 1322 headers = {'Content-Type': 'multipart/form-data; boundary={0}'.format( 1323 boundary.decode('utf8')[2:] 1324 )} 1325 1326 segment_id = 0 1327 while True: 1328 try: 1329 data = media_fp.read(self.chunk_size) 1330 except ValueError: 1331 break 1332 if not data: 1333 break 1334 body = [ 1335 boundary, 1336 b'Content-Disposition: form-data; name="command"', 1337 b'', 1338 b'APPEND', 1339 boundary, 1340 b'Content-Disposition: form-data; name="media_id"', 1341 b'', 1342 media_id_bytes, 1343 boundary, 1344 b'Content-Disposition: form-data; name="segment_index"', 1345 b'', 1346 str(segment_id).encode('utf-8'), 1347 boundary, 1348 'Content-Disposition: form-data; name="media"; filename="{0!r}"'.format(filename).encode('utf8'), 1349 b'Content-Type: application/octet-stream', 1350 b'', 1351 data, 1352 boundary + b'--' 1353 ] 1354 body_data = b'\r\n'.join(body) 1355 headers['Content-Length'] = str(len(body_data)) 1356 1357 resp = self._RequestChunkedUpload(url=url, 1358 headers=headers, 1359 data=body_data) 1360 1361 # The body of the response should be blank, but the normal decoding 1362 # raises a JSONDecodeError, so we should only do error checking 1363 # if the response is not blank. 1364 if resp.content.decode('utf-8'): 1365 return self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 1366 1367 segment_id += 1 1368 1369 try: 1370 media_fp.close() 1371 except Exception as e: 1372 pass 1373 1374 return True 1375 1376 def _UploadMediaChunkedFinalize(self, media_id): 1377 """Finalize chunked upload to Twitter. 1378 1379 Args: 1380 media_id (int): 1381 ID of the media file for which to finalize the upload. 1382 1383 Returns: 1384 json: JSON string of data from Twitter. 1385 """ 1386 url = '%s/media/upload.json' % self.upload_url 1387 1388 parameters = { 1389 'command': 'FINALIZE', 1390 'media_id': media_id 1391 } 1392 1393 resp = self._RequestUrl(url, 'POST', data=parameters) 1394 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 1395 1396 return data 1397 1398 def UploadMediaChunked(self, 1399 media, 1400 additional_owners=None, 1401 media_category=None): 1402 """Upload a media file to Twitter in multiple requests. 1403 1404 Args: 1405 media: 1406 File-like object to upload. 1407 additional_owners: additional Twitter users that are allowed to use 1408 The uploaded media. Should be a list of integers. Maximum 1409 number of additional owners is capped at 100 by Twitter. 1410 media_category: 1411 Category with which to identify media upload. Only use with Ads 1412 API & video files. 1413 1414 Returns: 1415 media_id: 1416 ID of the uploaded media returned by the Twitter API. Raises if 1417 unsuccesful. 1418 """ 1419 1420 media_id, media_fp, filename = self._UploadMediaChunkedInit(media=media, 1421 additional_owners=additional_owners, 1422 media_category=media_category) 1423 1424 append = self._UploadMediaChunkedAppend(media_id=media_id, 1425 media_fp=media_fp, 1426 filename=filename) 1427 1428 if not append: 1429 TwitterError('Media could not be uploaded.') 1430 1431 data = self._UploadMediaChunkedFinalize(media_id) 1432 1433 try: 1434 return data['media_id'] 1435 except KeyError: 1436 raise TwitterError('Media could not be uploaded.') 1437 1438 def _TweetTextWrap(self, 1439 status, 1440 char_lim=CHARACTER_LIMIT): 1441 1442 if not self._config: 1443 self.GetHelpConfiguration() 1444 1445 tweets = [] 1446 line = [] 1447 line_length = 0 1448 words = re.split(r'\s', status) 1449 1450 if len(words) == 1 and not is_url(words[0]): 1451 if len(words[0]) > CHARACTER_LIMIT: 1452 raise TwitterError("Unable to split status into tweetable parts. Word was: {0}/{1}".format(len(words[0]), char_lim)) 1453 else: 1454 tweets.append(words[0]) 1455 return tweets 1456 1457 for word in words: 1458 if len(word) > char_lim: 1459 raise TwitterError("Unable to split status into tweetable parts. Word was: {0}/{1}".format(len(word), char_lim)) 1460 new_len = line_length 1461 1462 if is_url(word): 1463 new_len = line_length + self._config['short_url_length_https'] + 1 1464 else: 1465 new_len += len(word) + 1 1466 1467 if new_len > CHARACTER_LIMIT: 1468 tweets.append(' '.join(line)) 1469 line = [word] 1470 line_length = new_len - line_length 1471 else: 1472 line.append(word) 1473 line_length = new_len 1474 1475 tweets.append(' '.join(line)) 1476 return tweets 1477 1478 def PostUpdates(self, 1479 status, 1480 continuation=None, 1481 **kwargs): 1482 """Post one or more twitter status messages from the authenticated user. 1483 1484 Unlike api.PostUpdate, this method will post multiple status updates 1485 if the message is longer than CHARACTER_LIMIT characters. 1486 1487 Args: 1488 status: 1489 The message text to be posted. 1490 May be longer than CHARACTER_LIMIT characters. 1491 continuation: 1492 The character string, if any, to be appended to all but the 1493 last message. Note that Twitter strips trailing '...' strings 1494 from messages. Consider using the unicode \u2026 character 1495 (horizontal ellipsis) instead. [Defaults to None] 1496 **kwargs: 1497 See api.PostUpdate for a list of accepted parameters. 1498 1499 Returns: 1500 A of list twitter.Status instance representing the messages posted. 1501 """ 1502 results = list() 1503 1504 if continuation is None: 1505 continuation = '' 1506 char_limit = CHARACTER_LIMIT - len(continuation) 1507 1508 tweets = self._TweetTextWrap(status=status, char_lim=char_limit) 1509 1510 if len(tweets) == 1: 1511 results.append(self.PostUpdate(status=tweets[0], **kwargs)) 1512 return results 1513 1514 for tweet in tweets[0:-1]: 1515 results.append(self.PostUpdate(status=tweet + continuation, **kwargs)) 1516 results.append(self.PostUpdate(status=tweets[-1], **kwargs)) 1517 1518 return results 1519 1520 def PostRetweet(self, status_id, trim_user=False): 1521 """Retweet a tweet with the Retweet API. 1522 1523 Args: 1524 status_id: 1525 The numerical id of the tweet that will be retweeted 1526 trim_user: 1527 If True the returned payload will only contain the user IDs, 1528 otherwise the payload will contain the full user data item. 1529 [Optional] 1530 1531 Returns: 1532 A twitter.Status instance representing the original tweet with retweet details embedded. 1533 """ 1534 try: 1535 if int(status_id) <= 0: 1536 raise TwitterError({'message': "'status_id' must be a positive number"}) 1537 except ValueError: 1538 raise TwitterError({'message': "'status_id' must be an integer"}) 1539 1540 url = '%s/statuses/retweet/%s.json' % (self.base_url, status_id) 1541 data = {'id': status_id} 1542 if trim_user: 1543 data['trim_user'] = 'true' 1544 resp = self._RequestUrl(url, 'POST', data=data) 1545 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 1546 1547 return Status.NewFromJsonDict(data) 1548 1549 def GetUserRetweets(self, 1550 count=None, 1551 since_id=None, 1552 max_id=None, 1553 trim_user=False): 1554 """Fetch the sequence of retweets made by the authenticated user. 1555 1556 Args: 1557 count: 1558 The number of status messages to retrieve. [Optional] 1559 since_id: 1560 Returns results with an ID greater than (that is, more recent 1561 than) the specified ID. There are limits to the number of 1562 Tweets which can be accessed through the API. If the limit of 1563 Tweets has occurred since the since_id, the since_id will be 1564 forced to the oldest ID available. [Optional] 1565 max_id: 1566 Returns results with an ID less than (that is, older than) or 1567 equal to the specified ID. [Optional] 1568 trim_user: 1569 If True the returned payload will only contain the user IDs, 1570 otherwise the payload will contain the full user data item. 1571 [Optional] 1572 1573 Returns: 1574 A sequence of twitter.Status instances, one for each message up to count 1575 """ 1576 return self.GetUserTimeline( 1577 since_id=since_id, 1578 count=count, 1579 max_id=max_id, 1580 trim_user=trim_user, 1581 exclude_replies=True, 1582 include_rts=True) 1583 1584 def GetReplies(self, 1585 since_id=None, 1586 count=None, 1587 max_id=None, 1588 trim_user=False): 1589 """Get a sequence of status messages representing the 20 most 1590 recent replies (status updates prefixed with @twitterID) to the 1591 authenticating user. 1592 1593 Args: 1594 since_id: 1595 Returns results with an ID greater than (that is, more recent 1596 than) the specified ID. There are limits to the number of 1597 Tweets which can be accessed through the API. If the limit of 1598 Tweets has occurred since the since_id, the since_id will be 1599 forced to the oldest ID available. [Optional] 1600 max_id: 1601 Returns results with an ID less than (that is, older than) or 1602 equal to the specified ID. [Optional] 1603 trim_user: 1604 If True the returned payload will only contain the user IDs, 1605 otherwise the payload will contain the full user data item. 1606 [Optional] 1607 1608 Returns: 1609 A sequence of twitter.Status instances, one for each reply to the user. 1610 """ 1611 return self.GetUserTimeline(since_id=since_id, count=count, max_id=max_id, trim_user=trim_user, 1612 exclude_replies=False, include_rts=False) 1613 1614 def GetRetweets(self, 1615 statusid, 1616 count=None, 1617 trim_user=False): 1618 """Returns up to 100 of the first retweets of the tweet identified 1619 by statusid 1620 1621 Args: 1622 statusid (int): 1623 The ID of the tweet for which retweets should be searched for 1624 count (int, optional): 1625 The number of status messages to retrieve. 1626 trim_user (bool, optional): 1627 If True the returned payload will only contain the user IDs, 1628 otherwise the payload will contain the full user data item. 1629 1630 Returns: 1631 A list of twitter.Status instances, which are retweets of statusid 1632 """ 1633 url = '%s/statuses/retweets/%s.json' % (self.base_url, statusid) 1634 parameters = { 1635 'trim_user': enf_type('trim_user', bool, trim_user), 1636 } 1637 1638 if count: 1639 parameters['count'] = enf_type('count', int, count) 1640 1641 resp = self._RequestUrl(url, 'GET', data=parameters) 1642 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 1643 1644 return [Status.NewFromJsonDict(s) for s in data] 1645 1646 def GetRetweeters(self, 1647 status_id, 1648 cursor=None, 1649 count=100, 1650 stringify_ids=False): 1651 """Returns a collection of up to 100 user IDs belonging to users who have 1652 retweeted the tweet specified by the status_id parameter. 1653 1654 Args: 1655 status_id: 1656 the tweet's numerical ID 1657 cursor: 1658 breaks the ids into pages of no more than 100. 1659 stringify_ids: 1660 returns the IDs as unicode strings. [Optional] 1661 1662 Returns: 1663 A list of user IDs 1664 """ 1665 url = '%s/statuses/retweeters/ids.json' % (self.base_url) 1666 parameters = { 1667 'id': enf_type('id', int, status_id), 1668 'stringify_ids': enf_type('stringify_ids', bool, stringify_ids), 1669 'count': count, 1670 } 1671 1672 result = [] 1673 1674 total_count = 0 1675 while True: 1676 if cursor: 1677 try: 1678 parameters['cursor'] = int(cursor) 1679 except ValueError: 1680 raise TwitterError({'message': "cursor must be an integer"}) 1681 resp = self._RequestUrl(url, 'GET', data=parameters) 1682 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 1683 result += [x for x in data['ids']] 1684 if 'next_cursor' in data: 1685 if data['next_cursor'] == 0 or data['next_cursor'] == data['previous_cursor']: 1686 break 1687 else: 1688 cursor = data['next_cursor'] 1689 total_count -= len(data['ids']) 1690 if total_count < 1: 1691 break 1692 else: 1693 break 1694 1695 return result 1696 1697 def GetRetweetsOfMe(self, 1698 count=None, 1699 since_id=None, 1700 max_id=None, 1701 trim_user=False, 1702 include_entities=True, 1703 include_user_entities=True): 1704 """Returns up to 100 of the most recent tweets of the user that have been 1705 retweeted by others. 1706 1707 Args: 1708 count: 1709 The number of retweets to retrieve, up to 100. 1710 Defaults to 20. [Optional] 1711 since_id: 1712 Returns results with an ID greater than 1713 (newer than) this ID. [Optional] 1714 max_id: 1715 Returns results with an ID less than or equal 1716 to this ID. [Optional] 1717 trim_user: 1718 When True, the user object for each tweet will 1719 only be an ID. [Optional] 1720 include_entities: 1721 When True, the tweet entities will be included. [Optional] 1722 include_user_entities: 1723 When True, the user entities will be included. [Optional] 1724 """ 1725 url = '%s/statuses/retweets_of_me.json' % self.base_url 1726 if count is not None: 1727 try: 1728 if int(count) > 100: 1729 raise TwitterError({'message': "'count' may not be greater than 100"}) 1730 except ValueError: 1731 raise TwitterError({'message': "'count' must be an integer"}) 1732 parameters = { 1733 'count': count, 1734 'since_id': since_id, 1735 'max_id': max_id, 1736 'trim_user': bool(trim_user), 1737 'include_entities': bool(include_entities), 1738 'include_user_entities': bool(include_user_entities), 1739 } 1740 1741 resp = self._RequestUrl(url, 'GET', data=parameters) 1742 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 1743 1744 return [Status.NewFromJsonDict(s) for s in data] 1745 1746 def _GetBlocksMutesPaged(self, 1747 endpoint, 1748 action, 1749 cursor=-1, 1750 skip_status=False, 1751 include_entities=True, 1752 stringify_ids=False): 1753 """ Fetch a page of the users (as twitter.User instances) 1754 blocked or muted by the currently authenticated user. 1755 1756 Args: 1757 endpoint (str): 1758 Either "mute" or "block". 1759 action (str): 1760 Either 'list' or 'ids' depending if you want to return fully-hydrated 1761 twitter.User objects or a list of user IDs as ints. 1762 cursor (int, optional): 1763 Should be set to -1 if you want the first page, thereafter denotes 1764 the page of users that you want to return. 1765 skip_status (bool, optional): 1766 If True the statuses will not be returned in the user items. 1767 include_entities (bool, optional): 1768 When True, the user entities will be included. 1769 1770 Returns: 1771 next_cursor, previous_cursor, list of twitter.User instances, 1772 one for each user. 1773 """ 1774 urls = { 1775 'mute': { 1776 'list': '%s/mutes/users/list.json' % self.base_url, 1777 'ids': '%s/mutes/users/ids.json' % self.base_url 1778 }, 1779 'block': { 1780 'list': '%s/blocks/list.json' % self.base_url, 1781 'ids': '%s/blocks/ids.json' % self.base_url 1782 } 1783 } 1784 1785 url = urls[endpoint][action] 1786 1787 result = [] 1788 parameters = { 1789 'skip_status': bool(skip_status), 1790 'include_entities': bool(include_entities), 1791 'stringify_ids': bool(stringify_ids), 1792 'cursor': cursor, 1793 } 1794 1795 resp = self._RequestUrl(url, 'GET', data=parameters) 1796 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 1797 1798 if action == 'ids': 1799 result += data.get('ids') 1800 else: 1801 result += [User.NewFromJsonDict(x) for x in data['users']] 1802 next_cursor = data.get('next_cursor', 0) 1803 previous_cursor = data.get('previous_cursor', 0) 1804 1805 return next_cursor, previous_cursor, result 1806 1807 def GetBlocks(self, 1808 skip_status=False, 1809 include_entities=False): 1810 """ Fetch the sequence of all users (as twitter.User instances), 1811 blocked by the currently authenticated user. 1812 1813 Args: 1814 skip_status (bool, optional): 1815 If True the statuses will not be returned in the user items. 1816 include_entities (bool, optional): 1817 When True, the user entities will be included. 1818 1819 Returns: 1820 A list of twitter.User instances, one for each blocked user. 1821 """ 1822 result = [] 1823 cursor = -1 1824 1825 while True: 1826 next_cursor, previous_cursor, users = self.GetBlocksPaged( 1827 cursor=cursor, 1828 skip_status=skip_status, 1829 include_entities=include_entities) 1830 result += users 1831 if next_cursor == 0 or next_cursor == previous_cursor: 1832 break 1833 else: 1834 cursor = next_cursor 1835 1836 return result 1837 1838 def GetBlocksPaged(self, 1839 cursor=-1, 1840 skip_status=False, 1841 include_entities=False): 1842 """ Fetch a page of the users (as twitter.User instances) 1843 blocked by the currently authenticated user. 1844 1845 Args: 1846 cursor (int, optional): 1847 Should be set to -1 if you want the first page, thereafter denotes 1848 the page of blocked users that you want to return. 1849 skip_status (bool, optional): 1850 If True the statuses will not be returned in the user items. 1851 include_entities (bool, optional): 1852 When True, the user entities will be included. 1853 1854 Returns: 1855 next_cursor, previous_cursor, list of twitter.User instances, 1856 one for each blocked user. 1857 """ 1858 return self._GetBlocksMutesPaged(endpoint='block', 1859 action='list', 1860 cursor=cursor, 1861 skip_status=skip_status, 1862 include_entities=include_entities) 1863 1864 def GetBlocksIDs(self, 1865 stringify_ids=False): 1866 """Fetch the sequence of all user IDs blocked by the 1867 currently authenticated user. 1868 1869 Args: 1870 stringify_ids (bool, optional): 1871 If True user IDs will be returned as strings rather than integers. 1872 1873 Returns: 1874 A list of user IDs for all blocked users. 1875 """ 1876 result = [] 1877 cursor = -1 1878 1879 while True: 1880 next_cursor, previous_cursor, user_ids = self.GetBlocksIDsPaged( 1881 cursor=cursor, 1882 stringify_ids=stringify_ids) 1883 result += user_ids 1884 if next_cursor == 0 or next_cursor == previous_cursor: 1885 break 1886 else: 1887 cursor = next_cursor 1888 1889 return result 1890 1891 def GetBlocksIDsPaged(self, 1892 cursor=-1, 1893 stringify_ids=False): 1894 """ Fetch a page of the user IDs blocked by the currently 1895 authenticated user. 1896 1897 Args: 1898 cursor (int, optional): 1899 Should be set to -1 if you want the first page, thereafter denotes 1900 the page of blocked users that you want to return. 1901 stringify_ids (bool, optional): 1902 If True user IDs will be returned as strings rather than integers. 1903 1904 Returns: 1905 next_cursor, previous_cursor, list of user IDs of blocked users. 1906 """ 1907 return self._GetBlocksMutesPaged(endpoint='block', 1908 action='ids', 1909 cursor=cursor, 1910 stringify_ids=stringify_ids) 1911 1912 def GetMutes(self, 1913 skip_status=False, 1914 include_entities=False): 1915 """ Fetch the sequence of all users (as twitter.User instances), 1916 muted by the currently authenticated user. 1917 1918 Args: 1919 skip_status (bool, optional): 1920 If True the statuses will not be returned in the user items. 1921 include_entities (bool, optional): 1922 When True, the user entities will be included. 1923 1924 Returns: 1925 A list of twitter.User instances, one for each muted user. 1926 """ 1927 result = [] 1928 cursor = -1 1929 1930 while True: 1931 next_cursor, previous_cursor, users = self.GetMutesPaged( 1932 cursor=cursor, 1933 skip_status=skip_status, 1934 include_entities=include_entities) 1935 result += users 1936 if next_cursor == 0 or next_cursor == previous_cursor: 1937 break 1938 else: 1939 cursor = next_cursor 1940 1941 return result 1942 1943 def GetMutesPaged(self, 1944 cursor=-1, 1945 skip_status=False, 1946 include_entities=False): 1947 """ Fetch a page of the users (as twitter.User instances) 1948 muted by the currently authenticated user. 1949 1950 Args: 1951 cursor (int, optional): 1952 Should be set to -1 if you want the first page, thereafter denotes 1953 the page of muted users that you want to return. 1954 skip_status (bool, optional): 1955 If True the statuses will not be returned in the user items. 1956 include_entities (bool, optional): 1957 When True, the user entities will be included. 1958 1959 Returns: 1960 next_cursor, previous_cursor, list of twitter.User instances, 1961 one for each muted user. 1962 """ 1963 1964 return self._GetBlocksMutesPaged(endpoint='mute', 1965 action='list', 1966 cursor=cursor, 1967 skip_status=skip_status, 1968 include_entities=include_entities) 1969 1970 def GetMutesIDs(self, 1971 stringify_ids=False): 1972 """Fetch the sequence of all user IDs muted by the 1973 currently authenticated user. 1974 1975 Args: 1976 stringify_ids (bool, optional): 1977 If True user IDs will be returned as strings rather than integers. 1978 1979 Returns: 1980 A list of user IDs for all muted users. 1981 """ 1982 result = [] 1983 cursor = -1 1984 1985 while True: 1986 next_cursor, previous_cursor, user_ids = self.GetMutesIDsPaged( 1987 cursor=cursor, 1988 stringify_ids=stringify_ids) 1989 result += user_ids 1990 if next_cursor == 0 or next_cursor == previous_cursor: 1991 break 1992 else: 1993 cursor = next_cursor 1994 1995 return result 1996 1997 def GetMutesIDsPaged(self, 1998 cursor=-1, 1999 stringify_ids=False): 2000 """ Fetch a page of the user IDs muted by the currently 2001 authenticated user. 2002 2003 Args: 2004 cursor (int, optional): 2005 Should be set to -1 if you want the first page, thereafter denotes 2006 the page of muted users that you want to return. 2007 stringify_ids (bool, optional): 2008 If True user IDs will be returned as strings rather than integers. 2009 2010 Returns: 2011 next_cursor, previous_cursor, list of user IDs of muted users. 2012 """ 2013 return self._GetBlocksMutesPaged(endpoint='mute', 2014 action='ids', 2015 cursor=cursor, 2016 stringify_ids=stringify_ids) 2017 2018 def _BlockMute(self, 2019 action, 2020 endpoint, 2021 user_id=None, 2022 screen_name=None, 2023 include_entities=True, 2024 skip_status=False): 2025 """Create or destroy a block or mute on behalf of the authenticated user. 2026 2027 Args: 2028 action (str): 2029 Either 'create' or 'destroy'. 2030 endpoint (str): 2031 Either 'block' or 'mute'. 2032 user_id (int, optional) 2033 The numerical ID of the user to block/mute. 2034 screen_name (str, optional): 2035 The screen name of the user to block/mute. 2036 include_entities (bool, optional): 2037 The entities node will not be included if set to False. 2038 skip_status (bool, optional): 2039 When set to False, the blocked User's statuses will not be included 2040 with the returned User object. 2041 Returns: 2042 twitter.User: twitter.User object representing the blocked/muted user. 2043 """ 2044 2045 urls = { 2046 'block': { 2047 'create': '%s/blocks/create.json' % (self.base_url), 2048 'destroy': '%s/blocks/destroy.json' % (self.base_url), 2049 }, 2050 'mute': { 2051 'create': '%s/mutes/users/create.json' % (self.base_url), 2052 'destroy': '%s/mutes/users/destroy.json' % (self.base_url) 2053 } 2054 } 2055 2056 url = urls[endpoint][action] 2057 post_data = {} 2058 2059 if user_id: 2060 post_data['user_id'] = enf_type('user_id', int, user_id) 2061 elif screen_name: 2062 post_data['screen_name'] = screen_name 2063 else: 2064 raise TwitterError("You must specify either a user_id or screen_name") 2065 2066 if include_entities: 2067 post_data['include_entities'] = enf_type('include_entities', bool, include_entities) 2068 if skip_status: 2069 post_data['skip_status'] = enf_type('skip_status', bool, skip_status) 2070 2071 resp = self._RequestUrl(url, 'POST', data=post_data) 2072 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 2073 2074 return User.NewFromJsonDict(data) 2075 2076 def CreateBlock(self, 2077 user_id=None, 2078 screen_name=None, 2079 include_entities=True, 2080 skip_status=False): 2081 """Blocks the user specified by either user_id or screen_name. 2082 2083 Args: 2084 user_id (int, optional) 2085 The numerical ID of the user to block. 2086 screen_name (str, optional): 2087 The screen name of the user to block. 2088 include_entities (bool, optional): 2089 The entities node will not be included if set to False. 2090 skip_status (bool, optional): 2091 When set to False, the blocked User's statuses will not be included 2092 with the returned User object. 2093 2094 Returns: 2095 A twitter.User instance representing the blocked user. 2096 """ 2097 return self._BlockMute(action='create', 2098 endpoint='block', 2099 user_id=user_id, 2100 screen_name=screen_name, 2101 include_entities=include_entities, 2102 skip_status=skip_status) 2103 2104 def DestroyBlock(self, 2105 user_id=None, 2106 screen_name=None, 2107 include_entities=True, 2108 skip_status=False): 2109 """Unlocks the user specified by either user_id or screen_name. 2110 2111 Args: 2112 user_id (int, optional) 2113 The numerical ID of the user to block. 2114 screen_name (str, optional): 2115 The screen name of the user to block. 2116 include_entities (bool, optional): 2117 The entities node will not be included if set to False. 2118 skip_status (bool, optional): 2119 When set to False, the blocked User's statuses will not be included 2120 with the returned User object. 2121 2122 Returns: 2123 A twitter.User instance representing the blocked user. 2124 """ 2125 return self._BlockMute(action='destroy', 2126 endpoint='block', 2127 user_id=user_id, 2128 screen_name=screen_name, 2129 include_entities=include_entities, 2130 skip_status=skip_status) 2131 2132 def CreateMute(self, 2133 user_id=None, 2134 screen_name=None, 2135 include_entities=True, 2136 skip_status=False): 2137 """Mutes the user specified by either user_id or screen_name. 2138 2139 Args: 2140 user_id (int, optional) 2141 The numerical ID of the user to mute. 2142 screen_name (str, optional): 2143 The screen name of the user to mute. 2144 include_entities (bool, optional): 2145 The entities node will not be included if set to False. 2146 skip_status (bool, optional): 2147 When set to False, the muted User's statuses will not be included 2148 with the returned User object. 2149 2150 Returns: 2151 A twitter.User instance representing the muted user. 2152 """ 2153 return self._BlockMute(action='create', 2154 endpoint='mute', 2155 user_id=user_id, 2156 screen_name=screen_name, 2157 include_entities=include_entities, 2158 skip_status=skip_status) 2159 2160 def DestroyMute(self, 2161 user_id=None, 2162 screen_name=None, 2163 include_entities=True, 2164 skip_status=False): 2165 """Unlocks the user specified by either user_id or screen_name. 2166 2167 Args: 2168 user_id (int, optional) 2169 The numerical ID of the user to mute. 2170 screen_name (str, optional): 2171 The screen name of the user to mute. 2172 include_entities (bool, optional): 2173 The entities node will not be included if set to False. 2174 skip_status (bool, optional): 2175 When set to False, the muted User's statuses will not be included 2176 with the returned User object. 2177 2178 Returns: 2179 A twitter.User instance representing the muted user. 2180 """ 2181 return self._BlockMute(action='destroy', 2182 endpoint='mute', 2183 user_id=user_id, 2184 screen_name=screen_name, 2185 include_entities=include_entities, 2186 skip_status=skip_status) 2187 2188 def _GetIDsPaged(self, 2189 url, 2190 user_id, 2191 screen_name, 2192 cursor, 2193 stringify_ids, 2194 count): 2195 """ 2196 This is the lowest level paging logic for fetching IDs. It is used 2197 solely by GetFollowerIDsPaged and GetFriendIDsPaged. It is not intended 2198 for other use. 2199 2200 See GetFollowerIDsPaged or GetFriendIDsPaged for an explanation of the 2201 input arguments. 2202 """ 2203 result = [] 2204 2205 parameters = {} 2206 if user_id is not None: 2207 parameters['user_id'] = user_id 2208 if screen_name is not None: 2209 parameters['screen_name'] = screen_name 2210 if count is not None: 2211 parameters['count'] = count 2212 parameters['stringify_ids'] = stringify_ids 2213 parameters['cursor'] = cursor 2214 2215 resp = self._RequestUrl(url, 'GET', data=parameters) 2216 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 2217 2218 if 'ids' in data: 2219 result.extend([x for x in data['ids']]) 2220 2221 next_cursor = data.get('next_cursor', 0) 2222 previous_cursor = data.get('previous_cursor', 0) 2223 2224 return next_cursor, previous_cursor, result 2225 2226 def GetFollowerIDsPaged(self, 2227 user_id=None, 2228 screen_name=None, 2229 cursor=-1, 2230 stringify_ids=False, 2231 count=5000): 2232 """Make a cursor driven call to return a list of one page followers. 2233 2234 The caller is responsible for handling the cursor value and looping 2235 to gather all of the data 2236 2237 Args: 2238 user_id: 2239 The twitter id of the user whose followers you are fetching. 2240 If not specified, defaults to the authenticated user. [Optional] 2241 screen_name: 2242 The twitter name of the user whose followers you are fetching. 2243 If not specified, defaults to the authenticated user. [Optional] 2244 cursor: 2245 Should be set to -1 for the initial call and then is used to 2246 control what result page Twitter returns. 2247 stringify_ids: 2248 if True then twitter will return the ids as strings instead of 2249 integers. [Optional] 2250 count: 2251 The number of user id's to retrieve per API request. Please be aware 2252 that this might get you rate-limited if set to a small number. 2253 By default Twitter will retrieve 5000 UIDs per call. [Optional] 2254 2255 Returns: 2256 next_cursor, previous_cursor, data sequence of user ids, 2257 one for each follower 2258 """ 2259 url = '%s/followers/ids.json' % self.base_url 2260 return self._GetIDsPaged(url=url, 2261 user_id=user_id, 2262 screen_name=screen_name, 2263 cursor=cursor, 2264 stringify_ids=stringify_ids, 2265 count=count) 2266 2267 def GetFriendIDsPaged(self, 2268 user_id=None, 2269 screen_name=None, 2270 cursor=-1, 2271 stringify_ids=False, 2272 count=5000): 2273 """Make a cursor driven call to return the list of all friends 2274 2275 The caller is responsible for handling the cursor value and looping 2276 to gather all of the data 2277 2278 Args: 2279 user_id: 2280 The twitter id of the user whose friends you are fetching. 2281 If not specified, defaults to the authenticated user. [Optional] 2282 screen_name: 2283 The twitter name of the user whose friends you are fetching. 2284 If not specified, defaults to the authenticated user. [Optional] 2285 cursor: 2286 Should be set to -1 for the initial call and then is used to 2287 control what result page Twitter returns. 2288 stringify_ids: 2289 if True then twitter will return the ids as strings instead of 2290 integers. [Optional] 2291 count: 2292 The number of user id's to retrieve per API request. Please be aware 2293 that this might get you rate-limited if set to a small number. 2294 By default Twitter will retrieve 5000 UIDs per call. [Optional] 2295 2296 Returns: 2297 next_cursor, previous_cursor, data sequence of twitter.User instances, 2298 one for each friend 2299 """ 2300 url = '%s/friends/ids.json' % self.base_url 2301 return self._GetIDsPaged(url, 2302 user_id, 2303 screen_name, 2304 cursor, 2305 stringify_ids, 2306 count) 2307 2308 def _GetFriendFollowerIDs(self, 2309 url=None, 2310 user_id=None, 2311 screen_name=None, 2312 cursor=None, 2313 count=None, 2314 stringify_ids=False, 2315 total_count=None): 2316 """ Common method for GetFriendIDs and GetFollowerIDs """ 2317 2318 count = 5000 2319 cursor = -1 2320 result = [] 2321 2322 if total_count: 2323 total_count = enf_type('total_count', int, total_count) 2324 2325 if total_count and total_count < count: 2326 count = total_count 2327 2328 while True: 2329 if total_count is not None and len(result) + count > total_count: 2330 break 2331 2332 next_cursor, previous_cursor, data = self._GetIDsPaged( 2333 url=url, 2334 user_id=user_id, 2335 screen_name=screen_name, 2336 cursor=cursor, 2337 stringify_ids=stringify_ids, 2338 count=count) 2339 2340 result.extend([x for x in data]) 2341 2342 if next_cursor == 0 or next_cursor == previous_cursor: 2343 break 2344 else: 2345 cursor = next_cursor 2346 2347 return result 2348 2349 def GetFollowerIDs(self, 2350 user_id=None, 2351 screen_name=None, 2352 cursor=None, 2353 stringify_ids=False, 2354 count=None, 2355 total_count=None): 2356 """Returns a list of twitter user id's for every person 2357 that is following the specified user. 2358 2359 Args: 2360 user_id: 2361 The id of the user to retrieve the id list for. [Optional] 2362 screen_name: 2363 The screen_name of the user to retrieve the id list for. [Optional] 2364 cursor: 2365 Specifies the Twitter API Cursor location to start at. 2366 Note: there are pagination limits. [Optional] 2367 stringify_ids: 2368 if True then twitter will return the ids as strings instead of 2369 integers. [Optional] 2370 count: 2371 The number of user id's to retrieve per API request. Please be aware 2372 that this might get you rate-limited if set to a small number. 2373 By default Twitter will retrieve 5000 UIDs per call. [Optional] 2374 total_count: 2375 The total amount of UIDs to retrieve. Good if the account has many 2376 followers and you don't want to get rate limited. The data returned 2377 might contain more UIDs if total_count is not a multiple of count 2378 (5000 by default). [Optional] 2379 2380 Returns: 2381 A list of integers, one for each user id. 2382 """ 2383 url = '%s/followers/ids.json' % self.base_url 2384 return self._GetFriendFollowerIDs(url=url, 2385 user_id=user_id, 2386 screen_name=screen_name, 2387 cursor=cursor, 2388 stringify_ids=stringify_ids, 2389 count=count, 2390 total_count=total_count) 2391 2392 def GetFriendIDs(self, 2393 user_id=None, 2394 screen_name=None, 2395 cursor=None, 2396 count=None, 2397 stringify_ids=False, 2398 total_count=None): 2399 """ Fetch a sequence of user ids, one for each friend. 2400 Returns a list of all the given user's friends' IDs. If no user_id or 2401 screen_name is given, the friends will be those of the authenticated 2402 user. 2403 2404 Args: 2405 user_id: 2406 The id of the user to retrieve the id list for. [Optional] 2407 screen_name: 2408 The screen_name of the user to retrieve the id list for. [Optional] 2409 cursor: 2410 Specifies the Twitter API Cursor location to start at. 2411 Note: there are pagination limits. [Optional] 2412 stringify_ids: 2413 if True then twitter will return the ids as strings instead of integers. 2414 [Optional] 2415 count: 2416 The number of user id's to retrieve per API request. Please be aware that 2417 this might get you rate-limited if set to a small number. 2418 By default Twitter will retrieve 5000 UIDs per call. [Optional] 2419 total_count: 2420 The total amount of UIDs to retrieve. Good if the account has many followers 2421 and you don't want to get rate limited. The data returned might contain more 2422 UIDs if total_count is not a multiple of count (5000 by default). [Optional] 2423 2424 Returns: 2425 A list of integers, one for each user id. 2426 """ 2427 url = '%s/friends/ids.json' % self.base_url 2428 return self._GetFriendFollowerIDs(url, 2429 user_id, 2430 screen_name, 2431 cursor, 2432 count, 2433 stringify_ids, 2434 total_count) 2435 2436 def _GetFriendsFollowersPaged(self, 2437 url=None, 2438 user_id=None, 2439 screen_name=None, 2440 cursor=-1, 2441 count=200, 2442 skip_status=False, 2443 include_user_entities=True): 2444 2445 """Make a cursor driven call to return the list of 1 page of friends 2446 or followers. 2447 2448 Args: 2449 url: 2450 Endpoint from which to get data. Either 2451 base_url+'/followers/list.json' or base_url+'/friends/list.json'. 2452 user_id: 2453 The twitter id of the user whose followers you are fetching. 2454 If not specified, defaults to the authenticated user. [Optional] 2455 screen_name: 2456 The twitter name of the user whose followers you are fetching. 2457 If not specified, defaults to the authenticated user. [Optional] 2458 cursor: 2459 Should be set to -1 for the initial call and then is used to 2460 control what result page Twitter returns. 2461 count: 2462 The number of users to return per page, up to a maximum of 200. 2463 Defaults to 200. [Optional] 2464 skip_status: 2465 If True the statuses will not be returned in the user items. 2466 [Optional] 2467 include_user_entities: 2468 When True, the user entities will be included. [Optional] 2469 2470 Returns: 2471 next_cursor, previous_cursor, data sequence of twitter.User 2472 instances, one for each follower 2473 """ 2474 2475 if user_id and screen_name: 2476 warnings.warn( 2477 "If both user_id and screen_name are specified, Twitter will " 2478 "return the followers of the user specified by screen_name, " 2479 "however this behavior is undocumented by Twitter and might " 2480 "change without warning.", stacklevel=2) 2481 2482 parameters = {} 2483 2484 if user_id is not None: 2485 parameters['user_id'] = user_id 2486 if screen_name is not None: 2487 parameters['screen_name'] = screen_name 2488 2489 try: 2490 parameters['count'] = int(count) 2491 except ValueError: 2492 raise TwitterError({'message': "count must be an integer"}) 2493 2494 parameters['skip_status'] = skip_status 2495 parameters['include_user_entities'] = include_user_entities 2496 parameters['cursor'] = cursor 2497 2498 resp = self._RequestUrl(url, 'GET', data=parameters) 2499 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 2500 2501 if 'users' in data: 2502 users = [User.NewFromJsonDict(user) for user in data['users']] 2503 else: 2504 users = [] 2505 2506 if 'next_cursor' in data: 2507 next_cursor = data['next_cursor'] 2508 else: 2509 next_cursor = 0 2510 if 'previous_cursor' in data: 2511 previous_cursor = data['previous_cursor'] 2512 else: 2513 previous_cursor = 0 2514 2515 return next_cursor, previous_cursor, users 2516 2517 def GetFollowersPaged(self, 2518 user_id=None, 2519 screen_name=None, 2520 cursor=-1, 2521 count=200, 2522 skip_status=False, 2523 include_user_entities=True): 2524 """Make a cursor driven call to return the list of all followers 2525 2526 Args: 2527 user_id: 2528 The twitter id of the user whose followers you are fetching. 2529 If not specified, defaults to the authenticated user. [Optional] 2530 screen_name: 2531 The twitter name of the user whose followers you are fetching. 2532 If not specified, defaults to the authenticated user. [Optional] 2533 cursor: 2534 Should be set to -1 for the initial call and then is used to 2535 control what result page Twitter returns. 2536 count: 2537 The number of users to return per page, up to a maximum of 200. 2538 Defaults to 200. [Optional] 2539 skip_status: 2540 If True the statuses will not be returned in the user items. 2541 [Optional] 2542 include_user_entities: 2543 When True, the user entities will be included. [Optional] 2544 2545 Returns: 2546 next_cursor, previous_cursor, data sequence of twitter.User 2547 instances, one for each follower 2548 """ 2549 url = '%s/followers/list.json' % self.base_url 2550 return self._GetFriendsFollowersPaged(url, 2551 user_id, 2552 screen_name, 2553 cursor, 2554 count, 2555 skip_status, 2556 include_user_entities) 2557 2558 def GetFriendsPaged(self, 2559 user_id=None, 2560 screen_name=None, 2561 cursor=-1, 2562 count=200, 2563 skip_status=False, 2564 include_user_entities=True): 2565 """Make a cursor driven call to return the list of all friends. 2566 2567 Args: 2568 user_id: 2569 The twitter id of the user whose friends you are fetching. 2570 If not specified, defaults to the authenticated user. [Optional] 2571 screen_name: 2572 The twitter name of the user whose friends you are fetching. 2573 If not specified, defaults to the authenticated user. [Optional] 2574 cursor: 2575 Should be set to -1 for the initial call and then is used to 2576 control what result page Twitter returns. 2577 count: 2578 The number of users to return per page, up to a current maximum of 2579 200. Defaults to 200. [Optional] 2580 skip_status: 2581 If True the statuses will not be returned in the user items. 2582 [Optional] 2583 include_user_entities: 2584 When True, the user entities will be included. [Optional] 2585 2586 Returns: 2587 next_cursor, previous_cursor, data sequence of twitter.User 2588 instances, one for each follower 2589 """ 2590 url = '%s/friends/list.json' % self.base_url 2591 return self._GetFriendsFollowersPaged(url, 2592 user_id, 2593 screen_name, 2594 cursor, 2595 count, 2596 skip_status, 2597 include_user_entities) 2598 2599 def _GetFriendsFollowers(self, 2600 url=None, 2601 user_id=None, 2602 screen_name=None, 2603 cursor=None, 2604 count=None, 2605 total_count=None, 2606 skip_status=False, 2607 include_user_entities=True): 2608 2609 """ Fetch the sequence of twitter.User instances, one for each friend 2610 or follower. 2611 2612 Args: 2613 url: 2614 URL to get. Either base_url + ('/followers/list.json' or 2615 '/friends/list.json'). 2616 user_id: 2617 The twitter id of the user whose friends you are fetching. 2618 If not specified, defaults to the authenticated user. [Optional] 2619 screen_name: 2620 The twitter name of the user whose friends you are fetching. 2621 If not specified, defaults to the authenticated user. [Optional] 2622 cursor: 2623 Should be set to -1 for the initial call and then is used to 2624 control what result page Twitter returns. 2625 count: 2626 The number of users to return per page, up to a maximum of 200. 2627 Defaults to 200. [Optional] 2628 total_count: 2629 The upper bound of number of users to return, defaults to None. 2630 skip_status: 2631 If True the statuses will not be returned in the user items. 2632 [Optional] 2633 include_user_entities: 2634 When True, the user entities will be included. [Optional] 2635 2636 Returns: 2637 A sequence of twitter.User instances, one for each friend or follower 2638 """ 2639 2640 if cursor is not None or count is not None: 2641 warnings.warn( 2642 "Use of 'cursor' and 'count' parameters are deprecated as of " 2643 "python-twitter 3.0. Please use GetFriendsPaged or " 2644 "GetFollowersPaged instead.", 2645 PythonTwitterDeprecationWarning330) 2646 2647 count = 200 2648 cursor = -1 2649 result = [] 2650 2651 if total_count: 2652 try: 2653 total_count = int(total_count) 2654 except ValueError: 2655 raise TwitterError({'message': "total_count must be an integer"}) 2656 2657 if total_count <= 200: 2658 count = total_count 2659 2660 while True: 2661 if total_count is not None and len(result) + count > total_count: 2662 break 2663 2664 next_cursor, previous_cursor, data = self._GetFriendsFollowersPaged( 2665 url, 2666 user_id, 2667 screen_name, 2668 cursor, 2669 count, 2670 skip_status, 2671 include_user_entities) 2672 2673 if next_cursor: 2674 cursor = next_cursor 2675 2676 result.extend(data) 2677 2678 if next_cursor == 0 or next_cursor == previous_cursor: 2679 break 2680 2681 return result 2682 2683 def GetFollowers(self, 2684 user_id=None, 2685 screen_name=None, 2686 cursor=None, 2687 count=None, 2688 total_count=None, 2689 skip_status=False, 2690 include_user_entities=True): 2691 """Fetch the sequence of twitter.User instances, one for each follower. 2692 2693 If both user_id and screen_name are specified, this call will return 2694 the followers of the user specified by screen_name, however this 2695 behavior is undocumented by Twitter and may change without warning. 2696 2697 Args: 2698 user_id: 2699 The twitter id of the user whose followers you are fetching. 2700 If not specified, defaults to the authenticated user. [Optional] 2701 screen_name: 2702 The twitter name of the user whose followers you are fetching. 2703 If not specified, defaults to the authenticated user. [Optional] 2704 cursor: 2705 Should be set to -1 for the initial call and then is used to 2706 control what result page Twitter returns. 2707 count: 2708 The number of users to return per page, up to a maximum of 200. 2709 Defaults to 200. [Optional] 2710 total_count: 2711 The upper bound of number of users to return, defaults to None. 2712 skip_status: 2713 If True the statuses will not be returned in the user items. [Optional] 2714 include_user_entities: 2715 When True, the user entities will be included. [Optional] 2716 2717 Returns: 2718 A sequence of twitter.User instances, one for each follower 2719 """ 2720 url = '%s/followers/list.json' % self.base_url 2721 return self._GetFriendsFollowers(url, 2722 user_id, 2723 screen_name, 2724 cursor, 2725 count, 2726 total_count, 2727 skip_status, 2728 include_user_entities) 2729 2730 def GetFriends(self, 2731 user_id=None, 2732 screen_name=None, 2733 cursor=None, 2734 count=None, 2735 total_count=None, 2736 skip_status=False, 2737 include_user_entities=True): 2738 """Fetch the sequence of twitter.User instances, one for each friend. 2739 2740 If both user_id and screen_name are specified, this call will return 2741 the followers of the user specified by screen_name, however this 2742 behavior is undocumented by Twitter and may change without warning. 2743 2744 Args: 2745 user_id: 2746 The twitter id of the user whose friends you are fetching. 2747 If not specified, defaults to the authenticated user. [Optional] 2748 screen_name: 2749 The twitter name of the user whose friends you are fetching. 2750 If not specified, defaults to the authenticated user. [Optional] 2751 cursor: 2752 Should be set to -1 for the initial call and then is used to 2753 control what result page Twitter returns. 2754 count: 2755 The number of users to return per page, up to a maximum of 200. 2756 Defaults to 200. [Optional] 2757 total_count: 2758 The upper bound of number of users to return, defaults to None. 2759 skip_status: 2760 If True the statuses will not be returned in the user items. 2761 [Optional] 2762 include_user_entities: 2763 When True, the user entities will be included. [Optional] 2764 2765 Returns: 2766 A sequence of twitter.User instances, one for each friend 2767 """ 2768 url = '%s/friends/list.json' % self.base_url 2769 return self._GetFriendsFollowers(url, 2770 user_id, 2771 screen_name, 2772 cursor, 2773 count, 2774 total_count, 2775 skip_status, 2776 include_user_entities) 2777 2778 def UsersLookup(self, 2779 user_id=None, 2780 screen_name=None, 2781 users=None, 2782 include_entities=True, 2783 return_json=False): 2784 """Fetch extended information for the specified users. 2785 2786 Users may be specified either as lists of either user_ids, 2787 screen_names, or twitter.User objects. The list of users that 2788 are queried is the union of all specified parameters. 2789 2790 No more than 100 users may be given per request. 2791 2792 Args: 2793 user_id (int, list, optional): 2794 A list of user_ids to retrieve extended information. 2795 screen_name (str, list, optional): 2796 A list of screen_names to retrieve extended information. 2797 users (list, optional): 2798 A list of twitter.User objects to retrieve extended information. 2799 include_entities (bool, optional): 2800 The entities node that may appear within embedded statuses will be 2801 excluded when set to False. 2802 return_json (bool, optional): 2803 If True JSON data will be returned, instead of twitter.User 2804 2805 Returns: 2806 A list of twitter.User objects for the requested users 2807 """ 2808 if not any([user_id, screen_name, users]): 2809 raise TwitterError("Specify at least one of user_id, screen_name, or users.") 2810 2811 url = '%s/users/lookup.json' % self.base_url 2812 parameters = { 2813 'include_entities': include_entities 2814 } 2815 uids = list() 2816 if user_id: 2817 uids.extend(user_id) 2818 if users: 2819 uids.extend([u.id for u in users]) 2820 if len(uids): 2821 parameters['user_id'] = ','.join([str(u) for u in uids]) 2822 if screen_name: 2823 parameters['screen_name'] = parse_arg_list(screen_name, 'screen_name') 2824 2825 if len(uids) > 100: 2826 raise TwitterError("No more than 100 users may be requested per request.") 2827 2828 resp = self._RequestUrl(url, 'GET', data=parameters) 2829 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 2830 2831 if return_json: 2832 return data 2833 else: 2834 return [User.NewFromJsonDict(u) for u in data] 2835 2836 def GetUser(self, 2837 user_id=None, 2838 screen_name=None, 2839 include_entities=True, 2840 return_json=False): 2841 """Returns a single user. 2842 2843 Args: 2844 user_id (int, optional): 2845 The id of the user to retrieve. 2846 screen_name (str, optional): 2847 The screen name of the user for whom to return results for. 2848 Either a user_id or screen_name is required for this method. 2849 include_entities (bool, optional): 2850 The entities node will be omitted when set to False. 2851 return_json (bool, optional): 2852 If True JSON data will be returned, instead of twitter.User 2853 2854 Returns: 2855 A twitter.User instance representing that user 2856 """ 2857 url = '%s/users/show.json' % (self.base_url) 2858 parameters = { 2859 'include_entities': include_entities 2860 } 2861 if user_id: 2862 parameters['user_id'] = user_id 2863 elif screen_name: 2864 parameters['screen_name'] = screen_name 2865 else: 2866 raise TwitterError("Specify at least one of user_id or screen_name.") 2867 2868 resp = self._RequestUrl(url, 'GET', data=parameters) 2869 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 2870 2871 if return_json: 2872 return data 2873 else: 2874 return User.NewFromJsonDict(data) 2875 2876 def GetDirectMessages(self, 2877 since_id=None, 2878 max_id=None, 2879 count=None, 2880 include_entities=True, 2881 skip_status=False, 2882 full_text=False, 2883 page=None, 2884 return_json=False): 2885 """Returns a list of the direct messages sent to the authenticating user. 2886 2887 Args: 2888 since_id: 2889 Returns results with an ID greater than (that is, more recent 2890 than) the specified ID. There are limits to the number of 2891 Tweets which can be accessed through the API. If the limit of 2892 Tweets has occurred since the since_id, the since_id will be 2893 forced to the oldest ID available. [Optional] 2894 max_id: 2895 Returns results with an ID less than (that is, older than) or 2896 equal to the specified ID. [Optional] 2897 count: 2898 Specifies the number of direct messages to try and retrieve, up to a 2899 maximum of 200. The value of count is best thought of as a limit to the 2900 number of Tweets to return because suspended or deleted content is 2901 removed after the count has been applied. [Optional] 2902 include_entities: 2903 The entities node will be omitted when set to False. 2904 [Optional] 2905 skip_status: 2906 When set to True statuses will not be included in the returned user 2907 objects. [Optional] 2908 full_text: 2909 When set to True full message will be included in the returned message 2910 object if message length is bigger than CHARACTER_LIMIT characters. [Optional] 2911 page: 2912 If you want more than 200 messages, you can use this and get 20 messages 2913 each time. You must recall it and increment the page value until it 2914 return nothing. You can't use count option with it. First value is 1 and 2915 not 0. 2916 return_json (bool, optional): 2917 If True JSON data will be returned, instead of twitter.User 2918 2919 Returns: 2920 A sequence of twitter.DirectMessage instances 2921 """ 2922 url = '%s/direct_messages.json' % self.base_url 2923 parameters = { 2924 'full_text': bool(full_text), 2925 'include_entities': bool(include_entities), 2926 'max_id': max_id, 2927 'since_id': since_id, 2928 'skip_status': bool(skip_status), 2929 } 2930 2931 if count: 2932 parameters['count'] = enf_type('count', int, count) 2933 if page: 2934 parameters['page'] = enf_type('page', int, page) 2935 2936 resp = self._RequestUrl(url, 'GET', data=parameters) 2937 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 2938 2939 if return_json: 2940 return data 2941 else: 2942 return [DirectMessage.NewFromJsonDict(x) for x in data] 2943 2944 def GetSentDirectMessages(self, 2945 since_id=None, 2946 max_id=None, 2947 count=None, 2948 page=None, 2949 include_entities=True, 2950 return_json=False): 2951 """Returns a list of the direct messages sent by the authenticating user. 2952 2953 Args: 2954 since_id (int, optional): 2955 Returns results with an ID greater than (that is, more recent 2956 than) the specified ID. There are limits to the number of 2957 Tweets which can be accessed through the API. If the limit of 2958 Tweets has occured since the since_id, the since_id will be 2959 forced to the oldest ID available. 2960 max_id (int, optional): 2961 Returns results with an ID less than (that is, older than) or 2962 equal to the specified ID. 2963 count (int, optional): 2964 Specifies the number of direct messages to try and retrieve, up to a 2965 maximum of 200. The value of count is best thought of as a limit to the 2966 number of Tweets to return because suspended or deleted content is 2967 removed after the count has been applied. 2968 page (int, optional): 2969 Specifies the page of results to retrieve. 2970 Note: there are pagination limits. [Optional] 2971 include_entities (bool, optional): 2972 The entities node will be omitted when set to False. 2973 return_json (bool, optional): 2974 If True JSON data will be returned, instead of twitter.User 2975 2976 Returns: 2977 A sequence of twitter.DirectMessage instances 2978 """ 2979 url = '%s/direct_messages/sent.json' % self.base_url 2980 2981 parameters = { 2982 'include_entities': bool(include_entities), 2983 'max_id': max_id, 2984 'since_id': since_id, 2985 } 2986 2987 if count: 2988 parameters['count'] = enf_type('count', int, count) 2989 if page: 2990 parameters['page'] = enf_type('page', int, page) 2991 2992 resp = self._RequestUrl(url, 'GET', data=parameters) 2993 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 2994 2995 if return_json: 2996 return data 2997 else: 2998 return [DirectMessage.NewFromJsonDict(x) for x in data] 2999 3000 def PostDirectMessage(self, 3001 text, 3002 user_id=None, 3003 screen_name=None, 3004 return_json=False): 3005 """Post a twitter direct message from the authenticated user. 3006 3007 Args: 3008 text: The message text to be posted. 3009 user_id: 3010 The ID of the user who should receive the direct message. 3011 return_json (bool, optional): 3012 If True JSON data will be returned, instead of twitter.DirectMessage 3013 Returns: 3014 A twitter.DirectMessage instance representing the message posted 3015 """ 3016 url = '%s/direct_messages/events/new.json' % self.base_url 3017 3018 # Hack to allow some sort of backwards compatibility with older versions 3019 # part of the fix for Issue #587 3020 if user_id is None and screen_name is not None: 3021 user_id = self.GetUser(screen_name=screen_name).id 3022 3023 event = { 3024 'event': { 3025 'type': 'message_create', 3026 'message_create': { 3027 'target': { 3028 'recipient_id': user_id, 3029 }, 3030 'message_data': { 3031 'text': text 3032 } 3033 } 3034 } 3035 } 3036 3037 resp = self._RequestUrl(url, 'POST', json=event) 3038 data = resp.json() 3039 3040 if return_json: 3041 return data 3042 else: 3043 dm = DirectMessage( 3044 created_at=data['event']['created_timestamp'], 3045 id=data['event']['id'], 3046 recipient_id=data['event']['message_create']['target']['recipient_id'], 3047 sender_id=data['event']['message_create']['sender_id'], 3048 text=data['event']['message_create']['message_data']['text'], 3049 ) 3050 dm._json = data 3051 return dm 3052 3053 def DestroyDirectMessage(self, message_id, include_entities=True, return_json=False): 3054 """Destroys the direct message specified in the required ID parameter. 3055 3056 The twitter.Api instance must be authenticated, and the 3057 authenticating user must be the recipient of the specified direct 3058 message. 3059 3060 Args: 3061 message_id: 3062 The id of the direct message to be destroyed 3063 return_json (bool, optional): 3064 If True JSON data will be returned, instead of twitter.User 3065 3066 Returns: 3067 A twitter.DirectMessage instance representing the message destroyed 3068 """ 3069 url = '%s/direct_messages/destroy.json' % self.base_url 3070 data = { 3071 'id': enf_type('message_id', int, message_id), 3072 'include_entities': enf_type('include_entities', bool, include_entities) 3073 } 3074 3075 resp = self._RequestUrl(url, 'POST', data=data) 3076 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3077 3078 if return_json: 3079 return data 3080 else: 3081 return DirectMessage.NewFromJsonDict(data) 3082 3083 def CreateFriendship(self, user_id=None, screen_name=None, follow=True, retweets=True, **kwargs): 3084 """Befriends the user specified by the user_id or screen_name. 3085 3086 Args: 3087 user_id (int, optional): 3088 A user_id to follow 3089 screen_name (str, optional) 3090 A screen_name to follow 3091 follow (bool, optional): 3092 Set to False to disable notifications for the target user 3093 retweets (bool, optional): 3094 Enable or disable retweets from the target user. 3095 3096 Returns: 3097 A twitter.User instance representing the befriended user. 3098 """ 3099 return self._AddOrEditFriendship(user_id=user_id, 3100 screen_name=screen_name, 3101 follow=follow, 3102 retweets=retweets, 3103 **kwargs) 3104 3105 def _AddOrEditFriendship(self, 3106 user_id=None, 3107 screen_name=None, 3108 uri_end='create', 3109 follow_key='follow', 3110 follow=True, 3111 **kwargs): 3112 """Shared method for Create/Update Friendship.""" 3113 url = '%s/friendships/%s.json' % (self.base_url, uri_end) 3114 data = {} 3115 if user_id: 3116 data['user_id'] = user_id 3117 elif screen_name: 3118 data['screen_name'] = screen_name 3119 else: 3120 raise TwitterError("Specify at least one of user_id or screen_name.") 3121 3122 follow_json = json.dumps(follow) 3123 data['{}'.format(follow_key)] = follow_json 3124 data.update(**kwargs) 3125 3126 resp = self._RequestUrl(url, 'POST', data=data) 3127 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3128 3129 return User.NewFromJsonDict(data) 3130 3131 def UpdateFriendship(self, 3132 user_id=None, 3133 screen_name=None, 3134 follow=True, 3135 retweets=True, 3136 **kwargs): 3137 """Updates a friendship with the user specified by the user_id or screen_name. 3138 3139 Args: 3140 user_id (int, optional): 3141 A user_id to update 3142 screen_name (str, optional): 3143 A screen_name to update 3144 follow (bool, optional): 3145 Set to False to disable notifications for the target user 3146 retweets (bool, optional): 3147 Enable or disable retweets from the target user. 3148 device: 3149 Set to False to disable notifications for the target user 3150 3151 Returns: 3152 A twitter.User instance representing the befriended user. 3153 """ 3154 return self._AddOrEditFriendship(user_id=user_id, 3155 screen_name=screen_name, 3156 follow=follow, 3157 follow_key='device', 3158 retweets=retweets, 3159 uri_end='update', 3160 **kwargs) 3161 3162 def DestroyFriendship(self, user_id=None, screen_name=None): 3163 """Discontinues friendship with a user_id or screen_name. 3164 3165 Args: 3166 user_id: 3167 A user_id to unfollow [Optional] 3168 screen_name: 3169 A screen_name to unfollow [Optional] 3170 3171 Returns: 3172 A twitter.User instance representing the discontinued friend. 3173 """ 3174 url = '%s/friendships/destroy.json' % self.base_url 3175 data = {} 3176 if user_id: 3177 data['user_id'] = user_id 3178 elif screen_name: 3179 data['screen_name'] = screen_name 3180 else: 3181 raise TwitterError("Specify at least one of user_id or screen_name.") 3182 3183 resp = self._RequestUrl(url, 'POST', data=data) 3184 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3185 3186 return User.NewFromJsonDict(data) 3187 3188 def ShowFriendship(self, 3189 source_user_id=None, 3190 source_screen_name=None, 3191 target_user_id=None, 3192 target_screen_name=None): 3193 """Returns information about the relationship between the two users. 3194 3195 Args: 3196 source_id: 3197 The user_id of the subject user [Optional] 3198 source_screen_name: 3199 The screen_name of the subject user [Optional] 3200 target_id: 3201 The user_id of the target user [Optional] 3202 target_screen_name: 3203 The screen_name of the target user [Optional] 3204 3205 Returns: 3206 A Twitter Json structure. 3207 """ 3208 url = '%s/friendships/show.json' % self.base_url 3209 data = {} 3210 if source_user_id: 3211 data['source_id'] = source_user_id 3212 elif source_screen_name: 3213 data['source_screen_name'] = source_screen_name 3214 else: 3215 raise TwitterError({'message': "Specify at least one of source_user_id or source_screen_name."}) 3216 if target_user_id: 3217 data['target_id'] = target_user_id 3218 elif target_screen_name: 3219 data['target_screen_name'] = target_screen_name 3220 else: 3221 raise TwitterError({'message': "Specify at least one of target_user_id or target_screen_name."}) 3222 3223 resp = self._RequestUrl(url, 'GET', data=data) 3224 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3225 3226 return data 3227 3228 def LookupFriendship(self, 3229 user_id=None, 3230 screen_name=None, 3231 return_json=False): 3232 """Lookup friendship status for user to authed user. 3233 3234 Users may be specified either as lists of either user_ids, 3235 screen_names, or twitter.User objects. The list of users that 3236 are queried is the union of all specified parameters. 3237 3238 Up to 100 users may be specified. 3239 3240 Args: 3241 user_id (int, User, or list of ints or Users, optional): 3242 A list of user_ids to retrieve extended information. 3243 screen_name (string, User, or list of strings or Users, optional): 3244 A list of screen_names to retrieve extended information. 3245 return_json (bool, optional): 3246 If True JSON data will be returned, instead of twitter.User 3247 3248 Returns: 3249 list: A list of twitter.UserStatus instance representing the 3250 friendship status between the specified users and the authenticated 3251 user. 3252 """ 3253 url = '%s/friendships/lookup.json' % (self.base_url) 3254 parameters = {} 3255 3256 if user_id: 3257 if isinstance(user_id, (list, tuple)): 3258 uids = list() 3259 for user in user_id: 3260 if isinstance(user, User): 3261 uids.append(user.id) 3262 else: 3263 uids.append(enf_type('user_id', int, user)) 3264 parameters['user_id'] = ",".join([str(uid) for uid in uids]) 3265 else: 3266 if isinstance(user_id, User): 3267 parameters['user_id'] = user_id.id 3268 else: 3269 parameters['user_id'] = enf_type('user_id', int, user_id) 3270 if screen_name: 3271 if isinstance(screen_name, (list, tuple)): 3272 sn_list = list() 3273 for user in screen_name: 3274 if isinstance(user, User): 3275 sn_list.append(user.screen_name) 3276 else: 3277 sn_list.append(enf_type('screen_name', str, user)) 3278 parameters['screen_name'] = ','.join(sn_list) 3279 else: 3280 if isinstance(screen_name, User): 3281 parameters['screen_name'] = screen_name.screen_name 3282 else: 3283 parameters['screen_name'] = enf_type('screen_name', str, screen_name) 3284 if not user_id and not screen_name: 3285 raise TwitterError("Specify at least one of user_id or screen_name.") 3286 3287 resp = self._RequestUrl(url, 'GET', data=parameters) 3288 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3289 3290 if return_json: 3291 return data 3292 else: 3293 return [UserStatus.NewFromJsonDict(x) for x in data] 3294 3295 def IncomingFriendship(self, 3296 cursor=None, 3297 stringify_ids=None): 3298 """Returns a collection of user IDs belonging to users who have 3299 pending request to follow the authenticated user. 3300 3301 Args: 3302 cursor: 3303 breaks the ids into pages of no more than 5000. 3304 stringify_ids: 3305 returns the IDs as unicode strings. [Optional] 3306 3307 Returns: 3308 A list of user IDs 3309 """ 3310 url = '%s/friendships/incoming.json' % (self.base_url) 3311 parameters = {} 3312 if stringify_ids: 3313 parameters['stringify_ids'] = 'true' 3314 result = [] 3315 3316 total_count = 0 3317 while True: 3318 if cursor: 3319 try: 3320 parameters['count'] = int(cursor) 3321 except ValueError: 3322 raise TwitterError({'message': "cursor must be an integer"}) 3323 resp = self._RequestUrl(url, 'GET', data=parameters) 3324 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3325 result += [x for x in data['ids']] 3326 if 'next_cursor' in data: 3327 if data['next_cursor'] == 0 or data['next_cursor'] == data['previous_cursor']: 3328 break 3329 else: 3330 cursor = data['next_cursor'] 3331 total_count -= len(data['ids']) 3332 if total_count < 1: 3333 break 3334 else: 3335 break 3336 3337 return result 3338 3339 def OutgoingFriendship(self, 3340 cursor=None, 3341 stringify_ids=None): 3342 """Returns a collection of user IDs for every protected user 3343 for whom the authenticated user has a pending follow request. 3344 3345 Args: 3346 cursor: 3347 breaks the ids into pages of no more than 5000. 3348 stringify_ids: 3349 returns the IDs as unicode strings. [Optional] 3350 3351 Returns: 3352 A list of user IDs 3353 """ 3354 url = '%s/friendships/outgoing.json' % (self.base_url) 3355 parameters = {} 3356 if stringify_ids: 3357 parameters['stringify_ids'] = 'true' 3358 result = [] 3359 3360 total_count = 0 3361 while True: 3362 if cursor: 3363 try: 3364 parameters['count'] = int(cursor) 3365 except ValueError: 3366 raise TwitterError({'message': "cursor must be an integer"}) 3367 resp = self._RequestUrl(url, 'GET', data=parameters) 3368 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3369 result += [x for x in data['ids']] 3370 if 'next_cursor' in data: 3371 if data['next_cursor'] == 0 or data['next_cursor'] == data['previous_cursor']: 3372 break 3373 else: 3374 cursor = data['next_cursor'] 3375 total_count -= len(data['ids']) 3376 if total_count < 1: 3377 break 3378 else: 3379 break 3380 3381 return result 3382 3383 def CreateFavorite(self, 3384 status=None, 3385 status_id=None, 3386 include_entities=True): 3387 """Favorites the specified status object or id as the authenticating user. 3388 3389 Returns the favorite status when successful. 3390 3391 Args: 3392 status_id (int, optional): 3393 The id of the twitter status to mark as a favorite. 3394 status (twitter.Status, optional): 3395 The twitter.Status object to mark as a favorite. 3396 include_entities (bool, optional): 3397 The entities node will be omitted when set to False. 3398 3399 Returns: 3400 A twitter.Status instance representing the newly-marked favorite. 3401 """ 3402 url = '%s/favorites/create.json' % self.base_url 3403 data = {} 3404 if status_id: 3405 data['id'] = status_id 3406 elif status: 3407 data['id'] = status.id 3408 else: 3409 raise TwitterError({'message': "Specify status_id or status"}) 3410 data['include_entities'] = enf_type('include_entities', bool, include_entities) 3411 3412 resp = self._RequestUrl(url, 'POST', data=data) 3413 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3414 3415 return Status.NewFromJsonDict(data) 3416 3417 def DestroyFavorite(self, 3418 status=None, 3419 status_id=None, 3420 include_entities=True): 3421 """Un-Favorites the specified status object or id as the authenticating user. 3422 3423 Returns the un-favorited status when successful. 3424 3425 Args: 3426 status_id (int, optional): 3427 The id of the twitter status to mark as a favorite. 3428 status (twitter.Status, optional): 3429 The twitter.Status object to mark as a favorite. 3430 include_entities (bool, optional): 3431 The entities node will be omitted when set to False. 3432 3433 Returns: 3434 A twitter.Status instance representing the newly-unmarked favorite. 3435 """ 3436 url = '%s/favorites/destroy.json' % self.base_url 3437 data = {} 3438 3439 if status_id: 3440 data['id'] = status_id 3441 elif status: 3442 data['id'] = status.id 3443 else: 3444 raise TwitterError({'message': "Specify status_id or status"}) 3445 data['include_entities'] = enf_type('include_entities', bool, include_entities) 3446 3447 resp = self._RequestUrl(url, 'POST', data=data) 3448 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3449 3450 return Status.NewFromJsonDict(data) 3451 3452 def GetFavorites(self, 3453 user_id=None, 3454 screen_name=None, 3455 count=None, 3456 since_id=None, 3457 max_id=None, 3458 include_entities=True, 3459 return_json=False): 3460 """Return a list of Status objects representing favorited tweets. 3461 3462 Returns up to 200 most recent tweets for the authenticated user. 3463 3464 Args: 3465 user_id (int, optional): 3466 Specifies the ID of the user for whom to return the 3467 favorites. Helpful for disambiguating when a valid user ID 3468 is also a valid screen name. 3469 screen_name (str, optional): 3470 Specifies the screen name of the user for whom to return the 3471 favorites. Helpful for disambiguating when a valid screen 3472 name is also a user ID. 3473 since_id (int, optional): 3474 Returns results with an ID greater than (that is, more recent 3475 than) the specified ID. There are limits to the number of 3476 Tweets which can be accessed through the API. If the limit of 3477 Tweets has occurred since the since_id, the since_id will be 3478 forced to the oldest ID available. 3479 max_id (int, optional): 3480 Returns only statuses with an ID less than (that is, older 3481 than) or equal to the specified ID. 3482 count (int, optional): 3483 Specifies the number of statuses to retrieve. May not be 3484 greater than 200. 3485 include_entities (bool, optional): 3486 The entities node will be omitted when set to False. 3487 return_json (bool, optional): 3488 If True JSON data will be returned, instead of twitter.User 3489 3490 Returns: 3491 A sequence of Status instances, one for each favorited tweet up to count 3492 """ 3493 parameters = {} 3494 url = '%s/favorites/list.json' % self.base_url 3495 if user_id: 3496 parameters['user_id'] = enf_type('user_id', int, user_id) 3497 elif screen_name: 3498 parameters['screen_name'] = screen_name 3499 if since_id: 3500 parameters['since_id'] = enf_type('since_id', int, since_id) 3501 if max_id: 3502 parameters['max_id'] = enf_type('max_id', int, max_id) 3503 if count: 3504 parameters['count'] = enf_type('count', int, count) 3505 parameters['include_entities'] = enf_type('include_entities', bool, include_entities) 3506 3507 resp = self._RequestUrl(url, 'GET', data=parameters) 3508 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3509 3510 if return_json: 3511 return data 3512 else: 3513 return [Status.NewFromJsonDict(x) for x in data] 3514 3515 def GetMentions(self, 3516 count=None, 3517 since_id=None, 3518 max_id=None, 3519 trim_user=False, 3520 contributor_details=False, 3521 include_entities=True, 3522 return_json=False): 3523 """Returns the 20 most recent mentions (status containing @screen_name) 3524 for the authenticating user. 3525 3526 Args: 3527 count: 3528 Specifies the number of tweets to try and retrieve, up to a maximum of 3529 200. The value of count is best thought of as a limit to the number of 3530 tweets to return because suspended or deleted content is removed after 3531 the count has been applied. [Optional] 3532 since_id: 3533 Returns results with an ID greater than (that is, more recent 3534 than) the specified ID. There are limits to the number of 3535 Tweets which can be accessed through the API. If the limit of 3536 Tweets has occurred since the since_id, the since_id will be 3537 forced to the oldest ID available. [Optional] 3538 max_id: 3539 Returns only statuses with an ID less than 3540 (that is, older than) the specified ID. [Optional] 3541 trim_user: 3542 When set to True, each tweet returned in a timeline will include a user 3543 object including only the status authors numerical ID. Omit this 3544 parameter to receive the complete user object. [Optional] 3545 contributor_details: 3546 If set to True, this parameter enhances the contributors element of the 3547 status response to include the screen_name of the contributor. By 3548 default only the user_id of the contributor is included. [Optional] 3549 include_entities: 3550 The entities node will be disincluded when set to False. [Optional] 3551 return_json (bool, optional): 3552 If True JSON data will be returned, instead of twitter.User 3553 3554 Returns: 3555 A sequence of twitter.Status instances, one for each mention of the user. 3556 """ 3557 url = '%s/statuses/mentions_timeline.json' % self.base_url 3558 3559 parameters = { 3560 'contributor_details': bool(contributor_details), 3561 'include_entities': bool(include_entities), 3562 'max_id': max_id, 3563 'since_id': since_id, 3564 'trim_user': bool(trim_user), 3565 } 3566 3567 if count: 3568 parameters['count'] = enf_type('count', int, count) 3569 3570 resp = self._RequestUrl(url, 'GET', data=parameters) 3571 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3572 3573 if return_json: 3574 return data 3575 else: 3576 return [Status.NewFromJsonDict(x) for x in data] 3577 3578 @staticmethod 3579 def _IDList(list_id, slug, owner_id, owner_screen_name): 3580 parameters = {} 3581 if list_id is not None: 3582 parameters['list_id'] = enf_type('list_id', int, list_id) 3583 elif slug is not None: 3584 parameters['slug'] = slug 3585 if owner_id is not None: 3586 parameters['owner_id'] = enf_type('owner_id', int, owner_id) 3587 elif owner_screen_name is not None: 3588 parameters['owner_screen_name'] = owner_screen_name 3589 else: 3590 raise TwitterError({'message': ( 3591 'If specifying a list by slug, an owner_id or ' 3592 'owner_screen_name must also be given.')}) 3593 else: 3594 raise TwitterError({'message': ( 3595 'Either list_id or slug and one of owner_id and ' 3596 'owner_screen_name must be passed.')}) 3597 3598 return parameters 3599 3600 def CreateList(self, name, mode=None, description=None): 3601 """Creates a new list with the give name for the authenticated user. 3602 3603 Args: 3604 name (str): 3605 New name for the list 3606 mode (str, optional): 3607 'public' or 'private'. Defaults to 'public'. 3608 description (str, optional): 3609 Description of the list. 3610 3611 Returns: 3612 twitter.list.List: A twitter.List instance representing the new list 3613 """ 3614 url = '%s/lists/create.json' % self.base_url 3615 parameters = {'name': name} 3616 if mode is not None: 3617 parameters['mode'] = mode 3618 if description is not None: 3619 parameters['description'] = description 3620 3621 resp = self._RequestUrl(url, 'POST', data=parameters) 3622 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3623 3624 return List.NewFromJsonDict(data) 3625 3626 def DestroyList(self, 3627 owner_screen_name=None, 3628 owner_id=None, 3629 list_id=None, 3630 slug=None): 3631 """Destroys the list identified by list_id or slug and one of 3632 owner_screen_name or owner_id. 3633 3634 Args: 3635 owner_screen_name (str, optional): 3636 The screen_name of the user who owns the list being requested 3637 by a slug. 3638 owner_id (int, optional): 3639 The user ID of the user who owns the list being requested 3640 by a slug. 3641 list_id (int, optional): 3642 The numerical id of the list. 3643 slug (str, optional): 3644 You can identify a list by its slug instead of its numerical id. 3645 If you decide to do so, note that you'll also have to specify 3646 the list owner using the owner_id or owner_screen_name parameters. 3647 3648 Returns: 3649 twitter.list.List: A twitter.List instance representing the 3650 removed list. 3651 """ 3652 url = '%s/lists/destroy.json' % self.base_url 3653 parameters = {} 3654 3655 parameters.update(self._IDList(list_id=list_id, 3656 slug=slug, 3657 owner_id=owner_id, 3658 owner_screen_name=owner_screen_name)) 3659 3660 resp = self._RequestUrl(url, 'POST', data=parameters) 3661 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3662 3663 return List.NewFromJsonDict(data) 3664 3665 def CreateSubscription(self, 3666 owner_screen_name=None, 3667 owner_id=None, 3668 list_id=None, 3669 slug=None): 3670 """Creates a subscription to a list by the authenticated user. 3671 3672 Args: 3673 owner_screen_name (str, optional): 3674 The screen_name of the user who owns the list being requested 3675 by a slug. 3676 owner_id (int, optional): 3677 The user ID of the user who owns the list being requested 3678 by a slug. 3679 list_id (int, optional): 3680 The numerical id of the list. 3681 slug (str, optional): 3682 You can identify a list by its slug instead of its numerical id. 3683 If you decide to do so, note that you'll also have to specify 3684 the list owner using the owner_id or owner_screen_name parameters. 3685 3686 Returns: 3687 twitter.user.User: A twitter.User instance representing the user subscribed 3688 """ 3689 url = '%s/lists/subscribers/create.json' % self.base_url 3690 parameters = {} 3691 3692 parameters.update(self._IDList(list_id=list_id, 3693 slug=slug, 3694 owner_id=owner_id, 3695 owner_screen_name=owner_screen_name)) 3696 3697 resp = self._RequestUrl(url, 'POST', data=parameters) 3698 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3699 3700 return User.NewFromJsonDict(data) 3701 3702 def DestroySubscription(self, 3703 owner_screen_name=None, 3704 owner_id=None, 3705 list_id=None, 3706 slug=None): 3707 """Destroys the subscription to a list for the authenticated user. 3708 3709 Args: 3710 owner_screen_name (str, optional): 3711 The screen_name of the user who owns the list being requested 3712 by a slug. 3713 owner_id (int, optional): 3714 The user ID of the user who owns the list being requested 3715 by a slug. 3716 list_id (int, optional): 3717 The numerical id of the list. 3718 slug (str, optional): 3719 You can identify a list by its slug instead of its numerical id. 3720 If you decide to do so, note that you'll also have to specify the 3721 list owner using the owner_id or owner_screen_name parameters. 3722 3723 Returns: 3724 twitter.list.List: A twitter.List instance representing 3725 the removed list. 3726 """ 3727 url = '%s/lists/subscribers/destroy.json' % (self.base_url) 3728 parameters = {} 3729 3730 parameters.update(self._IDList(list_id=list_id, 3731 slug=slug, 3732 owner_id=owner_id, 3733 owner_screen_name=owner_screen_name)) 3734 3735 resp = self._RequestUrl(url, 'POST', data=parameters) 3736 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3737 3738 return List.NewFromJsonDict(data) 3739 3740 def ShowSubscription(self, 3741 owner_screen_name=None, 3742 owner_id=None, 3743 list_id=None, 3744 slug=None, 3745 user_id=None, 3746 screen_name=None, 3747 include_entities=False, 3748 skip_status=False, 3749 return_json=False): 3750 """Check if the specified user is a subscriber of the specified list. 3751 3752 Returns the user if they are subscriber. 3753 3754 Args: 3755 owner_screen_name (str, optional): 3756 The screen_name of the user who owns the list being requested 3757 by a slug. 3758 owner_id (int, optional): 3759 The user ID of the user who owns the list being requested 3760 by a slug. 3761 list_id (int, optional): 3762 The numerical ID of the list. 3763 slug (str, optional): 3764 You can identify a list by its slug instead of its numerical ID. 3765 If you decide to do so, note that you'll also have to specify 3766 the list owner using the owner_id or owner_screen_name parameters. 3767 user_id (int, optional): 3768 The user_id or a list of user_id's to add to the list. 3769 If not given, then screen_name is required. 3770 screen_name (str, optional): 3771 The screen_name or a list of screen_name's to add to the list. 3772 If not given, then user_id is required. 3773 include_entities (bool, optional): 3774 If False, the timeline will not contain additional metadata. 3775 Defaults to True. 3776 skip_status (bool, optional): 3777 If True the statuses will not be returned in the user items. 3778 return_json (bool, optional): 3779 If True JSON data will be returned, instead of twitter.User 3780 3781 Returns: 3782 twitter.user.User: A twitter.User instance representing the user 3783 requested. 3784 """ 3785 url = '%s/lists/subscribers/show.json' % (self.base_url) 3786 parameters = {} 3787 3788 parameters.update(self._IDList(list_id=list_id, 3789 slug=slug, 3790 owner_id=owner_id, 3791 owner_screen_name=owner_screen_name)) 3792 3793 if user_id: 3794 parameters['user_id'] = enf_type('user_id', int, user_id) 3795 elif screen_name: 3796 parameters['screen_name'] = screen_name 3797 if skip_status: 3798 parameters['skip_status'] = True 3799 if include_entities: 3800 parameters['include_entities'] = True 3801 3802 resp = self._RequestUrl(url, 'GET', data=parameters) 3803 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3804 3805 if return_json: 3806 return data 3807 else: 3808 return User.NewFromJsonDict(data) 3809 3810 def GetSubscriptions(self, 3811 user_id=None, 3812 screen_name=None, 3813 count=20, 3814 cursor=-1, 3815 return_json=False): 3816 """Obtain a collection of the lists the specified user is 3817 subscribed to. If neither user_id or screen_name is specified, the 3818 data returned will be for the authenticated user. 3819 3820 The list will contain a maximum of 20 lists per page by default. 3821 3822 Does not include the user's own lists. 3823 3824 Args: 3825 user_id (int, optional): 3826 The ID of the user for whom to return results for. 3827 screen_name (str, optional): 3828 The screen name of the user for whom to return results for. 3829 count (int, optional): 3830 The amount of results to return per page. 3831 No more than 1000 results will ever be returned in a single 3832 page. Defaults to 20. 3833 cursor (int, optional): 3834 The "page" value that Twitter will use to start building the 3835 list sequence from. Use the value of -1 to start at the 3836 beginning. Twitter will return in the result the values for 3837 next_cursor and previous_cursor. 3838 return_json (bool, optional): 3839 If True JSON data will be returned, instead of twitter.User 3840 3841 Returns: 3842 twitter.list.List: A sequence of twitter.List instances, 3843 one for each list 3844 """ 3845 url = '%s/lists/subscriptions.json' % (self.base_url) 3846 parameters = {} 3847 parameters['cursor'] = enf_type('cursor', int, cursor) 3848 parameters['count'] = enf_type('count', int, count) 3849 if user_id is not None: 3850 parameters['user_id'] = enf_type('user_id', int, user_id) 3851 elif screen_name is not None: 3852 parameters['screen_name'] = screen_name 3853 3854 resp = self._RequestUrl(url, 'GET', data=parameters) 3855 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3856 3857 if return_json: 3858 return data 3859 else: 3860 return [List.NewFromJsonDict(x) for x in data['lists']] 3861 3862 def GetMemberships(self, 3863 user_id=None, 3864 screen_name=None, 3865 count=20, 3866 cursor=-1, 3867 filter_to_owned_lists=False, 3868 return_json=False): 3869 """Obtain the lists the specified user is a member of. If no user_id or 3870 screen_name is specified, the data returned will be for the 3871 authenticated user. 3872 3873 Returns a maximum of 20 lists per page by default. 3874 3875 Args: 3876 user_id (int, optional): 3877 The ID of the user for whom to return results for. 3878 screen_name (str, optional): 3879 The screen name of the user for whom to return 3880 results for. 3881 count (int, optional): 3882 The amount of results to return per page. 3883 No more than 1000 results will ever be returned in a single page. 3884 Defaults to 20. 3885 cursor (int, optional): 3886 The "page" value that Twitter will use to start building the list 3887 sequence from. Use the value of -1 to start at the beginning. 3888 Twitter will return in the result the values for next_cursor and 3889 previous_cursor. 3890 filter_to_owned_lists (bool, optional): 3891 Set to True to return only the lists the authenticating user 3892 owns, and the user specified by user_id or screen_name is a 3893 member of. Default value is False. 3894 return_json (bool, optional): 3895 If True JSON data will be returned, instead of twitter.User 3896 3897 Returns: 3898 list: A list of twitter.List instances, one for each list in which 3899 the user specified by user_id or screen_name is a member 3900 """ 3901 url = '%s/lists/memberships.json' % (self.base_url) 3902 parameters = {} 3903 if cursor is not None: 3904 parameters['cursor'] = enf_type('cursor', int, cursor) 3905 if count is not None: 3906 parameters['count'] = enf_type('count', int, count) 3907 if filter_to_owned_lists: 3908 parameters['filter_to_owned_lists'] = enf_type( 3909 'filter_to_owned_lists', bool, filter_to_owned_lists) 3910 3911 if user_id is not None: 3912 parameters['user_id'] = enf_type('user_id', int, user_id) 3913 elif screen_name is not None: 3914 parameters['screen_name'] = screen_name 3915 3916 resp = self._RequestUrl(url, 'GET', data=parameters) 3917 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3918 3919 if return_json: 3920 return data 3921 else: 3922 return [List.NewFromJsonDict(x) for x in data['lists']] 3923 3924 def GetListsList(self, 3925 screen_name=None, 3926 user_id=None, 3927 reverse=False, 3928 return_json=False): 3929 """Returns all lists the user subscribes to, including their own. 3930 If no user_id or screen_name is specified, the data returned will be 3931 for the authenticated user. 3932 3933 Args: 3934 screen_name (str, optional): 3935 Specifies the screen name of the user for whom to return the 3936 user_timeline. Helpful for disambiguating when a valid screen 3937 name is also a user ID. 3938 user_id (int, optional): 3939 Specifies the ID of the user for whom to return the 3940 user_timeline. Helpful for disambiguating when a valid user ID 3941 is also a valid screen name. 3942 reverse (bool, optional): 3943 If False, the owned lists will be returned first, othewise 3944 subscribed lists will be at the top. Returns a maximum of 100 3945 entries regardless. Defaults to False. 3946 return_json (bool, optional): 3947 If True JSON data will be returned, instead of twitter.User 3948 3949 Returns: 3950 list: A sequence of twitter.List instances. 3951 """ 3952 url = '%s/lists/list.json' % (self.base_url) 3953 parameters = {} 3954 if user_id: 3955 parameters['user_id'] = enf_type('user_id', int, user_id) 3956 elif screen_name: 3957 parameters['screen_name'] = screen_name 3958 if reverse: 3959 parameters['reverse'] = enf_type('reverse', bool, reverse) 3960 3961 resp = self._RequestUrl(url, 'GET', data=parameters) 3962 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 3963 3964 if return_json: 3965 return data 3966 else: 3967 return [List.NewFromJsonDict(x) for x in data] 3968 3969 def GetListTimeline(self, 3970 list_id=None, 3971 slug=None, 3972 owner_id=None, 3973 owner_screen_name=None, 3974 since_id=None, 3975 max_id=None, 3976 count=None, 3977 include_rts=True, 3978 include_entities=True, 3979 return_json=False): 3980 """Fetch the sequence of Status messages for a given List ID. 3981 3982 Args: 3983 list_id (int, optional): 3984 Specifies the ID of the list to retrieve. 3985 slug (str, optional): 3986 The slug name for the list to retrieve. If you specify None for the 3987 list_id, then you have to provide either a owner_screen_name or 3988 owner_id. 3989 owner_id (int, optional): 3990 Specifies the ID of the user for whom to return the 3991 list timeline. Helpful for disambiguating when a valid user ID 3992 is also a valid screen name. 3993 owner_screen_name (str, optional): 3994 Specifies the screen name of the user for whom to return the 3995 user_timeline. Helpful for disambiguating when a valid screen 3996 name is also a user ID. 3997 since_id (int, optional): 3998 Returns results with an ID greater than (that is, more recent than) 3999 the specified ID. There are limits to the number of Tweets which 4000 can be accessed through the API. 4001 If the limit of Tweets has occurred since the since_id, the 4002 since_id will be forced to the oldest ID available. 4003 max_id (int, optional): 4004 Returns only statuses with an ID less than (that is, older than) or 4005 equal to the specified ID. 4006 count (int, optional): 4007 Specifies the number of statuses to retrieve. 4008 May not be greater than 200. 4009 include_rts (bool, optional): 4010 If True, the timeline will contain native retweets (if they exist) 4011 in addition to the standard stream of tweets. 4012 include_entities (bool, optional): 4013 If False, the timeline will not contain additional metadata. 4014 Defaults to True. 4015 return_json (bool, optional): 4016 If True JSON data will be returned, instead of twitter.User 4017 4018 Returns: 4019 list: A list of twitter.status.Status instances, one for each 4020 message up to count. 4021 """ 4022 url = '%s/lists/statuses.json' % self.base_url 4023 parameters = {} 4024 4025 parameters.update(self._IDList(list_id=list_id, 4026 slug=slug, 4027 owner_id=owner_id, 4028 owner_screen_name=owner_screen_name)) 4029 4030 if since_id: 4031 parameters['since_id'] = enf_type('since_id', int, since_id) 4032 if max_id: 4033 parameters['max_id'] = enf_type('max_id', int, max_id) 4034 if count: 4035 parameters['count'] = enf_type('count', int, count) 4036 if not include_rts: 4037 parameters['include_rts'] = enf_type('include_rts', bool, include_rts) 4038 if not include_entities: 4039 parameters['include_entities'] = enf_type('include_entities', bool, include_entities) 4040 4041 resp = self._RequestUrl(url, 'GET', data=parameters) 4042 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 4043 4044 if return_json: 4045 return data 4046 else: 4047 return [Status.NewFromJsonDict(x) for x in data] 4048 4049 def GetListMembersPaged(self, 4050 list_id=None, 4051 slug=None, 4052 owner_id=None, 4053 owner_screen_name=None, 4054 cursor=-1, 4055 count=100, 4056 skip_status=False, 4057 include_entities=True): 4058 """Fetch the sequence of twitter.User instances, one for each member 4059 of the given list_id or slug. 4060 4061 Args: 4062 list_id (int, optional): 4063 Specifies the ID of the list to retrieve. 4064 slug (str, optional): 4065 The slug name for the list to retrieve. If you specify None for the 4066 list_id, then you have to provide either a owner_screen_name or 4067 owner_id. 4068 owner_id (int, optional): 4069 Specifies the ID of the user for whom to return the 4070 list timeline. Helpful for disambiguating when a valid user ID 4071 is also a valid screen name. 4072 owner_screen_name (str, optional): 4073 Specifies the screen name of the user for whom to return the 4074 user_timeline. Helpful for disambiguating when a valid screen 4075 name is also a user ID. 4076 cursor (int, optional): 4077 Should be set to -1 for the initial call and then is used to 4078 control what result page Twitter returns. 4079 skip_status (bool, optional): 4080 If True the statuses will not be returned in the user items. 4081 include_entities (bool, optional): 4082 If False, the timeline will not contain additional metadata. 4083 Defaults to True. 4084 4085 Returns: 4086 list: A sequence of twitter.user.User instances, one for each 4087 member of the twitter.list.List. 4088 """ 4089 url = '%s/lists/members.json' % self.base_url 4090 parameters = {} 4091 4092 parameters.update(self._IDList(list_id=list_id, 4093 slug=slug, 4094 owner_id=owner_id, 4095 owner_screen_name=owner_screen_name)) 4096 4097 if count: 4098 parameters['count'] = enf_type('count', int, count) 4099 if cursor: 4100 parameters['cursor'] = enf_type('cursor', int, cursor) 4101 4102 parameters['skip_status'] = enf_type('skip_status', bool, skip_status) 4103 parameters['include_entities'] = enf_type('include_entities', bool, include_entities) 4104 4105 resp = self._RequestUrl(url, 'GET', data=parameters) 4106 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 4107 next_cursor = data.get('next_cursor', 0) 4108 previous_cursor = data.get('previous_cursor', 0) 4109 users = [User.NewFromJsonDict(user) for user in data.get('users', [])] 4110 4111 return next_cursor, previous_cursor, users 4112 4113 def GetListMembers(self, 4114 list_id=None, 4115 slug=None, 4116 owner_id=None, 4117 owner_screen_name=None, 4118 skip_status=False, 4119 include_entities=False): 4120 """Fetch the sequence of twitter.User instances, one for each member 4121 of the given list_id or slug. 4122 4123 Args: 4124 list_id (int, optional): 4125 Specifies the ID of the list to retrieve. 4126 slug (str, optional): 4127 The slug name for the list to retrieve. If you specify None for the 4128 list_id, then you have to provide either a owner_screen_name or 4129 owner_id. 4130 owner_id (int, optional): 4131 Specifies the ID of the user for whom to return the 4132 list timeline. Helpful for disambiguating when a valid user ID 4133 is also a valid screen name. 4134 owner_screen_name (str, optional): 4135 Specifies the screen name of the user for whom to return the 4136 user_timeline. Helpful for disambiguating when a valid screen 4137 name is also a user ID. 4138 skip_status (bool, optional): 4139 If True the statuses will not be returned in the user items. 4140 include_entities (bool, optional): 4141 If False, the timeline will not contain additional metadata. 4142 Defaults to True. 4143 4144 Returns: 4145 list: A sequence of twitter.user.User instances, one for each 4146 member of the twitter.list.List. 4147 """ 4148 cursor = -1 4149 result = [] 4150 while True: 4151 next_cursor, previous_cursor, users = self.GetListMembersPaged( 4152 list_id=list_id, 4153 slug=slug, 4154 owner_id=owner_id, 4155 owner_screen_name=owner_screen_name, 4156 cursor=cursor, 4157 skip_status=skip_status, 4158 include_entities=include_entities) 4159 result += users 4160 4161 if next_cursor == 0 or next_cursor == previous_cursor: 4162 break 4163 else: 4164 cursor = next_cursor 4165 4166 return result 4167 4168 def CreateListsMember(self, 4169 list_id=None, 4170 slug=None, 4171 user_id=None, 4172 screen_name=None, 4173 owner_screen_name=None, 4174 owner_id=None): 4175 """Add a new member (or list of members) to the specified list. 4176 4177 Args: 4178 list_id (int, optional): 4179 The numerical id of the list. 4180 slug (str, optional): 4181 You can identify a list by its slug instead of its numerical id. 4182 If you decide to do so, note that you'll also have to specify the 4183 list owner using the owner_id or owner_screen_name parameters. 4184 user_id (int, optional): 4185 The user_id or a list of user_id's to add to the list. 4186 If not given, then screen_name is required. 4187 screen_name (str, optional): 4188 The screen_name or a list of screen_name's to add to the list. 4189 If not given, then user_id is required. 4190 owner_screen_name (str, optional): 4191 The screen_name of the user who owns the list being requested by 4192 a slug. 4193 owner_id (int, optional): 4194 The user ID of the user who owns the list being requested by 4195 a slug. 4196 4197 Returns: 4198 twitter.list.List: A twitter.List instance representing the list 4199 subscribed to. 4200 """ 4201 is_list = False 4202 parameters = {} 4203 4204 parameters.update(self._IDList(list_id=list_id, 4205 slug=slug, 4206 owner_id=owner_id, 4207 owner_screen_name=owner_screen_name)) 4208 4209 if user_id: 4210 if isinstance(user_id, list) or isinstance(user_id, tuple): 4211 is_list = True 4212 uids = [str(enf_type('user_id', int, uid)) for uid in user_id] 4213 parameters['user_id'] = ','.join(uids) 4214 else: 4215 parameters['user_id'] = enf_type('user_id', int, user_id) 4216 4217 elif screen_name: 4218 if isinstance(screen_name, list) or isinstance(screen_name, tuple): 4219 is_list = True 4220 parameters['screen_name'] = ','.join(screen_name) 4221 else: 4222 parameters['screen_name'] = screen_name 4223 if is_list: 4224 url = '%s/lists/members/create_all.json' % self.base_url 4225 else: 4226 url = '%s/lists/members/create.json' % self.base_url 4227 4228 resp = self._RequestUrl(url, 'POST', data=parameters) 4229 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 4230 4231 return List.NewFromJsonDict(data) 4232 4233 def DestroyListsMember(self, 4234 list_id=None, 4235 slug=None, 4236 owner_screen_name=None, 4237 owner_id=None, 4238 user_id=None, 4239 screen_name=None): 4240 """Destroys the subscription to a list for the authenticated user. 4241 4242 Args: 4243 list_id (int, optional): 4244 The numerical id of the list. 4245 slug (str, optional): 4246 You can identify a list by its slug instead of its numerical id. 4247 If you decide to do so, note that you'll also have to specify 4248 the list owner using the owner_id or owner_screen_name parameters. 4249 owner_screen_name (str, optional): 4250 The screen_name of the user who owns the list being requested by a 4251 slug. 4252 owner_id (int, optional): 4253 The user ID of the user who owns the list being requested by a slug. 4254 user_id (int, optional): 4255 The user_id or a list of user_id's to remove from the list. 4256 If not given, then screen_name is required. 4257 screen_name (str, optional): 4258 The screen_name or a list of Screen_name's to remove from the list. 4259 If not given, then user_id is required. 4260 4261 Returns: 4262 twitter.list.List: A twitter.List instance representing the 4263 removed list. 4264 """ 4265 is_list = False 4266 parameters = {} 4267 4268 parameters.update(self._IDList(list_id=list_id, 4269 slug=slug, 4270 owner_id=owner_id, 4271 owner_screen_name=owner_screen_name)) 4272 4273 if user_id: 4274 if isinstance(user_id, list) or isinstance(user_id, tuple): 4275 is_list = True 4276 uids = [str(enf_type('user_id', int, uid)) for uid in user_id] 4277 parameters['user_id'] = ','.join(uids) 4278 else: 4279 parameters['user_id'] = int(user_id) 4280 elif screen_name: 4281 if isinstance(screen_name, list) or isinstance(screen_name, tuple): 4282 is_list = True 4283 parameters['screen_name'] = ','.join(screen_name) 4284 else: 4285 parameters['screen_name'] = screen_name 4286 4287 if is_list: 4288 url = '%s/lists/members/destroy_all.json' % self.base_url 4289 else: 4290 url = '%s/lists/members/destroy.json' % self.base_url 4291 4292 resp = self._RequestUrl(url, 'POST', data=parameters) 4293 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 4294 4295 return List.NewFromJsonDict(data) 4296 4297 def GetListsPaged(self, 4298 user_id=None, 4299 screen_name=None, 4300 cursor=-1, 4301 count=20): 4302 """ Fetch the sequence of lists for a user. If no user_id or 4303 screen_name is passed, the data returned will be for the 4304 authenticated user. 4305 4306 Args: 4307 user_id (int, optional): 4308 The ID of the user for whom to return results for. 4309 screen_name (str, optional): 4310 The screen name of the user for whom to return results 4311 for. 4312 count (int, optional): 4313 The amount of results to return per page. No more than 1000 results 4314 will ever be returned in a single page. Defaults to 20. 4315 cursor (int, optional): 4316 The "page" value that Twitter will use to start building the list 4317 sequence from. Use the value of -1 to start at the beginning. 4318 Twitter will return in the result the values for next_cursor and 4319 previous_cursor. 4320 4321 Returns: 4322 next_cursor (int), previous_cursor (int), list of twitter.List 4323 instances, one for each list 4324 """ 4325 url = '%s/lists/ownerships.json' % self.base_url 4326 parameters = {} 4327 if user_id is not None: 4328 parameters['user_id'] = enf_type('user_id', int, user_id) 4329 elif screen_name is not None: 4330 parameters['screen_name'] = screen_name 4331 4332 if count is not None: 4333 parameters['count'] = enf_type('count', int, count) 4334 4335 parameters['cursor'] = enf_type('cursor', int, cursor) 4336 4337 resp = self._RequestUrl(url, 'GET', data=parameters) 4338 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 4339 4340 next_cursor = data.get('next_cursor', 0) 4341 previous_cursor = data.get('previous_cursor', 0) 4342 lists = [List.NewFromJsonDict(x) for x in data.get('lists', [])] 4343 4344 return next_cursor, previous_cursor, lists 4345 4346 def GetLists(self, 4347 user_id=None, 4348 screen_name=None): 4349 """Fetch the sequence of lists for a user. If no user_id or screen_name 4350 is passed, the data returned will be for the authenticated user. 4351 4352 Args: 4353 user_id: 4354 The ID of the user for whom to return results for. [Optional] 4355 screen_name: 4356 The screen name of the user for whom to return results 4357 for. [Optional] 4358 count: 4359 The amount of results to return per page. 4360 No more than 1000 results will ever be returned in a single page. 4361 Defaults to 20. [Optional] 4362 cursor: 4363 The "page" value that Twitter will use to start building the list 4364 sequence from. Use the value of -1 to start at the beginning. 4365 Twitter will return in the result the values for next_cursor and 4366 previous_cursor. [Optional] 4367 4368 Returns: 4369 A sequence of twitter.List instances, one for each list 4370 """ 4371 result = [] 4372 cursor = -1 4373 4374 while True: 4375 next_cursor, prev_cursor, lists = self.GetListsPaged( 4376 user_id=user_id, 4377 screen_name=screen_name, 4378 cursor=cursor) 4379 result += lists 4380 if next_cursor == 0 or next_cursor == prev_cursor: 4381 break 4382 else: 4383 cursor = next_cursor 4384 4385 return result 4386 4387 def UpdateProfile(self, 4388 name=None, 4389 profileURL=None, 4390 location=None, 4391 description=None, 4392 profile_link_color=None, 4393 include_entities=False, 4394 skip_status=False): 4395 """Update's the authenticated user's profile data. 4396 4397 Args: 4398 name (str, optional): 4399 Full name associated with the profile. 4400 profileURL (str, optional): 4401 URL associated with the profile. 4402 Will be prepended with "http://" if not present. 4403 location (str, optional): 4404 The city or country describing where the user of the account is located. 4405 The contents are not normalized or geocoded in any way. 4406 description (str, optional): 4407 A description of the user owning the account. 4408 profile_link_color (str, optional): 4409 hex value of profile color theme. formated without '#' or '0x'. Ex: FF00FF 4410 include_entities (bool, optional): 4411 The entities node will be omitted when set to False. 4412 skip_status (bool, optional): 4413 When set to either True, t or 1 then statuses will not be included 4414 in the returned user objects. 4415 4416 Returns: 4417 A twitter.User instance representing the modified user. 4418 """ 4419 url = '%s/account/update_profile.json' % (self.base_url) 4420 data = { 4421 'name': name, 4422 'url': profileURL, 4423 'location': location, 4424 'description': description, 4425 'profile_link_color': profile_link_color, 4426 'include_entities': include_entities, 4427 'skip_status': skip_status, 4428 } 4429 4430 resp = self._RequestUrl(url, 'POST', data=data) 4431 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 4432 4433 return User.NewFromJsonDict(data) 4434 4435 def UpdateImage(self, 4436 image, 4437 include_entities=False, 4438 skip_status=False): 4439 """Update a User's profile image. Change may not be immediately 4440 reflected due to image processing on Twitter's side. 4441 4442 Args: 4443 image (str): 4444 Location of local image file to use. 4445 include_entities (bool, optional): 4446 Include the entities node in the return data. 4447 skip_status (bool, optional): 4448 Include the User's last Status in the User entity returned. 4449 4450 Returns: 4451 (twitter.models.User): Updated User object. 4452 """ 4453 4454 url = '%s/account/update_profile_image.json' % (self.base_url) 4455 with open(image, 'rb') as image_file: 4456 encoded_image = base64.b64encode(image_file.read()) 4457 data = { 4458 'image': encoded_image 4459 } 4460 if include_entities: 4461 data['include_entities'] = 1 4462 if skip_status: 4463 data['skip_status'] = 1 4464 4465 resp = self._RequestUrl(url, 'POST', data=data) 4466 4467 if resp.status_code in [200, 201, 202]: 4468 return True 4469 if resp.status_code == 400: 4470 raise TwitterError({'message': "Image data could not be processed"}) 4471 if resp.status_code == 422: 4472 raise TwitterError({'message': "The image could not be resized or is too large."}) 4473 4474 def UpdateBanner(self, 4475 image, 4476 include_entities=False, 4477 skip_status=False): 4478 """Updates the authenticated users profile banner. 4479 4480 Args: 4481 image: 4482 Location of image in file system 4483 include_entities: 4484 If True, each tweet will include a node called "entities." 4485 This node offers a variety of metadata about the tweet in a 4486 discrete structure, including: user_mentions, urls, and hashtags. 4487 [Optional] 4488 4489 Returns: 4490 A twitter.List instance representing the list subscribed to 4491 """ 4492 url = '%s/account/update_profile_banner.json' % (self.base_url) 4493 with open(image, 'rb') as image_file: 4494 encoded_image = base64.b64encode(image_file.read()) 4495 data = { 4496 # When updated for API v1.1 use image, not banner 4497 # https://dev.twitter.com/docs/api/1.1/post/account/update_profile_banner 4498 # 'image': encoded_image 4499 'banner': encoded_image 4500 } 4501 if include_entities: 4502 data['include_entities'] = 1 4503 if skip_status: 4504 data['skip_status'] = 1 4505 4506 resp = self._RequestUrl(url, 'POST', data=data) 4507 4508 if resp.status_code in [200, 201, 202]: 4509 return True 4510 if resp.status_code == 400: 4511 raise TwitterError({'message': "Image data could not be processed"}) 4512 if resp.status_code == 422: 4513 raise TwitterError({'message': "The image could not be resized or is too large."}) 4514 4515 raise TwitterError({'message': "Unkown banner image upload issue"}) 4516 4517 def GetStreamSample(self, delimited=False, stall_warnings=True): 4518 """Returns a small sample of public statuses. 4519 4520 Args: 4521 delimited: 4522 Specifies a message length. [Optional] 4523 stall_warnings: 4524 Set to True to have Twitter deliver stall warnings. [Optional] 4525 4526 Returns: 4527 A Twitter stream 4528 """ 4529 url = '%s/statuses/sample.json' % self.stream_url 4530 parameters = { 4531 'delimited': bool(delimited), 4532 'stall_warnings': bool(stall_warnings) 4533 } 4534 resp = self._RequestStream(url, 'GET', data=parameters) 4535 for line in resp.iter_lines(): 4536 if line: 4537 data = self._ParseAndCheckTwitter(line.decode('utf-8')) 4538 yield data 4539 4540 def GetStreamFilter(self, 4541 follow=None, 4542 track=None, 4543 locations=None, 4544 languages=None, 4545 delimited=None, 4546 stall_warnings=None, 4547 filter_level=None): 4548 """Returns a filtered view of public statuses. 4549 4550 Args: 4551 follow: 4552 A list of user IDs to track. [Optional] 4553 track: 4554 A list of expressions to track. [Optional] 4555 locations: 4556 A list of Longitude,Latitude pairs (as strings) specifying 4557 bounding boxes for the tweets' origin. [Optional] 4558 delimited: 4559 Specifies a message length. [Optional] 4560 stall_warnings: 4561 Set to True to have Twitter deliver stall warnings. [Optional] 4562 languages: 4563 A list of Languages. 4564 Will only return Tweets that have been detected as being 4565 written in the specified languages. [Optional] 4566 filter_level: 4567 Specifies level of filtering applied to stream. 4568 Set to None, 'low' or 'medium'. [Optional] 4569 4570 Returns: 4571 A twitter stream 4572 """ 4573 if all((follow is None, track is None, locations is None)): 4574 raise ValueError({'message': "No filter parameters specified."}) 4575 url = '%s/statuses/filter.json' % self.stream_url 4576 data = {} 4577 if follow is not None: 4578 data['follow'] = ','.join(follow) 4579 if track is not None: 4580 data['track'] = ','.join(track) 4581 if locations is not None: 4582 data['locations'] = ','.join(locations) 4583 if delimited is not None: 4584 data['delimited'] = str(delimited) 4585 if stall_warnings is not None: 4586 data['stall_warnings'] = str(stall_warnings) 4587 if languages is not None: 4588 data['language'] = ','.join(languages) 4589 if filter_level is not None: 4590 data['filter_level'] = filter_level 4591 4592 resp = self._RequestStream(url, 'POST', data=data) 4593 for line in resp.iter_lines(): 4594 if line: 4595 data = self._ParseAndCheckTwitter(line.decode('utf-8')) 4596 yield data 4597 4598 def GetUserStream(self, 4599 replies='all', 4600 withuser='user', 4601 track=None, 4602 locations=None, 4603 delimited=None, 4604 stall_warnings=None, 4605 stringify_friend_ids=False, 4606 filter_level=None, 4607 session=None, 4608 include_keepalive=False): 4609 """Returns the data from the user stream. 4610 4611 Args: 4612 replies: 4613 Specifies whether to return additional @replies in the stream. 4614 Defaults to 'all'. 4615 withuser: 4616 Specifies whether to return information for just the authenticating 4617 user, or include messages from accounts the user follows. [Optional] 4618 track: 4619 A list of expressions to track. [Optional] 4620 locations: 4621 A list of Latitude,Longitude pairs (as strings) specifying 4622 bounding boxes for the tweets' origin. [Optional] 4623 delimited: 4624 Specifies a message length. [Optional] 4625 stall_warnings: 4626 Set to True to have Twitter deliver stall warnings. [Optional] 4627 stringify_friend_ids: 4628 Specifies whether to send the friends list preamble as an array of 4629 integers or an array of strings. [Optional] 4630 filter_level: 4631 Specifies level of filtering applied to stream. 4632 Set to None, low or medium. [Optional] 4633 4634 Returns: 4635 A twitter stream 4636 """ 4637 url = 'https://userstream.twitter.com/1.1/user.json' 4638 data = {} 4639 if stringify_friend_ids: 4640 data['stringify_friend_ids'] = 'true' 4641 if replies is not None: 4642 data['replies'] = replies 4643 if withuser is not None: 4644 data['with'] = withuser 4645 if track is not None: 4646 data['track'] = ','.join(track) 4647 if locations is not None: 4648 data['locations'] = ','.join(locations) 4649 if delimited is not None: 4650 data['delimited'] = str(delimited) 4651 if stall_warnings is not None: 4652 data['stall_warnings'] = str(stall_warnings) 4653 if filter_level is not None: 4654 data['filter_level'] = filter_level 4655 4656 resp = self._RequestStream(url, 'POST', data=data, session=session) 4657 # The Twitter streaming API sends keep-alive newlines every 30s if there has not been other 4658 # traffic, and specifies that streams should only be reset after three keep-alive ticks. 4659 # 4660 # The original implementation of this API didn't expose keep-alive signals to the user, 4661 # making it difficult to determine whether the connection should be hung up or not. 4662 # 4663 # https://dev.twitter.com/streaming/overview/connecting 4664 for line in resp.iter_lines(): 4665 if line: 4666 data = self._ParseAndCheckTwitter(line.decode('utf-8')) 4667 yield data 4668 elif include_keepalive: 4669 yield None 4670 4671 def VerifyCredentials(self, include_entities=None, skip_status=None, include_email=None): 4672 """Returns a twitter.User instance if the authenticating user is valid. 4673 4674 Args: 4675 include_entities: 4676 Specifies whether to return additional @replies in the stream. 4677 skip_status: 4678 When set to either true, t or 1 statuses will not be included in the 4679 returned user object. 4680 include_email: 4681 Use of this parameter requires whitelisting. 4682 When set to true email will be returned in the user objects as a string. 4683 If the user does not have an email address on their account, or if the 4684 email address is un-verified, null will be returned. If your app is 4685 not whitelisted, then the 'email' key will not be present in the json 4686 response. 4687 4688 Returns: 4689 A twitter.User instance representing that user if the 4690 credentials are valid, None otherwise. 4691 """ 4692 url = '%s/account/verify_credentials.json' % self.base_url 4693 data = { 4694 'include_entities': enf_type('include_entities', bool, include_entities), 4695 'skip_status': enf_type('skip_status', bool, skip_status), 4696 'include_email': 'true' if enf_type('include_email', bool, include_email) else 'false', 4697 } 4698 4699 resp = self._RequestUrl(url, 'GET', data) 4700 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 4701 4702 return User.NewFromJsonDict(data) 4703 4704 def SetCache(self, cache): 4705 """Override the default cache. Set to None to prevent caching. 4706 4707 Args: 4708 cache: 4709 An instance that supports the same API as the twitter._FileCache 4710 """ 4711 if cache == DEFAULT_CACHE: 4712 self._cache = _FileCache() 4713 else: 4714 self._cache = cache 4715 4716 def SetUrllib(self, urllib): 4717 """Override the default urllib implementation. 4718 4719 Args: 4720 urllib: 4721 An instance that supports the same API as the urllib2 module 4722 """ 4723 self._urllib = urllib 4724 4725 def SetCacheTimeout(self, cache_timeout): 4726 """Override the default cache timeout. 4727 4728 Args: 4729 cache_timeout: 4730 Time, in seconds, that responses should be reused. 4731 """ 4732 self._cache_timeout = cache_timeout 4733 4734 def SetUserAgent(self, user_agent): 4735 """Override the default user agent. 4736 4737 Args: 4738 user_agent: 4739 A string that should be send to the server as the user-agent. 4740 """ 4741 self._request_headers['User-Agent'] = user_agent 4742 4743 def SetXTwitterHeaders(self, client, url, version): 4744 """Set the X-Twitter HTTP headers that will be sent to the server. 4745 4746 Args: 4747 client: 4748 The client name as a string. Will be sent to the server as 4749 the 'X-Twitter-Client' header. 4750 url: 4751 The URL of the meta.xml as a string. Will be sent to the server 4752 as the 'X-Twitter-Client-URL' header. 4753 version: 4754 The client version as a string. Will be sent to the server 4755 as the 'X-Twitter-Client-Version' header. 4756 """ 4757 self._request_headers['X-Twitter-Client'] = client 4758 self._request_headers['X-Twitter-Client-URL'] = url 4759 self._request_headers['X-Twitter-Client-Version'] = version 4760 4761 def SetSource(self, source): 4762 """Suggest the "from source" value to be displayed on the Twitter web site. 4763 4764 The value of the 'source' parameter must be first recognized by 4765 the Twitter server. 4766 4767 New source values are authorized on a case by case basis by the 4768 Twitter development team. 4769 4770 Args: 4771 source: 4772 The source name as a string. Will be sent to the server as 4773 the 'source' parameter. 4774 """ 4775 self._default_params['source'] = source 4776 4777 def InitializeRateLimit(self): 4778 """ Make a call to the Twitter API to get the rate limit 4779 status for the currently authenticated user or application. 4780 4781 Returns: 4782 None. 4783 4784 """ 4785 _sleep = self.sleep_on_rate_limit 4786 if self.sleep_on_rate_limit: 4787 self.sleep_on_rate_limit = False 4788 4789 url = '%s/application/rate_limit_status.json' % self.base_url 4790 4791 resp = self._RequestUrl(url, 'GET') # No-Cache 4792 data = self._ParseAndCheckTwitter(resp.content.decode('utf-8')) 4793 4794 self.sleep_on_rate_limit = _sleep 4795 self.rate_limit = RateLimit(**data) 4796 4797 def CheckRateLimit(self, url): 4798 """ Checks a URL to see the rate limit status for that endpoint. 4799 4800 Args: 4801 url (str): 4802 URL to check against the current rate limits. 4803 4804 Returns: 4805 namedtuple: EndpointRateLimit namedtuple. 4806 4807 """ 4808 if not self.rate_limit.__dict__.get('resources', None): 4809 self.InitializeRateLimit() 4810 4811 if url: 4812 limit = self.rate_limit.get_limit(url) 4813 4814 return limit 4815 4816 def _BuildUrl(self, url, path_elements=None, extra_params=None): 4817 # Break url into constituent parts 4818 (scheme, netloc, path, params, query, fragment) = urlparse(url) 4819 4820 # Add any additional path elements to the path 4821 if path_elements: 4822 # Filter out the path elements that have a value of None 4823 filtered_elements = [i for i in path_elements if i] 4824 if not path.endswith('/'): 4825 path += '/' 4826 path += '/'.join(filtered_elements) 4827 4828 # Add any additional query parameters to the query string 4829 if extra_params and len(extra_params) > 0: 4830 extra_query = self._EncodeParameters(extra_params) 4831 # Add it to the existing query 4832 if query: 4833 query += '&' + extra_query 4834 else: 4835 query = extra_query 4836 4837 # Return the rebuilt URL 4838 return urlunparse((scheme, netloc, path, params, query, fragment)) 4839 4840 def _InitializeRequestHeaders(self, request_headers): 4841 if request_headers: 4842 self._request_headers = request_headers 4843 else: 4844 self._request_headers = {} 4845 4846 def _InitializeUserAgent(self): 4847 user_agent = 'Python-urllib/%s (python-twitter/%s)' % \ 4848 (urllib_version, __version__) 4849 self.SetUserAgent(user_agent) 4850 4851 def _InitializeDefaultParameters(self): 4852 self._default_params = {} 4853 4854 @staticmethod 4855 def _DecompressGzippedResponse(response): 4856 raw_data = response.read() 4857 if response.headers.get('content-encoding', None) == 'gzip': 4858 url_data = gzip.GzipFile(fileobj=io.StringIO(raw_data)).read() 4859 else: 4860 url_data = raw_data 4861 return url_data 4862 4863 @staticmethod 4864 def _EncodeParameters(parameters): 4865 """Return a string in key=value&key=value form. 4866 4867 Values of None are not included in the output string. 4868 4869 Args: 4870 parameters (dict): dictionary of query parameters to be converted into a 4871 string for encoding and sending to Twitter. 4872 4873 Returns: 4874 A URL-encoded string in "key=value&key=value" form 4875 """ 4876 if parameters is None: 4877 return None 4878 if not isinstance(parameters, dict): 4879 raise TwitterError("`parameters` must be a dict.") 4880 else: 4881 params = dict() 4882 for k, v in parameters.items(): 4883 if v is not None: 4884 if getattr(v, 'encode', None): 4885 v = v.encode('utf8') 4886 params.update({k: v}) 4887 return urlencode(params) 4888 4889 def _ParseAndCheckTwitter(self, json_data): 4890 """Try and parse the JSON returned from Twitter and return 4891 an empty dictionary if there is any error. 4892 4893 This is a purely defensive check because during some Twitter 4894 network outages it will return an HTML failwhale page. 4895 """ 4896 try: 4897 data = json.loads(json_data) 4898 except ValueError: 4899 if "<title>Twitter / Over capacity</title>" in json_data: 4900 raise TwitterError({'message': "Capacity Error"}) 4901 if "<title>Twitter / Error</title>" in json_data: 4902 raise TwitterError({'message': "Technical Error"}) 4903 if "Exceeded connection limit for user" in json_data: 4904 raise TwitterError({'message': "Exceeded connection limit for user"}) 4905 if "Error 401 Unauthorized" in json_data: 4906 raise TwitterError({'message': "Unauthorized"}) 4907 raise TwitterError({'Unknown error': '{0}'.format(json_data)}) 4908 self._CheckForTwitterError(data) 4909 return data 4910 4911 @staticmethod 4912 def _CheckForTwitterError(data): 4913 """Raises a TwitterError if twitter returns an error message. 4914 4915 Args: 4916 data (dict): 4917 A python dict created from the Twitter json response 4918 4919 Raises: 4920 (twitter.TwitterError): TwitterError wrapping the twitter error 4921 message if one exists. 4922 """ 4923 # Twitter errors are relatively unlikely, so it is faster 4924 # to check first, rather than try and catch the exception 4925 if 'error' in data: 4926 raise TwitterError(data['error']) 4927 if 'errors' in data: 4928 raise TwitterError(data['errors']) 4929 4930 def _RequestChunkedUpload(self, url, headers, data): 4931 try: 4932 return requests.post( 4933 url, 4934 headers=headers, 4935 data=data, 4936 auth=self.__auth, 4937 timeout=self._timeout, 4938 proxies=self.proxies 4939 ) 4940 except requests.RequestException as e: 4941 raise TwitterError(str(e)) 4942 4943 def _RequestUrl(self, url, verb, data=None, json=None, enforce_auth=True): 4944 """Request a url. 4945 4946 Args: 4947 url: 4948 The web location we want to retrieve. 4949 verb: 4950 Either POST or GET. 4951 data: 4952 A dict of (str, unicode) key/value pairs. 4953 4954 Returns: 4955 A JSON object. 4956 """ 4957 if enforce_auth: 4958 if not self.__auth: 4959 raise TwitterError("The twitter.Api instance must be authenticated.") 4960 4961 if url and self.sleep_on_rate_limit: 4962 limit = self.CheckRateLimit(url) 4963 4964 if limit.remaining == 0: 4965 try: 4966 stime = max(int(limit.reset - time.time()) + 10, 0) 4967 logger.debug('Rate limited requesting [%s], sleeping for [%s]', url, stime) 4968 time.sleep(stime) 4969 except ValueError: 4970 pass 4971 4972 if not data: 4973 data = {} 4974 4975 if verb == 'POST': 4976 if data: 4977 if 'media_ids' in data: 4978 url = self._BuildUrl(url, extra_params={'media_ids': data['media_ids']}) 4979 resp = self._session.post(url, data=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) 4980 elif 'media' in data: 4981 resp = self._session.post(url, files=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) 4982 else: 4983 resp = self._session.post(url, data=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) 4984 elif json: 4985 resp = self._session.post(url, json=json, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) 4986 else: 4987 resp = 0 # POST request, but without data or json 4988 4989 elif verb == 'GET': 4990 data['tweet_mode'] = self.tweet_mode 4991 url = self._BuildUrl(url, extra_params=data) 4992 resp = self._session.get(url, auth=self.__auth, timeout=self._timeout, proxies=self.proxies) 4993 4994 else: 4995 resp = 0 # if not a POST or GET request 4996 4997 if url and self.rate_limit: 4998 limit = resp.headers.get('x-rate-limit-limit', 0) 4999 remaining = resp.headers.get('x-rate-limit-remaining', 0) 5000 reset = resp.headers.get('x-rate-limit-reset', 0) 5001 5002 self.rate_limit.set_limit(url, limit, remaining, reset) 5003 5004 return resp 5005 5006 def _RequestStream(self, url, verb, data=None, session=None): 5007 """Request a stream of data. 5008 5009 Args: 5010 url: 5011 The web location we want to retrieve. 5012 verb: 5013 Either POST or GET. 5014 data: 5015 A dict of (str, unicode) key/value pairs. 5016 5017 Returns: 5018 A twitter stream. 5019 """ 5020 session = session or requests.Session() 5021 5022 if verb == 'POST': 5023 try: 5024 return session.post(url, data=data, stream=True, 5025 auth=self.__auth, 5026 timeout=self._timeout, 5027 proxies=self.proxies) 5028 except requests.RequestException as e: 5029 raise TwitterError(str(e)) 5030 if verb == 'GET': 5031 url = self._BuildUrl(url, extra_params=data) 5032 try: 5033 return session.get(url, stream=True, auth=self.__auth, 5034 timeout=self._timeout, proxies=self.proxies) 5035 except requests.RequestException as e: 5036 raise TwitterError(str(e)) 5037 return 0 # if not a POST or GET request 5038