def get_closest_media( self, tweet, log_index: Optional[str] = None ) -> Optional[Tuple[TweetCache, TweetCache, List[str]]]: """ Attempt to get the closest media element associated with this tweet and handle any errors if they occur Args: tweet: tweepy.models.Status log_index (Optional[str]): Index to use for system logs. Defaults to SYSTEM Returns: Optional[List] """ log_index = log_index or 'SYSTEM' try: original_cache, media_cache, media = self.twitter.get_closest_media( tweet) except tweepy.error.TweepError as error: # Error 136 means we are blocked if error.api_code == 136: # noinspection PyBroadException try: api.update_status( f"@{tweet.author.screen_name} Sorry, it looks like the author of this post has blocked us. For more information, please refer to:\nhttps://github.com/FujiMakoto/twitter-saucenao/#blocked-by", in_reply_to_status_id=tweet.id, auto_populate_reply_metadata=True) except Exception as error: self.log.exception( f"[{log_index}] An exception occurred while trying to inform a user that an account has blocked us" ) raise TwSauceNoMediaException # We attempted to process a tweet from a user that has restricted access to their account elif error.api_code in [179, 385]: self.log.info( f"[{log_index}] Skipping a tweet we don't have permission to view" ) raise TwSauceNoMediaException # Someone got impatient and deleted a tweet before we could get too it elif error.api_code == 144: self.log.info( f"[{log_index}] Skipping a tweet that no longer exists") raise TwSauceNoMediaException # Something unfamiliar happened, log an error for later review else: self.log.error( f"[{log_index}] Skipping due to unknown Twitter error: {error.api_code} - {error.reason}" ) raise TwSauceNoMediaException # Still here? Yay! We have something then. return original_cache, media_cache, media
def _retry(_lines): _lines = self._shorten_reply(_lines) try: _msg = ''.join(map(str, _lines)) return api.update_status(_msg, **kwargs) except tweepy.TweepError as error: if error.api_code != 186: raise error return False
def _post(self, msg: str, to: Optional[int], media_ids: Optional[List[int]] = None, sensitive: bool = False): """ Perform a twitter API status update Args: msg (str): Message to send to (Optional[int]): Status ID we are replying to media_ids (Optional[List[int]]): List of media ID's sensitive (bool): Whether or not this tweet contains NSFW media Returns: """ kwargs = {'possibly_sensitive': sensitive} if to: kwargs['in_reply_to_status_id'] = to kwargs['auto_populate_reply_metadata'] = True if media_ids: kwargs['media_ids'] = media_ids try: return api.update_status(msg, **kwargs) except tweepy.error.TweepError as error: if error.api_code == 136: self.log.warning( "A user requested our presence, then blocked us before we could respond. Wow." ) # We attempted to process a tweet from a user that has restricted access to their account elif error.api_code in [179, 385]: self.log.info( f"Attempted to reply to a deleted tweet or a tweet we don't have permission to view" ) raise TwSauceNoMediaException # Someone got impatient and deleted a tweet before we could get too it elif error.api_code == 144: self.log.info(f"Not replying to a tweet that no longer exists") raise TwSauceNoMediaException # Video was too short. Can happen if we're using natural previews. Repost without the video clip elif error.api_code == 324: self.log.info( f"Video preview for was too short to upload to Twitter") return self._post(msg=msg, to=to, sensitive=sensitive) # Something unfamiliar happened, log an error for later review else: self.log.error( f"Unable to post due to an unknown Twitter error: {error.api_code} - {error.reason}" )
def _post(self, msg: typing.Union[str, typing.List[ReplyLine]], to: typing.Optional[int], media_ids: typing.Optional[typing.List[int]] = None, sensitive: bool = False): """ Perform a twitter API status update Args: msg (Union[str, List[ReplyLine]]): Message to send to (typing.Optional[int]): Status ID we are replying to media_ids (typing.Optional[List[int]]): List of media ID's sensitive (bool): Whether or not this tweet contains NSFW media Returns: """ kwargs = {'possibly_sensitive': sensitive} if to: kwargs['in_reply_to_status_id'] = to kwargs['auto_populate_reply_metadata'] = True if media_ids: kwargs['media_ids'] = media_ids lines = msg if isinstance(msg, list) else None if lines: msg = ''.join(map(str, lines)) try: return api.update_status(msg, **kwargs) except tweepy.error.TweepError as error: if error.api_code == 136: self.log.warning( "A user requested our presence, then blocked us before we could respond. Wow." ) # We attempted to process a tweet from a user that has restricted access to their account elif error.api_code in [179, 385]: self.log.info( f"Attempted to reply to a deleted tweet or a tweet we don't have permission to view" ) raise TwSauceNoMediaException # Someone got impatient and deleted a tweet before we could get too it elif error.api_code == 144: self.log.info(f"Not replying to a tweet that no longer exists") raise TwSauceNoMediaException # Video was too short. Can happen if we're using natural previews. Repost without the video clip elif error.api_code == 324: self.log.info( f"Video preview for was too short to upload to Twitter") return self._post(msg=msg, to=to, sensitive=sensitive) # Something unfamiliar happened, log an error for later review elif error.api_code == 186 and lines: self.log.debug("Post is too long; scrubbing message length") def _retry(_lines): _lines = self._shorten_reply(_lines) try: _msg = ''.join(map(str, _lines)) return api.update_status(_msg, **kwargs) except tweepy.TweepError as error: if error.api_code != 186: raise error return False # Shorten the post as much as we can until it fits while True: try: success = _retry(lines) except IndexError: self.log.warning( f"Failed to shorten response message to tweet {to} enough" ) break if not success: self.log.debug( f"Tweet to {to} still not short enough; running another pass" ) continue self.log.debug(f"Tweet for {to} shortened successfully") break else: self.log.error( f"Unable to post due to an unknown Twitter error: {error.api_code} - {error.reason}" )
def send_reply(self, tweet_cache: TweetCache, media_cache: TweetCache, sauce_cache: TweetSauceCache, tracemoe_sauce: Optional[dict] = None, requested: bool = True, blocked: bool = False) -> None: """ Return the source of the image Args: tweet_cache (TweetCache): The tweet to reply to media_cache (TweetCache): The tweet containing media elements sauce_cache (Optional[GenericSource]): The sauce found (or None if nothing was found) tracemoe_sauce (Optional[dict]): Tracemoe sauce query, if enabled requested (bool): True if the lookup was requested, or False if this is a monitored user account blocked (bool): If True, the account posting this has blocked the SauceBot Returns: None """ tweet = tweet_cache.tweet sauce = sauce_cache.sauce if sauce is None: if requested: media = TweetManager.extract_media(media_cache.tweet) if not media: return yandex_url = f"https://yandex.com/images/search?url={media[sauce_cache.index_no]}&rpt=imageview" tinyeye_url = f"https://www.tineye.com/search?url={media[sauce_cache.index_no]}" google_url = f"https://www.google.com/searchbyimage?image_url={media[sauce_cache.index_no]}&safe=off" api.update_status( f"@{tweet.author.screen_name} Sorry, I couldn't find anything (●´ω`●)ゞ\nYour image may be cropped too much, or the artist may simply not exist in any of SauceNao's databases.\n\nTry checking one of these search engines!\n{yandex_url}\n{google_url}\n{tinyeye_url}", in_reply_to_status_id=tweet.id) return # For limiting the length of the title/author repr = reprlib.Repr() repr.maxstring = 32 # Add additional sauce URL's from trace.moe if available sauce_urls = [] if tracemoe_sauce: if self._anime_link in ['anilist', 'animal', 'all']: sauce_urls.append( f"https://anilist.co/anime/{tracemoe_sauce['anilist_id']}/" ) if self._anime_link in ['myanimelist', 'animal', 'all' ] and tracemoe_sauce.get('mal_id'): sauce_urls.append( f"https://myanimelist.net/anime/{tracemoe_sauce['mal_id']}/" ) if self._anime_link in ['anidb', 'all']: sauce_urls.append(sauce.url) # H-Misc doesn't have a source to link to, so we need to try and provide the full title if sauce.index not in ['H-Misc', 'E-Hentai']: title = repr.repr(sauce.title).strip("'") else: repr.maxstring = 128 title = repr.repr(sauce.title).strip("'") # Format the similarity string similarity = f'𝗔𝗰𝗰𝘂𝗿𝗮𝗰𝘆: {sauce.similarity}% ( ' if sauce.similarity >= 85.0: similarity = similarity + '🔵 High )' elif sauce.similarity >= 70.0: similarity = similarity + '🟡 Medium )' else: similarity = similarity + '🟠 Low )' if requested: reply = f"@{tweet.author.screen_name} I found this in the {sauce.index} database!\n" else: reply = f"Need the sauce? I found it in the {sauce.index} database!\n" # If it's a Pixiv source, try and get their Twitter handle (this is considered most important and displayed first) twitter_sauce = None if isinstance(sauce, PixivSource): twitter_sauce = self.pixiv.get_author_twitter( sauce.data['member_id']) if twitter_sauce: reply += f"\n𝗔𝗿𝘁𝗶𝘀𝘁𝘀 𝗧𝘄𝗶𝘁𝘁𝗲𝗿: {twitter_sauce}" # Print the author name if available if sauce.author_name: author = repr.repr(sauce.author_name).strip("'") reply += f"\n𝗔𝘂𝘁𝗵𝗼𝗿: {author}" # Omit the title for Pixiv results since it's usually always non-romanized Japanese and not very helpful if not isinstance(sauce, PixivSource): reply += f"\n𝗧𝗶𝘁𝗹𝗲: {title}" # Add the episode number and timestamp for video sources if isinstance(sauce, VideoSource): if sauce.episode: reply += f"\n𝗘𝗽𝗶𝘀𝗼𝗱𝗲: {sauce.episode}" if sauce.timestamp: reply += f" ( ⏱️ {sauce.timestamp} )" # Add the chapter for manga sources if isinstance(sauce, MangaSource): if sauce.chapter: reply += f"\n𝗖𝗵𝗮𝗽𝘁𝗲𝗿: {sauce.chapter}" # Display our confidence rating reply += f"\n{similarity}" # Source URL's are not available in some indexes if sauce_urls: reply += "\n\n" reply += "\n".join(sauce_urls) elif sauce.source_url: reply += f"\n\n{sauce.source_url}" # Some Booru posts have bad source links cited, so we should always provide a Booru link with the source URL if isinstance(sauce, BooruSource) and sauce.source_url != sauce.url: reply += f"\n{sauce.url}" # Try and append bot instructions with monitored posts. This might make our post too long, though. if not requested: _reply = reply reply += f"\n\nNeed sauce elsewhere? Just follow and (@)mention me in a reply and I'll be right over!" try: if tracemoe_sauce and (not tracemoe_sauce['is_adult'] or self._nsfw_previews): tw_response = self.twython.upload_video(media=io.BytesIO( tracemoe_sauce['preview']), media_type='video/mp4') comment = api.update_status( reply, in_reply_to_status_id=tweet.id, auto_populate_reply_metadata=True, media_ids=[tw_response['media_id']], possibly_sensitive=tracemoe_sauce['is_adult']) else: comment = api.update_status(reply, in_reply_to_status_id=tweet.id, auto_populate_reply_metadata=True) except tweepy.TweepError as error: if error.api_code == 186 and not requested: self.log.info( "Post is too long; scrubbing bot instructions from message" ) # noinspection PyUnboundLocalVariable comment = api.update_status(_reply, in_reply_to_status_id=tweet.id, auto_populate_reply_metadata=True) else: raise error # If we've been blocked by this user and have the artists Twitter handle, send the artist a DMCA guide if blocked: if twitter_sauce: self.log.warning( f"Sending {twitter_sauce} DMCA takedown advice") api.update_status( f"""{twitter_sauce} This account has stolen your artwork and blocked me for crediting you. このアカウントはあなたの絵を盗んで、私があなたを明記したらブロックされちゃいました https://github.com/FujiMakoto/twitter-saucenao/blob/master/DMCA.md https://help.twitter.com/forms/dmca""", in_reply_to_status_id=comment.id, auto_populate_reply_metadata=True) else: api.update_status( f"This account has blocked {self.my.name}. For more information, please refer to:\n" "https://github.com/FujiMakoto/twitter-saucenao#art-thieves-saucebot-has-been-blocked-by", in_reply_to_status_id=comment.id, auto_populate_reply_metadata=True)