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 " ] )