2021-08-27 15:42:34 -05:00
# -*- coding: utf-8 -*-
import os
import paths
import time
import logging
2022-11-16 15:33:27 -06:00
import webbrowser
2021-08-27 15:42:34 -05:00
import wx
2022-02-24 12:43:06 -06:00
import mastodon
2023-03-23 11:58:42 -06:00
import demoji
2021-08-27 15:42:34 -05:00
import config
import config_utils
import output
import application
2023-04-03 15:17:03 -06:00
from mastodon import MastodonError , MastodonAPIError , MastodonNotFoundError , MastodonUnauthorizedError
2021-08-27 15:42:34 -05:00
from pubsub import pub
from mysc.thread_utils import call_threaded
from sessions import base
2022-11-18 16:29:53 -06:00
from sessions.mastodon import utils , streaming
2021-08-27 15:42:34 -05:00
log = logging . getLogger ( "sessions.mastodonSession" )
2022-11-15 16:13:16 -06:00
MASTODON_VERSION = "4.0.1"
2021-08-27 15:42:34 -05:00
class Session ( base . baseSession ):
def __init__ ( self , * args , ** kwargs ):
super ( Session , self ) . __init__ ( * args , ** kwargs )
self . config_spec = "mastodon.defaults"
self . supported_languages = []
2022-02-24 16:08:29 -06:00
self . type = "mastodon"
2022-11-13 22:17:28 -06:00
self . db [ "pagination_info" ] = dict ()
2022-11-16 10:06:14 -06:00
self . char_limit = 500
2022-12-23 13:58:10 -06:00
self . post_visibility = "public"
self . expand_spoilers = False
2022-11-19 19:36:21 -06:00
pub . subscribe ( self . on_status , "mastodon.status_received" )
2022-11-25 17:30:57 -06:00
pub . subscribe ( self . on_status_updated , "mastodon.status_updated" )
2022-11-19 19:36:21 -06:00
pub . subscribe ( self . on_notification , "mastodon.notification_received" )
2021-08-27 15:42:34 -05:00
def login ( self , verify_credentials = True ):
if self . settings [ "mastodon" ][ "access_token" ] != None and self . settings [ "mastodon" ][ "instance" ] != None :
try :
log . debug ( "Logging in to Mastodon instance {} ..." . format ( self . settings [ "mastodon" ][ "instance" ]))
2023-01-29 14:34:36 -06:00
self . api = mastodon . Mastodon ( access_token = self . settings [ "mastodon" ][ "access_token" ], api_base_url = self . settings [ "mastodon" ][ "instance" ], mastodon_version = MASTODON_VERSION , user_agent = "TWBlue/ {} " . format ( application . version ))
2021-08-27 15:42:34 -05:00
if verify_credentials == True :
credentials = self . api . account_verify_credentials ()
self . db [ "user_name" ] = credentials [ "username" ]
self . db [ "user_id" ] = credentials [ "id" ]
self . settings [ "mastodon" ][ "user_name" ] = credentials [ "username" ]
self . logged = True
log . debug ( "Logged." )
self . counter = 0
2023-01-05 17:16:34 -06:00
except MastodonError :
log . exception ( "The login attempt failed." )
2021-08-27 15:42:34 -05:00
self . logged = False
else :
self . logged = False
raise Exceptions . RequireCredentialsSessionError
def authorise ( self ):
if self . logged == True :
raise Exceptions . AlreadyAuthorisedError ( "The authorisation process is not needed at this time." )
2022-11-16 15:33:27 -06:00
authorisation_dialog = wx . TextEntryDialog ( None , _ ( "Please enter your instance URL." ), _ ( "Mastodon instance" ))
answer = authorisation_dialog . ShowModal ()
instance = authorisation_dialog . GetValue ()
authorisation_dialog . Destroy ()
if answer != wx . ID_OK :
return
2022-11-17 16:19:47 -06:00
try :
client_id , client_secret = mastodon . Mastodon . create_app ( "TWBlue" , api_base_url = authorisation_dialog . GetValue (), website = "https://twblue.es" )
2023-02-05 18:59:16 -06:00
temporary_api = mastodon . Mastodon ( client_id = client_id , client_secret = client_secret , api_base_url = instance , mastodon_version = MASTODON_VERSION , user_agent = "TWBlue/ {} " . format ( application . version ))
2022-11-17 16:19:47 -06:00
auth_url = temporary_api . auth_request_url ()
except MastodonError :
dlg = wx . MessageDialog ( None , _ ( "We could not connect to your mastodon instance. Please verify that the domain exists and the instance is accessible via a web browser." ), _ ( "Instance error" ), wx . ICON_ERROR )
dlg . ShowModal ()
dlg . Destroy ()
return
2022-11-16 15:33:27 -06:00
webbrowser . open_new_tab ( auth_url )
verification_dialog = wx . TextEntryDialog ( None , _ ( "Enter the verification code" ), _ ( "PIN code authorization" ))
answer = verification_dialog . ShowModal ()
code = verification_dialog . GetValue ()
verification_dialog . Destroy ()
if answer != wx . ID_OK :
return
2022-11-17 16:19:47 -06:00
try :
access_token = temporary_api . log_in ( code = verification_dialog . GetValue ())
except MastodonError :
dlg = wx . MessageDialog ( None , _ ( "We could not authorice your mastodon 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
self . create_session_folder ()
self . get_configuration ()
2022-11-16 15:33:27 -06:00
self . settings [ "mastodon" ][ "access_token" ] = access_token
self . settings [ "mastodon" ][ "instance" ] = instance
self . settings . write ()
2022-11-17 16:19:47 -06:00
return True
2021-08-27 15:42:34 -05:00
def get_user_info ( self ):
""" Retrieves some information required by TWBlue for setup."""
# retrieve the current user's UTC offset so we can calculate dates properly.
offset = time . timezone if ( time . localtime () . tm_isdst == 0 ) else time . altzone
offset = offset / 60 / 60 * - 1
self . db [ "utc_offset" ] = offset
2022-12-23 13:58:10 -06:00
instance = self . api . instance ()
2021-08-27 15:42:34 -05:00
if len ( self . supported_languages ) == 0 :
2022-12-23 13:58:10 -06:00
self . supported_languages = instance . languages
2021-08-27 15:42:34 -05:00
self . get_lists ()
self . get_muted_users ()
2022-11-16 10:06:14 -06:00
# determine instance custom characters limit.
2022-11-30 11:19:51 -06:00
if hasattr ( instance , "configuration" ) and hasattr ( instance . configuration , "statuses" ) and hasattr ( instance . configuration . statuses , "max_characters" ):
self . char_limit = instance . configuration . statuses . max_characters
2022-12-23 13:58:10 -06:00
# User preferences for some things.
preferences = self . api . preferences ()
self . post_visibility = preferences . get ( "posting:default:visibility" )
self . expand_spoilers = preferences . get ( "reading:expand:spoilers" )
2021-08-27 15:42:34 -05:00
self . settings . write ()
def get_lists ( self ):
""" Gets the lists that the user is subscribed to and stores them in the database. Returns None."""
self . db [ "lists" ] = self . api . lists ()
def get_muted_users ( self ):
### ToDo: Use a function to retrieve all muted users.
self . db [ "muted_users" ] = self . api . mutes ()
def get_user_alias ( self , user ):
2023-03-23 11:58:42 -06:00
if user . display_name == None or user . display_name == "" :
display_name = user . username
else :
display_name = user . display_name
2021-08-27 15:42:34 -05:00
aliases = self . settings . get ( "user-aliases" )
if aliases == None :
log . error ( "Aliases are not defined for this config spec." )
2023-03-23 11:58:42 -06:00
return self . demoji_user ( display_name )
user_alias = aliases . get ( user . id )
2021-08-27 15:42:34 -05:00
if user_alias != None :
return user_alias
2023-03-23 11:58:42 -06:00
return self . demoji_user ( display_name )
def demoji_user ( self , name ):
if self . settings [ "general" ][ "hide_emojis" ] == True :
return demoji . replace ( name , "" )
return name
2022-11-07 12:24:56 -06:00
2022-11-09 17:08:48 -06:00
def order_buffer ( self , name , data , ignore_older = False ):
2022-11-08 12:19:41 -06:00
num = 0
last_id = None
if self . db . get ( name ) == None :
self . db [ name ] = []
objects = self . db [ name ]
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
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
if utils . find_item ( i , self . db [ name ]) == None :
if self . settings [ "general" ][ "reverse_timelines" ] == False : objects . append ( i )
else : objects . insert ( 0 , i )
num = num + 1
self . db [ name ] = objects
return num
2022-11-08 13:22:27 -06:00
2022-11-25 17:30:57 -06:00
def update_item ( self , name , item ):
if name not in self . db :
return False
items = self . db [ name ]
if type ( items ) != list :
return False
# determine item position in buffer.
item_position = next (( x for x in range ( len ( items )) if items [ x ] . id == item . id ), None )
if item_position != None :
self . db [ name ][ item_position ] = item
2022-11-25 17:43:03 -06:00
return item_position
2022-11-25 17:30:57 -06:00
return False
2022-11-08 13:22:27 -06:00
def api_call ( 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 )
2023-03-23 11:58:42 -06:00
while finished == False and tries < 5 :
2022-11-08 13:22:27 -06:00
try :
val = getattr ( self . api , call_name )( * args , ** kwargs )
finished = True
2023-03-23 11:58:42 -06:00
except Exception as e :
2022-11-08 13:22:27 -06:00
output . speak ( str ( e ))
2023-04-03 15:17:03 -06:00
if isinstance ( e , MastodonAPIError ):
log . exception ( "API Error returned when making a Call on {} . Call name= {} , args= {} , kwargs= {} " . format ( self . get_name (), call_name , args , kwargs ))
raise e
2022-11-08 13:22:27 -06:00
val = None
2023-04-03 15:17:03 -06:00
tries = tries + 1
time . sleep ( 5 )
2023-03-23 11:58:42 -06:00
if tries == 4 and finished == False :
raise e
2022-11-08 13:22:27 -06:00
if report_success :
output . speak ( _ ( " %s succeeded." ) % action )
2023-03-23 11:58:42 -06:00
if _sound != None :
self . sound . play ( _sound )
2022-11-08 13:22:27 -06:00
return val
2022-11-08 17:53:59 -06:00
2023-03-23 13:17:55 -06:00
def send_post ( self , reply_to = None , visibility = None , posts = []):
2022-11-08 17:53:59 -06:00
""" Convenience function to send a thread. """
in_reply_to_id = reply_to
2022-11-16 13:28:45 -06:00
for obj in posts :
2022-11-11 15:51:16 -06:00
text = obj . get ( "text" )
2022-11-08 17:53:59 -06:00
if len ( obj [ "attachments" ]) == 0 :
2023-03-23 11:58:42 -06:00
try :
item = self . api_call ( call_name = "status_post" , status = text , _sound = "tweet_send.ogg" , in_reply_to_id = in_reply_to_id , visibility = visibility , sensitive = obj [ "sensitive" ], spoiler_text = obj [ "spoiler_text" ])
# If it fails, let's basically send an event with all passed info so we will catch it later.
except Exception as e :
2023-03-23 13:17:55 -06:00
pub . sendMessage ( "mastodon.error_post" , name = self . get_name (), reply_to = reply_to , visibility = visibility , posts = posts )
2023-03-23 11:58:42 -06:00
return
2022-11-11 15:51:16 -06:00
if item != None :
in_reply_to_id = item [ "id" ]
2022-11-08 17:53:59 -06:00
else :
media_ids = []
2023-03-23 11:58:42 -06:00
try :
poll = None
if len ( obj [ "attachments" ]) == 1 and obj [ "attachments" ][ 0 ][ "type" ] == "poll" :
poll = self . api . make_poll ( options = obj [ "attachments" ][ 0 ][ "options" ], expires_in = obj [ "attachments" ][ 0 ][ "expires_in" ], multiple = obj [ "attachments" ][ 0 ][ "multiple" ], hide_totals = obj [ "attachments" ][ 0 ][ "hide_totals" ])
else :
for i in obj [ "attachments" ]:
media = self . api_call ( "media_post" , media_file = i [ "file" ], description = i [ "description" ], synchronous = True )
media_ids . append ( media . id )
item = self . api_call ( call_name = "status_post" , status = text , _sound = "tweet_send.ogg" , in_reply_to_id = in_reply_to_id , media_ids = media_ids , visibility = visibility , poll = poll , sensitive = obj [ "sensitive" ], spoiler_text = obj [ "spoiler_text" ])
if item != None :
in_reply_to_id = item [ "id" ]
except Exception as e :
2023-03-23 13:17:55 -06:00
pub . sendMessage ( "mastodon.error_post" , name = self . get_name (), reply_to = reply_to , visibility = visibility , posts = posts )
2023-03-23 11:58:42 -06:00
return
2022-11-14 17:51:27 -06:00
def get_name ( self ):
instance = self . settings [ "mastodon" ][ "instance" ]
instance = instance . replace ( "https://" , "" )
2022-11-16 11:01:52 -06:00
user = self . settings [ "mastodon" ][ "user_name" ]
2022-11-18 16:29:53 -06:00
return "Mastodon: {} @ {} " . format ( user , instance )
def start_streaming ( self ):
2023-02-05 19:09:27 -06:00
if self . settings [ "general" ][ "disable_streaming" ]:
log . info ( "Streaming is disabled for session {} . Skipping..." . format ( self . get_name ()))
2022-11-18 16:29:53 -06:00
return
listener = streaming . StreamListener ( session_name = self . get_name (), user_id = self . db [ "user_id" ])
2023-01-29 14:34:36 -06:00
try :
stream_healthy = self . api . stream_healthy ()
if stream_healthy == True :
2023-02-05 18:59:16 -06:00
self . user_stream = self . api . stream_user ( listener , run_async = True , reconnect_async = True , reconnect_async_wait_sec = 30 )
self . direct_stream = self . api . stream_direct ( listener , run_async = True , reconnect_async = True , reconnect_async_wait_sec = 30 )
2023-01-29 14:34:36 -06:00
log . debug ( "Started streams for session {} ." . format ( self . get_name ()))
except Exception as e :
log . exception ( "Detected streaming unhealthy in {} session." . format ( self . get_name ()))
2022-11-18 16:29:53 -06:00
def stop_streaming ( self ):
if config . app [ "app-settings" ][ "no_streaming" ]:
return
def check_streams ( self ):
2023-02-05 18:59:16 -06:00
pass
2022-11-18 16:29:53 -06:00
def check_buffers ( self , status ):
buffers = []
buffers . append ( "home_timeline" )
if status . account . id == self . db [ "user_id" ]:
buffers . append ( "sent" )
return buffers
def on_status ( self , status , session_name ):
# Discard processing the status if the streaming sends a tweet for another account.
if self . get_name () != session_name :
return
buffers = self . check_buffers ( status )
for b in buffers [::]:
num = self . order_buffer ( b , [ status ])
if num == 0 :
buffers . remove ( b )
2022-11-19 19:36:21 -06:00
pub . sendMessage ( "mastodon.new_item" , session_name = self . get_name (), item = status , _buffers = buffers )
2022-11-25 17:30:57 -06:00
def on_status_updated ( self , status , session_name ):
# Discard processing the status if the streaming sends a tweet for another account.
if self . get_name () != session_name :
return
2022-11-25 17:43:03 -06:00
buffers = {}
2022-11-25 17:30:57 -06:00
for b in list ( self . db . keys ()):
updated = self . update_item ( b , status )
2022-11-25 17:43:03 -06:00
if updated != False :
buffers [ b ] = updated
2022-11-25 17:30:57 -06:00
pub . sendMessage ( "mastodon.updated_item" , session_name = self . get_name (), item = status , _buffers = buffers )
2022-11-19 19:36:21 -06:00
def on_notification ( self , notification , session_name ):
# Discard processing the notification if the streaming sends a tweet for another account.
if self . get_name () != session_name :
return
buffers = []
obj = None
if notification . type == "mention" :
buffers = [ "mentions" ]
2023-02-03 11:31:57 -06:00
obj = notification
2022-11-19 19:36:21 -06:00
elif notification . type == "follow" :
buffers = [ "followers" ]
obj = notification . account
for b in buffers [::]:
num = self . order_buffer ( b , [ obj ])
if num == 0 :
buffers . remove ( b )
2022-12-19 16:07:45 -06:00
pub . sendMessage ( "mastodon.new_item" , session_name = self . get_name (), item = obj , _buffers = buffers )
# Now, add notification to its buffer.
num = self . order_buffer ( "notifications" , [ notification ])
if num > 0 :
pub . sendMessage ( "mastodon.new_item" , session_name = self . get_name (), item = notification , _buffers = [ "notifications" ])