2014-11-12 20:41:29 -06:00
# -*- coding: utf-8 -*-
2018-08-17 17:42:41 -05:00
""" This is the main session needed to access all Twitter Features. """
import os
import time
import logging
2018-08-18 23:09:08 -05:00
import webbrowser
2017-07-30 04:05:32 -05:00
import wx
2022-11-17 16:19:47 -06:00
import tweepy
2022-05-13 13:04:12 -05:00
import demoji
2016-08-06 14:47:42 -05:00
import config
2014-11-12 20:41:29 -06:00
import output
2015-05-02 03:41:28 -04:00
import application
2021-10-27 15:29:15 -05:00
import appkeys
2015-02-01 21:13:18 -06:00
from pubsub import pub
2021-10-07 09:20:06 -05:00
from tweepy . errors import TweepyException , Forbidden , NotFound
2021-05-14 09:52:19 -05:00
from tweepy . models import User as UserModel
2018-08-17 17:42:41 -05:00
from mysc . thread_utils import call_threaded
from keys import keyring
from sessions import base
2018-08-18 23:09:08 -05:00
from sessions . twitter import utils , compose
2018-08-16 17:26:19 -05:00
from sessions . twitter . long_tweets import tweets , twishort
2021-06-28 17:03:26 -05:00
from . import reduce , streaming
2018-11-22 13:35:19 -06:00
from . wxUI import authorisationDialog
2018-08-17 17:42:41 -05:00
log = logging . getLogger ( " sessions.twitterSession " )
2014-11-12 20:41:29 -06:00
2018-08-17 05:12:49 -05:00
class Session ( base . baseSession ) :
2021-06-16 16:18:41 -05:00
""" A session object where we will save configuration, the twitter object and a local storage for saving the items retrieved through the Twitter API methods """
2014-11-12 20:41:29 -06:00
2021-06-16 16:18:41 -05:00
def order_buffer ( self , name , data , ignore_older = True ) :
""" Put new items in the local database.
name str : The name for the buffer stored in the dictionary .
data list : A list with tweets .
ignore_older bool : if set to True , items older than the first element on the list will be ignored .
returns the number of items that have been added in this execution """
if name == " direct_messages " :
return self . order_direct_messages ( data )
num = 0
last_id = None
2022-01-10 05:05:27 -06:00
if self . db . get ( name ) == None :
2021-06-16 16:18:41 -05:00
self . db [ name ] = [ ]
2022-01-10 05:05:27 -06:00
if self . db . get ( " users " ) == None :
2021-06-16 16:18:41 -05:00
self . db [ " users " ] = { }
2021-06-23 13:40:21 -05:00
objects = self . db [ name ]
2021-06-16 16:18:41 -05:00
if ignore_older and len ( self . db [ name ] ) > 0 :
if self . settings [ " general " ] [ " reverse_timelines " ] == False :
last_id = self . db [ name ] [ 0 ] . id
else :
last_id = self . db [ name ] [ - 1 ] . id
2021-06-25 16:25:51 -05:00
self . add_users_from_results ( data )
2021-06-16 16:18:41 -05:00
for i in data :
if ignore_older and last_id != None :
if i . id < last_id :
log . error ( " Ignoring an older tweet... Last id: {0} , tweet id: {1} " . format ( last_id , i . id ) )
continue
2021-07-16 10:22:51 -05:00
if utils . find_item ( i , self . db [ name ] ) == None and utils . is_allowed ( i , self . settings , name ) == True :
2021-06-16 16:18:41 -05:00
if i == False : continue
2021-06-25 16:25:51 -05:00
reduced_object = reduce . reduce_tweet ( i )
reduced_object = self . check_quoted_status ( reduced_object )
reduced_object = self . check_long_tweet ( reduced_object )
if self . settings [ " general " ] [ " reverse_timelines " ] == False : objects . append ( reduced_object )
else : objects . insert ( 0 , reduced_object )
2021-06-16 16:18:41 -05:00
num = num + 1
2021-06-23 13:40:21 -05:00
self . db [ name ] = objects
2021-06-16 16:18:41 -05:00
return num
2014-11-12 20:41:29 -06:00
2021-06-16 16:18:41 -05:00
def order_people ( self , name , data ) :
""" Put new items on the local database. Useful for cursored buffers (followers, friends, users of a list and searches)
name str : The name for the buffer stored in the dictionary .
data list : A list with items and some information about cursors .
returns the number of items that have been added in this execution """
num = 0
if ( name in self . db ) == False :
self . db [ name ] = [ ]
2021-06-23 13:40:21 -05:00
objects = self . db [ name ]
2021-06-16 16:18:41 -05:00
for i in data :
2021-07-16 10:22:51 -05:00
if utils . find_item ( i , self . db [ name ] ) == None :
2021-06-23 13:40:21 -05:00
if self . settings [ " general " ] [ " reverse_timelines " ] == False : objects . append ( i )
else : objects . insert ( 0 , i )
2021-06-16 16:18:41 -05:00
num = num + 1
2021-06-23 13:40:21 -05:00
self . db [ name ] = objects
2021-06-16 16:18:41 -05:00
return num
2014-11-12 20:41:29 -06:00
2021-06-16 16:18:41 -05:00
def order_direct_messages ( self , data ) :
""" Add incoming and sent direct messages to their corresponding database items.
data list : A list of direct messages to add .
returns the number of incoming messages processed in this execution , and sends an event with data regarding amount of sent direct messages added . """
incoming = 0
sent = 0
if ( " direct_messages " in self . db ) == False :
self . db [ " direct_messages " ] = [ ]
2021-06-23 13:40:21 -05:00
if ( " sent_direct_messages " in self . db ) == False :
self . db [ " sent_direct_messages " ] = [ ]
objects = self . db [ " direct_messages " ]
sent_objects = self . db [ " sent_direct_messages " ]
2021-06-16 16:18:41 -05:00
for i in data :
# Twitter returns sender_id as str, which must be converted to int in order to match to our user_id object.
if int ( i . message_create [ " sender_id " ] ) == self . db [ " user_id " ] :
2021-07-16 10:22:51 -05:00
if " sent_direct_messages " in self . db and utils . find_item ( i , self . db [ " sent_direct_messages " ] ) == None :
2021-06-23 13:40:21 -05:00
if self . settings [ " general " ] [ " reverse_timelines " ] == False : sent_objects . append ( i )
else : sent_objects . insert ( 0 , i )
2021-06-16 16:18:41 -05:00
sent = sent + 1
else :
2021-07-16 10:22:51 -05:00
if utils . find_item ( i , self . db [ " direct_messages " ] ) == None :
2021-06-23 13:40:21 -05:00
if self . settings [ " general " ] [ " reverse_timelines " ] == False : objects . append ( i )
else : objects . insert ( 0 , i )
2021-06-16 16:18:41 -05:00
incoming = incoming + 1
2021-06-25 16:49:23 -05:00
self . db [ " direct_messages " ] = objects
self . db [ " sent_direct_messages " ] = sent_objects
2022-11-15 11:54:59 -06:00
pub . sendMessage ( " sent-dms-updated " , total = sent , session_name = self . get_name ( ) )
2021-06-16 16:18:41 -05:00
return incoming
2018-08-13 11:26:55 -05:00
2021-06-16 16:18:41 -05:00
def __init__ ( self , * args , * * kwargs ) :
super ( Session , self ) . __init__ ( * args , * * kwargs )
# Adds here the optional cursors objects.
cursors = dict ( direct_messages = - 1 )
self . db [ " cursors " ] = cursors
self . reconnection_function_active = False
self . counter = 0
self . lists = [ ]
2021-06-27 18:05:35 -05:00
# As users are cached for accessing them with not too many twitter calls,
# there could be a weird situation where a deleted user who sent direct messages to the current account will not be able to be retrieved at twitter.
# So we need to store an "user deleted" object in the cache, but have the ID of the deleted user in a local reference.
# This will be especially useful because if the user reactivates their account later, TWblue will try to retrieve such user again at startup.
# If we wouldn't implement this approach, TWBlue would save permanently the "deleted user" object.
self . deleted_users = { }
2021-08-30 10:51:26 -05:00
self . type = " twitter "
2021-06-28 17:03:26 -05:00
pub . subscribe ( self . handle_new_status , " newStatus " )
2021-06-29 17:16:53 -05:00
pub . subscribe ( self . handle_connected , " streamConnected " )
2014-11-12 20:41:29 -06:00
2018-08-17 05:12:49 -05:00
# @_require_configuration
2021-06-16 16:18:41 -05:00
def login ( self , verify_credentials = True ) :
""" Log into twitter using credentials from settings.
if the user account isn ' t authorised, it needs to call self.authorise() before login. " " "
if self . settings [ " twitter " ] [ " user_key " ] != None and self . settings [ " twitter " ] [ " user_secret " ] != None :
try :
log . debug ( " Logging in to twitter... " )
2022-02-15 15:56:08 -06:00
self . auth = tweepy . OAuth1UserHandler ( consumer_key = appkeys . twitter_api_key , consumer_secret = appkeys . twitter_api_secret , access_token = self . settings [ " twitter " ] [ " user_key " ] , access_token_secret = self . settings [ " twitter " ] [ " user_secret " ] )
2021-06-16 16:18:41 -05:00
self . twitter = tweepy . API ( self . auth )
2021-10-27 15:29:15 -05:00
self . twitter_v2 = tweepy . Client ( consumer_key = appkeys . twitter_api_key , consumer_secret = appkeys . twitter_api_secret , access_token = self . settings [ " twitter " ] [ " user_key " ] , access_token_secret = self . settings [ " twitter " ] [ " user_secret " ] )
2021-06-16 16:18:41 -05:00
if verify_credentials == True :
self . credentials = self . twitter . verify_credentials ( )
self . logged = True
log . debug ( " Logged. " )
self . counter = 0
except IOError :
log . error ( " The login attempt failed. " )
self . logged = False
else :
self . logged = False
raise Exceptions . RequireCredentialsSessionError
2014-11-12 20:41:29 -06:00
2021-06-16 16:18:41 -05:00
def authorise ( self ) :
""" Authorises a Twitter account. This function needs to be called for each new session, after self.get_configuration() and before self.login() """
if self . logged == True :
raise Exceptions . AlreadyAuthorisedError ( " The authorisation process is not needed at this time. " )
2022-11-17 16:19:47 -06:00
auth = tweepy . OAuth1UserHandler ( appkeys . twitter_api_key , appkeys . twitter_api_secret )
redirect_url = auth . get_authorization_url ( )
webbrowser . open_new_tab ( redirect_url )
verification_dialog = wx . TextEntryDialog ( None , _ ( " Enter your PIN code here " ) , _ ( " Authorising account... " ) )
answer = verification_dialog . ShowModal ( )
code = verification_dialog . GetValue ( )
verification_dialog . Destroy ( )
if answer != wx . ID_OK :
return
try :
auth . get_access_token ( code )
except TweepyException :
dlg = wx . MessageDialog ( None , _ ( " We could not authorice your Twitter account to be used in TWBlue. This might be caused due to an incorrect verification code. Please try to add the session again. " ) , _ ( " Authorization error " ) , wx . ICON_ERROR )
dlg . ShowModal ( )
dlg . Destroy ( )
return False
self . create_session_folder ( )
self . get_configuration ( )
self . settings [ " twitter " ] [ " user_key " ] = auth . access_token
self . settings [ " twitter " ] [ " user_secret " ] = auth . access_token_secret
2021-06-16 16:18:41 -05:00
self . settings . write ( )
2022-11-17 16:19:47 -06:00
return True
2014-11-12 20:41:29 -06:00
2021-06-16 16:18:41 -05:00
def api_call ( self , call_name , action = " " , _sound = None , report_success = False , report_failure = True , preexec_message = " " , * args , * * kwargs ) :
""" Make a call to the Twitter API. If there is a connectionError or another exception not related to Twitter, It will call the method again at least 25 times, waiting a while between calls. Useful for post methods.
If twitter returns an error , it will not call the method anymore .
call_name str : The method to call
action str : What you are doing on twitter , it will be reported to the user if report_success is set to True .
for example " following @tw_blue2 " will be reported as " following @tw_blue2 succeeded " .
_sound str : a sound to play if the call is executed properly .
report_success and report_failure bool : These are self explanatory . True or False .
preexec_message str : A message to speak to the user while the method is running , example : " trying to follow x user " . """
finished = False
tries = 0
if preexec_message :
output . speak ( preexec_message , True )
while finished == False and tries < 25 :
try :
val = getattr ( self . twitter , call_name ) ( * args , * * kwargs )
finished = True
2021-09-26 03:58:25 -05:00
except TweepyException as e :
2021-10-07 09:20:06 -05:00
output . speak ( str ( e ) )
2021-06-16 16:18:41 -05:00
val = None
2021-11-08 16:12:47 -06:00
if type ( e ) != NotFound and type ( e ) != Forbidden :
2021-06-16 16:18:41 -05:00
tries = tries + 1
time . sleep ( 5 )
2021-10-07 09:20:06 -05:00
elif report_failure :
output . speak ( _ ( " %s failed. Reason: %s " ) % ( action , str ( e ) ) )
2021-06-16 16:18:41 -05:00
finished = True
2016-12-19 11:43:32 -06:00
# except:
# tries = tries + 1
# time.sleep(5)
2021-06-16 16:18:41 -05:00
if report_success :
output . speak ( _ ( " %s succeeded. " ) % action )
if _sound != None : self . sound . play ( _sound )
return val
2014-11-12 20:41:29 -06:00
2021-11-10 15:14:40 -06:00
def api_call_v2 ( self , call_name , action = " " , _sound = None , report_success = False , report_failure = True , preexec_message = " " , * args , * * kwargs ) :
finished = False
tries = 0
if preexec_message :
output . speak ( preexec_message , True )
while finished == False and tries < 25 :
try :
val = getattr ( self . twitter_v2 , call_name ) ( * args , * * kwargs )
finished = True
except TweepyException as e :
log . exception ( " Error sending the tweet. " )
output . speak ( str ( e ) )
val = None
if type ( e ) != NotFound and type ( e ) != Forbidden :
tries = tries + 1
time . sleep ( 5 )
elif report_failure :
output . speak ( _ ( " %s failed. Reason: %s " ) % ( action , str ( e ) ) )
finished = True
if report_success :
output . speak ( _ ( " %s succeeded. " ) % action )
if _sound != None : self . sound . play ( _sound )
return val
2021-06-16 16:18:41 -05:00
def search ( self , name , * args , * * kwargs ) :
""" Search in twitter, passing args and kwargs as arguments to the Twython function. """
2021-09-26 03:58:25 -05:00
tl = self . twitter . search_tweets ( * args , * * kwargs )
2021-06-16 16:18:41 -05:00
tl . reverse ( )
return tl
2015-03-24 17:07:14 -06:00
2018-08-17 05:12:49 -05:00
# @_require_login
2021-06-16 16:18:41 -05:00
def get_favourites_timeline ( self , name , * args , * * kwargs ) :
""" Gets favourites for the authenticated user or a friend or follower.
name str : Name for storage in the database .
args and kwargs are passed directly to the Twython function . """
tl = self . call_paged ( " favorites " , * args , * * kwargs )
return self . order_buffer ( name , tl )
2014-11-12 20:41:29 -06:00
2022-01-10 04:35:23 -06:00
def call_paged ( self , update_function , name , * args , * * kwargs ) :
2021-06-16 16:18:41 -05:00
""" Makes a call to the Twitter API methods several times. Useful for get methods.
this function is needed for retrieving more than 200 items .
update_function str : The function to call . This function must be child of self . twitter
args and kwargs are passed to update_function .
returns a list with all items retrieved . """
results = [ ]
2022-01-10 04:35:23 -06:00
if self . db . get ( name ) == None or self . db . get ( name ) == [ ] :
2022-01-10 05:06:31 -06:00
since_id = None
2022-01-10 04:35:23 -06:00
else :
if self . settings [ " general " ] [ " reverse_timelines " ] == False :
2022-01-10 05:05:27 -06:00
since_id = self . db [ name ] [ - 1 ] . id
2022-01-10 04:35:23 -06:00
else :
2022-01-10 05:05:27 -06:00
since_id = self . db [ name ] [ 0 ] . id
data = getattr ( self . twitter , update_function ) ( count = self . settings [ " general " ] [ " max_tweets_per_call " ] , since_id = since_id , * args , * * kwargs )
2021-06-16 16:18:41 -05:00
results . extend ( data )
results . reverse ( )
return results
2014-11-12 20:41:29 -06:00
2018-08-17 05:12:49 -05:00
# @_require_login
2021-06-16 16:18:41 -05:00
def get_user_info ( self ) :
""" Retrieves some information required by TWBlue for setup. """
f = self . twitter . get_settings ( )
sn = f [ " screen_name " ]
self . settings [ " twitter " ] [ " user_name " ] = sn
self . db [ " user_name " ] = sn
self . db [ " user_id " ] = self . twitter . get_user ( screen_name = sn ) . id
try :
self . db [ " utc_offset " ] = f [ " time_zone " ] [ " utc_offset " ]
except KeyError :
self . db [ " utc_offset " ] = - time . timezone
# Get twitter's supported languages and save them in a global variable
#so we won't call to this method once per session.
if len ( application . supported_languages ) == 0 :
application . supported_languages = self . twitter . supported_languages ( )
self . get_lists ( )
self . get_muted_users ( )
self . settings . write ( )
2014-11-12 20:41:29 -06:00
2018-08-17 05:12:49 -05:00
# @_require_login
2021-06-16 16:18:41 -05:00
def get_lists ( self ) :
""" Gets the lists that the user is subscribed to and stores them in the database. Returns None. """
2021-09-26 03:58:25 -05:00
self . db [ " lists " ] = self . twitter . get_lists ( reverse = True )
2014-11-12 20:41:29 -06:00
2018-08-17 05:12:49 -05:00
# @_require_login
2021-06-16 16:18:41 -05:00
def get_muted_users ( self ) :
""" Gets muted users (oh really?). """
2021-09-26 03:58:25 -05:00
self . db [ " muted_users " ] = self . twitter . get_muted_ids ( )
2015-05-26 20:23:02 -05:00
2018-08-17 05:12:49 -05:00
# @_require_login
2021-06-16 16:18:41 -05:00
def get_stream ( self , name , function , * args , * * kwargs ) :
""" Retrieves the items for a regular stream.
name str : Name to save items to the database .
function str : A function to get the items . """
last_id = - 1
if name in self . db :
try :
if self . db [ name ] [ 0 ] [ " id " ] > self . db [ name ] [ - 1 ] [ " id " ] :
last_id = self . db [ name ] [ 0 ] [ " id " ]
else :
last_id = self . db [ name ] [ - 1 ] [ " id " ]
except IndexError :
pass
tl = self . call_paged ( function , sinze_id = last_id , * args , * * kwargs )
self . order_buffer ( name , tl )
2014-11-12 20:41:29 -06:00
2021-06-16 16:18:41 -05:00
def get_cursored_stream ( self , name , function , items = " users " , get_previous = False , * args , * * kwargs ) :
""" Gets items for API calls that require using cursors to paginate the results.
name str : Name to save it in the database .
function str : Function that provides the items .
items : When the function returns the list with results , items will tell how the order function should be look . for example get_followers_list returns a list and users are under list [ " users " ] , here the items should point to " users " .
get_previous bool : wether this function will be used to get previous items in a buffer or load the buffer from scratch .
returns number of items retrieved . """
items_ = [ ]
try :
if " cursor " in self . db [ name ] and get_previous :
cursor = self . db [ name ] [ " cursor " ]
else :
cursor = - 1
except KeyError :
cursor = - 1
if cursor != - 1 :
tl = getattr ( self . twitter , function ) ( cursor = cursor , count = self . settings [ " general " ] [ " max_tweets_per_call " ] , * args , * * kwargs )
else :
tl = getattr ( self . twitter , function ) ( count = self . settings [ " general " ] [ " max_tweets_per_call " ] , * args , * * kwargs )
tl [ items ] . reverse ( )
num = self . order_cursored_buffer ( name , tl [ items ] )
# Recently, Twitter's new endpoints have cursor if there are more results.
if " next_cursor " in tl :
self . db [ name ] [ " cursor " ] = tl [ " next_cursor " ]
else :
self . db [ name ] [ " cursor " ] = 0
return num
2014-11-12 20:41:29 -06:00
2021-06-16 16:18:41 -05:00
def check_connection ( self ) :
""" Restart the Twitter object every 5 executions. It is useful for dealing with requests timeout and other oddities. """
log . debug ( " Executing check connection... " )
self . counter + = 1
if self . counter > = 4 :
log . debug ( " Restarting connection after 5 minutes. " )
del self . twitter
self . logged = False
self . login ( False )
self . counter = 0
2015-05-01 01:48:42 -04:00
2021-06-16 16:18:41 -05:00
def check_quoted_status ( self , tweet ) :
""" Helper for get_quoted_tweet. Get a quoted status inside a tweet and create a special tweet with all info available.
tweet dict : A tweet dictionary .
Returns a quoted tweet or the original tweet if is not a quote """
status = tweets . is_long ( tweet )
if status != False and config . app [ " app-settings " ] [ " handle_longtweets " ] :
quoted_tweet = self . get_quoted_tweet ( tweet )
return quoted_tweet
return tweet
2015-09-29 08:38:05 -05:00
2021-06-16 16:18:41 -05:00
def get_quoted_tweet ( self , tweet ) :
""" Process a tweet and extract all information related to the quote. """
quoted_tweet = tweet
if hasattr ( tweet , " full_text " ) :
value = " full_text "
else :
value = " text "
2021-06-25 16:25:51 -05:00
if hasattr ( quoted_tweet , " entities " ) :
setattr ( quoted_tweet , value , utils . expand_urls ( getattr ( quoted_tweet , value ) , quoted_tweet . entities ) )
if hasattr ( quoted_tweet , " is_quote_status " ) == True and hasattr ( quoted_tweet , " quoted_status " ) :
2021-06-16 16:18:41 -05:00
original_tweet = quoted_tweet . quoted_status
2021-06-25 16:25:51 -05:00
elif hasattr ( quoted_tweet , " retweeted_status " ) and hasattr ( quoted_tweet . retweeted_status , " is_quote_status " ) == True and hasattr ( quoted_tweet . retweeted_status , " quoted_status " ) :
2021-06-16 16:18:41 -05:00
original_tweet = quoted_tweet . retweeted_status . quoted_status
else :
return quoted_tweet
original_tweet = self . check_long_tweet ( original_tweet )
if hasattr ( original_tweet , " full_text " ) :
value = " full_text "
elif hasattr ( original_tweet , " message " ) :
value = " message "
else :
value = " text "
2021-06-25 16:25:51 -05:00
if hasattr ( original_tweet , " entities " ) :
setattr ( original_tweet , value , utils . expand_urls ( getattr ( original_tweet , value ) , original_tweet . entities ) )
# ToDo: Shall we check whether we should add show_screen_names here?
return compose . compose_quoted_tweet ( quoted_tweet , original_tweet , session = self )
2015-09-29 08:38:05 -05:00
2021-06-16 16:18:41 -05:00
def check_long_tweet ( self , tweet ) :
""" Process a tweet and add extra info if it ' s a long tweet made with Twyshort.
tweet dict : a tweet object .
returns a tweet with a new argument message , or original tweet if it ' s not a long tweet. " " "
2021-06-25 16:25:51 -05:00
long = False
if hasattr ( tweet , " entities " ) and tweet . entities . get ( " urls " ) :
long = twishort . is_long ( tweet )
2021-06-16 16:18:41 -05:00
if long != False and config . app [ " app-settings " ] [ " handle_longtweets " ] :
message = twishort . get_full_text ( long )
if hasattr ( tweet , " quoted_status " ) :
tweet . quoted_status . message = message
if tweet . quoted_status . message == False : return False
tweet . quoted_status . twishort = True
2021-06-25 16:25:51 -05:00
if hasattr ( tweet . quoted_status , " entities " ) and tweet . quoted_status . entities . get ( " user_mentions " ) :
for i in tweet . quoted_status . entities [ " user_mentions " ] :
if " @ %s " % ( i [ " screen_name " ] ) not in tweet . quoted_status . message and i [ " screen_name " ] != self . get_user ( tweet . user ) . screen_name :
if hasattr ( tweet . quoted_status , " retweeted_status " ) and self . get_user ( tweet . retweeted_status . user ) . screen_name == i [ " screen_name " ] :
continue
tweet . quoted_status . message = u " @ %s %s " % ( i [ " screen_name " ] , tweet . message )
2021-06-16 16:18:41 -05:00
else :
tweet . message = message
if tweet . message == False : return False
tweet . twishort = True
2021-06-25 16:25:51 -05:00
if hasattr ( tweet , " entities " ) and tweet . entities . get ( " user_mentions " ) :
for i in tweet . entities [ " user_mentions " ] :
if " @ %s " % ( i [ " screen_name " ] ) not in tweet . message and i [ " screen_name " ] != self . get_user ( tweet . user ) . screen_name :
if hasattr ( tweet , " retweeted_status " ) and self . get_user ( tweet . retweeted_status . user ) . screen_name == i [ " screen_name " ] :
continue
tweet . message = u " @ %s %s " % ( i [ " screen_name " ] , tweet . message )
2021-06-16 16:18:41 -05:00
return tweet
2018-07-17 10:58:09 -05:00
2021-06-16 16:18:41 -05:00
def get_user ( self , id ) :
""" Returns an user object associated with an ID.
id str : User identifier , provided by Twitter .
returns a tweepy user object . """
2021-07-03 14:05:02 -05:00
if hasattr ( id , " id_str " ) :
log . error ( " Called get_user function by passing a full user id as a parameter. " )
id = id . id_str
2021-06-27 18:05:35 -05:00
# Check if the user has been added to the list of deleted users previously.
if id in self . deleted_users :
log . debug ( " Returning user {} from the list of deleted users. " . format ( id ) )
return self . deleted_users [ id ]
2021-06-25 13:11:33 -05:00
if ( " users " in self . db ) == False or ( str ( id ) in self . db [ " users " ] ) == False :
2021-06-25 16:25:51 -05:00
log . debug ( " Requesting user id {} as it is not present in the users database. " . format ( id ) )
2021-06-16 16:18:41 -05:00
try :
user = self . twitter . get_user ( id = id )
2021-09-26 03:58:25 -05:00
except TweepyException as err :
2021-06-16 16:18:41 -05:00
user = UserModel ( None )
user . screen_name = " deleted_user "
user . id = id
user . name = _ ( " Deleted account " )
2021-10-07 09:20:06 -05:00
if type ( err ) == NotFound :
2021-06-27 18:05:35 -05:00
self . deleted_users [ id ] = user
return user
else :
log . exception ( " Error when attempting to retrieve an user from Twitter. " )
return user
2021-06-24 09:52:10 -05:00
users = self . db [ " users " ]
2021-06-25 16:25:51 -05:00
users [ user . id_str ] = user
2021-06-24 09:52:10 -05:00
self . db [ " users " ] = users
2021-07-06 13:59:34 -05:00
user . name = self . get_user_alias ( user )
2021-06-16 16:18:41 -05:00
return user
else :
2021-07-06 13:59:34 -05:00
user = self . db [ " users " ] [ str ( id ) ]
user . name = self . get_user_alias ( user )
return user
def get_user_alias ( self , user ) :
""" Retrieves an alias for the passed user model, if exists.
@ user Tweepy . models . user : An user object .
"""
aliases = self . settings . get ( " user-aliases " )
if aliases == None :
log . error ( " Aliases are not defined for this config spec. " )
2022-05-13 13:04:12 -05:00
return self . demoji_user ( user . name )
2021-07-06 13:59:34 -05:00
user_alias = aliases . get ( user . id_str )
if user_alias != None :
return user_alias
2022-05-13 13:04:12 -05:00
return self . demoji_user ( user . name )
def demoji_user ( self , name ) :
if self . settings [ " general " ] [ " hide_emojis " ] == True :
return demoji . replace ( name , " " )
return name
2018-07-25 11:22:57 -05:00
2021-06-16 16:18:41 -05:00
def get_user_by_screen_name ( self , screen_name ) :
""" Returns an user identifier associated with a screen_name.
screen_name str : User name , such as tw_blue2 , provided by Twitter .
returns an user ID . """
if ( " users " in self . db ) == False :
user = utils . if_user_exists ( self . twitter , screen_name )
2021-06-24 09:52:10 -05:00
users = self . db [ " users " ]
2021-06-25 13:11:33 -05:00
users [ user [ " id " ] ] = user
2021-06-24 09:52:10 -05:00
self . db [ " users " ] = users
2021-06-25 13:11:33 -05:00
return user [ " id " ]
2021-06-16 16:18:41 -05:00
else :
for i in list ( self . db [ " users " ] . keys ( ) ) :
if self . db [ " users " ] [ i ] . screen_name == screen_name :
2021-06-25 13:11:33 -05:00
return self . db [ " users " ] [ i ] . id
2021-06-16 16:18:41 -05:00
user = utils . if_user_exists ( self . twitter , screen_name )
2021-06-24 09:52:10 -05:00
users = self . db [ " users " ]
2021-06-25 13:11:33 -05:00
users [ user . id ] = user
2021-06-24 09:52:10 -05:00
self . db [ " users " ] = users
2021-06-25 13:11:33 -05:00
return user . id
2021-01-27 10:36:44 -06:00
2021-06-16 16:18:41 -05:00
def save_users ( self , user_ids ) :
""" Adds all new users to the users database. """
if len ( user_ids ) == 0 :
return
log . debug ( " Received %d user IDS to be added in the database. " % ( len ( user_ids ) ) )
2021-06-27 18:05:35 -05:00
users_to_retrieve = [ user_id for user_id in user_ids if ( user_id not in self . db [ " users " ] and user_id not in self . deleted_users ) ]
2021-06-16 16:18:41 -05:00
# Remove duplicates
users_to_retrieve = list ( dict . fromkeys ( users_to_retrieve ) )
if len ( users_to_retrieve ) == 0 :
return
log . debug ( " TWBlue will get %d new users from Twitter. " % ( len ( users_to_retrieve ) ) )
2021-06-27 18:05:35 -05:00
try :
2021-09-26 03:58:25 -05:00
users = self . twitter . lookup_users ( user_id = users_to_retrieve , tweet_mode = " extended " )
2021-06-27 18:05:35 -05:00
users_db = self . db [ " users " ]
for user in users :
users_db [ user . id_str ] = user
log . debug ( " Added %d new users " % ( len ( users ) ) )
self . db [ " users " ] = users_db
2021-09-26 03:58:25 -05:00
except TweepyException as err :
2021-10-07 09:20:06 -05:00
if type ( err ) == NotFound : # User not found.
2021-06-27 18:05:35 -05:00
log . error ( " The specified users {} were not found in twitter. " . format ( user_ids ) )
# Creates a deleted user object for every user_id not found here.
# This will make TWBlue to not waste Twitter API calls when attempting to retrieve those users again.
# As deleted_users is not saved across restarts, when restarting TWBlue, it will retrieve the correct users if they enabled their accounts.
for id in users_to_retrieve :
user = UserModel ( None )
user . screen_name = " deleted_user "
user . id = id
user . name = _ ( " Deleted account " )
self . deleted_users [ id ] = user
else :
log . exception ( " An exception happened while attempting to retrieve a list of users from direct messages in Twitter. " )
2021-06-25 16:25:51 -05:00
def add_users_from_results ( self , data ) :
users = self . db [ " users " ]
for i in data :
if hasattr ( i , " user " ) :
if isinstance ( i . user , str ) :
log . warning ( " A String was passed to be added as an user. This is normal only if TWBlue tried to load a conversation. " )
continue
if ( i . user . id_str in self . db [ " users " ] ) == False :
users [ i . user . id_str ] = i . user
if hasattr ( i , " quoted_status " ) and ( i . quoted_status . user . id_str in self . db [ " users " ] ) == False :
users [ i . quoted_status . user . id_str ] = i . quoted_status . user
if hasattr ( i , " retweeted_status " ) and ( i . retweeted_status . user . id_str in self . db [ " users " ] ) == False :
users [ i . retweeted_status . user . id_str ] = i . retweeted_status . user
self . db [ " users " ] = users
2021-06-28 17:03:26 -05:00
def start_streaming ( self ) :
2021-07-04 09:44:48 -05:00
if config . app [ " app-settings " ] [ " no_streaming " ] :
return
2022-11-15 11:54:59 -06:00
self . stream = streaming . Stream ( twitter_api = self . twitter , session_name = self . get_name ( ) , user_id = self . db [ " user_id " ] , muted_users = self . db [ " muted_users " ] , consumer_key = appkeys . twitter_api_key , consumer_secret = appkeys . twitter_api_secret , access_token = self . settings [ " twitter " ] [ " user_key " ] , access_token_secret = self . settings [ " twitter " ] [ " user_secret " ] , chunk_size = 1025 )
2021-09-26 03:58:25 -05:00
self . stream_thread = call_threaded ( self . stream . filter , follow = self . stream . users , stall_warnings = True )
2021-07-02 09:52:21 -05:00
def stop_streaming ( self ) :
2021-07-04 09:44:48 -05:00
if config . app [ " app-settings " ] [ " no_streaming " ] :
return
if hasattr ( self , " stream " ) :
self . stream . running = False
log . debug ( " Stream stopped for accounr {} " . format ( self . db [ " user_name " ] ) )
2021-06-28 17:03:26 -05:00
2022-11-15 11:54:59 -06:00
def handle_new_status ( self , status , session_name ) :
2021-06-29 05:05:20 -05:00
""" Handles a new status present in the Streaming API. """
2021-07-03 11:51:11 -05:00
if self . logged == False :
return
2021-06-29 05:05:20 -05:00
# Discard processing the status if the streaming sends a tweet for another account.
2022-11-15 11:54:59 -06:00
if self . get_name ( ) != session_name :
2021-06-28 17:03:26 -05:00
return
2021-06-29 05:05:20 -05:00
# the Streaming API sends non-extended tweets with an optional parameter "extended_tweets" which contains full_text and other data.
# so we have to make sure we check it before processing the normal status.
# As usual, we handle also quotes and retweets at first.
2022-04-06 15:11:55 -05:00
if hasattr ( status , " retweeted_status " ) and hasattr ( status . retweeted_status , " quoted_status " ) and status . retweeted_status . quoted_status . truncated :
status . retweeted_status . quoted_status . _json = { * * status . retweeted_status . quoted_status . _json , * * status . retweeted_status . quoted_status . _json [ " extended_tweet " ] }
2021-06-29 05:05:20 -05:00
if hasattr ( status , " retweeted_status " ) and hasattr ( status . retweeted_status , " extended_tweet " ) :
status . retweeted_status . _json = { * * status . retweeted_status . _json , * * status . retweeted_status . _json [ " extended_tweet " ] }
# compose.compose_tweet requires the parent tweet to have a full_text field, so we have to add it to retweets here.
status . _json [ " full_text " ] = status . _json [ " text " ]
2022-04-06 15:11:55 -05:00
elif hasattr ( status , " quoted_status " ) and hasattr ( status . quoted_status , " extended_tweet " ) :
2021-06-29 05:05:20 -05:00
status . quoted_status . _json = { * * status . quoted_status . _json , * * status . quoted_status . _json [ " extended_tweet " ] }
2021-06-28 17:03:26 -05:00
if status . truncated :
2021-06-29 05:05:20 -05:00
status . _json = { * * status . _json , * * status . _json [ " extended_tweet " ] }
# Sends status to database, where it will be reduced and changed according to our needs.
2021-06-29 17:55:36 -05:00
buffers_to_send = [ ]
2021-09-26 03:58:25 -05:00
if status . user . id_str in self . stream . users :
2021-06-29 17:55:36 -05:00
buffers_to_send . append ( " home_timeline " )
if status . user . id == self . db [ " user_id " ] :
buffers_to_send . append ( " sent_tweets " )
for user in status . entities [ " user_mentions " ] :
if user [ " id " ] == self . db [ " user_id " ] :
2021-07-02 10:11:50 -05:00
buffers_to_send . append ( " mentions " )
2021-06-29 17:55:36 -05:00
users_with_timeline = [ user . split ( " - " ) [ 0 ] for user in self . db . keys ( ) if user . endswith ( " -timeline " ) ]
for user in users_with_timeline :
if status . user . id_str == user :
buffers_to_send . append ( " {} -timeline " . format ( user ) )
for buffer in buffers_to_send [ : : ] :
num = self . order_buffer ( buffer , [ status ] )
if num == 0 :
buffers_to_send . remove ( buffer )
2022-04-06 15:11:55 -05:00
# However, we have to do the "reduce and change" process here because the status we sent to the db is going to be a different object that the one sent to controller.
2021-07-03 14:04:14 -05:00
status = reduce . reduce_tweet ( status )
2021-06-29 17:55:36 -05:00
status = self . check_quoted_status ( status )
status = self . check_long_tweet ( status )
# Send it to the main controller object.
2022-11-15 11:54:59 -06:00
pub . sendMessage ( " newTweet " , data = status , session_name = self . get_name ( ) , _buffers = buffers_to_send )
2021-06-29 17:16:53 -05:00
def check_streams ( self ) :
2021-07-04 09:44:48 -05:00
if config . app [ " app-settings " ] [ " no_streaming " ] :
return
if not hasattr ( self , " stream " ) :
return
2021-06-29 17:16:53 -05:00
log . debug ( " Status of running stream for user {} : {} " . format ( self . db [ " user_name " ] , self . stream . running ) )
if self . stream . running == False :
self . start_streaming ( )
2022-11-15 11:54:59 -06:00
def handle_connected ( self , session_name ) :
2021-07-03 11:51:11 -05:00
if self . logged == False :
return
2022-11-15 11:54:59 -06:00
if session_name != self . get_name ( ) :
log . debug ( " Connected streaming endpoint on session {} " . format ( session_name ) )
2021-11-05 11:49:51 -06:00
def send_tweet ( self , * tweets ) :
""" Convenience function to send a thread. """
in_reply_to_status_id = None
for obj in tweets :
2021-11-11 15:13:58 -06:00
# When quoting a tweet, the tweet_data dict might contain a parameter called quote_tweet_id. Let's add it, or None, so quotes will be posted successfully.
2021-11-05 11:49:51 -06:00
if len ( obj [ " attachments " ] ) == 0 :
2021-11-11 15:13:58 -06:00
item = self . api_call_v2 ( call_name = " create_tweet " , text = obj [ " text " ] , _sound = " tweet_send.ogg " , in_reply_to_tweet_id = in_reply_to_status_id , poll_duration_minutes = obj [ " poll_period " ] , poll_options = obj [ " poll_options " ] , quote_tweet_id = obj . get ( " quote_tweet_id " ) )
2021-11-11 08:36:29 -06:00
in_reply_to_status_id = item . data [ " id " ]
2021-11-05 11:49:51 -06:00
else :
media_ids = [ ]
for i in obj [ " attachments " ] :
img = self . api_call ( " media_upload " , filename = i [ " file " ] )
2021-11-08 16:12:47 -06:00
if i [ " type " ] == " photo " :
self . api_call ( call_name = " create_media_metadata " , media_id = img . media_id , alt_text = i [ " description " ] )
2021-11-05 11:49:51 -06:00
media_ids . append ( img . media_id )
2021-12-08 12:49:20 -06:00
item = self . api_call_v2 ( call_name = " create_tweet " , text = obj [ " text " ] , _sound = " tweet_send.ogg " , in_reply_to_tweet_id = in_reply_to_status_id , media_ids = media_ids , poll_duration_minutes = obj [ " poll_period " ] , poll_options = obj [ " poll_options " ] , quote_tweet_id = obj . get ( " quote_tweet_id " ) )
2021-11-11 08:36:29 -06:00
in_reply_to_status_id = item . data [ " id " ]
2021-11-05 13:10:45 -06:00
def reply ( self , text = " " , in_reply_to_status_id = None , attachments = [ ] , * args , * * kwargs ) :
if len ( attachments ) == 0 :
2021-11-10 16:32:14 -06:00
item = self . api_call_v2 ( call_name = " create_tweet " , text = text , _sound = " reply_send.ogg " , in_reply_to_tweet_id = in_reply_to_status_id , * args , * * kwargs )
2021-11-05 13:10:45 -06:00
else :
media_ids = [ ]
for i in attachments :
img = self . api_call ( " media_upload " , filename = i [ " file " ] )
2021-11-08 16:12:47 -06:00
if i [ " type " ] == " photo " :
self . api_call ( call_name = " create_media_metadata " , media_id = img . media_id , alt_text = i [ " description " ] )
2021-11-05 13:10:45 -06:00
media_ids . append ( img . media_id )
2021-12-08 12:49:20 -06:00
item = self . api_call_v2 ( call_name = " create_tweet " , text = text , _sound = " reply_send.ogg " , in_reply_to_tweet_id = in_reply_to_status_id , media_ids = media_ids , * args , * * kwargs )
2021-11-10 12:21:07 -06:00
def direct_message ( self , text , recipient , attachment = None , * args , * * kwargs ) :
if attachment == None :
item = self . api_call ( call_name = " send_direct_message " , recipient_id = recipient , text = text )
else :
if attachment [ " type " ] == " photo " :
media_category = " DmImage "
elif attachment [ " type " ] == " gif " :
media_category = " DmGif "
elif attachment [ " type " ] == " video " :
media_category = " DmVideo "
media = self . api_call ( " media_upload " , filename = attachment [ " file " ] , media_category = media_category )
item = self . api_call ( call_name = " send_direct_message " , recipient_id = recipient , text = text , attachment_type = " media " , attachment_media_id = media . media_id )
if item != None :
sent_dms = self . db [ " sent_direct_messages " ]
if self . settings [ " general " ] [ " reverse_timelines " ] == False :
sent_dms . append ( item )
else :
sent_dms . insert ( 0 , item )
self . db [ " sent_direct_messages " ] = sent_dms
2022-11-15 11:54:59 -06:00
pub . sendMessage ( " sent-dm " , data = item , session_name = self . get_name ( ) )
2022-11-14 17:51:27 -06:00
def get_name ( self ) :
2022-11-16 11:01:52 -06:00
if self . logged :
return " Twitter: {} " . format ( self . db [ " user_name " ] )
else :
return " Twitter: {} " . format ( self . settings [ " twitter " ] [ " user_name " ] )