Merge pull request #496 from MCV-Software/mastodon

Initial Mastodon Support
This commit is contained in:
Manuel Cortez 2022-11-25 17:50:08 -06:00 committed by GitHub
commit 768f0bc396
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 4381 additions and 1499 deletions

View File

@ -1,4 +1,6 @@
wxpython wxpython
pytest
coverage
wheel wheel
future future
configobj configobj
@ -29,6 +31,7 @@ backports.functools_lru_cache
cx_freeze cx_freeze
tweepy tweepy
twitter-text-parser twitter-text-parser
mastodon.py
pyenchant pyenchant
sqlitedict sqlitedict
cx-Logging cx-Logging

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from . import base as base from . import base as base
from . import twitter as twitter from . import twitter as twitter
from . import mastodon as mastodon

View File

@ -139,3 +139,6 @@ class Buffer(object):
self.session.db[self.name+"_pos"]=self.buffer.list.get_selected() self.session.db[self.name+"_pos"]=self.buffer.list.get_selected()
except AttributeError: except AttributeError:
pass pass
def view_item(self):
pass

View File

@ -1,2 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from .base import BaseBuffer from .base import BaseBuffer
from .mentions import MentionsBuffer
from .conversations import ConversationBuffer, ConversationListBuffer
from .users import UserBuffer

View File

@ -1,141 +1,498 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" Common logic to all buffers in TWBlue.""" import time
import logging
import wx import wx
import output
import sound
import widgetUtils import widgetUtils
import arrow
import webbrowser
import output
import config
import sound
import languageHandler
import logging
from audio_services import youtube_utils
from controller.buffers.base import base
from controller.mastodon import messages
from sessions.mastodon import compose, utils, templates
from mysc.thread_utils import call_threaded
from pubsub import pub
from extra import ocr
from wxUI import buffers, dialogs, commonMessageDialogs
from wxUI.dialogs.mastodon import menus
from wxUI.dialogs.mastodon import dialogs as mastodon_dialogs
log = logging.getLogger("controller.buffers.base.base") log = logging.getLogger("controller.buffers.mastodon.base")
class Buffer(object): class BaseBuffer(base.Buffer):
""" A basic buffer object. This should be the base class for all other derived buffers.""" def __init__(self, parent, function, name, sessionObject, account, sound=None, compose_func="compose_post", *args, **kwargs):
super(BaseBuffer, self).__init__(parent, function, *args, **kwargs)
log.debug("Initializing buffer %s, account %s" % (name, account,))
self.create_buffer(parent, name)
self.invisible = True
self.name = name
self.type = self.buffer.type
self.session = sessionObject
self.compose_function = getattr(compose, compose_func)
log.debug("Compose_function: %s" % (self.compose_function,))
self.account = account
self.buffer.account = account
self.bind_events()
self.sound = sound
if "-timeline" in self.name or "-followers" in self.name or "-following" in self.name:
self.finished_timeline = False
def __init__(self, parent=None, function=None, session=None, *args, **kwargs): def create_buffer(self, parent, name):
"""Inits the main controller for this buffer: self.buffer = buffers.mastodon.basePanel(parent, name)
@ parent wx.Treebook object: Container where we will put this buffer.
@ function str or None: function to be called periodically and update items on this buffer.
@ session sessionmanager.session object or None: Session handler for settings, database and data access.
"""
super(Buffer, self).__init__()
self.function = function
# Compose_function will be used to render an object on this buffer. Normally, signature is as follows:
# compose_function(item, db, relative_times, show_screen_names=False, session=None)
# Read more about compose functions in sessions/twitter/compose.py.
self.compose_function = None
self.args = args
self.kwargs = kwargs
# This will be used as a reference to the wx.Panel object wich stores the buffer GUI.
self.buffer = None
# This should countains the account associated to this buffer.
self.account = ""
# This controls whether the start_stream function should be called when starting the program.
self.needs_init = True
# if this is set to False, the buffer will be ignored on the invisible interface.
self.invisible = False
# Control variable, used to track time of execution for calls to start_stream.
self.execution_time = 0
def clear_list(self): def get_buffer_name(self):
pass """ Get buffer name from a set of different techniques."""
# firstly let's take the easier buffers.
def get_event(self, ev): basic_buffers = dict(home_timeline=_("Home"), local_timeline=_("Local"), federated_timeline=_("Federated"), mentions=_("Mentions"), bookmarks=_("Bookmarks"), direct_messages=_("Direct messages"), sent=_("Sent"), favorites=_("Favorites"), followers=_("Followers"), following=_("Following"), blocked=_("Blocked users"), muted=_("Muted users"), notifications=_("Notifications"))
""" Catch key presses in the WX interface and generate the corresponding event names.""" if self.name in list(basic_buffers.keys()):
if ev.GetKeyCode() == wx.WXK_RETURN and ev.ControlDown(): event = "audio" return basic_buffers[self.name]
elif ev.GetKeyCode() == wx.WXK_RETURN: event = "url" # Check user timelines
elif ev.GetKeyCode() == wx.WXK_F5: event = "volume_down" elif hasattr(self, "username"):
elif ev.GetKeyCode() == wx.WXK_F6: event = "volume_up" if "-timeline" in self.name:
elif ev.GetKeyCode() == wx.WXK_DELETE and ev.ShiftDown(): event = "clear_list" return _(u"{username}'s timeline").format(username=self.username,)
elif ev.GetKeyCode() == wx.WXK_DELETE: event = "destroy_status" elif "-followers" in self.name:
# Raise a Special event when pressed Shift+F10 because Wx==4.1.x does not seems to trigger this by itself. return _(u"{username}'s followers").format(username=self.username,)
# See https://github.com/manuelcortez/TWBlue/issues/353 elif "-following" in self.name:
elif ev.GetKeyCode() == wx.WXK_F10 and ev.ShiftDown(): event = "show_menu" return _(u"{username}'s following").format(username=self.username,)
else: log.error("Error getting name for buffer %s" % (self.name,))
event = None return _(u"Unknown buffer")
ev.Skip()
if event != None:
try:
### ToDo: Remove after WX fixes issue #353 in the widgets.
if event == "show_menu":
return self.show_menu(widgetUtils.MENU, pos=self.buffer.list.list.GetPosition())
getattr(self, event)()
except AttributeError:
pass
def volume_down(self):
""" Decreases volume by 5%"""
if self.session.settings["sound"]["volume"] > 0.0:
if self.session.settings["sound"]["volume"] <= 0.05:
self.session.settings["sound"]["volume"] = 0.0
else:
self.session.settings["sound"]["volume"] -=0.05
sound.URLPlayer.player.audio_set_volume(int(self.session.settings["sound"]["volume"]*100.0))
self.session.sound.play("volume_changed.ogg")
self.session.settings.write()
def volume_up(self):
""" Increases volume by 5%."""
if self.session.settings["sound"]["volume"] < 1.0:
if self.session.settings["sound"]["volume"] >= 0.95:
self.session.settings["sound"]["volume"] = 1.0
else:
self.session.settings["sound"]["volume"] +=0.05
sound.URLPlayer.player.audio_set_volume(int(self.session.settings["sound"]["volume"]*100))
self.session.sound.play("volume_changed.ogg")
self.session.settings.write()
def start_stream(self, mandatory=False, play_sound=True):
pass
def get_more_items(self):
output.speak(_(u"This action is not supported for this buffer"), True)
def put_items_on_list(self, items):
pass
def remove_buffer(self):
return False
def remove_item(self, item):
f = self.buffer.list.get_selected()
self.buffer.list.remove_item(item)
self.buffer.list.select_item(f)
def bind_events(self):
pass
def get_object(self):
return self.buffer
def get_message(self):
pass
def set_list_position(self, reversed=False):
if reversed == False:
self.buffer.list.select_item(-1)
else:
self.buffer.list.select_item(0)
def reply(self):
pass
def send_message(self):
pass
def share_item(self):
pass
def can_share(self):
pass
def destroy_status(self):
pass
def post_status(self, *args, **kwargs): def post_status(self, *args, **kwargs):
title = _("Post")
caption = _("Write your post here")
post = messages.post(session=self.session, title=title, caption=caption)
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, visibility=post.get_visibility(), **kwargs)
if hasattr(post.message, "destroy"):
post.message.destroy()
def get_formatted_message(self):
return self.compose_function(self.get_item(), self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])[1]
def get_message(self):
post = self.get_item()
if post == None:
return
template = self.session.settings["templates"]["post"]
t = templates.render_post(post, template, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"])
return t
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type))
log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs))
count = self.session.settings["general"]["max_posts_per_call"]
min_id = None
# toDo: Implement reverse timelines properly here.
if (self.name != "favorites" and self.name != "bookmarks") and self.name in self.session.db and len(self.session.db[self.name]) > 0:
min_id = self.session.db[self.name][-1].id
try:
results = getattr(self.session.api, self.function)(min_id=min_id, limit=count, *self.args, **self.kwargs)
results.reverse()
except Exception as e:
log.exception("Error %s" % (str(e)))
return
number_of_items = self.session.order_buffer(self.name, results)
log.debug("Number of items retrieved: %d" % (number_of_items,))
if hasattr(self, "finished_timeline") and self.finished_timeline == False:
if "-timeline" in self.name:
self.username = self.session.db[self.name][0]["account"].username
self.finished_timeline = True
self.put_items_on_list(number_of_items)
if number_of_items > 0 and self.name != "sent_posts" and self.name != "sent_direct_messages" and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True:
self.session.sound.play(self.sound)
# Autoread settings
if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(number_of_items)
return number_of_items
def auto_read(self, number_of_items):
if number_of_items == 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False:
if self.session.settings["general"]["reverse_timelines"] == False:
post = self.session.db[self.name][-1]
else:
post = self.session.db[self.name][0]
output.speak(_("New post in {0}").format(self.get_buffer_name()))
output.speak(" ".join(self.compose_function(post, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])))
elif number_of_items > 1 and self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False:
output.speak(_("{0} new posts in {1}.").format(number_of_items, self.get_buffer_name()))
def get_more_items(self):
elements = []
if self.session.settings["general"]["reverse_timelines"] == False:
max_id = self.session.db[self.name][0].id
else:
max_id = self.session.db[self.name][-1].id
try:
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], *self.args, **self.kwargs)
except Exception as e:
log.exception("Error %s" % (str(e)))
return
items_db = self.session.db[self.name]
for i in items:
if utils.find_item(i, self.session.db[self.name]) == None:
elements.append(i)
if self.session.settings["general"]["reverse_timelines"] == False:
items_db.insert(0, i)
else:
items_db.append(i)
self.session.db[self.name] = items_db
selection = self.buffer.list.get_selected()
log.debug("Retrieved %d items from cursored search in function %s." % (len(elements), self.function))
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *post)
else:
for i in items:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)
output.speak(_(u"%s items retrieved") % (str(len(elements))), True)
def remove_buffer(self, force=False):
if "-timeline" in self.name:
if force == False:
dlg = commonMessageDialogs.remove_buffer()
else:
dlg = widgetUtils.YES
if dlg == widgetUtils.YES:
if self.kwargs.get("id") in self.session.settings["other_buffers"]["timelines"]:
self.session.settings["other_buffers"]["timelines"].remove(self.kwargs.get("id"))
self.session.settings.write()
if self.name in self.session.db:
self.session.db.pop(self.name)
return True
elif dlg == widgetUtils.NO:
return False
else:
output.speak(_(u"This buffer is not a timeline; it can't be deleted."), True)
return False
def put_items_on_list(self, number_of_items):
list_to_use = self.session.db[self.name]
if number_of_items == 0 and self.session.settings["general"]["persist_size"] == 0: return
log.debug("The list contains %d items " % (self.buffer.list.get_count(),))
log.debug("Putting %d items on the list" % (number_of_items,))
if self.buffer.list.get_count() == 0:
for i in list_to_use:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
self.buffer.set_position(self.session.settings["general"]["reverse_timelines"])
elif self.buffer.list.get_count() > 0 and number_of_items > 0:
if self.session.settings["general"]["reverse_timelines"] == False:
items = list_to_use[len(list_to_use)-number_of_items:]
for i in items:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
else:
items = list_to_use[0:number_of_items]
items.reverse()
for i in items:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *post)
log.debug("Now the list contains %d items " % (self.buffer.list.get_count(),))
def add_new_item(self, item):
post = self.compose_function(item, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
if self.session.settings["general"]["reverse_timelines"] == False:
self.buffer.list.insert_item(False, *post)
else:
self.buffer.list.insert_item(True, *post)
if self.name in self.session.settings["other_buffers"]["autoread_buffers"] and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and self.session.settings["sound"]["session_mute"] == False:
output.speak(" ".join(post[:2]), speech=self.session.settings["reporting"]["speech_reporting"], braille=self.session.settings["reporting"]["braille_reporting"])
def update_item(self, item, position):
post = self.compose_function(item, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.list.SetItem(position, 1, post[1])
def bind_events(self):
log.debug("Binding events...")
self.buffer.set_focus_function(self.onFocus)
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_status, self.buffer.post)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.share_item, self.buffer.boost)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.send_message, self.buffer.dm)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.reply, self.buffer.reply)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.toggle_favorite, self.buffer.fav)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.toggle_bookmark, self.buffer.bookmark)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_KEY_DOWN, self.show_menu_by_key)
def show_menu(self, ev, pos=0, *args, **kwargs):
if self.buffer.list.get_count() == 0:
return
menu = menus.base()
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.fav, menuitem=menu.fav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.unfav, menuitem=menu.unfav)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.url_, menuitem=menu.openUrl)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.audio, menuitem=menu.play)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.view, menuitem=menu.view)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.copy, menuitem=menu.copy)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.destroy_status, menuitem=menu.remove)
if hasattr(menu, "openInBrowser"):
widgetUtils.connect_event(menu, widgetUtils.MENU, self.open_in_browser, menuitem=menu.openInBrowser)
if pos != 0:
self.buffer.PopupMenu(menu, pos)
else:
self.buffer.PopupMenu(menu, self.buffer.list.list.GetPosition())
def view(self, *args, **kwargs):
pub.sendMessage("execute-action", action="view_item")
def copy(self, *args, **kwargs):
pub.sendMessage("execute-action", action="copy_to_clipboard")
def user_actions(self, *args, **kwargs):
pub.sendMessage("execute-action", action="follow")
def fav(self, *args, **kwargs):
pub.sendMessage("execute-action", action="add_to_favourites")
def unfav(self, *args, **kwargs):
pub.sendMessage("execute-action", action="remove_from_favourites")
def delete_item_(self, *args, **kwargs):
pub.sendMessage("execute-action", action="delete_item")
def url_(self, *args, **kwargs):
self.url()
def show_menu_by_key(self, ev):
if self.buffer.list.get_count() == 0:
return
if ev.GetKeyCode() == wx.WXK_WINDOWS_MENU:
self.show_menu(widgetUtils.MENU, pos=self.buffer.list.list.GetPosition())
def get_item(self):
index = self.buffer.list.get_selected()
if index > -1 and self.session.db.get(self.name) != None:
return self.session.db[self.name][index]
def can_share(self):
post = self.get_item()
if post.visibility == "direct":
return False
return True
def reply(self, *args):
item = self.get_item()
visibility = item.visibility
if visibility == "direct":
title = _("Conversation with {}").format(item.account.username)
caption = _("Write your message here")
else:
title = _("Reply to {}").format(item.account.username)
caption = _("Write your reply here")
if item.reblog != None:
users = ["@{} ".format(user.acct) for user in item.reblog.mentions if user.id != self.session.db["user_id"]]
if item.reblog.account.acct != item.account.acct and "@{} ".format(item.reblog.account.acct) not in users:
users.append("@{} ".format(item.reblog.account.acct))
else:
users = ["@{} ".format(user.acct) for user in item.mentions if user.id != self.session.db["user_id"]]
if "@{} ".format(item.account.acct) not in users and item.account.id != self.session.db["user_id"]:
users.insert(0, "@{} ".format(item.account.acct))
users_str = "".join(users)
post = messages.post(session=self.session, title=title, caption=caption, text=users_str)
visibility_settings = dict(public=0, unlisted=1, private=2, direct=3)
post.message.visibility.SetSelection(visibility_settings.get(visibility))
# Respect content warning settings.
if item.sensitive:
post.message.sensitive.SetValue(item.sensitive)
post.message.spoiler.ChangeValue(item.spoiler_text)
post.message.on_sensitivity_changed()
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, reply_to=item.id, posts=post_data, visibility=post.get_visibility())
if hasattr(post.message, "destroy"):
post.message.destroy()
def send_message(self, *args, **kwargs):
item = self.get_item()
title = _("Conversation with {}").format(item.account.username)
caption = _("Write your message here")
if item.reblog != None:
users = ["@{} ".format(user.acct) for user in item.reblog.mentions if user.id != self.session.db["user_id"]]
if item.reblog.account.acct != item.account.acct and "@{} ".format(item.reblog.account.acct) not in users:
users.append("@{} ".format(item.reblog.account.acct))
else:
users = ["@{} ".format(user.acct) for user in item.mentions if user.id != self.session.db["user_id"]]
if item.account.acct not in users and item.account.id != self.session.db["user_id"]:
users.insert(0, "@{} ".format(item.account.acct))
users_str = "".join(users)
post = messages.post(session=self.session, title=title, caption=caption, text=users_str)
post.message.visibility.SetSelection(3)
if item.sensitive:
post.message.sensitive.SetValue(item.sensitive)
post.message.spoiler.ChangeValue(item.spoiler_text)
post.message.on_sensitivity_changed()
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, visibility="direct", reply_to=item.id)
if hasattr(post.message, "destroy"):
post.message.destroy()
def share_item(self, *args, **kwargs):
if self.can_share() == False:
return output.speak(_("This action is not supported on conversation posts."))
post = self.get_item()
id = post.id
if self.session.settings["general"]["boost_mode"] == "ask":
answer = mastodon_dialogs.boost_question()
if answer == True:
self._direct_boost(id)
else:
self._direct_boost(id)
def _direct_boost(self, id):
item = self.session.api_call(call_name="status_reblog", _sound="retweet_send.ogg", id=id)
def onFocus(self, *args, **kwargs):
post = self.get_item()
if self.session.settings["general"]["relative_times"] == True:
original_date = arrow.get(self.session.db[self.name][self.buffer.list.get_selected()].created_at)
ts = original_date.humanize(locale=languageHandler.getLanguage())
self.buffer.list.list.SetItem(self.buffer.list.get_selected(), 2, ts)
if self.session.settings['sound']['indicate_audio'] and utils.is_audio_or_video(post):
self.session.sound.play("audio.ogg")
if self.session.settings['sound']['indicate_img'] and utils.is_image(post):
self.session.sound.play("image.ogg")
can_share = self.can_share()
pub.sendMessage("toggleShare", shareable=can_share)
self.buffer.boost.Enable(can_share)
def audio(self, url='', *args, **kwargs):
if sound.URLPlayer.player.is_playing():
return sound.URLPlayer.stop_audio()
item = self.get_item()
if item == None:
return
urls = utils.get_media_urls(item)
if len(urls) == 1:
url=urls[0]
elif len(urls) > 1:
urls_list = dialogs.urlList.urlList()
urls_list.populate_list(urls)
if urls_list.get_response() == widgetUtils.OK:
url=urls_list.get_string()
if hasattr(urls_list, "destroy"): urls_list.destroy()
if url != '':
# try:
sound.URLPlayer.play(url, self.session.settings["sound"]["volume"])
# except:
# log.error("Exception while executing audio method.")
def url(self, url='', announce=True, *args, **kwargs):
if url == '':
post = self.get_item()
if post.reblog != None:
urls = utils.find_urls(post.reblog)
else:
urls = utils.find_urls(post)
if len(urls) == 1:
url=urls[0]
elif len(urls) > 1:
urls_list = dialogs.urlList.urlList()
urls_list.populate_list(urls)
if urls_list.get_response() == widgetUtils.OK:
url=urls_list.get_string()
if hasattr(urls_list, "destroy"): urls_list.destroy()
if url != '':
if announce:
output.speak(_(u"Opening URL..."), True)
webbrowser.open_new_tab(url)
def clear_list(self):
dlg = commonMessageDialogs.clear_list()
if dlg == widgetUtils.YES:
self.session.db[self.name] = []
self.buffer.list.clear()
def destroy_status(self, *args, **kwargs):
index = self.buffer.list.get_selected()
item = self.session.db[self.name][index]
if item.account.id != self.session.db["user_id"] or item.reblog != None:
output.speak(_("You can delete only your own posts."))
return
answer = mastodon_dialogs.delete_post_dialog()
if answer == True:
items = self.session.db[self.name]
try:
self.session.api.status_delete(id=item.id)
items.pop(index)
self.buffer.list.remove_item(index)
except Exception as e:
self.session.sound.play("error.ogg")
self.session.db[self.name] = items
def user_details(self):
item = self.get_item()
pass pass
def save_positions(self): def get_item_url(self):
try: post = self.get_item()
self.session.db[self.name+"_pos"]=self.buffer.list.get_selected() if post.reblog != None:
except AttributeError: return post.reblog.url
return post.url
def open_in_browser(self, *args, **kwargs):
url = self.get_item_url()
output.speak(_("Opening item in web browser..."))
webbrowser.open(url)
def add_to_favorites(self):
item = self.get_item()
if item.reblog != None:
item = item.reblog
call_threaded(self.session.api_call, call_name="status_favourite", preexec_message=_("Adding to favorites..."), _sound="favourite.ogg", id=item.id)
def remove_from_favorites(self):
item = self.get_item()
if item.reblog != None:
item = item.reblog
call_threaded(self.session.api_call, call_name="status_unfavourite", preexec_message=_("Removing from favorites..."), _sound="favourite.ogg", id=item.id)
def toggle_favorite(self, *args, **kwargs):
item = self.get_item()
if item.reblog != None:
item = item.reblog
item = self.session.api.status(item.id)
if item.favourited == False:
call_threaded(self.session.api_call, call_name="status_favourite", preexec_message=_("Adding to favorites..."), _sound="favourite.ogg", id=item.id)
else:
call_threaded(self.session.api_call, call_name="status_unfavourite", preexec_message=_("Removing from favorites..."), _sound="favourite.ogg", id=item.id)
def toggle_bookmark(self, *args, **kwargs):
item = self.get_item()
if item.reblog != None:
item = item.reblog
item = self.session.api.status(item.id)
if item.bookmarked == False:
call_threaded(self.session.api_call, call_name="status_bookmark", preexec_message=_("Adding to bookmarks..."), _sound="favourite.ogg", id=item.id)
else:
call_threaded(self.session.api_call, call_name="status_unbookmark", preexec_message=_("Removing from bookmarks..."), _sound="favourite.ogg", id=item.id)
def view_item(self):
post = self.get_item()
# Update object so we can retrieve newer stats
post = self.session.api.status(id=post.id)
print(post)
msg = messages.viewPost(post, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url())
def ocr_image(self):
post = self.get_item()
media_list = []
pass pass

View File

@ -0,0 +1,241 @@
# -*- coding: utf-8 -*-
import time
import logging
import wx
import widgetUtils
import output
from controller.mastodon import messages
from controller.buffers.mastodon.base import BaseBuffer
from mysc.thread_utils import call_threaded
from sessions.mastodon import utils, templates
from wxUI import buffers, commonMessageDialogs
log = logging.getLogger("controller.buffers.mastodon.conversations")
class ConversationListBuffer(BaseBuffer):
def create_buffer(self, parent, name):
self.buffer = buffers.mastodon.conversationListPanel(parent, name)
def get_item(self):
index = self.buffer.list.get_selected()
if index > -1 and self.session.db.get(self.name) != None and len(self.session.db[self.name]) > index:
return self.session.db[self.name][index]["last_status"]
def get_conversation(self):
index = self.buffer.list.get_selected()
if index > -1 and self.session.db.get(self.name) != None:
return self.session.db[self.name][index]
def get_formatted_message(self):
return self.compose_function(self.get_conversation(), self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])[1]
def get_message(self):
conversation = self.get_conversation()
if conversation == None:
return
template = self.session.settings["templates"]["conversation"]
post_template = self.session.settings["templates"]["post"]
t = templates.render_conversation(conversation=conversation, template=template, post_template=post_template, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"])
return t
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type))
log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs))
count = self.session.settings["general"]["max_posts_per_call"]
min_id = None
# toDo: Implement reverse timelines properly here.
# if (self.name != "favorites" and self.name != "bookmarks") and self.name in self.session.db and len(self.session.db[self.name]) > 0:
# min_id = self.session.db[self.name][-1].id
try:
results = getattr(self.session.api, self.function)(min_id=min_id, limit=count, *self.args, **self.kwargs)
results.reverse()
except Exception as e:
log.exception("Error %s" % (str(e)))
return
new_position, number_of_items = self.order_buffer(results)
log.debug("Number of items retrieved: %d" % (number_of_items,))
self.put_items_on_list(number_of_items)
if new_position > -1:
self.buffer.list.select_item(new_position)
if number_of_items > 0 and self.name != "sent_posts" and self.name != "sent_direct_messages" and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True:
self.session.sound.play(self.sound)
# Autoread settings
if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(number_of_items)
return number_of_items
def get_more_items(self):
elements = []
if self.session.settings["general"]["reverse_timelines"] == False:
max_id = self.session.db[self.name][0].last_status.id
else:
max_id = self.session.db[self.name][-1].last_status.id
try:
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], *self.args, **self.kwargs)
except Exception as e:
log.exception("Error %s" % (str(e)))
return
items_db = self.session.db[self.name]
for i in items:
if utils.find_item(i, self.session.db[self.name]) == None:
elements.append(i)
if self.session.settings["general"]["reverse_timelines"] == False:
items_db.insert(0, i)
else:
items_db.append(i)
self.session.db[self.name] = items_db
selection = self.buffer.list.get_selected()
log.debug("Retrieved %d items from cursored search in function %s." % (len(elements), self.function))
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
conversation = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *conversation)
else:
for i in items:
conversation = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *conversation)
self.buffer.list.select_item(selection)
output.speak(_(u"%s items retrieved") % (str(len(elements))), True)
def get_item_position(self, conversation):
for i in range(len(self.session.db[self.name])):
if self.session.db[self.name][i].id == conversation.id:
return i
def order_buffer(self, data):
num = 0
focus_object = None
if self.session.db.get(self.name) == None:
self.session.db[self.name] = []
objects = self.session.db[self.name]
for i in data:
position = self.get_item_position(i)
if position != None:
conversation = self.session.db[self.name][position]
if conversation.last_status.id != i.last_status.id:
focus_object = i
objects.pop(position)
self.buffer.list.remove_item(position)
if self.session.settings["general"]["reverse_timelines"] == False:
objects.append(i)
else:
objects.insert(0, i)
num = num+1
else:
if self.session.settings["general"]["reverse_timelines"] == False:
objects.append(i)
else:
objects.insert(0, i)
num = num+1
self.session.db[self.name] = objects
if focus_object == None:
return (-1, num)
new_position = self.get_item_position(focus_object)
if new_position != None:
return (new_position, num)
return (-1, num)
def bind_events(self):
log.debug("Binding events...")
self.buffer.set_focus_function(self.onFocus)
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_status, self.buffer.post)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.reply, self.buffer.reply)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_KEY_DOWN, self.show_menu_by_key)
def fav(self):
pass
def unfav(self):
pass
def can_share(self):
return False
def send_message(self):
return self.reply()
def onFocus(self, *args, **kwargs):
post = self.get_item()
if self.session.settings['sound']['indicate_audio'] and utils.is_audio_or_video(post):
self.session.sound.play("audio.ogg")
if self.session.settings['sound']['indicate_img'] and utils.is_image(post):
self.session.sound.play("image.ogg")
def destroy_status(self):
pass
def reply(self, *args):
item = self.get_item()
conversation = self.get_conversation()
visibility = item.visibility
title = _("Reply to conversation with {}").format(conversation.accounts[0].username)
caption = _("Write your message here")
users = ["@{} ".format(user.acct) for user in conversation.accounts]
users_str = "".join(users)
post = messages.post(session=self.session, title=title, caption=caption, text=users_str)
visibility_settings = dict(public=0, unlisted=1, private=2, direct=3)
post.message.visibility.SetSelection(visibility_settings.get(visibility))
if item.sensitive:
post.message.sensitive.SetValue(item.sensitive)
post.message.spoiler.ChangeValue(item.spoiler_text)
post.message.on_sensitivity_changed()
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, reply_to=item.id, posts=post_data, visibility=visibility)
if hasattr(post.message, "destroy"):
post.message.destroy()
class ConversationBuffer(BaseBuffer):
def __init__(self, post, *args, **kwargs):
self.post = post
super(ConversationBuffer, self).__init__(*args, **kwargs)
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type))
log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs))
self.post = self.session.api.status(id=self.post.id)
# toDo: Implement reverse timelines properly here.
try:
results = []
items = getattr(self.session.api, self.function)(*self.args, **self.kwargs)
[results.append(item) for item in items.ancestors]
results.append(self.post)
[results.append(item) for item in items.descendants]
except Exception as e:
log.exception("Error %s" % (str(e)))
return
number_of_items = self.session.order_buffer(self.name, results)
log.debug("Number of items retrieved: %d" % (number_of_items,))
self.put_items_on_list(number_of_items)
if number_of_items > 0 and self.name != "sent_posts" and self.name != "sent_direct_messages" and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True:
self.session.sound.play(self.sound)
# Autoread settings
if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(number_of_items)
return number_of_items
def get_more_items(self):
output.speak(_(u"This action is not supported for this buffer"), True)
def remove_buffer(self, force=False):
if force == False:
dlg = commonMessageDialogs.remove_buffer()
else:
dlg = widgetUtils.YES
if dlg == widgetUtils.YES:
if self.name in self.session.db:
self.session.db.pop(self.name)
return True
elif dlg == widgetUtils.NO:
return False

View File

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
import time
import logging
from controller.buffers.mastodon.base import BaseBuffer
from sessions.mastodon import utils
log = logging.getLogger("controller.buffers.mastodon.mentions")
class MentionsBuffer(BaseBuffer):
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type))
log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs))
count = self.session.settings["general"]["max_posts_per_call"]
min_id = None
# toDo: Implement reverse timelines properly here.
# if self.name != "favorites" and self.name in self.session.db and len(self.session.db[self.name]) > 0:
# min_id = self.session.db[self.name][-1].id
try:
items = getattr(self.session.api, self.function)(min_id=min_id, limit=count, exclude_types=["follow", "favourite", "reblog", "poll", "follow_request"], *self.args, **self.kwargs)
items.reverse()
results = [item.status for item in items if item.get("status") and item.type == "mention"]
except Exception as e:
log.exception("Error %s" % (str(e)))
return
number_of_items = self.session.order_buffer(self.name, results)
log.debug("Number of items retrieved: %d" % (number_of_items,))
self.put_items_on_list(number_of_items)
if number_of_items > 0 and self.name != "sent_posts" and self.name != "sent_direct_messages" and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True:
self.session.sound.play(self.sound)
# Autoread settings
if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(number_of_items)
return number_of_items
def get_more_items(self):
elements = []
if self.session.settings["general"]["reverse_timelines"] == False:
max_id = self.session.db[self.name][0].id
else:
max_id = self.session.db[self.name][-1].id
try:
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], exclude_types=["follow", "favourite", "reblog", "poll", "follow_request"], *self.args, **self.kwargs)
items = [item.status for item in items if item.get("status") and item.type == "mention"]
except Exception as e:
log.exception("Error %s" % (str(e)))
return
items_db = self.session.db[self.name]
for i in items:
if utils.find_item(i, self.session.db[self.name]) == None:
elements.append(i)
if self.session.settings["general"]["reverse_timelines"] == False:
items_db.insert(0, i)
else:
items_db.append(i)
self.session.db[self.name] = items_db
selection = self.buffer.list.get_selected()
log.debug("Retrieved %d items from cursored search in function %s." % (len(elements), self.function))
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *post)
else:
for i in items:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)
output.speak(_(u"%s items retrieved") % (str(len(elements))), True)

View File

@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
import time
import logging
import wx
import widgetUtils
import output
from mysc.thread_utils import call_threaded
from controller.buffers.mastodon.base import BaseBuffer
from controller.mastodon import messages
from sessions.mastodon import templates, utils
from wxUI import buffers, commonMessageDialogs
log = logging.getLogger("controller.buffers.mastodon.conversations")
class UserBuffer(BaseBuffer):
def create_buffer(self, parent, name):
self.buffer = buffers.mastodon.userPanel(parent, name)
def get_message(self):
user = self.get_item()
if user == None:
return
template = self.session.settings["templates"]["person"]
t = templates.render_user(user=user, template=template, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"])
return t
def bind_events(self):
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.post_status, self.buffer.post)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.send_message, self.buffer.message)
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.user_actions, self.buffer.actions)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu)
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_KEY_DOWN, self.show_menu_by_key)
def fav(self):
pass
def unfav(self):
pass
def can_share(self):
return False
def reply(self, *args, **kwargs):
return self.send_message()
def send_message(self, *args, **kwargs):
item = self.get_item()
title = _("New conversation with {}").format(item.username)
caption = _("Write your message here")
users_str = "@{} ".format(item.acct)
post = messages.post(session=self.session, title=title, caption=caption, text=users_str)
post.message.visibility.SetSelection(3)
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, visibility="direct")
if hasattr(post.message, "destroy"):
post.message.destroy()
def audio(self):
pass
def url(self):
pass
def destroy_status(self):
pass
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type))
log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs))
count = self.session.settings["general"]["max_posts_per_call"]
# toDo: Implement reverse timelines properly here.
try:
results = getattr(self.session.api, self.function)(limit=count, *self.args, **self.kwargs)
if hasattr(results, "_pagination_next") and self.name not in self.session.db["pagination_info"]:
self.session.db["pagination_info"][self.name] = results._pagination_next
results.reverse()
except Exception as e:
log.exception("Error %s" % (str(e)))
return
number_of_items = self.session.order_buffer(self.name, results)
log.debug("Number of items retrieved: %d" % (number_of_items,))
if hasattr(self, "finished_timeline") and self.finished_timeline == False:
if "-followers" in self.name or "-following" in self.name:
self.username = self.session.api.account(id=self.kwargs.get("id")).username
self.finished_timeline = True
self.put_items_on_list(number_of_items)
if number_of_items > 0 and self.name != "sent_posts" and self.name != "sent_direct_messages" and self.sound != None and self.session.settings["sound"]["session_mute"] == False and self.name not in self.session.settings["other_buffers"]["muted_buffers"] and play_sound == True:
self.session.sound.play(self.sound)
# Autoread settings
if avoid_autoreading == False and mandatory == True and number_of_items > 0 and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(number_of_items)
return number_of_items
def get_more_items(self):
elements = []
pagination_info = self.session.db["pagination_info"].get(self.name)
if pagination_info == None:
output.speak(_("There are no more items in this buffer."))
return
try:
items = self.session.api.fetch_next(pagination_info)
if hasattr(items, "_pagination_next"):
self.session.db["pagination_info"][self.name] = items._pagination_next
except Exception as e:
log.exception("Error %s" % (str(e)))
return
items_db = self.session.db[self.name]
for i in items:
if utils.find_item(i, self.session.db[self.name]) == None:
elements.append(i)
if self.session.settings["general"]["reverse_timelines"] == False:
items_db.insert(0, i)
else:
items_db.append(i)
self.session.db[self.name] = items_db
selection = self.buffer.list.get_selected()
log.debug("Retrieved %d items from cursored search in function %s." % (len(elements), self.function))
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *post)
else:
for i in items:
post = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)
output.speak(_(u"%s items retrieved") % (str(len(elements))), True)
def get_item_url(self):
item = self.get_item()
return item.url
def user_details(self):
item = self.get_item()
pass
def add_to_favorites(self):
pass
def remove_from_favorites(self):
pass
def toggle_favorite(self):
pass
def view_item(self):
item = self.get_item()
print(item)
def ocr_image(self):
pass
def remove_buffer(self, force=False):
if "-followers" in self.name:
if force == False:
dlg = commonMessageDialogs.remove_buffer()
else:
dlg = widgetUtils.YES
if dlg == widgetUtils.YES:
if self.kwargs.get("id") in self.session.settings["other_buffers"]["followers_timelines"]:
self.session.settings["other_buffers"]["followers_timelines"].remove(self.kwargs.get("id"))
self.session.settings.write()
if self.name in self.session.db:
self.session.db.pop(self.name)
return True
elif dlg == widgetUtils.NO:
return False
elif "-following" in self.name:
if force == False:
dlg = commonMessageDialogs.remove_buffer()
else:
dlg = widgetUtils.YES
if dlg == widgetUtils.YES:
if self.kwargs.get("id") in self.session.settings["other_buffers"]["following_timelines"]:
self.session.settings["other_buffers"]["following_timelines"].remove(self.kwargs.get("id"))
self.session.settings.write()
if self.name in self.session.db:
self.session.db.pop(self.name)
return True
elif dlg == widgetUtils.NO:
return False
elif "-searchUser" in self.name:
if force == False:
dlg = commonMessageDialogs.remove_buffer()
else:
dlg = widgetUtils.YES
if dlg == widgetUtils.YES:
if self.name in self.session.db:
self.session.db.pop(self.name)
return True
elif dlg == widgetUtils.NO:
return False
else:
output.speak(_(u"This buffer is not a timeline; it can't be deleted."), True)
return False

View File

@ -1,9 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import time import time
import wx import wx
from wxUI import buffers, dialogs, commonMessageDialogs, menus
from controller import user
from controller import messages
import widgetUtils import widgetUtils
import arrow import arrow
import webbrowser import webbrowser
@ -19,7 +16,10 @@ from mysc.thread_utils import call_threaded
from tweepy.errors import TweepyException from tweepy.errors import TweepyException
from tweepy.cursor import Cursor from tweepy.cursor import Cursor
from pubsub import pub from pubsub import pub
from extra import ocr
from sessions.twitter.long_tweets import twishort, tweets from sessions.twitter.long_tweets import twishort, tweets
from wxUI import buffers, dialogs, commonMessageDialogs, menus
from controller.twitter import user, messages
log = logging.getLogger("controller.buffers") log = logging.getLogger("controller.buffers")
@ -35,9 +35,9 @@ class BaseBuffer(base.Buffer):
super(BaseBuffer, self).__init__(parent, function, *args, **kwargs) super(BaseBuffer, self).__init__(parent, function, *args, **kwargs)
log.debug("Initializing buffer %s, account %s" % (name, account,)) log.debug("Initializing buffer %s, account %s" % (name, account,))
if bufferType != None: if bufferType != None:
self.buffer = getattr(buffers, bufferType)(parent, name) self.buffer = getattr(buffers.twitter, bufferType)(parent, name)
else: else:
self.buffer = buffers.basePanel(parent, name) self.buffer = buffers.twitter.basePanel(parent, name)
self.invisible = True self.invisible = True
self.name = name self.name = name
self.type = self.buffer.type self.type = self.buffer.type
@ -471,6 +471,7 @@ class BaseBuffer(base.Buffer):
def onFocus(self, *args, **kwargs): def onFocus(self, *args, **kwargs):
tweet = self.get_tweet() tweet = self.get_tweet()
if self.session.settings["general"]["relative_times"] == True:
# fix this: # fix this:
original_date = arrow.get(self.session.db[self.name][self.buffer.list.get_selected()].created_at, locale="en") original_date = arrow.get(self.session.db[self.name][self.buffer.list.get_selected()].created_at, locale="en")
ts = original_date.humanize(locale=languageHandler.getLanguage()) ts = original_date.humanize(locale=languageHandler.getLanguage())
@ -580,3 +581,83 @@ class BaseBuffer(base.Buffer):
url = self.get_item_url() url = self.get_item_url()
output.speak(_(u"Opening item in web browser...")) output.speak(_(u"Opening item in web browser..."))
webbrowser.open(url) webbrowser.open(url)
def add_to_favorites(self):
id = self.get_tweet().id
call_threaded(self.session.api_call, call_name="create_favorite", _sound="favourite.ogg", id=id)
def remove_from_favorites(self):
id = self.get_tweet().id
call_threaded(self.session.api_call, call_name="destroy_favorite", id=id)
def toggle_favorite(self):
id = self.get_tweet().id
tweet = self.session.twitter.get_status(id=id, include_ext_alt_text=True, tweet_mode="extended")
if tweet.favorited == False:
call_threaded(self.session.api_call, call_name="create_favorite", _sound="favourite.ogg", id=id)
else:
call_threaded(self.session.api_call, call_name="destroy_favorite", id=id)
def view_item(self):
if self.type == "dm" or self.name == "direct_messages":
non_tweet = self.get_formatted_message()
item = self.get_right_tweet()
original_date = arrow.get(int(item.created_timestamp))
date = original_date.shift(seconds=self.session.db["utc_offset"]).format(_(u"MMM D, YYYY. H:m"), locale=languageHandler.getLanguage())
msg = messages.viewTweet(non_tweet, [], False, date=date)
else:
tweet, tweetsList = self.get_full_tweet()
msg = messages.viewTweet(tweet, tweetsList, utc_offset=self.session.db["utc_offset"], item_url=self.get_item_url())
def reverse_geocode(self, geocoder):
try:
tweet = self.get_tweet()
if tweet.coordinates != None:
x = tweet.coordinates["coordinates"][0]
y = tweet.coordinates["coordinates"][1]
address = geocoder.reverse_geocode(y, x, language = languageHandler.curLang)
return address
else:
output.speak(_("There are no coordinates in this tweet"))
# except GeocoderError:
# output.speak(_(u"There are no results for the coordinates in this tweet"))
except ValueError:
output.speak(_(u"Error decoding coordinates. Try again later."))
except KeyError:
pass
except AttributeError:
pass
def ocr_image(self):
tweet = self.get_tweet()
media_list = []
if hasattr(tweet, "entities") and tweet.entities.get("media") != None:
[media_list.append(i) for i in tweet.entities["media"] if i not in media_list]
elif hasattr(tweet, "retweeted_status") and tweet.retweeted_status.get("media") != None:
[media_list.append(i) for i in tweet.retweeted_status.entities["media"] if i not in media_list]
elif hasattr(tweet, "quoted_status") and tweet.quoted_status.entities.get("media") != None:
[media_list.append(i) for i in tweet.quoted_status.entities["media"] if i not in media_list]
if len(media_list) > 1:
image_list = [_(u"Picture {0}").format(i,) for i in range(0, len(media_list))]
dialog = dialogs.urlList.urlList(title=_(u"Select the picture"))
if dialog.get_response() == widgetUtils.OK:
img = media_list[dialog.get_item()]
else:
return
elif len(media_list) == 1:
img = media_list[0]
else:
output.speak(_(u"Invalid buffer"))
return
if self.session.settings["mysc"]["ocr_language"] != "":
ocr_lang = self.session.settings["mysc"]["ocr_language"]
else:
ocr_lang = ocr.OCRSpace.short_langs.index(tweet.lang)
ocr_lang = ocr.OCRSpace.OcrLangs[ocr_lang]
api = ocr.OCRSpace.OCRSpaceAPI()
try:
text = api.OCR_URL(img["media_url"], lang=ocr_lang)
except ocr.OCRSpace.APIError as er:
output.speak(_(u"Unable to extract text"))
return
msg = messages.viewTweet(text["ParsedText"], [], False)

View File

@ -6,12 +6,12 @@ import output
import config import config
import languageHandler import languageHandler
import logging import logging
from controller import messages
from sessions.twitter import compose, utils, templates from sessions.twitter import compose, utils, templates
from mysc.thread_utils import call_threaded from mysc.thread_utils import call_threaded
from tweepy.errors import TweepyException from tweepy.errors import TweepyException
from pubsub import pub from pubsub import pub
from wxUI import commonMessageDialogs from wxUI import commonMessageDialogs
from controller.twitter import messages
from . import base from . import base
log = logging.getLogger("controller.buffers.twitter.dmBuffer") log = logging.getLogger("controller.buffers.twitter.dmBuffer")
@ -69,7 +69,7 @@ class DirectMessagesBuffer(base.BaseBuffer):
self.session.db["sent_direct_messages"] = sent_dms self.session.db["sent_direct_messages"] = sent_dms
user_ids = [item.message_create["sender_id"] for item in items] user_ids = [item.message_create["sender_id"] for item in items]
self.session.save_users(user_ids) self.session.save_users(user_ids)
pub.sendMessage("more-sent-dms", data=sent, account=self.session.db["user_name"]) pub.sendMessage("twitter.more_sent_dms", data=sent, account=self.session.db["user_name"])
selected = self.buffer.list.get_selected() selected = self.buffer.list.get_selected()
if self.session.settings["general"]["reverse_timelines"] == True: if self.session.settings["general"]["reverse_timelines"] == True:
for i in received: for i in received:
@ -162,3 +162,10 @@ class SentDirectMessagesBuffer(DirectMessagesBuffer):
dm = self.get_right_tweet() dm = self.get_right_tweet()
t = templates.render_dm(dm, template, self.session, relative_times=self.session.settings["general"]["relative_times"], offset_seconds=self.session.db["utc_offset"]) t = templates.render_dm(dm, template, self.session, relative_times=self.session.settings["general"]["relative_times"], offset_seconds=self.session.db["utc_offset"])
return t return t
def view_item(self):
non_tweet = self.get_formatted_message()
item = self.get_right_tweet()
original_date = arrow.get(int(item.created_timestamp))
date = original_date.shift(seconds=self.session.db["utc_offset"]).format(_(u"MMM D, YYYY. H:m"), locale=languageHandler.getLanguage())
msg = messages.viewTweet(non_tweet, [], False, date=date)

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from wxUI import dialogs, commonMessageDialogs
import widgetUtils import widgetUtils
import logging import logging
from tweepy.cursor import Cursor from tweepy.cursor import Cursor
from wxUI import dialogs, commonMessageDialogs
from . import base from . import base
log = logging.getLogger("controller.buffers.twitter.listBuffer") log = logging.getLogger("controller.buffers.twitter.listBuffer")

View File

@ -1,8 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import time import time
from wxUI import commonMessageDialogs, menus
from controller import user
from controller import messages
import widgetUtils import widgetUtils
import webbrowser import webbrowser
import output import output
@ -11,7 +8,9 @@ import logging
from mysc.thread_utils import call_threaded from mysc.thread_utils import call_threaded
from tweepy.errors import TweepyException from tweepy.errors import TweepyException
from pubsub import pub from pubsub import pub
from controller.twitter import user, messages
from sessions.twitter import compose, templates from sessions.twitter import compose, templates
from wxUI import commonMessageDialogs, menus
from . import base from . import base
log = logging.getLogger("controller.buffers.twitter.peopleBuffer") log = logging.getLogger("controller.buffers.twitter.peopleBuffer")
@ -248,3 +247,8 @@ class PeopleBuffer(base.BaseBuffer):
tweet = self.get_tweet() tweet = self.get_tweet()
url = "https://twitter.com/{screen_name}".format(screen_name=tweet.screen_name) url = "https://twitter.com/{screen_name}".format(screen_name=tweet.screen_name)
return url return url
def view_item(self):
item_url = self.get_item_url()
non_tweet = self.get_formatted_message()
msg = messages.viewTweet(non_tweet, [], False, item_url=item_url)

View File

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import time import time
import locale import locale
from wxUI import commonMessageDialogs
import widgetUtils import widgetUtils
import logging import logging
from tweepy.errors import TweepyException from tweepy.errors import TweepyException
from wxUI import commonMessageDialogs
from . import base, people from . import base, people
log = logging.getLogger("controller.buffers.twitter.searchBuffer") log = logging.getLogger("controller.buffers.twitter.searchBuffer")
@ -62,6 +62,10 @@ class ConversationBuffer(SearchBuffer):
last_thread_id = None last_thread_id = None
last_reply_id = None last_reply_id = None
def __init__(self, tweet, *args, **kwargs):
self.tweet = tweet
super(ConversationBuffer, self).__init__(*args, **kwargs)
def start_stream(self, start=False, mandatory=False, play_sound=True, avoid_autoreading=False): def start_stream(self, start=False, mandatory=False, play_sound=True, avoid_autoreading=False):
current_time = time.time() current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory == True: if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory == True:

View File

@ -1,15 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import time import time
import wx import wx
from wxUI import buffers, commonMessageDialogs, menus
from controller import user, messages
from controller import messages
import widgetUtils import widgetUtils
import output import output
import logging import logging
from mysc.thread_utils import call_threaded from mysc.thread_utils import call_threaded
from tweepy.errors import TweepyException from tweepy.errors import TweepyException
from pubsub import pub from pubsub import pub
from wxUI import buffers, commonMessageDialogs, menus
from controller.twitter import user, messages
from controller.buffers import base from controller.buffers import base
log = logging.getLogger("controller.buffers.twitter.trends") log = logging.getLogger("controller.buffers.twitter.trends")
@ -21,7 +20,7 @@ class TrendsBuffer(base.Buffer):
self.session = sessionObject self.session = sessionObject
self.account = account self.account = account
self.invisible = True self.invisible = True
self.buffer = buffers.trendsPanel(parent, name) self.buffer = buffers.twitter.trendsPanel(parent, name)
self.buffer.account = account self.buffer.account = account
self.type = self.buffer.type self.type = self.buffer.type
self.bind_events() self.bind_events()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
import wx
import logging
from pubsub import pub
from wxUI.dialogs.mastodon import dialogs
from wxUI.dialogs.mastodon import search as search_dialogs
from wxUI.dialogs.mastodon import dialogs
from wxUI import commonMessageDialogs
from sessions.twitter import utils
from . import userActions
log = logging.getLogger("controller.mastodon.handler")
class Handler(object):
def __init__(self):
super(Handler, self).__init__()
def create_buffers(self, session, createAccounts=True, controller=None):
session.get_user_info()
name = session.get_name()
if createAccounts == True:
pub.sendMessage("core.create_account", name=name, session_id=session.session_id, logged=True)
root_position =controller.view.search(name, name)
for i in session.settings['general']['buffer_order']:
if i == 'home':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Home"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="timeline_home", name="home_timeline", sessionObject=session, account=name, sound="tweet_received.ogg"))
elif i == 'local':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Local"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="timeline_local", name="local_timeline", sessionObject=session, account=name, sound="tweet_received.ogg"))
elif i == 'federated':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Federated"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="timeline_public", name="federated_timeline", sessionObject=session, account=name, sound="tweet_received.ogg"))
elif i == 'mentions':
pub.sendMessage("createBuffer", buffer_type="MentionsBuffer", session_type=session.type, buffer_title=_("Mentions"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="notifications", name="mentions", sessionObject=session, account=name, sound="mention_received.ogg"))
elif i == 'direct_messages':
pub.sendMessage("createBuffer", buffer_type="ConversationListBuffer", session_type=session.type, buffer_title=_("Direct messages"), parent_tab=root_position, start=False, kwargs=dict(compose_func="compose_conversation", parent=controller.view.nb, function="conversations", name="direct_messages", sessionObject=session, account=name, sound="dm_received.ogg"))
elif i == 'sent':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Sent"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="account_statuses", name="sent", sessionObject=session, account=name, sound="tweet_received.ogg", id=session.db["user_id"]))
elif i == 'favorites':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Favorites"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="favourites", name="favorites", sessionObject=session, account=name, sound="favourite.ogg"))
elif i == 'bookmarks':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Bookmarks"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="bookmarks", name="bookmarks", sessionObject=session, account=name, sound="favourite.ogg"))
elif i == 'followers':
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Followers"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_followers", name="followers", sessionObject=session, account=name, sound="update_followers.ogg", id=session.db["user_id"]))
elif i == 'following':
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Following"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_following", name="following", sessionObject=session, account=name, sound="update_followers.ogg", id=session.db["user_id"]))
elif i == 'muted':
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Muted users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="mutes", name="muted", sessionObject=session, account=name))
elif i == 'blocked':
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Blocked users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="blocks", name="blocked", sessionObject=session, account=name))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="timelines", account=name))
timelines_position =controller.view.search("timelines", name)
for i in session.settings["other_buffers"]["timelines"]:
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=i, parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="account_statuses", name="%s-timeline".format(i), sessionObject=session, account=name, sound="tweet_timeline.ogg", id=i))
for i in session.settings["other_buffers"]["followers_timelines"]:
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Followers for {}").format(i), parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_followers", name="%s-followers" % (i,), sessionObject=session, account=name, sound="new_event.ogg", id=i))
for i in session.settings["other_buffers"]["following_timelines"]:
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Following for {}").format(i), parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_following", name="%s-following" % (i,), sessionObject=session, account=name, sound="new_event.ogg", id=i))
# pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Lists"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="lists", name))
# lists_position =controller.view.search("lists", session.db["user_name"])
# for i in session.settings["other_buffers"]["lists"]:
# pub.sendMessage("createBuffer", buffer_type="ListBuffer", session_type=session.type, buffer_title=_(u"List for {}").format(i), parent_tab=lists_position, start=False, kwargs=dict(parent=controller.view.nb, function="list_timeline", name="%s-list" % (i,), sessionObject=session, name, bufferType=None, sound="list_tweet.ogg", list_id=utils.find_list(i, session.db["lists"]), include_ext_alt_text=True, tweet_mode="extended"))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Searches"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="searches", account=name))
# searches_position =controller.view.search("searches", session.db["user_name"])
# for i in session.settings["other_buffers"]["tweet_searches"]:
# pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_(u"Search for {}").format(i), parent_tab=searches_position, start=False, kwargs=dict(parent=controller.view.nb, function="search_tweets", name="%s-searchterm" % (i,), sessionObject=session, name, bufferType="searchPanel", sound="search_updated.ogg", q=i, include_ext_alt_text=True, tweet_mode="extended"))
# for i in session.settings["other_buffers"]["trending_topic_buffers"]:
# pub.sendMessage("createBuffer", buffer_type="TrendsBuffer", session_type=session.type, buffer_title=_("Trending topics for %s") % (i), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="%s_tt" % (i,), sessionObject=session, name, trendsFor=i, sound="trends_updated.ogg"))
def start_buffer(self, controller, buffer):
if hasattr(buffer, "finished_timeline") and buffer.finished_timeline == False:
change_title = True
else:
change_title = False
try:
buffer.start_stream(play_sound=False)
except Exception as err:
log.exception("Error %s starting buffer %s on account %s, with args %r and kwargs %r." % (str(err), buffer.name, buffer.account, buffer.args, buffer.kwargs))
if change_title:
pub.sendMessage("buffer-title-changed", buffer=buffer)
def open_conversation(self, controller, buffer):
post = buffer.get_item()
if post.reblog != None:
post = post.reblog
conversations_position =controller.view.search("direct_messages", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="ConversationBuffer", session_type=buffer.session.type, buffer_title=_("Conversation with {0}").format(post.account.acct), parent_tab=conversations_position, start=True, kwargs=dict(parent=controller.view.nb, function="status_context", name="%s-conversation" % (post.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="search_updated.ogg", post=post, id=post.id))
def follow(self, buffer):
if not hasattr(buffer, "get_item"):
return
item = buffer.get_item()
if buffer.type == "user":
users = [item.acct]
elif buffer.type == "baseBuffer":
if item.reblog != None:
users = [user.acct for user in item.reblog.mentions if user.id != buffer.session.db["user_id"]]
if item.reblog.account.acct not in users and item.account.id != buffer.session.db["user_id"]:
users.insert(0, item.reblog.account.acct)
else:
users = [user.acct for user in item.mentions if user.id != buffer.session.db["user_id"]]
if item.account.acct not in users:
users.insert(0, item.account.acct)
u = userActions.userActions(buffer.session, users)
def search(self, controller, session, value):
log.debug("Creating a new search...")
dlg = search_dialogs.searchDialog(value)
if dlg.ShowModal() == wx.ID_OK and dlg.term.GetValue() != "":
term = dlg.term.GetValue()
searches_position =controller.view.search("searches", session.get_name())
if dlg.posts.GetValue() == True:
if term not in session.settings["other_buffers"]["post_searches"]:
session.settings["other_buffers"]["post_searches"].append(term)
session.settings.write()
# pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_("Search for {}").format(term), parent_tab=searches_position, start=True, kwargs=dict(parent=controller.view.nb, function="search_tweets", name="%s-searchterm" % (term,), sessionObject=session, account=session.get_name(), bufferType="searchPanel", sound="search_updated.ogg", q=term, include_ext_alt_text=True, tweet_mode="extended"))
else:
log.error("A buffer for the %s search term is already created. You can't create a duplicate buffer." % (term,))
return
elif dlg.users.GetValue() == True:
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Search for {}").format(term), parent_tab=searches_position, start=True, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_search", name="%s-searchUser" % (term,), sessionObject=session, account=session.get_name(), sound="search_updated.ogg", q=term))
dlg.Destroy()
# ToDo: explore how to play sound & save config differently.
# currently, TWBlue will play the sound and save the config for the timeline even if the buffer did not load or something else.
def open_timeline(self, controller, buffer):
if not hasattr(buffer, "get_item"):
return
item = buffer.get_item()
if buffer.type == "user":
users = [item.acct]
elif buffer.type == "baseBuffer":
if item.reblog != None:
users = [user.acct for user in item.reblog.mentions if user.id != buffer.session.db["user_id"]]
if item.reblog.account.acct not in users and item.account.id != buffer.session.db["user_id"]:
users.insert(0, item.reblog.account.acct)
else:
users = [user.acct for user in item.mentions if user.id != buffer.session.db["user_id"]]
if item.account.acct not in users and item.account.id != buffer.session.db["user_id"]:
users.insert(0, item.account.acct)
u = userActions.UserTimeline(buffer.session, users)
if u.dialog.ShowModal() == wx.ID_OK:
action = u.process_action()
if action == None:
return
user = u.user
if action == "posts":
if user.statuses_count == 0:
dialogs.no_posts()
return
if user.id in buffer.session.settings["other_buffers"]["timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=buffer.session.type, buffer_title=_("Timeline for {}").format(user.username,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, function="account_statuses", name="%s-timeline" % (user.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="tweet_timeline.ogg", id=user.id))
buffer.session.settings["other_buffers"]["timelines"].append(user.id)
buffer.session.sound.play("create_timeline.ogg")
elif action == "followers":
if user.followers_count == 0:
dialogs.no_followers()
return
if user.id in buffer.session.settings["other_buffers"]["followers_timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=buffer.session.type, buffer_title=_("Followers for {}").format(user.username,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_followers", name="%s-followers" % (user.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="new_event.ogg", id=user.id))
buffer.session.settings["other_buffers"]["followers_timelines"].append(user.id)
buffer.session.sound.play("create_timeline.ogg")
elif action == "following":
if user.following_count == 0:
dialogs.no_following()
return
if user.id in buffer.session.settings["other_buffers"]["following_timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=buffer.session.type, buffer_title=_("Following for {}").format(user.username,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="account_following", name="%s-followers" % (user.id,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="new_event.ogg", id=user.id))
buffer.session.settings["other_buffers"]["following_timelines"].append(user.id)
buffer.session.sound.play("create_timeline.ogg")
buffer.session.settings.write()

View File

@ -0,0 +1,227 @@
# -*- coding: utf-8 -*-
import os
import wx
import widgetUtils
import config
import output
from controller.twitter import messages
from sessions.mastodon import templates
from wxUI.dialogs.mastodon import postDialogs
class post(messages.basicTweet):
def __init__(self, session, title, caption, text="", *args, **kwargs):
# take max character limit from session as this might be different for some instances.
self.max = session.char_limit
self.title = title
self.session = session
self.message = postDialogs.Post(caption=caption, text=text, *args, **kwargs)
self.message.SetTitle(title)
self.message.text.SetInsertionPoint(len(self.message.text.GetValue()))
widgetUtils.connect_event(self.message.spellcheck, widgetUtils.BUTTON_PRESSED, self.spellcheck)
widgetUtils.connect_event(self.message.text, widgetUtils.ENTERED_TEXT, self.text_processor)
widgetUtils.connect_event(self.message.translate, widgetUtils.BUTTON_PRESSED, self.translate)
widgetUtils.connect_event(self.message.add, widgetUtils.BUTTON_PRESSED, self.on_attach)
widgetUtils.connect_event(self.message.remove_attachment, widgetUtils.BUTTON_PRESSED, self.remove_attachment)
# ToDo: Add autocomplete feature to mastodon and uncomment this.
# widgetUtils.connect_event(self.message.autocomplete_users, widgetUtils.BUTTON_PRESSED, self.autocomplete_users)
widgetUtils.connect_event(self.message.add_post, widgetUtils.BUTTON_PRESSED, self.add_post)
widgetUtils.connect_event(self.message.remove_post, widgetUtils.BUTTON_PRESSED, self.remove_post)
self.attachments = []
self.thread = []
self.text_processor()
def add_post(self, event, update_gui=True, *args, **kwargs):
text = self.message.text.GetValue()
attachments = self.attachments[::]
postdata = dict(text=text, attachments=attachments, sensitive=self.message.sensitive.GetValue(), spoiler_text=None)
if postdata.get("sensitive") == True:
postdata.update(spoiler_text=self.message.spoiler.GetValue())
self.thread.append(postdata)
self.attachments = []
if update_gui:
self.message.reset_controls()
self.message.add_item(item=[text, len(attachments)], list_type="post")
self.message.text.SetFocus()
self.text_processor()
def get_post_data(self):
self.add_post(event=None, update_gui=False)
return self.thread
def text_processor(self, *args, **kwargs):
super(post, self).text_processor(*args, **kwargs)
if len(self.thread) > 0:
if hasattr(self.message, "posts"):
self.message.posts.Enable(True)
self.message.remove_post.Enable(True)
else:
self.message.posts.Enable(False)
self.message.remove_post.Enable(False)
if len(self.attachments) > 0:
self.message.attachments.Enable(True)
self.message.remove_attachment.Enable(True)
else:
self.message.attachments.Enable(False)
self.message.remove_attachment.Enable(False)
if len(self.message.text.GetValue()) > 0 or len(self.attachments) > 0:
self.message.add_post.Enable(True)
else:
self.message.add_post.Enable(False)
def remove_post(self, *args, **kwargs):
post = self.message.posts.GetFocusedItem()
if post > -1 and len(self.thread) > post:
self.thread.pop(post)
self.message.remove_item(list_type="post")
self.text_processor()
self.message.text.SetFocus()
def can_attach(self):
if len(self.attachments) == 0:
return True
elif len(self.attachments) == 1 and (self.attachments[0]["type"] == "poll" or self.attachments[0]["type"] == "video" or self.attachments[0]["type"] == "audio"):
return False
elif len(self.attachments) < 4:
return True
return False
def on_attach(self, *args, **kwargs):
can_attach = self.can_attach()
menu = self.message.attach_menu(can_attach)
self.message.Bind(wx.EVT_MENU, self.on_attach_image, self.message.add_image)
self.message.Bind(wx.EVT_MENU, self.on_attach_video, self.message.add_video)
self.message.Bind(wx.EVT_MENU, self.on_attach_audio, self.message.add_audio)
self.message.Bind(wx.EVT_MENU, self.on_attach_poll, self.message.add_poll)
self.message.PopupMenu(menu, self.message.add.GetPosition())
def on_attach_image(self, *args, **kwargs):
can_attach = self.can_attach()
big_media_present = False
for a in self.attachments:
if a["type"] == "video" or a["type"] == "audio" or a["type"] == "poll":
big_media_present = True
break
if can_attach == False or big_media_present == True:
return self.message.unable_to_attach_file()
image, description = self.message.get_image()
if image != None:
if image.endswith("gif"):
image_type = "gif"
else:
image_type = "photo"
imageInfo = {"type": image_type, "file": image, "description": description}
if len(self.attachments) > 0 and image_type == "gif":
return self.message.unable_to_attach_file()
self.attachments.append(imageInfo)
self.message.add_item(item=[os.path.basename(imageInfo["file"]), imageInfo["type"], imageInfo["description"]])
self.text_processor()
def on_attach_video(self, *args, **kwargs):
if len(self.attachments) >= 4:
return self.message.unable_to_attach_file()
can_attach = self.can_attach()
big_media_present = False
for a in self.attachments:
if a["type"] == "video" or a["type"] == "audio" or a["type"] == "poll":
big_media_present = True
break
if can_attach == False or big_media_present == True:
return self.message.unable_to_attach_file()
video = self.message.get_video()
if video != None:
videoInfo = {"type": "video", "file": video, "description": ""}
self.attachments.append(videoInfo)
self.message.add_item(item=[os.path.basename(videoInfo["file"]), videoInfo["type"], videoInfo["description"]])
self.text_processor()
def on_attach_audio(self, *args, **kwargs):
if len(self.attachments) >= 4:
return self.message.unable_to_attach_file()
can_attach = self.can_attach()
big_media_present = False
for a in self.attachments:
if a["type"] == "video" or a["type"] == "audio" or a["type"] == "poll":
big_media_present = True
break
if can_attach == False or big_media_present == True:
return self.message.unable_to_attach_file()
audio = self.message.get_audio()
if audio != None:
audioInfo = {"type": "audio", "file": audio, "description": ""}
self.attachments.append(audioInfo)
self.message.add_item(item=[os.path.basename(audioInfo["file"]), audioInfo["type"], audioInfo["description"]])
self.text_processor()
def on_attach_poll(self, *args, **kwargs):
if len(self.attachments) > 0:
return self.message.unable_to_attach_poll()
can_attach = self.can_attach()
big_media_present = False
for a in self.attachments:
if a["type"] == "video" or a["type"] == "audio" or a["type"] == "poll":
big_media_present = True
break
if can_attach == False or big_media_present == True:
return self.message.unable_to_attach_file()
dlg = postDialogs.poll()
if dlg.ShowModal() == wx.ID_OK:
day = 86400
periods = [300, 1800, 3600, 21600, day, day*2, day*3, day*4, day*5, day*6, day*7]
period = periods[dlg.period.GetSelection()]
poll_options = dlg.get_options()
multiple = dlg.multiple.GetValue()
hide_totals = dlg.hide_votes.GetValue()
data = dict(type="poll", file="", description=_("Poll with {} options").format(len(poll_options)), options=poll_options, expires_in=period, multiple=multiple, hide_totals=hide_totals)
self.attachments.append(data)
self.message.add_item(item=[data["file"], data["type"], data["description"]])
self.text_processor()
dlg.Destroy()
def get_data(self):
self.add_post(event=None, update_gui=False)
return self.thread
def get_visibility(self):
visibility_settings = ["public", "unlisted", "private", "direct"]
return visibility_settings[self.message.visibility.GetSelection()]
class viewPost(post):
def __init__(self, post, offset_hours=0, date="", item_url=""):
if post.reblog != None:
post = post.reblog
author = post.account.display_name if post.account.display_name != "" else post.account.username
title = _(u"Post from {}").format(author)
image_description = templates.process_image_descriptions(post.media_attachments)
text = templates.process_text(post, safe=False)
date = templates.process_date(post.created_at, relative_times=False, offset_hours=offset_hours)
privacy_settings = dict(public=_("Public"), unlisted=_("Not listed"), private=_("followers only"), direct=_("Direct"))
privacy = privacy_settings.get(post.visibility)
boost_count = str(post.reblogs_count)
favs_count = str(post.favourites_count)
# Gets the client from where this post was made.
source_obj = post.get("application")
if source_obj == None:
source = _("Remote instance")
else:
source = source_obj.get("name")
self.message = postDialogs.viewPost(text=text, boosts_count=boost_count, favs_count=favs_count, source=source, date=date, privacy=privacy)
self.message.SetTitle(title)
if image_description != "":
self.message.image_description.Enable(True)
self.message.image_description.ChangeValue(image_description)
widgetUtils.connect_event(self.message.spellcheck, widgetUtils.BUTTON_PRESSED, self.spellcheck)
if item_url != "":
self.message.enable_button("share")
widgetUtils.connect_event(self.message.share, widgetUtils.BUTTON_PRESSED, self.share)
self.item_url = item_url
widgetUtils.connect_event(self.message.translateButton, widgetUtils.BUTTON_PRESSED, self.translate)
self.message.ShowModal()
# We won't need text_processor in this dialog, so let's avoid it.
def text_processor(self):
pass
def share(self, *args, **kwargs):
if hasattr(self, "item_url"):
output.copy(self.item_url)
output.speak(_("Link copied to clipboard."))

View File

@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
import logging
import widgetUtils
import output
from wxUI.dialogs.mastodon import userActions as userActionsDialog
from wxUI.dialogs.mastodon import userTimeline as userTimelineDialog
from pubsub import pub
from mastodon import MastodonError, MastodonNotFoundError
from extra.autocompletionUsers import completion
log = logging.getLogger("controller.mastodon.userActions")
class BasicUserSelector(object):
def __init__(self, session, users=[]):
super(BasicUserSelector, self).__init__()
self.session = session
self.create_dialog(users=users)
def create_dialog(self, users):
pass
def autocomplete_users(self, *args, **kwargs):
c = completion.autocompletionUsers(self.dialog, self.session.session_id)
c.show_menu("dm")
def search_user(self, user):
try:
user = self.session.api.account_lookup(user)
return user
except MastodonError:
log.exception("Error searching for user %s.".format(user))
class userActions(BasicUserSelector):
def __init__(self, *args, **kwargs):
super(userActions, self).__init__(*args, **kwargs)
if self.dialog.get_response() == widgetUtils.OK:
self.process_action()
def create_dialog(self, users):
self.dialog = userActionsDialog.UserActionsDialog(users)
widgetUtils.connect_event(self.dialog.autocompletion, widgetUtils.BUTTON_PRESSED, self.autocomplete_users)
def process_action(self):
action = self.dialog.get_action()
user = self.dialog.get_user()
user = self.search_user(user)
if user == None:
return
getattr(self, action)(user)
def follow(self, user):
try:
self.session.api.account_follow(user.id)
except MastodonError as err:
output.speak("Error %s" % (str(err)), True)
def unfollow(self, user):
try:
result = self.session.api.account_unfollow(user.id)
except MastodonError as err:
output.speak("Error %s" % (str(err)), True)
def mute(self, user):
try:
id = self.session.api.account_mute(user.id)
except MastodonError as err:
output.speak("Error %s" % (str(err)), True)
def unmute(self, user):
try:
id = self.session.api.account_unmute(user.id)
except MastodonError as err:
output.speak("Error %s" % (str(err)), True)
def block(self, user):
try:
id = self.session.api.account_block(user.id)
except MastodonError as err:
output.speak("Error %s" % (str(err)), True)
def unblock(self, user):
try:
id = self.session.api.account_unblock(user.id)
except MastodonError as err:
output.speak("Error %s" % (str(err)), True)
class UserTimeline(BasicUserSelector):
def create_dialog(self, users):
self.dialog = userTimelineDialog.UserTimeline(users)
widgetUtils.connect_event(self.dialog.autocompletion, widgetUtils.BUTTON_PRESSED, self.autocomplete_users)
def process_action(self):
action = self.dialog.get_action()
user = self.dialog.get_user()
user = self.search_user(user)
if user == None:
return
self.user = user
return action

View File

@ -1,25 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import webbrowser
import threading
import logging import logging
import sound_lib
import paths import paths
import widgetUtils
import config import config
import languageHandler import languageHandler
import output
import application import application
import config_utils
import keys
from collections import OrderedDict
from pubsub import pub from pubsub import pub
from mysc import autostart as autostart_windows from mysc import autostart as autostart_windows
from wxUI.dialogs import configuration from wxUI.dialogs import configuration
from wxUI import commonMessageDialogs from wxUI import commonMessageDialogs
from extra.autocompletionUsers import scan, manage
from extra.ocr import OCRSpace
from .editTemplateController import EditTemplate
log = logging.getLogger("Settings") log = logging.getLogger("Settings")
@ -132,234 +121,3 @@ class globalSettingsController(object):
config.app["proxy"]["user"] = self.dialog.get_value("proxy", "user") config.app["proxy"]["user"] = self.dialog.get_value("proxy", "user")
config.app["proxy"]["password"] = self.dialog.get_value("proxy", "password") config.app["proxy"]["password"] = self.dialog.get_value("proxy", "password")
config.app.write() config.app.write()
class accountSettingsController(globalSettingsController):
def __init__(self, buffer, window):
self.user = buffer.session.db["user_name"]
self.buffer = buffer
self.window = window
self.config = buffer.session.settings
super(accountSettingsController, self).__init__()
def create_config(self):
self.dialog.create_general_account()
widgetUtils.connect_event(self.dialog.general.userAutocompletionScan, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_scan)
widgetUtils.connect_event(self.dialog.general.userAutocompletionManage, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_manage)
self.dialog.set_value("general", "relative_time", self.config["general"]["relative_times"])
self.dialog.set_value("general", "show_screen_names", self.config["general"]["show_screen_names"])
self.dialog.set_value("general", "hide_emojis", self.config["general"]["hide_emojis"])
self.dialog.set_value("general", "itemsPerApiCall", self.config["general"]["max_tweets_per_call"])
self.dialog.set_value("general", "reverse_timelines", self.config["general"]["reverse_timelines"])
rt = self.config["general"]["retweet_mode"]
if rt == "ask":
self.dialog.set_value("general", "retweet_mode", _(u"Ask"))
elif rt == "direct":
self.dialog.set_value("general", "retweet_mode", _(u"Retweet without comments"))
else:
self.dialog.set_value("general", "retweet_mode", _(u"Retweet with comments"))
self.dialog.set_value("general", "persist_size", str(self.config["general"]["persist_size"]))
self.dialog.set_value("general", "load_cache_in_memory", self.config["general"]["load_cache_in_memory"])
self.dialog.create_reporting()
self.dialog.set_value("reporting", "speech_reporting", self.config["reporting"]["speech_reporting"])
self.dialog.set_value("reporting", "braille_reporting", self.config["reporting"]["braille_reporting"])
tweet_template = self.config["templates"]["tweet"]
dm_template = self.config["templates"]["dm"]
sent_dm_template = self.config["templates"]["dm_sent"]
person_template = self.config["templates"]["person"]
self.dialog.create_templates(tweet_template=tweet_template, dm_template=dm_template, sent_dm_template=sent_dm_template, person_template=person_template)
widgetUtils.connect_event(self.dialog.templates.tweet, widgetUtils.BUTTON_PRESSED, self.edit_tweet_template)
widgetUtils.connect_event(self.dialog.templates.dm, widgetUtils.BUTTON_PRESSED, self.edit_dm_template)
widgetUtils.connect_event(self.dialog.templates.sent_dm, widgetUtils.BUTTON_PRESSED, self.edit_sent_dm_template)
widgetUtils.connect_event(self.dialog.templates.person, widgetUtils.BUTTON_PRESSED, self.edit_person_template)
self.dialog.create_other_buffers()
buffer_values = self.get_buffers_list()
self.dialog.buffers.insert_buffers(buffer_values)
self.dialog.buffers.connect_hook_func(self.toggle_buffer_active)
widgetUtils.connect_event(self.dialog.buffers.toggle_state, widgetUtils.BUTTON_PRESSED, self.toggle_state)
widgetUtils.connect_event(self.dialog.buffers.up, widgetUtils.BUTTON_PRESSED, self.dialog.buffers.move_up)
widgetUtils.connect_event(self.dialog.buffers.down, widgetUtils.BUTTON_PRESSED, self.dialog.buffers.move_down)
self.dialog.create_ignored_clients(self.config["twitter"]["ignored_clients"])
widgetUtils.connect_event(self.dialog.ignored_clients.add, widgetUtils.BUTTON_PRESSED, self.add_ignored_client)
widgetUtils.connect_event(self.dialog.ignored_clients.remove, widgetUtils.BUTTON_PRESSED, self.remove_ignored_client)
self.input_devices = sound_lib.input.Input.get_device_names()
self.output_devices = sound_lib.output.Output.get_device_names()
self.soundpacks = []
[self.soundpacks.append(i) for i in os.listdir(paths.sound_path()) if os.path.isdir(os.path.join(paths.sound_path(), i)) == True ]
self.dialog.create_sound(self.input_devices, self.output_devices, self.soundpacks)
self.dialog.set_value("sound", "volumeCtrl", self.config["sound"]["volume"]*100)
self.dialog.set_value("sound", "input", self.config["sound"]["input_device"])
self.dialog.set_value("sound", "output", self.config["sound"]["output_device"])
self.dialog.set_value("sound", "session_mute", self.config["sound"]["session_mute"])
self.dialog.set_value("sound", "soundpack", self.config["sound"]["current_soundpack"])
self.dialog.set_value("sound", "indicate_audio", self.config["sound"]["indicate_audio"])
self.dialog.set_value("sound", "indicate_geo", self.config["sound"]["indicate_geo"])
self.dialog.set_value("sound", "indicate_img", self.config["sound"]["indicate_img"])
self.dialog.create_extras(OCRSpace.translatable_langs)
self.dialog.set_value("extras", "sndup_apiKey", self.config["sound"]["sndup_api_key"])
language_index = OCRSpace.OcrLangs.index(self.config["mysc"]["ocr_language"])
self.dialog.extras.ocr_lang.SetSelection(language_index)
self.dialog.realize()
self.dialog.set_title(_(u"Account settings for %s") % (self.user,))
self.response = self.dialog.get_response()
def edit_tweet_template(self, *args, **kwargs):
template = self.config["templates"]["tweet"]
control = EditTemplate(template=template, type="tweet")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["tweet"] = result
self.config.write()
self.dialog.templates.tweet.SetLabel(_("Edit template for tweets. Current template: {}").format(result))
def edit_dm_template(self, *args, **kwargs):
template = self.config["templates"]["dm"]
control = EditTemplate(template=template, type="dm")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["dm"] = result
self.config.write()
self.dialog.templates.dm.SetLabel(_("Edit template for direct messages. Current template: {}").format(result))
def edit_sent_dm_template(self, *args, **kwargs):
template = self.config["templates"]["dm_sent"]
control = EditTemplate(template=template, type="dm")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["dm_sent"] = result
self.config.write()
self.dialog.templates.sent_dm.SetLabel(_("Edit template for sent direct messages. Current template: {}").format(result))
def edit_person_template(self, *args, **kwargs):
template = self.config["templates"]["person"]
control = EditTemplate(template=template, type="person")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["person"] = result
self.config.write()
self.dialog.templates.person.SetLabel(_("Edit template for persons. Current template: {}").format(result))
def save_configuration(self):
if self.config["general"]["relative_times"] != self.dialog.get_value("general", "relative_time"):
self.needs_restart = True
log.debug("Triggered app restart due to change in relative times.")
self.config["general"]["relative_times"] = self.dialog.get_value("general", "relative_time")
self.config["general"]["show_screen_names"] = self.dialog.get_value("general", "show_screen_names")
self.config["general"]["hide_emojis"] = self.dialog.get_value("general", "hide_emojis")
self.config["general"]["max_tweets_per_call"] = self.dialog.get_value("general", "itemsPerApiCall")
if self.config["general"]["load_cache_in_memory"] != self.dialog.get_value("general", "load_cache_in_memory"):
self.config["general"]["load_cache_in_memory"] = self.dialog.get_value("general", "load_cache_in_memory")
self.needs_restart = True
log.debug("Triggered app restart due to change in database strategy management.")
if self.config["general"]["persist_size"] != self.dialog.get_value("general", "persist_size"):
if self.dialog.get_value("general", "persist_size") == '':
self.config["general"]["persist_size"] =-1
else:
try:
self.config["general"]["persist_size"] = int(self.dialog.get_value("general", "persist_size"))
except ValueError:
output.speak("Invalid cache size, setting to default.",True)
self.config["general"]["persist_size"] =1764
if self.config["general"]["reverse_timelines"] != self.dialog.get_value("general", "reverse_timelines"):
self.needs_restart = True
log.debug("Triggered app restart due to change in timeline order.")
self.config["general"]["reverse_timelines"] = self.dialog.get_value("general", "reverse_timelines")
rt = self.dialog.get_value("general", "retweet_mode")
if rt == _(u"Ask"):
self.config["general"]["retweet_mode"] = "ask"
elif rt == _(u"Retweet without comments"):
self.config["general"]["retweet_mode"] = "direct"
else:
self.config["general"]["retweet_mode"] = "comment"
buffers_list = self.dialog.buffers.get_list()
if buffers_list != self.config["general"]["buffer_order"]:
self.needs_restart = True
log.debug("Triggered app restart due to change in buffer ordering.")
self.config["general"]["buffer_order"] = buffers_list
self.config["reporting"]["speech_reporting"] = self.dialog.get_value("reporting", "speech_reporting")
self.config["reporting"]["braille_reporting"] = self.dialog.get_value("reporting", "braille_reporting")
self.config["mysc"]["ocr_language"] = OCRSpace.OcrLangs[self.dialog.extras.ocr_lang.GetSelection()]
if self.config["sound"]["input_device"] != self.dialog.sound.get("input"):
self.config["sound"]["input_device"] = self.dialog.sound.get("input")
try:
self.buffer.session.sound.input.set_device(self.buffer.session.sound.input.find_device_by_name(self.config["sound"]["input_device"]))
except:
self.config["sound"]["input_device"] = "default"
if self.config["sound"]["output_device"] != self.dialog.sound.get("output"):
self.config["sound"]["output_device"] = self.dialog.sound.get("output")
try:
self.buffer.session.sound.output.set_device(self.buffer.session.sound.output.find_device_by_name(self.config["sound"]["output_device"]))
except:
self.config["sound"]["output_device"] = "default"
self.config["sound"]["volume"] = self.dialog.get_value("sound", "volumeCtrl")/100.0
self.config["sound"]["session_mute"] = self.dialog.get_value("sound", "session_mute")
self.config["sound"]["current_soundpack"] = self.dialog.sound.get("soundpack")
self.config["sound"]["indicate_audio"] = self.dialog.get_value("sound", "indicate_audio")
self.config["sound"]["indicate_geo"] = self.dialog.get_value("sound", "indicate_geo")
self.config["sound"]["indicate_img"] = self.dialog.get_value("sound", "indicate_img")
self.config["sound"]["sndup_api_key"] = self.dialog.get_value("extras", "sndup_apiKey")
self.buffer.session.sound.config = self.config["sound"]
self.buffer.session.sound.check_soundpack()
self.config.write()
def toggle_state(self,*args,**kwargs):
return self.dialog.buffers.change_selected_item()
def on_autocompletion_scan(self, *args, **kwargs):
configuration = scan.autocompletionScan(self.buffer.session.settings, self.buffer, self.window)
to_scan = configuration.show_dialog()
if to_scan == True:
configuration.prepare_progress_dialog()
t = threading.Thread(target=configuration.scan)
t.start()
def on_autocompletion_manage(self, *args, **kwargs):
configuration = manage.autocompletionManage(self.buffer.session)
configuration.show_settings()
def add_ignored_client(self, *args, **kwargs):
client = commonMessageDialogs.get_ignored_client()
if client == None: return
if client not in self.config["twitter"]["ignored_clients"]:
self.config["twitter"]["ignored_clients"].append(client)
self.dialog.ignored_clients.append(client)
def remove_ignored_client(self, *args, **kwargs):
if self.dialog.ignored_clients.get_clients() == 0: return
id = self.dialog.ignored_clients.get_client_id()
self.config["twitter"]["ignored_clients"].pop(id)
self.dialog.ignored_clients.remove_(id)
def get_buffers_list(self):
all_buffers=OrderedDict()
all_buffers['home']=_(u"Home")
all_buffers['mentions']=_(u"Mentions")
all_buffers['dm']=_(u"Direct Messages")
all_buffers['sent_dm']=_(u"Sent direct messages")
all_buffers['sent_tweets']=_(u"Sent tweets")
all_buffers['favorites']=_(u"Likes")
all_buffers['followers']=_(u"Followers")
all_buffers['friends']=_(u"Friends")
all_buffers['blocks']=_(u"Blocked users")
all_buffers['muted']=_(u"Muted users")
list_buffers = []
hidden_buffers=[]
all_buffers_keys = list(all_buffers.keys())
# Check buffers shown first.
for i in self.config["general"]["buffer_order"]:
if i in all_buffers_keys:
list_buffers.append((i, all_buffers[i], True))
# This second pass will retrieve all hidden buffers.
for i in all_buffers_keys:
if i not in self.config["general"]["buffer_order"]:
hidden_buffers.append((i, all_buffers[i], False))
list_buffers.extend(hidden_buffers)
return list_buffers
def toggle_buffer_active(self, ev):
change = self.dialog.buffers.get_event(ev)
if change == True:
self.dialog.buffers.change_selected_item()

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,340 @@
# -*- coding: utf-8 -*-
import logging
import widgetUtils
import output
from pubsub import pub
from tweepy.errors import TweepyException, Forbidden
from mysc import restart
from sessions.twitter import utils, compose
from controller import userSelector
from wxUI import dialogs, commonMessageDialogs
from . import filters, lists, settings, userActions, trendingTopics, user
log = logging.getLogger("controller.twitter.handler")
class Handler(object):
def __init__(self):
super(Handler, self).__init__()
def create_buffers(self, session, createAccounts=True, controller=None):
session.get_user_info()
name = session.get_name()
if createAccounts == True:
pub.sendMessage("core.create_account", name=name, session_id=session.session_id, logged=True)
root_position =controller.view.search(name, name)
for i in session.settings['general']['buffer_order']:
if i == 'home':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Home"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="home_timeline", name="home_timeline", sessionObject=session, account=session.get_name(), sound="tweet_received.ogg", include_ext_alt_text=True, tweet_mode="extended"))
elif i == 'mentions':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Mentions"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="mentions_timeline", name="mentions", sessionObject=session, account=session.get_name(), sound="mention_received.ogg", include_ext_alt_text=True, tweet_mode="extended"))
elif i == 'dm':
pub.sendMessage("createBuffer", buffer_type="DirectMessagesBuffer", session_type=session.type, buffer_title=_("Direct messages"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_direct_messages", name="direct_messages", sessionObject=session, account=session.get_name(), bufferType="dmPanel", compose_func="compose_direct_message", sound="dm_received.ogg"))
elif i == 'sent_dm':
pub.sendMessage("createBuffer", buffer_type="SentDirectMessagesBuffer", session_type=session.type, buffer_title=_("Sent direct messages"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function=None, name="sent_direct_messages", sessionObject=session, account=session.get_name(), bufferType="dmPanel", compose_func="compose_direct_message"))
elif i == 'sent_tweets':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Sent tweets"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="user_timeline", name="sent_tweets", sessionObject=session, account=session.get_name(), screen_name=session.db["user_name"], include_ext_alt_text=True, tweet_mode="extended"))
elif i == 'favorites':
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Likes"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_favorites", name="favourites", sessionObject=session, account=session.get_name(), sound="favourite.ogg", include_ext_alt_text=True, tweet_mode="extended"))
elif i == 'followers':
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_("Followers"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_followers", name="followers", sessionObject=session, account=session.get_name(), sound="update_followers.ogg", screen_name=session.db["user_name"]))
elif i == 'friends':
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_("Following"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_friends", name="friends", sessionObject=session, account=session.get_name(), screen_name=session.db["user_name"]))
elif i == 'blocks':
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_("Blocked users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_blocks", name="blocked", sessionObject=session, account=session.get_name()))
elif i == 'muted':
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_("Muted users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_mutes", name="muted", sessionObject=session, account=session.get_name()))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="timelines", account=name))
timelines_position =controller.view.search("timelines", name)
for i in session.settings["other_buffers"]["timelines"]:
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_(u"Timeline for {}").format(i,), parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="user_timeline", name="%s-timeline" % (i,), sessionObject=session, account=session.get_name(), sound="tweet_timeline.ogg", bufferType=None, user_id=i, include_ext_alt_text=True, tweet_mode="extended"))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Likes timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="favs_timelines", account=name))
favs_timelines_position =controller.view.search("favs_timelines", name)
for i in session.settings["other_buffers"]["favourites_timelines"]:
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Likes for {}").format(i,), parent_tab=favs_timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_favorites", name="%s-favorite" % (i,), sessionObject=session, account=name, bufferType=None, sound="favourites_timeline_updated.ogg", user_id=i, include_ext_alt_text=True, tweet_mode="extended"))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Followers timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="followers_timelines", account=session.get_name()))
followers_timelines_position =controller.view.search("followers_timelines", name)
for i in session.settings["other_buffers"]["followers_timelines"]:
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_("Followers for {}").format(i,), parent_tab=followers_timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_followers", name="%s-followers" % (i,), sessionObject=session, account=session.get_name(), sound="new_event.ogg", user_id=i))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Following timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="friends_timelines", account=name))
friends_timelines_position =controller.view.search("friends_timelines", name)
for i in session.settings["other_buffers"]["friends_timelines"]:
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_(u"Friends for {}").format(i,), parent_tab=friends_timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="get_friends", name="%s-friends" % (i,), sessionObject=session, account=session.get_name(), sound="new_event.ogg", user_id=i))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Lists"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="lists", account=name))
lists_position =controller.view.search("lists", name)
for i in session.settings["other_buffers"]["lists"]:
pub.sendMessage("createBuffer", buffer_type="ListBuffer", session_type=session.type, buffer_title=_(u"List for {}").format(i), parent_tab=lists_position, start=False, kwargs=dict(parent=controller.view.nb, function="list_timeline", name="%s-list" % (i,), sessionObject=session, account=session.get_name(), bufferType=None, sound="list_tweet.ogg", list_id=utils.find_list(i, session.db["lists"]), include_ext_alt_text=True, tweet_mode="extended"))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Searches"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="searches", account=name))
searches_position =controller.view.search("searches", name)
for i in session.settings["other_buffers"]["tweet_searches"]:
pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_(u"Search for {}").format(i), parent_tab=searches_position, start=False, kwargs=dict(parent=controller.view.nb, function="search_tweets", name="%s-searchterm" % (i,), sessionObject=session, account=session.get_name(), bufferType="searchPanel", sound="search_updated.ogg", q=i, include_ext_alt_text=True, tweet_mode="extended"))
for i in session.settings["other_buffers"]["trending_topic_buffers"]:
pub.sendMessage("createBuffer", buffer_type="TrendsBuffer", session_type=session.type, buffer_title=_("Trending topics for %s") % (i), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="%s_tt" % (i,), sessionObject=session, account=session.get_name(), trendsFor=i, sound="trends_updated.ogg"))
def filter(self, buffer):
# Let's prevent filtering of some buffers (people buffers, direct messages, events and sent items).
if (buffer.name == "direct_messages" or buffer.name == "sent_tweets") or buffer.type == "people":
output.speak(_("Filters cannot be applied on this buffer"))
return
new_filter = filters.filter(buffer)
def manage_filters(self, session):
manage_filters = filters.filterManager(session)
def view_user_lists(self, buffer):
if not hasattr(buffer, "get_right_tweet"):
return
tweet = buffer.get_right_tweet()
if buffer.type == "people":
users = [tweet.screen_name]
elif buffer.type == "dm":
users = [buffer.session.get_user(tweet.message_create["sender_id"]).screen_name]
else:
users = utils.get_all_users(tweet, buffer.session)
selector = userSelector.userSelector(users=users, session_id=buffer.session.session_id)
user = selector.get_user()
if user == None:
return
l = lists.listsController(buffer.session, user=user)
def add_to_list(self, controller, buffer):
if not hasattr(buffer, "get_right_tweet"):
return
tweet = buffer.get_right_tweet()
if buffer.type == "people":
users = [tweet.screen_name]
elif buffer.type == "dm":
users = [buffer.session.get_user(tweet.message_create["sender_id"]).screen_name]
else:
users = utils.get_all_users(tweet, buffer.session)
selector = userSelector.userSelector(users=users, session_id=buffer.session.session_id)
user = selector.get_user()
if user == None:
return
dlg = dialogs.lists.addUserListDialog()
dlg.populate_list([compose.compose_list(item) for item in buffer.session.db["lists"]])
if dlg.get_response() == widgetUtils.OK:
try:
list = buffer.session.twitter.add_list_member(list_id=buffer.session.db["lists"][dlg.get_item()].id, screen_name=user)
older_list = utils.find_item(buffer.session.db["lists"][dlg.get_item()].id, buffer.session.db["lists"])
listBuffer = controller.search_buffer("%s-list" % (buffer.session.db["lists"][dlg.get_item()].name.lower()), buff.session.get_name())
if listBuffer != None:
listBuffer.get_user_ids()
buffer.session.db["lists"].pop(older_list)
buffer.session.db["lists"].append(list)
except TweepyException as e:
log.exception("error %s" % (str(e)))
output.speak("error %s" % (str(e)))
def remove_from_list(self, controller, buffer):
if not hasattr(buffer, "get_right_tweet"):
return
tweet = buffer.get_right_tweet()
if buffer.type == "people":
users = [tweet.screen_name]
elif buffer.type == "dm":
users = [buffer.session.get_user(tweet.message_create["sender_id"]).screen_name]
else:
users = utils.get_all_users(tweet, buffer.session)
selector = userSelector.userSelector(users=users, session_id=buffer.session.session_id)
user = selector.get_user()
if user == None:
return
dlg = dialogs.lists.removeUserListDialog()
dlg.populate_list([compose.compose_list(item) for item in buffer.session.db["lists"]])
if dlg.get_response() == widgetUtils.OK:
try:
list = buffer.session.twitter.remove_list_member(list_id=buffer.session.db["lists"][dlg.get_item()].id, screen_name=user)
older_list = utils.find_item(buffer.session.db["lists"][dlg.get_item()].id, buffer.session.db["lists"])
listBuffer = controller.search_buffer("%s-list" % (buffer.session.db["lists"][dlg.get_item()].name.lower()), buffer.session.get_name())
if listBuffer != None:
listBuffer.get_user_ids()
buffer.session.db["lists"].pop(older_list)
buffer.session.db["lists"].append(list)
except TweepyException as e:
output.speak("error %s" % (str(e)))
log.exception("error %s" % (str(e)))
def list_manager(self, session, lists_buffer_position):
return lists.listsController(session=session, lists_buffer_position=lists_buffer_position)
def account_settings(self, buffer, controller):
d = settings.accountSettingsController(buffer, controller)
if d.response == widgetUtils.OK:
d.save_configuration()
if d.needs_restart == True:
commonMessageDialogs.needs_restart()
buffer.session.settings.write()
buffer.session.save_persistent_data()
restart.restart_program()
def follow(self, buffer):
if not hasattr(buffer, "get_right_tweet"):
return
tweet = buffer.get_right_tweet()
if buffer.type == "people":
users = [tweet.screen_name]
elif buffer.type == "dm":
users = [buffer.session.get_user(tweet.message_create["sender_id"]).screen_name]
else:
users = utils.get_all_users(tweet, buffer.session)
u = userActions.userActionsController(buffer, users)
def add_alias(self, buffer):
if not hasattr(buffer, "get_right_tweet"):
return
tweet = buffer.get_right_tweet()
if buffer.type == "people":
users = [tweet.screen_name]
elif buffer.type == "dm":
users = [buffer.session.get_user(tweet.message_create["sender_id"]).screen_name]
else:
users = utils.get_all_users(tweet, buffer.session)
dlg = dialogs.userAliasDialogs.addAliasDialog(_("Add an user alias"), users)
if dlg.get_response() == widgetUtils.OK:
user, alias = dlg.get_user()
if user == "" or alias == "":
return
user_id = buffer.session.get_user_by_screen_name(user)
buffer.session.settings["user-aliases"][str(user_id)] = alias
buffer.session.settings.write()
output.speak(_("Alias has been set correctly for {}.").format(user))
pub.sendMessage("alias-added")
# ToDo: explore how to play sound & save config differently.
# currently, TWBlue will play the sound and save the config for the timeline even if the buffer did not load or something else.
def open_timeline(self, controller, buffer, default="tweets"):
if not hasattr(buffer, "get_right_tweet"):
return
tweet = buffer.get_right_tweet()
if buffer.type == "people":
users = [tweet.screen_name]
elif buffer.type == "dm":
users = [buffer.session.get_user(tweet.message_create["sender_id"]).screen_name]
else:
users = utils.get_all_users(tweet, buffer.session)
dlg = dialogs.userSelection.selectUserDialog(users=users, default=default)
if dlg.get_response() == widgetUtils.OK:
usr = utils.if_user_exists(buffer.session.twitter, dlg.get_user())
if usr != None:
if usr == dlg.get_user():
commonMessageDialogs.suspended_user()
return
if usr.protected == True:
if usr.following == False:
commonMessageDialogs.no_following()
return
tl_type = dlg.get_action()
if tl_type == "tweets":
if usr.statuses_count == 0:
commonMessageDialogs.no_tweets()
return
if usr.id_str in buffer.session.settings["other_buffers"]["timelines"]:
commonMessageDialogs.timeline_exist()
return
timelines_position =controller.view.search("timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=buffer.session.type, buffer_title=_("Timeline for {}").format(usr.screen_name,), parent_tab=timelines_position, start=True, kwargs=dict(parent=controller.view.nb, function="user_timeline", name="%s-timeline" % (usr.id_str,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="tweet_timeline.ogg", bufferType=None, user_id=usr.id_str, include_ext_alt_text=True, tweet_mode="extended"))
buffer.session.settings["other_buffers"]["timelines"].append(usr.id_str)
buffer.session.sound.play("create_timeline.ogg")
elif tl_type == "favourites":
if usr.favourites_count == 0:
commonMessageDialogs.no_favs()
return
if usr.id_str in buffer.session.settings["other_buffers"]["favourites_timelines"]:
commonMessageDialogs.timeline_exist()
return
favs_timelines_position =controller.view.search("favs_timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=buffer.session.type, buffer_title=_("Likes for {}").format(usr.screen_name,), parent_tab=favs_timelines_position, start=True, kwargs=dict(parent=controller.view.nb, function="get_favorites", name="%s-favorite" % (usr.id_str,), sessionObject=buffer.session, account=buffer.session.get_name(), bufferType=None, sound="favourites_timeline_updated.ogg", user_id=usr.id_str, include_ext_alt_text=True, tweet_mode="extended"))
buffer.session.settings["other_buffers"]["favourites_timelines"].append(usr.id_str)
buffer.session.sound.play("create_timeline.ogg")
elif tl_type == "followers":
if usr.followers_count == 0:
commonMessageDialogs.no_followers()
return
if usr.id_str in buffer.session.settings["other_buffers"]["followers_timelines"]:
commonMessageDialogs.timeline_exist()
return
followers_timelines_position =controller.view.search("followers_timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=buffer.session.type, buffer_title=_("Followers for {}").format(usr.screen_name,), parent_tab=followers_timelines_position, start=True, kwargs=dict(parent=controller.view.nb, function="get_followers", name="%s-followers" % (usr.id_str,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="new_event.ogg", user_id=usr.id_str))
buffer.session.settings["other_buffers"]["followers_timelines"].append(usr.id_str)
buffer.session.sound.play("create_timeline.ogg")
elif tl_type == "friends":
if usr.friends_count == 0:
commonMessageDialogs.no_friends()
return
if usr.id_str in buffer.session.settings["other_buffers"]["friends_timelines"]:
commonMessageDialogs.timeline_exist()
return
friends_timelines_position =controller.view.search("friends_timelines", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=buffer.session.type, buffer_title=_("Friends for {}").format(usr.screen_name,), parent_tab=friends_timelines_position, start=True, kwargs=dict(parent=controller.view.nb, function="get_friends", name="%s-friends" % (usr.id_str,), sessionObject=buffer.session, account=buffer.session.get_name(), sound="new_event.ogg", user_id=usr.id_str))
buffer.session.settings["other_buffers"]["friends_timelines"].append(usr.id_str)
buffer.session.sound.play("create_timeline.ogg")
else:
commonMessageDialogs.user_not_exist()
buffer.session.settings.write()
def open_conversation(self, controller, buffer):
tweet = buffer.get_right_tweet()
if hasattr(tweet, "retweeted_status") and tweet.retweeted_status != None:
tweet = tweet.retweeted_status
user = buffer.session.get_user(tweet.user).screen_name
searches_position =controller.view.search("searches", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="ConversationBuffer", session_type=buffer.session.type, buffer_title=_(u"Conversation with {0}").format(user), parent_tab=searches_position, start=True, kwargs=dict(tweet=tweet, parent=controller.view.nb, function="search_tweets", name="%s-searchterm" % (tweet.id,), sessionObject=buffer.session, account=buffer.session.get_name(), bufferType="searchPanel", sound="search_updated.ogg", since_id=tweet.id, q="@{0}".format(user)))
def get_trending_topics(self, controller, session):
trends = trendingTopics.trendingTopicsController(session)
if trends.dialog.get_response() == widgetUtils.OK:
woeid = trends.get_woeid()
if woeid in session.settings["other_buffers"]["trending_topic_buffers"]:
return
root_position =controller.view.search(session.get_name(), session.get_name())
pub.sendMessage("createBuffer", buffer_type="TrendsBuffer", session_type=session.type, buffer_title=_("Trending topics for %s") % (trends.get_string()), parent_tab=root_position, start=True, kwargs=dict(parent=controller.view.nb, name="%s_tt" % (woeid,), sessionObject=session, account=session.get_name(), trendsFor=woeid, sound="trends_updated.ogg"))
session.settings["other_buffers"]["trending_topic_buffers"].append(str(woeid))
session.settings.write()
def start_buffer(self, controller, buffer):
if hasattr(buffer, "finished_timeline") and buffer.finished_timeline == False:
change_title = True
else:
change_title = False
try:
if "mentions" in buffer.name or "direct_messages" in buffer.name:
buffer.start_stream()
else:
buffer.start_stream(play_sound=False)
except TweepyException as err:
log.exception("Error %s starting buffer %s on account %s, with args %r and kwargs %r." % (str(err), buffer.name, buffer.account, buffer.args, buffer.kwargs))
# Determine if this error was caused by a block applied to the current user (IE permission errors).
if type(err) == Forbidden:
buff = controller.view.search(buffer.name, buffer.account)
buffer.remove_buffer(force=True)
commonMessageDialogs.blocked_timeline()
if controller.get_current_buffer() == buffer:
controller.right()
controller.view.delete_buffer(buff)
controller.buffers.remove(buffer)
del buffer
if change_title:
pub.sendMessage("buffer-title-changed", buffer=buffer)
def update_profile(self, session):
r = user.profileController(session)
def search(self, controller, session, value):
log.debug("Creating a new search...")
dlg = dialogs.search.searchDialog(value)
if dlg.get_response() == widgetUtils.OK and dlg.get("term") != "":
term = dlg.get("term")
searches_position =controller.view.search("searches", session.get_name())
if dlg.get("tweets") == True:
if term not in session.settings["other_buffers"]["tweet_searches"]:
session.settings["other_buffers"]["tweet_searches"].append(term)
session.settings.write()
args = {"lang": dlg.get_language(), "result_type": dlg.get_result_type()}
pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_("Search for {}").format(term), parent_tab=searches_position, start=True, kwargs=dict(parent=controller.view.nb, function="search_tweets", name="%s-searchterm" % (term,), sessionObject=session, account=session.get_name(), bufferType="searchPanel", sound="search_updated.ogg", q=term, include_ext_alt_text=True, tweet_mode="extended"))
else:
log.error("A buffer for the %s search term is already created. You can't create a duplicate buffer." % (term,))
return
elif dlg.get("users") == True:
pub.sendMessage("createBuffer", buffer_type="SearchPeopleBuffer", session_type=session.type, buffer_title=_("Search for {}").format(term), parent_tab=searches_position, start=True, kwargs=dict(parent=controller.view.nb, function="search_users", name="%s-searchUser" % (term,), sessionObject=session, account=session.get_name(), bufferType=None, sound="search_updated.ogg", q=term))
dlg.Destroy()

View File

@ -10,9 +10,10 @@ from pubsub import pub
log = logging.getLogger("controller.listsController") log = logging.getLogger("controller.listsController")
class listsController(object): class listsController(object):
def __init__(self, session, user=None): def __init__(self, session, user=None, lists_buffer_position=0):
super(listsController, self).__init__() super(listsController, self).__init__()
self.session = session self.session = session
self.lists_buffer_position = lists_buffer_position
if user == None: if user == None:
self.dialog = lists.listViewer() self.dialog = lists.listViewer()
self.dialog.populate_list(self.get_all_lists()) self.dialog.populate_list(self.get_all_lists())
@ -90,7 +91,9 @@ class listsController(object):
def open_list_as_buffer(self, *args, **kwargs): def open_list_as_buffer(self, *args, **kwargs):
if self.dialog.lista.get_count() == 0: return if self.dialog.lista.get_count() == 0: return
list = self.session.db["lists"][self.dialog.get_item()] list = self.session.db["lists"][self.dialog.get_item()]
pub.sendMessage("create-new-buffer", buffer="list", account=self.session.db["user_name"], create=list.name) pub.sendMessage("createBuffer", buffer_type="ListBuffer", session_type=self.session.type, buffer_title=_("List for {}").format(list.name), parent_tab=self.lists_buffer_position, start=True, kwargs=dict(function="list_timeline", name="%s-list" % (list.name,), sessionObject=self.session, account=self.session.get_name(), bufferType=None, sound="list_tweet.ogg", list_id=list.id, include_ext_alt_text=True, tweet_mode="extended"))
self.session.settings["other_buffers"]["lists"].append(list.name)
self.session.settings.write()
def subscribe(self, *args, **kwargs): def subscribe(self, *args, **kwargs):
if self.dialog.lista.get_count() == 0: return if self.dialog.lista.get_count() == 0: return

View File

@ -8,7 +8,7 @@ import output
import sound import sound
import config import config
from pubsub import pub from pubsub import pub
from twitter_text import parse_tweet from twitter_text.parse_tweet import parse_tweet
from wxUI.dialogs import twitterDialogs, urlList from wxUI.dialogs import twitterDialogs, urlList
from wxUI import commonMessageDialogs from wxUI import commonMessageDialogs
from extra import translator, SpellChecker from extra import translator, SpellChecker
@ -23,7 +23,7 @@ class basicTweet(object):
self.max = max self.max = max
self.title = title self.title = title
self.session = session self.session = session
self.message = getattr(twitterDialogs, messageType)(title=title, caption=caption, message=text, *args, **kwargs) self.message = getattr(twitterDialogs, messageType)(title=title, caption=caption, message=text, max_length=max, *args, **kwargs)
self.message.text.SetValue(text) self.message.text.SetValue(text)
self.message.text.SetInsertionPoint(len(self.message.text.GetValue())) self.message.text.SetInsertionPoint(len(self.message.text.GetValue()))
widgetUtils.connect_event(self.message.spellcheck, widgetUtils.BUTTON_PRESSED, self.spellcheck) widgetUtils.connect_event(self.message.spellcheck, widgetUtils.BUTTON_PRESSED, self.spellcheck)
@ -185,6 +185,7 @@ class tweet(basicTweet):
def text_processor(self, *args, **kwargs): def text_processor(self, *args, **kwargs):
super(tweet, self).text_processor(*args, **kwargs) super(tweet, self).text_processor(*args, **kwargs)
if len(self.thread) > 0: if len(self.thread) > 0:
if hasattr(self.message, "tweets"):
self.message.tweets.Enable(True) self.message.tweets.Enable(True)
self.message.remove_tweet.Enable(True) self.message.remove_tweet.Enable(True)
else: else:

View File

@ -0,0 +1,247 @@
# -*- coding: utf-8 -*-
import os
import threading
import logging
import sound_lib
import paths
import widgetUtils
import output
from collections import OrderedDict
from wxUI import commonMessageDialogs
from extra.autocompletionUsers import scan, manage
from extra.ocr import OCRSpace
from controller.settings import globalSettingsController
from . templateEditor import EditTemplate
log = logging.getLogger("Settings")
class accountSettingsController(globalSettingsController):
def __init__(self, buffer, window):
self.user = buffer.session.db["user_name"]
self.buffer = buffer
self.window = window
self.config = buffer.session.settings
super(accountSettingsController, self).__init__()
def create_config(self):
self.dialog.create_general_account()
widgetUtils.connect_event(self.dialog.general.userAutocompletionScan, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_scan)
widgetUtils.connect_event(self.dialog.general.userAutocompletionManage, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_manage)
self.dialog.set_value("general", "relative_time", self.config["general"]["relative_times"])
self.dialog.set_value("general", "show_screen_names", self.config["general"]["show_screen_names"])
self.dialog.set_value("general", "hide_emojis", self.config["general"]["hide_emojis"])
self.dialog.set_value("general", "itemsPerApiCall", self.config["general"]["max_tweets_per_call"])
self.dialog.set_value("general", "reverse_timelines", self.config["general"]["reverse_timelines"])
rt = self.config["general"]["retweet_mode"]
if rt == "ask":
self.dialog.set_value("general", "retweet_mode", _(u"Ask"))
elif rt == "direct":
self.dialog.set_value("general", "retweet_mode", _(u"Retweet without comments"))
else:
self.dialog.set_value("general", "retweet_mode", _(u"Retweet with comments"))
self.dialog.set_value("general", "persist_size", str(self.config["general"]["persist_size"]))
self.dialog.set_value("general", "load_cache_in_memory", self.config["general"]["load_cache_in_memory"])
self.dialog.create_reporting()
self.dialog.set_value("reporting", "speech_reporting", self.config["reporting"]["speech_reporting"])
self.dialog.set_value("reporting", "braille_reporting", self.config["reporting"]["braille_reporting"])
tweet_template = self.config["templates"]["tweet"]
dm_template = self.config["templates"]["dm"]
sent_dm_template = self.config["templates"]["dm_sent"]
person_template = self.config["templates"]["person"]
self.dialog.create_templates(tweet_template=tweet_template, dm_template=dm_template, sent_dm_template=sent_dm_template, person_template=person_template)
widgetUtils.connect_event(self.dialog.templates.tweet, widgetUtils.BUTTON_PRESSED, self.edit_tweet_template)
widgetUtils.connect_event(self.dialog.templates.dm, widgetUtils.BUTTON_PRESSED, self.edit_dm_template)
widgetUtils.connect_event(self.dialog.templates.sent_dm, widgetUtils.BUTTON_PRESSED, self.edit_sent_dm_template)
widgetUtils.connect_event(self.dialog.templates.person, widgetUtils.BUTTON_PRESSED, self.edit_person_template)
self.dialog.create_other_buffers()
buffer_values = self.get_buffers_list()
self.dialog.buffers.insert_buffers(buffer_values)
self.dialog.buffers.connect_hook_func(self.toggle_buffer_active)
widgetUtils.connect_event(self.dialog.buffers.toggle_state, widgetUtils.BUTTON_PRESSED, self.toggle_state)
widgetUtils.connect_event(self.dialog.buffers.up, widgetUtils.BUTTON_PRESSED, self.dialog.buffers.move_up)
widgetUtils.connect_event(self.dialog.buffers.down, widgetUtils.BUTTON_PRESSED, self.dialog.buffers.move_down)
self.dialog.create_ignored_clients(self.config["twitter"]["ignored_clients"])
widgetUtils.connect_event(self.dialog.ignored_clients.add, widgetUtils.BUTTON_PRESSED, self.add_ignored_client)
widgetUtils.connect_event(self.dialog.ignored_clients.remove, widgetUtils.BUTTON_PRESSED, self.remove_ignored_client)
self.input_devices = sound_lib.input.Input.get_device_names()
self.output_devices = sound_lib.output.Output.get_device_names()
self.soundpacks = []
[self.soundpacks.append(i) for i in os.listdir(paths.sound_path()) if os.path.isdir(os.path.join(paths.sound_path(), i)) == True ]
self.dialog.create_sound(self.input_devices, self.output_devices, self.soundpacks)
self.dialog.set_value("sound", "volumeCtrl", int(self.config["sound"]["volume"]*100))
self.dialog.set_value("sound", "input", self.config["sound"]["input_device"])
self.dialog.set_value("sound", "output", self.config["sound"]["output_device"])
self.dialog.set_value("sound", "session_mute", self.config["sound"]["session_mute"])
self.dialog.set_value("sound", "soundpack", self.config["sound"]["current_soundpack"])
self.dialog.set_value("sound", "indicate_audio", self.config["sound"]["indicate_audio"])
self.dialog.set_value("sound", "indicate_geo", self.config["sound"]["indicate_geo"])
self.dialog.set_value("sound", "indicate_img", self.config["sound"]["indicate_img"])
self.dialog.create_extras(OCRSpace.translatable_langs)
self.dialog.set_value("extras", "sndup_apiKey", self.config["sound"]["sndup_api_key"])
language_index = OCRSpace.OcrLangs.index(self.config["mysc"]["ocr_language"])
self.dialog.extras.ocr_lang.SetSelection(language_index)
self.dialog.realize()
self.dialog.set_title(_(u"Account settings for %s") % (self.user,))
self.response = self.dialog.get_response()
def edit_tweet_template(self, *args, **kwargs):
template = self.config["templates"]["tweet"]
control = EditTemplate(template=template, type="tweet")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["tweet"] = result
self.config.write()
self.dialog.templates.tweet.SetLabel(_("Edit template for tweets. Current template: {}").format(result))
def edit_dm_template(self, *args, **kwargs):
template = self.config["templates"]["dm"]
control = EditTemplate(template=template, type="dm")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["dm"] = result
self.config.write()
self.dialog.templates.dm.SetLabel(_("Edit template for direct messages. Current template: {}").format(result))
def edit_sent_dm_template(self, *args, **kwargs):
template = self.config["templates"]["dm_sent"]
control = EditTemplate(template=template, type="dm")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["dm_sent"] = result
self.config.write()
self.dialog.templates.sent_dm.SetLabel(_("Edit template for sent direct messages. Current template: {}").format(result))
def edit_person_template(self, *args, **kwargs):
template = self.config["templates"]["person"]
control = EditTemplate(template=template, type="person")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["person"] = result
self.config.write()
self.dialog.templates.person.SetLabel(_("Edit template for persons. Current template: {}").format(result))
def save_configuration(self):
if self.config["general"]["relative_times"] != self.dialog.get_value("general", "relative_time"):
self.needs_restart = True
log.debug("Triggered app restart due to change in relative times.")
self.config["general"]["relative_times"] = self.dialog.get_value("general", "relative_time")
self.config["general"]["show_screen_names"] = self.dialog.get_value("general", "show_screen_names")
self.config["general"]["hide_emojis"] = self.dialog.get_value("general", "hide_emojis")
self.config["general"]["max_tweets_per_call"] = self.dialog.get_value("general", "itemsPerApiCall")
if self.config["general"]["load_cache_in_memory"] != self.dialog.get_value("general", "load_cache_in_memory"):
self.config["general"]["load_cache_in_memory"] = self.dialog.get_value("general", "load_cache_in_memory")
self.needs_restart = True
log.debug("Triggered app restart due to change in database strategy management.")
if self.config["general"]["persist_size"] != self.dialog.get_value("general", "persist_size"):
if self.dialog.get_value("general", "persist_size") == '':
self.config["general"]["persist_size"] =-1
else:
try:
self.config["general"]["persist_size"] = int(self.dialog.get_value("general", "persist_size"))
except ValueError:
output.speak("Invalid cache size, setting to default.",True)
self.config["general"]["persist_size"] =1764
if self.config["general"]["reverse_timelines"] != self.dialog.get_value("general", "reverse_timelines"):
self.needs_restart = True
log.debug("Triggered app restart due to change in timeline order.")
self.config["general"]["reverse_timelines"] = self.dialog.get_value("general", "reverse_timelines")
rt = self.dialog.get_value("general", "retweet_mode")
if rt == _(u"Ask"):
self.config["general"]["retweet_mode"] = "ask"
elif rt == _(u"Retweet without comments"):
self.config["general"]["retweet_mode"] = "direct"
else:
self.config["general"]["retweet_mode"] = "comment"
buffers_list = self.dialog.buffers.get_list()
if buffers_list != self.config["general"]["buffer_order"]:
self.needs_restart = True
log.debug("Triggered app restart due to change in buffer ordering.")
self.config["general"]["buffer_order"] = buffers_list
self.config["reporting"]["speech_reporting"] = self.dialog.get_value("reporting", "speech_reporting")
self.config["reporting"]["braille_reporting"] = self.dialog.get_value("reporting", "braille_reporting")
self.config["mysc"]["ocr_language"] = OCRSpace.OcrLangs[self.dialog.extras.ocr_lang.GetSelection()]
if self.config["sound"]["input_device"] != self.dialog.sound.get("input"):
self.config["sound"]["input_device"] = self.dialog.sound.get("input")
try:
self.buffer.session.sound.input.set_device(self.buffer.session.sound.input.find_device_by_name(self.config["sound"]["input_device"]))
except:
self.config["sound"]["input_device"] = "default"
if self.config["sound"]["output_device"] != self.dialog.sound.get("output"):
self.config["sound"]["output_device"] = self.dialog.sound.get("output")
try:
self.buffer.session.sound.output.set_device(self.buffer.session.sound.output.find_device_by_name(self.config["sound"]["output_device"]))
except:
self.config["sound"]["output_device"] = "default"
self.config["sound"]["volume"] = self.dialog.get_value("sound", "volumeCtrl")/100.0
self.config["sound"]["session_mute"] = self.dialog.get_value("sound", "session_mute")
self.config["sound"]["current_soundpack"] = self.dialog.sound.get("soundpack")
self.config["sound"]["indicate_audio"] = self.dialog.get_value("sound", "indicate_audio")
self.config["sound"]["indicate_geo"] = self.dialog.get_value("sound", "indicate_geo")
self.config["sound"]["indicate_img"] = self.dialog.get_value("sound", "indicate_img")
self.config["sound"]["sndup_api_key"] = self.dialog.get_value("extras", "sndup_apiKey")
self.buffer.session.sound.config = self.config["sound"]
self.buffer.session.sound.check_soundpack()
self.config.write()
def toggle_state(self,*args,**kwargs):
return self.dialog.buffers.change_selected_item()
def on_autocompletion_scan(self, *args, **kwargs):
configuration = scan.autocompletionScan(self.buffer.session.settings, self.buffer, self.window)
to_scan = configuration.show_dialog()
if to_scan == True:
configuration.prepare_progress_dialog()
t = threading.Thread(target=configuration.scan)
t.start()
def on_autocompletion_manage(self, *args, **kwargs):
configuration = manage.autocompletionManage(self.buffer.session)
configuration.show_settings()
def add_ignored_client(self, *args, **kwargs):
client = commonMessageDialogs.get_ignored_client()
if client == None: return
if client not in self.config["twitter"]["ignored_clients"]:
self.config["twitter"]["ignored_clients"].append(client)
self.dialog.ignored_clients.append(client)
def remove_ignored_client(self, *args, **kwargs):
if self.dialog.ignored_clients.get_clients() == 0: return
id = self.dialog.ignored_clients.get_client_id()
self.config["twitter"]["ignored_clients"].pop(id)
self.dialog.ignored_clients.remove_(id)
def get_buffers_list(self):
all_buffers=OrderedDict()
all_buffers['home']=_(u"Home")
all_buffers['mentions']=_(u"Mentions")
all_buffers['dm']=_(u"Direct Messages")
all_buffers['sent_dm']=_(u"Sent direct messages")
all_buffers['sent_tweets']=_(u"Sent tweets")
all_buffers['favorites']=_(u"Likes")
all_buffers['followers']=_(u"Followers")
all_buffers['friends']=_(u"Friends")
all_buffers['blocks']=_(u"Blocked users")
all_buffers['muted']=_(u"Muted users")
list_buffers = []
hidden_buffers=[]
all_buffers_keys = list(all_buffers.keys())
# Check buffers shown first.
for i in self.config["general"]["buffer_order"]:
if i in all_buffers_keys:
list_buffers.append((i, all_buffers[i], True))
# This second pass will retrieve all hidden buffers.
for i in all_buffers_keys:
if i not in self.config["general"]["buffer_order"]:
hidden_buffers.append((i, all_buffers[i], False))
list_buffers.extend(hidden_buffers)
return list_buffers
def toggle_buffer_active(self, ev):
change = self.dialog.buffers.get_event(ev)
if change == True:
self.dialog.buffers.change_selected_item()

View File

@ -29,42 +29,42 @@ class userActionsController(object):
def follow(self, user): def follow(self, user):
try: try:
self.session.twitter.create_friendship(screen_name=user ) self.session.twitter.create_friendship(screen_name=user )
pub.sendMessage("restartStreaming", session=self.session.session_id) pub.sendMessage("twitter.restart_streaming", session=self.session.session_id)
except TweepyException as err: except TweepyException as err:
output.speak("Error %s" % (str(err)), True) output.speak("Error %s" % (str(err)), True)
def unfollow(self, user): def unfollow(self, user):
try: try:
id = self.session.twitter.destroy_friendship(screen_name=user ) id = self.session.twitter.destroy_friendship(screen_name=user )
pub.sendMessage("restartStreaming", session=self.session.session_id) pub.sendMessage("twitter.restart_streaming", session=self.session.session_id)
except TweepyException as err: except TweepyException as err:
output.speak("Error %s" % (str(err)), True) output.speak("Error %s" % (str(err)), True)
def mute(self, user): def mute(self, user):
try: try:
id = self.session.twitter.create_mute(screen_name=user ) id = self.session.twitter.create_mute(screen_name=user )
pub.sendMessage("restartStreaming", session=self.session.session_id) pub.sendMessage("twitter.restart_streaming", session=self.session.session_id)
except TweepyException as err: except TweepyException as err:
output.speak("Error %s" % (str(err)), True) output.speak("Error %s" % (str(err)), True)
def unmute(self, user): def unmute(self, user):
try: try:
id = self.session.twitter.destroy_mute(screen_name=user ) id = self.session.twitter.destroy_mute(screen_name=user )
pub.sendMessage("restartStreaming", session=self.session.session_id) pub.sendMessage("twitter.restart_streaming", session=self.session.session_id)
except TweepyException as err: except TweepyException as err:
output.speak("Error %s" % (str(err)), True) output.speak("Error %s" % (str(err)), True)
def report(self, user): def report(self, user):
try: try:
id = self.session.twitter.report_spam(screen_name=user ) id = self.session.twitter.report_spam(screen_name=user )
pub.sendMessage("restartStreaming", session=self.session.session_id) pub.sendMessage("twitter.restart_streaming", session=self.session.session_id)
except TweepyException as err: except TweepyException as err:
output.speak("Error %s" % (str(err)), True) output.speak("Error %s" % (str(err)), True)
def block(self, user): def block(self, user):
try: try:
id = self.session.twitter.create_block(screen_name=user ) id = self.session.twitter.create_block(screen_name=user )
pub.sendMessage("restartStreaming", session=self.session.session_id) pub.sendMessage("twitter.restart_streaming", session=self.session.session_id)
except TweepyException as err: except TweepyException as err:
output.speak("Error %s" % (str(err)), True) output.speak("Error %s" % (str(err)), True)

View File

@ -21,7 +21,7 @@ def parse(s):
lst.remove(item) lst.remove(item)
#end if #end if
if len(lst) > 1: #more than one key, parse error if len(lst) > 1: #more than one key, parse error
raise ValueError, 'unknown modifier %s' % lst[0] raise ValueError('unknown modifier %s' % lst[0])
return (m, lst[0].lower()) return (m, lst[0].lower())
class AtspiThread(threading.Thread): class AtspiThread(threading.Thread):
def run(self): def run(self):

View File

@ -6,27 +6,26 @@ from wxUI.dialogs import baseDialog
class keystrokeEditorDialog(baseDialog.BaseWXDialog): class keystrokeEditorDialog(baseDialog.BaseWXDialog):
def __init__(self): def __init__(self):
super(keystrokeEditorDialog, self).__init__(parent=None, id=-1, title=_(u"Keystroke editor")) super(keystrokeEditorDialog, self).__init__(parent=None, id=-1, title=_(u"Keystroke editor"))
panel = wx.Panel(self)
self.actions = [] self.actions = []
sizer = wx.BoxSizer(wx.VERTICAL) sizer = wx.BoxSizer(wx.VERTICAL)
keysText = wx.StaticText(panel, -1, _(u"Select a keystroke to edit")) keysText = wx.StaticText(self, -1, _(u"Select a keystroke to edit"))
self.keys = widgets.list(self, _(u"Action"), _(u"Keystroke"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL, size=(400, 450)) self.keys = widgets.list(self, _(u"Action"), _(u"Keystroke"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL, size=(400, 450))
self.keys.list.SetFocus() self.keys.list.SetFocus()
firstSizer = wx.BoxSizer(wx.HORIZONTAL) firstSizer = wx.BoxSizer(wx.HORIZONTAL)
firstSizer.Add(keysText, 0, wx.ALL, 5) firstSizer.Add(keysText, 0, wx.ALL, 5)
firstSizer.Add(self.keys.list, 0, wx.ALL, 5) firstSizer.Add(self.keys.list, 0, wx.ALL, 5)
self.edit = wx.Button(panel, -1, _(u"Edit")) self.edit = wx.Button(self, -1, _(u"Edit"))
self.edit.SetDefault() self.edit.SetDefault()
self.undefine = wx.Button(panel, -1, _("Undefine keystroke")) self.undefine = wx.Button(self, -1, _("Undefine keystroke"))
self.execute = wx.Button(panel, -1, _(u"Execute action")) self.execute = wx.Button(self, -1, _(u"Execute action"))
close = wx.Button(panel, wx.ID_CANCEL, _(u"Close")) close = wx.Button(self, wx.ID_CANCEL, _(u"Close"))
secondSizer = wx.BoxSizer(wx.HORIZONTAL) secondSizer = wx.BoxSizer(wx.HORIZONTAL)
secondSizer.Add(self.edit, 0, wx.ALL, 5) secondSizer.Add(self.edit, 0, wx.ALL, 5)
secondSizer.Add(self.execute, 0, wx.ALL, 5) secondSizer.Add(self.execute, 0, wx.ALL, 5)
secondSizer.Add(close, 0, wx.ALL, 5) secondSizer.Add(close, 0, wx.ALL, 5)
sizer.Add(firstSizer, 0, wx.ALL, 5) sizer.Add(firstSizer, 0, wx.ALL, 5)
sizer.Add(secondSizer, 0, wx.ALL, 5) sizer.Add(secondSizer, 0, wx.ALL, 5)
panel.SetSizer(sizer) self.SetSizer(sizer)
self.SetClientSize(sizer.CalcMin()) self.SetClientSize(sizer.CalcMin())
def put_keystrokes(self, actions, keystrokes): def put_keystrokes(self, actions, keystrokes):

53
src/mastodon.defaults Normal file
View File

@ -0,0 +1,53 @@
[mastodon]
access_token = string(default="")
instance = string(default="")
user_name = string(default="")
ignored_clients = list(default=list())
[general]
relative_times = boolean(default=True)
max_posts_per_call = integer(default=40)
reverse_timelines = boolean(default=False)
persist_size = integer(default=0)
load_cache_in_memory=boolean(default=True)
show_screen_names = boolean(default=False)
buffer_order = list(default=list('home', 'local', 'mentions', 'direct_messages', 'sent', 'favorites', 'bookmarks', 'followers', 'following', 'blocked', 'muted', 'notifications'))
boost_mode = string(default="ask")
[sound]
volume = float(default=1.0)
input_device = string(default="Default")
output_device = string(default="Default")
session_mute = boolean(default=False)
current_soundpack = string(default="FreakyBlue")
indicate_audio = boolean(default=True)
indicate_img = boolean(default=True)
[other_buffers]
timelines = list(default=list())
searches = list(default=list())
lists = list(default=list())
followers_timelines = list(default=list())
following_timelines = list(default=list())
trending_topic_buffers = list(default=list())
muted_buffers = list(default=list())
autoread_buffers = list(default=list(mentions, direct_messages, events))
[mysc]
spelling_language = string(default="")
save_followers_in_autocompletion_db = boolean(default=False)
save_friends_in_autocompletion_db = boolean(default=False)
ocr_language = string(default="")
[reporting]
braille_reporting = boolean(default=True)
speech_reporting = boolean(default=True)
[templates]
post = string(default="$display_name, $safe_text $image_descriptions $date. $visibility. $source")
person = string(default="$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
conversation = string(default="Conversation with $users. Last message: $last_post")
[filters]
[user-aliases]

View File

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
testmodules = ["test.test_cache"]
suite = unittest.TestSuite()
for t in testmodules:
try:
# If the module defines a suite() function, call it to get the suite.
mod = __import__(t, globals(), locals(), ['suite'])
suitefn = getattr(mod, 'suite')
suite.addTest(suitefn())
except (ImportError, AttributeError):
# else, just load all the test cases from the module.
suite.addTest(unittest.defaultTestLoader.loadTestsFromName(t))
unittest.TextTestRunner(verbosity=2).run(suite)

View File

@ -1,42 +1,25 @@
# -*- coding: cp1252 -*- # -*- coding: cp1252 -*-
#from config_utils import Configuration, ConfigurationResetException """ Lightweigth module that saves session position across global config and performs validation of config files. """
from __future__ import unicode_literals
from builtins import object
import config
import paths
import os import os
import logging import logging
import config
import paths
log = logging.getLogger("sessionmanager.manager") log = logging.getLogger("sessionmanager.manager")
from sessions import session_exceptions from sessions import session_exceptions
manager = None manager = None
def setup(): def setup():
""" Creates the singleton instance used within TWBlue to access this object. """
global manager global manager
if not manager: if not manager:
manager = sessionManager() manager = sessionManager()
class sessionManager(object): class sessionManager(object):
# def __init__(self):
# FILE = "sessions.conf"
# SPEC = "app-configuration.defaults"
# try:
# self.main = Configuration(paths.config_path(FILE), paths.app_path(SPEC))
# except ConfigurationResetException:
# pass
def get_current_session(self): def get_current_session(self):
""" Returns the currently focused session, if valid. """
if self.is_valid(config.app["sessions"]["current_session"]): if self.is_valid(config.app["sessions"]["current_session"]):
return config.app["sessions"]["current_session"] return config.app["sessions"]["current_session"]
else:
return False
def add_session(self, id):
log.debug("Adding a new session: %s" % (id,))
path = os.path.join(paths.config_path(), id)
if not os.path.exists(path):
log.debug("Creating %s path" % (os.path.join(paths.config_path(), path),))
os.mkdir(path)
config.app["sessions"]["sessions"].append(id)
def set_current_session(self, sessionID): def set_current_session(self, sessionID):
config.app["sessions"]["current_session"] = sessionID config.app["sessions"]["current_session"] = sessionID

View File

@ -1,38 +1,51 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import shutil """ Module to perform session actions such as addition, removal or display of the global settings dialogue. """
import widgetUtils
import output
from . import wxUI as view
from controller import settings
import paths
import time import time
import os import os
import logging import logging
import widgetUtils
import sessions import sessions
from sessions.twitter import session import output
from . import manager import paths
import config_utils import config_utils
import config import config
from pubsub import pub
from tweepy.errors import TweepyException from tweepy.errors import TweepyException
from controller import settings
from sessions.twitter import session as TwitterSession
from sessions.mastodon import session as MastodonSession
from . import manager
from . import wxUI as view
log = logging.getLogger("sessionmanager.sessionManager") log = logging.getLogger("sessionmanager.sessionManager")
class sessionManagerController(object): class sessionManagerController(object):
def __init__(self, started=False): def __init__(self, started: bool = False):
""" Class constructor.
Creates the SessionManager class controller, responsible for the accounts within TWBlue. From this dialog, users can add/Remove accounts, or load the global settings dialog.
:param started: Indicates whether this object is being created during application startup (when no other controller has been instantiated) or not. It is important for us to know this, as we won't show the button to open global settings dialog if the application has been started. Users must choose the corresponding option in the menu bar.
:type started: bool
"""
super(sessionManagerController, self).__init__() super(sessionManagerController, self).__init__()
log.debug("Setting up the session manager.") log.debug("Setting up the session manager.")
self.started = started self.started = started
# Initialize the manager, responsible for storing session objects.
manager.setup() manager.setup()
self.view = view.sessionManagerWindow() self.view = view.sessionManagerWindow()
widgetUtils.connect_event(self.view.new, widgetUtils.BUTTON_PRESSED, self.manage_new_account) pub.subscribe(self.manage_new_account, "sessionmanager.new_account")
widgetUtils.connect_event(self.view.remove, widgetUtils.BUTTON_PRESSED, self.remove) pub.subscribe(self.remove_account, "sessionmanager.remove_account")
if self.started == False: if self.started == False:
widgetUtils.connect_event(self.view.configuration, widgetUtils.BUTTON_PRESSED, self.configuration) pub.subscribe(self.configuration, "sessionmanager.configuration")
else: else:
self.view.hide_configuration() self.view.hide_configuration()
# Store a temporary copy of new and removed sessions, so we will perform actions on them during call to on_ok.
self.new_sessions = {} self.new_sessions = {}
self.removed_sessions = [] self.removed_sessions = []
def fill_list(self): def fill_list(self):
""" Fills the session list with all valid sessions that could be found in config path. """
sessionsList = [] sessionsList = []
reserved_dirs = ["dicts"] reserved_dirs = ["dicts"]
log.debug("Filling the sessions list.") log.debug("Filling the sessions list.")
@ -51,10 +64,16 @@ class sessionManagerController(object):
output.speak("An exception was raised while attempting to clean malformed session data. See the error log for details. If this message persists, contact the developers.",True) output.speak("An exception was raised while attempting to clean malformed session data. See the error log for details. If this message persists, contact the developers.",True)
os.exception("Exception thrown while removing malformed session") os.exception("Exception thrown while removing malformed session")
continue continue
name = config_test["twitter"]["user_name"] if config_test.get("twitter") != None:
name = _("{account_name} (Twitter)").format(account_name=config_test["twitter"]["user_name"])
if config_test["twitter"]["user_key"] != "" and config_test["twitter"]["user_secret"] != "": if config_test["twitter"]["user_key"] != "" and config_test["twitter"]["user_secret"] != "":
sessionsList.append(name) sessionsList.append(name)
self.sessions.append(i) self.sessions.append(dict(type="twitter", id=i))
elif config_test.get("mastodon") != None:
name = _("{account_name} (Mastodon)").format(account_name=config_test["mastodon"]["user_name"])
if config_test["mastodon"]["instance"] != "" and config_test["mastodon"]["access_token"] != "":
sessionsList.append(name)
self.sessions.append(dict(type="mastodon", id=i))
else: else:
try: try:
log.debug("Deleting session %s" % (i,)) log.debug("Deleting session %s" % (i,))
@ -65,6 +84,7 @@ class sessionManagerController(object):
self.view.fill_list(sessionsList) self.view.fill_list(sessionsList)
def show(self): def show(self):
""" Displays the session manager dialog. """
if self.view.get_response() == widgetUtils.OK: if self.view.get_response() == widgetUtils.OK:
self.do_ok() self.do_ok()
# else: # else:
@ -73,49 +93,49 @@ class sessionManagerController(object):
def do_ok(self): def do_ok(self):
log.debug("Starting sessions...") log.debug("Starting sessions...")
for i in self.sessions: for i in self.sessions:
if (i in sessions.sessions) == True: continue # Skip already created sessions. Useful when session manager controller is not created during startup.
s = session.Session(i) if sessions.sessions.get(i.get("id")) != None:
continue
# Create the session object based in session type.
if i.get("type") == "twitter":
s = TwitterSession.Session(i.get("id"))
elif i.get("type") == "mastodon":
s = MastodonSession.Session(i.get("id"))
s.get_configuration() s.get_configuration()
if i not in config.app["sessions"]["ignored_sessions"]: if i.get("id") not in config.app["sessions"]["ignored_sessions"]:
try: try:
s.login() s.login()
except TweepyException: except TweepyException:
self.show_auth_error(s.settings["twitter"]["user_name"]) self.show_auth_error(s.settings["twitter"]["user_name"])
continue continue
sessions.sessions[i] = s sessions.sessions[i.get("id")] = s
self.new_sessions[i] = s self.new_sessions[i.get("id")] = s
# self.view.destroy() # self.view.destroy()
def show_auth_error(self, user_name): def show_auth_error(self, user_name):
error = view.auth_error(user_name) error = view.auth_error(user_name)
def manage_new_account(self, *args, **kwargs): def manage_new_account(self, type):
if self.view.new_account_dialog() == widgetUtils.YES: # Generic settings for all account types.
location = (str(time.time())[-6:]) location = (str(time.time())[-6:])
log.debug("Creating session in the %s path" % (location,)) log.debug("Creating %s session in the %s path" % (type, location))
s = session.Session(location) if type == "twitter":
manager.manager.add_session(location) s = TwitterSession.Session(location)
s.get_configuration() elif type == "mastodon":
# try: s = MastodonSession.Session(location)
s.authorise() result = s.authorise()
self.sessions.append(location) if result == True:
self.sessions.append(dict(id=location, type=type))
self.view.add_new_session_to_list() self.view.add_new_session_to_list()
s.settings.write()
# except:
# log.exception("Error authorising the session")
# self.view.show_unauthorised_error()
# return
def remove(self, *args, **kwargs): def remove_account(self, index):
if self.view.remove_account_dialog() == widgetUtils.YES: selected_account = self.sessions[index]
selected_account = self.sessions[self.view.get_selected()] self.view.remove_session(index)
self.view.remove_session(self.view.get_selected()) self.removed_sessions.append(selected_account.get("id"))
self.removed_sessions.append(selected_account)
self.sessions.remove(selected_account) self.sessions.remove(selected_account)
shutil.rmtree(path=os.path.join(paths.config_path(), selected_account), ignore_errors=True) shutil.rmtree(path=os.path.join(paths.config_path(), selected_account.get("id")), ignore_errors=True)
def configuration(self):
def configuration(self, *args, **kwargs):
""" Opens the global settings dialogue.""" """ Opens the global settings dialogue."""
d = settings.globalSettingsController() d = settings.globalSettingsController()
if d.response == widgetUtils.OK: if d.response == widgetUtils.OK:

View File

@ -1,10 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals """ Base GUI (Wx) class for the Session manager module."""
import wx import wx
from pubsub import pub
from multiplatform_widgets import widgets from multiplatform_widgets import widgets
import application
class sessionManagerWindow(wx.Dialog): class sessionManagerWindow(wx.Dialog):
""" Dialog that displays all session managing capabilities to users. """
def __init__(self): def __init__(self):
super(sessionManagerWindow, self).__init__(parent=None, title=_(u"Session manager"), size=wx.DefaultSize) super(sessionManagerWindow, self).__init__(parent=None, title=_(u"Session manager"), size=wx.DefaultSize)
panel = wx.Panel(self) panel = wx.Panel(self)
@ -16,8 +17,11 @@ class sessionManagerWindow(wx.Dialog):
listSizer.Add(self.list.list, 0, wx.ALL, 5) listSizer.Add(self.list.list, 0, wx.ALL, 5)
sizer.Add(listSizer, 0, wx.ALL, 5) sizer.Add(listSizer, 0, wx.ALL, 5)
self.new = wx.Button(panel, -1, _(u"New account"), size=wx.DefaultSize) self.new = wx.Button(panel, -1, _(u"New account"), size=wx.DefaultSize)
self.new.Bind(wx.EVT_BUTTON, self.on_new_account)
self.remove = wx.Button(panel, -1, _(u"Remove account")) self.remove = wx.Button(panel, -1, _(u"Remove account"))
self.remove.Bind(wx.EVT_BUTTON, self.on_remove)
self.configuration = wx.Button(panel, -1, _(u"Global Settings")) self.configuration = wx.Button(panel, -1, _(u"Global Settings"))
self.configuration.Bind(wx.EVT_BUTTON, self.on_configuration)
ok = wx.Button(panel, wx.ID_OK, size=wx.DefaultSize) ok = wx.Button(panel, wx.ID_OK, size=wx.DefaultSize)
ok.SetDefault() ok.SetDefault()
cancel = wx.Button(panel, wx.ID_CANCEL, size=wx.DefaultSize) cancel = wx.Button(panel, wx.ID_CANCEL, size=wx.DefaultSize)
@ -42,11 +46,29 @@ class sessionManagerWindow(wx.Dialog):
if self.list.get_count() == 0: if self.list.get_count() == 0:
wx.MessageDialog(None, _(u"You need to configure an account."), _(u"Account Error"), wx.ICON_ERROR).ShowModal() wx.MessageDialog(None, _(u"You need to configure an account."), _(u"Account Error"), wx.ICON_ERROR).ShowModal()
return return
self.controller.do_ok()
self.EndModal(wx.ID_OK) self.EndModal(wx.ID_OK)
def new_account_dialog(self): def on_new_account(self, *args, **kwargs):
return wx.MessageDialog(self, _(u"The request to authorize your Twitter account will be opened in your browser. You only need to do this once. Would you like to continue?"), _(u"Authorization"), wx.YES_NO).ShowModal() menu = wx.Menu()
twitter = menu.Append(wx.ID_ANY, _("Twitter"))
mastodon = menu.Append(wx.ID_ANY, _("Mastodon"))
menu.Bind(wx.EVT_MENU, self.on_new_twitter_account, twitter)
menu.Bind(wx.EVT_MENU, self.on_new_mastodon_account, mastodon)
self.PopupMenu(menu, self.new.GetPosition())
def on_new_mastodon_account(self, *args, **kwargs):
dlg = wx.MessageDialog(self, _("You will be prompted for your Mastodon data (instance URL, email address and password) so we can authorise TWBlue in your instance. Would you like to authorise your account now?"), _(u"Authorization"), wx.YES_NO)
response = dlg.ShowModal()
dlg.Destroy()
if response == wx.ID_YES:
pub.sendMessage("sessionmanager.new_account", type="mastodon")
def on_new_twitter_account(self, *args, **kwargs):
dlg = wx.MessageDialog(self, _(u"The request to authorize your Twitter account will be opened in your browser. You only need to do this once. Would you like to continue?"), _(u"Authorization"), wx.YES_NO)
response = dlg.ShowModal()
dlg.Destroy()
if response == wx.ID_YES:
pub.sendMessage("sessionmanager.new_account", type="twitter")
def add_new_session_to_list(self): def add_new_session_to_list(self):
total = self.list.get_count() total = self.list.get_count()
@ -61,8 +83,16 @@ class sessionManagerWindow(wx.Dialog):
def get_response(self): def get_response(self):
return self.ShowModal() return self.ShowModal()
def remove_account_dialog(self): def on_remove(self, *args, **kwargs):
return wx.MessageDialog(self, _(u"Do you really want to delete this account?"), _(u"Remove account"), wx.YES_NO).ShowModal() dlg = wx.MessageDialog(self, _(u"Do you really want to delete this account?"), _(u"Remove account"), wx.YES_NO)
response = dlg.ShowModal()
dlg.Destroy()
if response == wx.ID_YES:
selected = self.list.get_selected()
pub.sendMessage("sessionmanager.remove_account", index=selected)
def on_configuration(self, *args, **kwargs):
pub.sendMessage("sessionmanager.configuration")
def get_selected(self): def get_selected(self):
return self.list.get_selected() return self.list.get_selected()

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" this package contains code related to Sessions. """ this package contains code related to Sessions.
In TWBlue, a session module defines everything a social network needs to be used in the program.""" In TWBlue, a session module defines everything a social network needs to be used in the program."""
from __future__ import unicode_literals
# let's define a global object for storing sessions across the program. # let's define a global object for storing sessions across the program.
sessions = {} sessions = {}

View File

@ -6,10 +6,12 @@ import output
import time import time
import sound import sound
import logging import logging
import config
import config_utils import config_utils
import sqlitedict import sqlitedict
import application import application
from . import session_exceptions as Exceptions from . import session_exceptions as Exceptions
log = logging.getLogger("sessionmanager.session") log = logging.getLogger("sessionmanager.session")
class baseSession(object): class baseSession(object):
@ -52,6 +54,13 @@ class baseSession(object):
def is_logged(self): def is_logged(self):
return self.logged return self.logged
def create_session_folder(self):
path = os.path.join(paths.config_path(), self.session_id)
if not os.path.exists(path):
log.debug("Creating %s path" % (os.path.join(paths.config_path(), path),))
os.mkdir(path)
config.app["sessions"]["sessions"].append(id)
def get_configuration(self): def get_configuration(self):
""" Get settings for a session.""" """ Get settings for a session."""
file_ = "%s/session.conf" % (self.session_id,) file_ = "%s/session.conf" % (self.session_id,)
@ -60,6 +69,9 @@ class baseSession(object):
self.init_sound() self.init_sound()
self.load_persistent_data() self.load_persistent_data()
def get_name(self):
pass
def init_sound(self): def init_sound(self):
try: self.sound = sound.soundSystem(self.settings["sound"]) try: self.sound = sound.soundSystem(self.settings["sound"])
except: pass except: pass

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
import arrow
import languageHandler
from . import utils, templates
def compose_post(post, db, relative_times, show_screen_names):
if show_screen_names == False:
user = post.account.get("display_name")
if user == "":
user = post.account.get("username")
else:
user = post.account.get("acct")
original_date = arrow.get(post.created_at)
if relative_times:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = original_date.shift(hours=db["utc_offset"]).format(_("dddd, MMMM D, YYYY H:m"), locale=languageHandler.curLang[:2])
if post.reblog != None:
text = _("Boosted from @{}: {}").format(post.reblog.account.acct, templates.process_text(post.reblog))
else:
text = templates.process_text(post)
source = post.get("application", "")
# "" means remote user, None for legacy apps so we should cover both sides.
if source != None and source != "":
source = source.get("name", "")
else:
source = ""
return [user+", ", text, ts+", ", source]
def compose_user(user, db, relative_times=True, show_screen_names=False):
original_date = arrow.get(user.created_at)
if relative_times:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = original_date.shift(hours=db["utc_offset"]).format(_("dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
name = user.display_name
if name == "":
name = user.get("username")
return [_("%s (@%s). %s followers, %s following, %s posts. Joined %s") % (name, user.acct, user.followers_count, user.following_count, user.statuses_count, ts)]
def compose_conversation(conversation, db, relative_times, show_screen_names):
users = []
for account in conversation.accounts:
if account.display_name != "":
users.append(account.display_name)
else:
users.append(account.username)
users = ", ".join(users)
last_post = compose_post(conversation.last_status, db, relative_times, show_screen_names)
text = _("Last message from {}: {}").format(last_post[0], last_post[1])
return [users, text, last_post[-2], last_post[-1]]

View File

@ -0,0 +1,290 @@
# -*- coding: utf-8 -*-
import os
import paths
import time
import logging
import webbrowser
import wx
import mastodon
import config
import config_utils
import output
import application
from mastodon import MastodonError, MastodonNotFoundError, MastodonUnauthorizedError
from pubsub import pub
from mysc.thread_utils import call_threaded
from sessions import base
from sessions.mastodon import utils, streaming
from .wxUI import authorisationDialog
log = logging.getLogger("sessions.mastodonSession")
MASTODON_VERSION = "4.0.1"
class Session(base.baseSession):
def __init__(self, *args, **kwargs):
super(Session, self).__init__(*args, **kwargs)
self.config_spec = "mastodon.defaults"
self.supported_languages = []
self.type = "mastodon"
self.db["pagination_info"] = dict()
self.char_limit = 500
pub.subscribe(self.on_status, "mastodon.status_received")
pub.subscribe(self.on_status_updated, "mastodon.status_updated")
pub.subscribe(self.on_notification, "mastodon.notification_received")
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"]))
self.api = mastodon.Mastodon(access_token=self.settings["mastodon"]["access_token"], api_base_url=self.settings["mastodon"]["instance"], mastodon_version=MASTODON_VERSION)
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
except IOError:
log.error("The login attempt failed.")
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.")
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
try:
client_id, client_secret = mastodon.Mastodon.create_app("TWBlue", api_base_url=authorisation_dialog.GetValue(), website="https://twblue.es")
temporary_api = mastodon.Mastodon(client_id=client_id, client_secret=client_secret, api_base_url=instance, mastodon_version=MASTODON_VERSION)
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
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
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()
self.settings["mastodon"]["access_token"] = access_token
self.settings["mastodon"]["instance"] = instance
self.settings.write()
return True
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
if len(self.supported_languages) == 0:
self.supported_languages = self.api.instance().languages
self.get_lists()
self.get_muted_users()
# determine instance custom characters limit.
instance = self.api.instance()
if hasattr(instance, "max_toot_chars"):
self.char_limit = instance.max_toot_chars
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):
aliases = self.settings.get("user-aliases")
if aliases == None:
log.error("Aliases are not defined for this config spec.")
return user.name
user_alias = aliases.get(user.id_str)
if user_alias != None:
return user_alias
return user.name
def order_buffer(self, name, data, ignore_older=False):
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
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
return item_position
return False
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)
while finished==False and tries < 25:
try:
val = getattr(self.api, call_name)(*args, **kwargs)
finished = True
except MastodonError as e:
output.speak(str(e))
val = None
if type(e) != MastodonNotFoundError and type(e) != MastodonUnauthorizedError :
tries = tries+1
time.sleep(5)
elif report_failure:
output.speak(_("%s failed. Reason: %s") % (action, str(e)))
finished = True
# except:
# tries = tries + 1
# time.sleep(5)
if report_success:
output.speak(_("%s succeeded.") % action)
if _sound != None: self.sound.play(_sound)
return val
def send_post(self, reply_to=None, users=None, visibility=None, posts=[]):
""" Convenience function to send a thread. """
in_reply_to_id = reply_to
for obj in posts:
text = obj.get("text")
if len(obj["attachments"]) == 0:
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 item != None:
in_reply_to_id = item["id"]
else:
media_ids = []
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"]:
img = self.api_call("media_post", media_file=i["file"], description=i["description"])
media_ids.append(img.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"]
def get_name(self):
instance = self.settings["mastodon"]["instance"]
instance = instance.replace("https://", "")
user = self.settings["mastodon"]["user_name"]
return "Mastodon: {}@{}".format(user, instance)
def start_streaming(self):
if config.app["app-settings"]["no_streaming"]:
return
listener = streaming.StreamListener(session_name=self.get_name(), user_id=self.db["user_id"])
self.user_stream = self.api.stream_user(listener, run_async=True)
self.direct_stream = self.api.stream_direct(listener, run_async=True)
def stop_streaming(self):
if config.app["app-settings"]["no_streaming"]:
return
# if hasattr(self, "user_stream"):
# self.user_stream.close()
# log.debug("Stream stopped for accounr {}".format(self.db["user_name"]))
# if hasattr(self, "direct_stream"):
# self.direct_stream.close()
# log.debug("Stream stopped for accounr {}".format(self.db["user_name"]))
def check_streams(self):
if config.app["app-settings"]["no_streaming"]:
return
if not hasattr(self, "user_stream"):
return
if self.user_stream.is_alive() == False or self.user_stream.is_receiving() == False or self.direct_stream.is_alive() == False or self.direct_stream.is_receiving() == False:
self.start_streaming()
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)
pub.sendMessage("mastodon.new_item", session_name=self.get_name(), item=status, _buffers=buffers)
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
buffers = {}
for b in list(self.db.keys()):
updated = self.update_item(b, status)
if updated != False:
buffers[b] = updated
pub.sendMessage("mastodon.updated_item", session_name=self.get_name(), item=status, _buffers=buffers)
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"]
obj = notification.status
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)
pub.sendMessage("mastodon.new_item", session_name=self.get_name(), item=obj, _buffers=buffers)

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
import mastodon
from pubsub import pub
class StreamListener(mastodon.StreamListener):
def __init__(self, session_name, user_id):
self.session_name = session_name
self.user_id = user_id
super(StreamListener, self).__init__()
def on_update(self, status):
pub.sendMessage("mastodon.status_received", status=status, session_name=self.session_name)
def on_status_update(self, status):
pub.sendMessage("mastodon.status_updated", status=status, session_name=self.session_name)
def on_conversation(self, conversation):
pub.sendMessage("mastodon.conversation_received", conversation=conversation, session_name=self.session_name)
def on_notification(self, notification):
pub.sendMessage("mastodon.notification_received", notification=notification, session_name=self.session_name)
def on_unknown_event(self, event, payload):
log.error("Unknown event: {} with payload as {}".format(event, payload))

View File

@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
import re
import arrow
import languageHandler
from string import Template
from . import utils, compose
# Define variables that would be available for all template objects.
# This will be used for the edit template dialog.
# Available variables for post objects.
# safe_text will be the content warning in case a post contains one, text will always be the full text, no matter if has a content warning or not.
post_variables = ["date", "display_name", "screen_name", "source", "lang", "safe_text", "text", "image_descriptions", "visibility"]
person_variables = ["display_name", "screen_name", "description", "followers", "following", "favorites", "posts", "created_at"]
conversation_variables = ["users", "last_post"]
# Default, translatable templates.
post_default_template = _("$display_name, $text $image_descriptions $date. $source")
dm_sent_default_template = _("Dm to $recipient_display_name, $text $date")
person_default_template = _("$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
def process_date(field, relative_times=True, offset_hours=0):
original_date = arrow.get(field)
if relative_times == True:
ts = original_date.humanize(locale=languageHandler.curLang[:2])
else:
ts = original_date.shift(hours=offset_hours).format(_("dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
return ts
def process_text(post, safe=True):
# text = utils.clean_mentions(utils.StripChars(text))
if safe == True and post.sensitive == True and post.spoiler_text != "":
return _("Content warning: {}").format(post.spoiler_text)
return utils.html_filter(post.content)
def process_image_descriptions(media_attachments):
""" Attempt to extract information for image descriptions. """
image_descriptions = []
for media in media_attachments:
if media.get("description") != None and media.get("description") != "":
image_descriptions.append(media.get("description"))
idescriptions = ""
for image in image_descriptions:
idescriptions = idescriptions + _("Image description: {}").format(image) + "\n"
return idescriptions
def remove_unneeded_variables(template, variables):
for variable in variables:
template = re.sub("\$"+variable, "", template)
return template
def render_post(post, template, relative_times=False, offset_hours=0):
""" Renders any given post according to the passed template.
Available data for posts will be stored in the following variables:
$date: Creation date.
$display_name: User profile name.
$screen_name: User screen name, this is the same name used to reference the user in Twitter.
$ source: Source client from where the current tweet was sent.
$lang: Two letter code for the automatically detected language for the tweet. This detection is performed by Twitter.
$safe_text: Safe text to display. If a content warning is applied in posts, display those instead of the whole post.
$text: Toot text. This always displays the full text, even if there is a content warning present.
$image_descriptions: Information regarding image descriptions added by twitter users.
$visibility: post's visibility: public, not listed, followers only or direct.
"""
global post_variables
available_data = dict()
created_at = process_date(post.created_at, relative_times, offset_hours)
available_data.update(date=created_at)
# user.
display_name = post.account.display_name
if display_name == "":
display_name = post.account.username
available_data.update(display_name=display_name, screen_name=post.account.acct)
# Source client from where tweet was originated.
source = ""
if hasattr(post, "application") and post.application != None:
available_data.update(source=post.application.get("name"))
if post.reblog != None:
text = _("Boosted from @{}: {}").format(post.reblog.account.acct, process_text(post.reblog, safe=False), )
safe_text = _("Boosted from @{}: {}").format(post.reblog.account.acct, process_text(post.reblog), )
else:
text = process_text(post, safe=False)
safe_text = process_text(post)
visibility_settings = dict(public=_("Public"), unlisted=_("Not listed"), private=_("Followers only"), direct=_("Direct"))
visibility = visibility_settings.get(post.visibility)
available_data.update(lang=post.language, text=text, safe_text=safe_text, visibility=visibility)
# process image descriptions
image_descriptions = ""
if post.reblog != None:
image_descriptions = process_image_descriptions(post.reblog.media_attachments)
else:
image_descriptions = process_image_descriptions(post.media_attachments)
if image_descriptions != "":
available_data.update(image_descriptions=image_descriptions)
result = Template(_(template)).safe_substitute(**available_data)
result = remove_unneeded_variables(result, post_variables)
return result
def render_user(user, template, relative_times=True, offset_hours=0):
""" Renders persons by using the provided template.
Available data will be stored in the following variables:
$display_name: The name of the user, as theyve defined it. Not necessarily a persons name. Typically capped at 50 characters, but subject to change.
$screen_name: The screen name, handle, or alias that this user identifies themselves with.
$description: The user-defined UTF-8 string describing their account.
$followers: The number of followers this account currently has. This value might be inaccurate.
$following: The number of users this account is following (AKA their followings). This value might be inaccurate.
$posts: The number of Tweets (including retweets) issued by the user. This value might be inaccurate.
$created_at: The date and time that the user account was created on Twitter.
"""
global person_variables
display_name = user.display_name
if display_name == "":
display_name = user.username
available_data = dict(display_name=display_name, screen_name=user.acct, followers=user.followers_count, following=user.following_count, posts=user.statuses_count)
# Nullable values.
nullables = ["description"]
for nullable in nullables:
if hasattr(user, nullable) and getattr(user, nullable) != None:
available_data[nullable] = getattr(user, nullable)
created_at = process_date(user.created_at, relative_times=relative_times, offset_hours=offset_hours)
available_data.update(created_at=created_at)
result = Template(_(template)).safe_substitute(**available_data)
result = remove_unneeded_variables(result, person_variables)
return result
def render_conversation(conversation, template, post_template, relative_times=False, offset_hours=0):
users = []
for account in conversation.accounts:
if account.display_name != "":
users.append(account.display_name)
else:
users.append(account.username)
users = ", ".join(users)
last_post = render_post(conversation.last_status, post_template, relative_times=relative_times, offset_hours=offset_hours)
available_data = dict(users=users, last_post=last_post)
result = Template(_(template)).safe_substitute(**available_data)
result = remove_unneeded_variables(result, conversation_variables)
return result

View File

@ -0,0 +1,60 @@
import re
from html.parser import HTMLParser
url_re = re.compile('<a\s*href=[\'|"](.*?)[\'"].*?>')
class HTMLFilter(HTMLParser):
text = ""
def handle_data(self, data):
self.text += data
def handle_starttag(self, tag, attrs):
if tag == "br":
self.text = self.text+"\n"
def html_filter(data):
f = HTMLFilter()
f.feed(data)
return f.text
def find_item(item, listItems):
for i in range(0, len(listItems)):
if listItems[i].id == item.id:
return i
if hasattr(item, "reblog") and item.reblog != None and item.reblog.id == listItems[i].id:
return i
return None
def is_audio_or_video(post):
if post.reblog != None:
return is_audio_or_video(post.reblog)
# Checks firstly for Mastodon native videos and audios.
for media in post.media_attachments:
if media["type"] == "video" or media["type"] == "audio":
return True
def is_image(post):
if post.reblog != None:
return is_image(post.reblog)
# Checks firstly for Mastodon native videos and audios.
for media in post.media_attachments:
if media["type"] == "gifv" or media["type"] == "image":
return True
def get_media_urls(post):
if hasattr(post, "reblog") and post.reblog != None:
return get_media_urls(post.reblog)
urls = []
for media in post.media_attachments:
if media.get("type") == "audio" or media.get("type") == "video":
urls.append(media.get("url"))
return urls
def find_urls(post, include_tags=False):
urls = url_re.findall(post.content)
if include_tags == False:
for tag in post.tags:
for url in urls[::]:
if url.lower().endswith("/tags/"+tag["name"]):
urls.remove(url)
return urls

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
import wx
class authorisationDialog(wx.Dialog):
def __init__(self):
super(authorisationDialog, self).__init__(parent=None, title=_(u"Authorising account..."))
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
static1 = wx.StaticText(panel, wx.NewId(), _("URL of mastodon instance:"))
self.instance = wx.TextCtrl(panel, -1)
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
sizer1.Add(static1, 0, wx.ALL, 5)
sizer1.Add(self.instance, 0, wx.ALL, 5)
sizer.Add(sizer1, 0, wx.ALL, 5)
static2 = wx.StaticText(panel, wx.NewId(), _("Email address:"))
self.email = wx.TextCtrl(panel, -1)
sizer2 = wx.BoxSizer(wx.HORIZONTAL)
sizer2.Add(static2, 0, wx.ALL, 5)
sizer2.Add(self.email, 0, wx.ALL, 5)
sizer.Add(sizer2, 0, wx.ALL, 5)
static3 = wx.StaticText(panel, wx.NewId(), _("Password:"))
self.password = wx.TextCtrl(panel, -1)
sizer3 = wx.BoxSizer(wx.HORIZONTAL)
sizer3.Add(static3, 0, wx.ALL, 5)
sizer3.Add(self.password, 0, wx.ALL, 5)
sizer.Add(sizer3, 0, wx.ALL, 5)
self.ok = wx.Button(panel, wx.ID_OK)
self.cancel = wx.Button(panel, wx.ID_CANCEL)
sizer4 = wx.BoxSizer(wx.HORIZONTAL)
sizer4.Add(self.ok, 0, wx.ALL, 5)
sizer4.Add(self.cancel, 0, wx.ALL, 5)
sizer.Add(sizer4, 0, wx.ALL, 5)
panel.SetSizer(sizer)
min = sizer.CalcMin()
self.SetClientSize(min)

View File

@ -5,13 +5,13 @@ import time
import logging import logging
import webbrowser import webbrowser
import wx import wx
import tweepy
import demoji import demoji
import config import config
import output import output
import application import application
import appkeys import appkeys
from pubsub import pub from pubsub import pub
import tweepy
from tweepy.errors import TweepyException, Forbidden, NotFound from tweepy.errors import TweepyException, Forbidden, NotFound
from tweepy.models import User as UserModel from tweepy.models import User as UserModel
from mysc.thread_utils import call_threaded from mysc.thread_utils import call_threaded
@ -106,10 +106,8 @@ class Session(base.baseSession):
else: objects.insert(0, i) else: objects.insert(0, i)
incoming = incoming+1 incoming = incoming+1
self.db["direct_messages"] = objects self.db["direct_messages"] = objects
self.db["sent_direct_messages"] = sent_objects self.db["sent_direct_messages"] = sent_objects
pub.sendMessage("sent-dms-updated", total=sent, account=self.db["user_name"]) pub.sendMessage("twitter.sent_dms_updated", total=sent, session_name=self.get_name())
return incoming return incoming
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -127,8 +125,8 @@ class Session(base.baseSession):
# If we wouldn't implement this approach, TWBlue would save permanently the "deleted user" object. # If we wouldn't implement this approach, TWBlue would save permanently the "deleted user" object.
self.deleted_users = {} self.deleted_users = {}
self.type = "twitter" self.type = "twitter"
pub.subscribe(self.handle_new_status, "newStatus") pub.subscribe(self.handle_new_status, "twitter.new_status")
pub.subscribe(self.handle_connected, "streamConnected") pub.subscribe(self.handle_connected, "twitter.stream_connected")
# @_require_configuration # @_require_configuration
def login(self, verify_credentials=True): def login(self, verify_credentials=True):
@ -137,8 +135,7 @@ class Session(base.baseSession):
if self.settings["twitter"]["user_key"] != None and self.settings["twitter"]["user_secret"] != None: if self.settings["twitter"]["user_key"] != None and self.settings["twitter"]["user_secret"] != None:
try: try:
log.debug("Logging in to twitter...") log.debug("Logging in to twitter...")
self.auth = tweepy.OAuth1UserHandler(appkeys.twitter_api_key, appkeys.twitter_api_secret) 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"])
self.auth.set_access_token(self.settings["twitter"]["user_key"], self.settings["twitter"]["user_secret"])
self.twitter = tweepy.API(self.auth) self.twitter = tweepy.API(self.auth)
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"]) 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"])
if verify_credentials == True: if verify_credentials == True:
@ -153,37 +150,32 @@ class Session(base.baseSession):
self.logged = False self.logged = False
raise Exceptions.RequireCredentialsSessionError raise Exceptions.RequireCredentialsSessionError
# @_require_configuration
def authorise(self): 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()""" """ 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: if self.logged == True:
raise Exceptions.AlreadyAuthorisedError("The authorisation process is not needed at this time.") raise Exceptions.AlreadyAuthorisedError("The authorisation process is not needed at this time.")
else: auth = tweepy.OAuth1UserHandler(appkeys.twitter_api_key, appkeys.twitter_api_secret)
self.auth = tweepy.OAuth1UserHandler(appkeys.twitter_api_key, appkeys.twitter_api_secret) redirect_url = auth.get_authorization_url()
redirect_url = self.auth.get_authorization_url()
webbrowser.open_new_tab(redirect_url) webbrowser.open_new_tab(redirect_url)
self.authorisation_dialog = authorisationDialog() verification_dialog = wx.TextEntryDialog(None, _("Enter your PIN code here"), _("Authorising account..."))
self.authorisation_dialog.cancel.Bind(wx.EVT_BUTTON, self.authorisation_cancelled) answer = verification_dialog.ShowModal()
self.authorisation_dialog.ok.Bind(wx.EVT_BUTTON, self.authorisation_accepted) code = verification_dialog.GetValue()
self.authorisation_dialog.ShowModal() verification_dialog.Destroy()
if answer != wx.ID_OK:
def verify_authorisation(self, pincode): return
self.auth.get_access_token(pincode) try:
self.settings["twitter"]["user_key"] = self.auth.access_token auth.get_access_token(code)
self.settings["twitter"]["user_secret"] = self.auth.access_token_secret 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
self.settings.write() self.settings.write()
del self.auth return True
def authorisation_cancelled(self, *args, **kwargs):
""" Destroy the authorization dialog. """
self.authorisation_dialog.Destroy()
del self.authorisation_dialog
def authorisation_accepted(self, *args, **kwargs):
""" Gets the PIN code entered by user and validate it through Twitter."""
pincode = self.authorisation_dialog.text.GetValue()
self.verify_authorisation(pincode)
self.authorisation_dialog.Destroy()
def api_call(self, call_name, action="", _sound=None, report_success=False, report_failure=True, preexec_message="", *args, **kwargs): 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. """ 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.
@ -556,7 +548,7 @@ class Session(base.baseSession):
def start_streaming(self): def start_streaming(self):
if config.app["app-settings"]["no_streaming"]: if config.app["app-settings"]["no_streaming"]:
return return
self.stream = streaming.Stream(twitter_api=self.twitter, user=self.db["user_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) 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)
self.stream_thread = call_threaded(self.stream.filter, follow=self.stream.users, stall_warnings=True) self.stream_thread = call_threaded(self.stream.filter, follow=self.stream.users, stall_warnings=True)
def stop_streaming(self): def stop_streaming(self):
@ -566,12 +558,12 @@ class Session(base.baseSession):
self.stream.running = False self.stream.running = False
log.debug("Stream stopped for accounr {}".format(self.db["user_name"])) log.debug("Stream stopped for accounr {}".format(self.db["user_name"]))
def handle_new_status(self, status, user): def handle_new_status(self, status, session_name):
""" Handles a new status present in the Streaming API. """ """ Handles a new status present in the Streaming API. """
if self.logged == False: if self.logged == False:
return return
# Discard processing the status if the streaming sends a tweet for another account. # Discard processing the status if the streaming sends a tweet for another account.
if self.db["user_name"] != user: if self.get_name() != session_name:
return return
# the Streaming API sends non-extended tweets with an optional parameter "extended_tweets" which contains full_text and other data. # 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. # so we have to make sure we check it before processing the normal status.
@ -608,7 +600,7 @@ class Session(base.baseSession):
status = self.check_quoted_status(status) status = self.check_quoted_status(status)
status = self.check_long_tweet(status) status = self.check_long_tweet(status)
# Send it to the main controller object. # Send it to the main controller object.
pub.sendMessage("newTweet", data=status, user=self.db["user_name"], _buffers=buffers_to_send) pub.sendMessage("twitter.new_tweet", data=status, session_name=self.get_name(), _buffers=buffers_to_send)
def check_streams(self): def check_streams(self):
if config.app["app-settings"]["no_streaming"]: if config.app["app-settings"]["no_streaming"]:
@ -619,11 +611,11 @@ class Session(base.baseSession):
if self.stream.running == False: if self.stream.running == False:
self.start_streaming() self.start_streaming()
def handle_connected(self, user): def handle_connected(self, session_name):
if self.logged == False: if self.logged == False:
return return
if user != self.db["user_name"]: if session_name != self.get_name():
log.debug("Connected streaming endpoint on account {}".format(user)) log.debug("Connected streaming endpoint on session {}".format(session_name))
def send_tweet(self, *tweets): def send_tweet(self, *tweets):
""" Convenience function to send a thread. """ """ Convenience function to send a thread. """
@ -674,4 +666,10 @@ class Session(base.baseSession):
else: else:
sent_dms.insert(0, item) sent_dms.insert(0, item)
self.db["sent_direct_messages"] = sent_dms self.db["sent_direct_messages"] = sent_dms
pub.sendMessage("sent-dm", data=item, user=self.db["user_name"]) pub.sendMessage("twitter.sent_dm", data=item, session_name=self.get_name())
def get_name(self):
if self.logged:
return "Twitter: {}".format(self.db["user_name"])
else:
return "Twitter: {}".format(self.settings["twitter"]["user_name"])

View File

@ -14,13 +14,13 @@ log = logging.getLogger("sessions.twitter.streaming")
class Stream(tweepy.Stream): class Stream(tweepy.Stream):
def __init__(self, twitter_api, user, user_id, muted_users=[], *args, **kwargs): def __init__(self, twitter_api, session_name, user_id, muted_users=[], *args, **kwargs):
super(Stream, self).__init__(*args, **kwargs) super(Stream, self).__init__(*args, **kwargs)
log.debug("Starting streaming listener for account {}".format(user)) log.debug("Starting streaming listener for session {}".format(session_name))
self.started = False self.started = False
self.users = [] self.users = []
self.api = twitter_api self.api = twitter_api
self.user = user self.session_name = session_name
self.user_id = user_id self.user_id = user_id
friends = self.api.get_friend_ids() friends = self.api.get_friend_ids()
log.debug("Retrieved {} friends to add to the streaming listener.".format(len(friends))) log.debug("Retrieved {} friends to add to the streaming listener.".format(len(friends)))
@ -33,15 +33,15 @@ class Stream(tweepy.Stream):
log.debug("Streaming listener started with {} users to follow.".format(len(self.users))) log.debug("Streaming listener started with {} users to follow.".format(len(self.users)))
def on_connect(self): def on_connect(self):
pub.sendMessage("streamConnected", user=self.user) pub.sendMessage("twitter.stream_connected", session_name=self.session_name)
def on_exception(self, ex): def on_exception(self, ex):
log.exception("Exception received on streaming endpoint for user {}".format(self.user)) log.exception("Exception received on streaming endpoint for session {}".format(self.session_name))
def on_status(self, status): def on_status(self, status):
""" Checks data arriving as a tweet. """ """ Checks data arriving as a tweet. """
# Hide replies to users not followed by current account. # Hide replies to users not followed by current account.
if status.in_reply_to_user_id_str != None and status.in_reply_to_user_id_str not in self.users and status.user.screen_name != self.user: if status.in_reply_to_user_id_str != None and status.in_reply_to_user_id_str not in self.users and status.user.id != self.user_id:
return return
if status.user.id_str in self.users: if status.user.id_str in self.users:
pub.sendMessage("newStatus", status=status, user=self.user) pub.sendMessage("twitter.new_status", status=status, session_name=self.session_name)

View File

@ -231,7 +231,7 @@ def filter_tweet(tweet, tweet_data, settings, buffer_name):
if word not in getattr(tweet, value): if word not in getattr(tweet, value):
return False return False
if settings["filters"][i]["in_lang"] == "True": if settings["filters"][i]["in_lang"] == "True":
if getattr(tweet, lang) not in settings["filters"][i]["languages"]: if getattr(tweet, "lang") not in settings["filters"][i]["languages"]:
return False return False
elif settings["filters"][i]["in_lang"] == "False": elif settings["filters"][i]["in_lang"] == "False":
if tweet.lang in settings["filters"][i]["languages"]: if tweet.lang in settings["filters"][i]["languages"]:

View File

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
import wx import wx
class authorisationDialog(wx.Dialog): class authorisationDialog(wx.Dialog):

View File

@ -111,7 +111,8 @@ class URLStream(object):
self.prepared = False self.prepared = False
log.debug("URL Player initialized") log.debug("URL Player initialized")
# LibVLC controls. # LibVLC controls.
self.instance = vlc.Instance() self.instance = vlc.Instance("--quiet")
self.instance.log_unset()
self.player = self.instance.media_player_new() self.player = self.instance.media_player_new()
self.event_manager = self.player.event_manager() self.event_manager = self.player.event_manager()
self.event_manager.event_attach(vlc.EventType.MediaPlayerEndReached, self.end_callback) self.event_manager.event_attach(vlc.EventType.MediaPlayerEndReached, self.end_callback)

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
import sys
import types
import pytest
import os
import sqlitedict
import shutil
from unittest import mock
# Mock sound module, so LibVLc won't complain.
sound_module = types.ModuleType("sound")
sys.modules["sound"] = sound_module
sound_module.soundManager = mock.MagicMock(name="sound.soundManager")
from sessions import base
# path where we will save our test config, as we can't rely on paths module due to pytest's paths being different.
session_path = os.path.join(os.getcwd(), "config", "testing")
@pytest.fixture
def session():
""" Configures a fake base session from where we can test things. """
global session_path
s = base.baseSession("testing")
if os.path.exists(session_path) == False:
os.mkdir(session_path)
# Patches paths.app_path and paths.config_path, so we will not have issues during session configuration.
with mock.patch("paths.app_path", return_value=os.getcwd()) as app_path:
with mock.patch("paths.config_path", return_value=os.path.join(os.getcwd(), "config")) as config_path:
s.get_configuration()
yield s
# Session's cleanup code.
if os.path.exists(session_path):
shutil.rmtree(session_path)
del s
@pytest.fixture
def dataset():
""" Generates a sample dataset"""
dataset = dict(home_timeline=["message" for i in range(10000)], mentions_timeline=["mention" for i in range(20000)])
yield dataset
### Testing database being read from disk.
def test_cache_in_disk_unlimited_size(session, dataset):
""" Tests cache database being read from disk, storing the whole datasets. """
session.settings["general"]["load_cache_in_memory"] = False
session.settings["general"]["persist_size"] = -1
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
session.save_persistent_data()
assert isinstance(session.db, sqlitedict.SqliteDict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert len(session.db.get("home_timeline")) == 10000
assert len(session.db.get("mentions_timeline")) == 20000
session.db.close()
def test_cache_in_disk_limited_dataset(session, dataset):
""" Tests wether the cache stores only the amount of items we ask it to store. """
session.settings["general"]["load_cache_in_memory"] = False
session.settings["general"]["persist_size"] = 100
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
# We need to save and load the db again because we cannot modify buffers' size while the database is opened.
# As TWBlue reads directly from db when reading from disk, an attempt to modify buffers size while Blue is reading the db
# Might cause an out of sync error between the GUI lists and the database.
# So we perform the changes to buffer size when loading data during app startup if the DB is read from disk.
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, sqlitedict.SqliteDict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert len(session.db.get("home_timeline")) == 100
assert len(session.db.get("mentions_timeline")) == 100
session.db.close()
def test_cache_in_disk_limited_dataset_unreversed(session):
"""Test if the cache is saved properly in unreversed buffers, when newest items are at the end of the list. """
dataset = dict(home_timeline=[i for i in range(20)], mentions_timeline=[i for i in range(20)])
session.settings["general"]["load_cache_in_memory"] = False
session.settings["general"]["persist_size"] = 10
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
# We need to save and load the db again because we cannot modify buffers' size while the database is opened.
# As TWBlue reads directly from db when reading from disk, an attempt to modify buffers size while Blue is reading the db
# Might cause an out of sync error between the GUI lists and the database.
# So we perform the changes to buffer size when loading data during app startup if the DB is read from disk.
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, sqlitedict.SqliteDict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert session.db.get("home_timeline")[0] == 10
assert session.db.get("mentions_timeline")[0] == 10
assert session.db.get("home_timeline")[-1] == 19
assert session.db.get("mentions_timeline")[-1] == 19
session.db.close()
def test_cache_in_disk_limited_dataset_reversed(session):
"""Test if the cache is saved properly in reversed buffers, when newest items are at the start of the list. """
dataset = dict(home_timeline=[i for i in range(19, -1, -1)], mentions_timeline=[i for i in range(19, -1, -1)])
session.settings["general"]["load_cache_in_memory"] = False
session.settings["general"]["persist_size"] = 10
session.settings["general"]["reverse_timelines"] = True
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
# We need to save and load the db again because we cannot modify buffers' size while the database is opened.
# As TWBlue reads directly from db when reading from disk, an attempt to modify buffers size while Blue is reading the db
# Might cause an out of sync error between the GUI lists and the database.
# So we perform the changes to buffer size when loading data during app startup if the DB is read from disk.
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, sqlitedict.SqliteDict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert session.db.get("home_timeline")[0] == 19
assert session.db.get("mentions_timeline")[0] == 19
assert session.db.get("home_timeline")[-1] == 10
assert session.db.get("mentions_timeline")[-1] == 10
session.db.close()
### Testing database being loaded into memory. Those tests should give the same results than before
### but as we have different code depending whether we load db into memory or read it from disk,
### We need to test this anyways.
def test_cache_in_memory_unlimited_size(session, dataset):
""" Tests cache database being loaded in memory, storing the whole datasets. """
session.settings["general"]["load_cache_in_memory"] = True
session.settings["general"]["persist_size"] = -1
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, dict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert len(session.db.get("home_timeline")) == 10000
assert len(session.db.get("mentions_timeline")) == 20000
def test_cache_in_memory_limited_dataset(session, dataset):
""" Tests wether the cache stores only the amount of items we ask it to store, when loaded in memory. """
session.settings["general"]["load_cache_in_memory"] = True
session.settings["general"]["persist_size"] = 100
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, dict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert len(session.db.get("home_timeline")) == 100
assert len(session.db.get("mentions_timeline")) == 100
def test_cache_in_memory_limited_dataset_unreversed(session):
"""Test if the cache is saved properly when loaded in memory in unreversed buffers, when newest items are at the end of the list. """
dataset = dict(home_timeline=[i for i in range(20)], mentions_timeline=[i for i in range(20)])
session.settings["general"]["load_cache_in_memory"] = True
session.settings["general"]["persist_size"] = 10
session.load_persistent_data()
assert len(session.db)==1
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, dict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert session.db.get("home_timeline")[0] == 10
assert session.db.get("mentions_timeline")[0] == 10
assert session.db.get("home_timeline")[-1] == 19
assert session.db.get("mentions_timeline")[-1] == 19
def test_cache_in_memory_limited_dataset_reversed(session):
"""Test if the cache is saved properly in reversed buffers, when newest items are at the start of the list. This test if for db read into memory. """
dataset = dict(home_timeline=[i for i in range(19, -1, -1)], mentions_timeline=[i for i in range(19, -1, -1)])
session.settings["general"]["load_cache_in_memory"] = True
session.settings["general"]["persist_size"] = 10
session.settings["general"]["reverse_timelines"] = True
session.load_persistent_data()
session.db["home_timeline"] = dataset["home_timeline"]
session.db["mentions_timeline"] = dataset["mentions_timeline"]
session.save_persistent_data()
session.db = dict()
session.load_persistent_data()
assert isinstance(session.db, dict)
assert session.db.get("home_timeline") != None
assert session.db.get("mentions_timeline") != None
assert session.db.get("home_timeline")[0] == 19
assert session.db.get("mentions_timeline")[0] == 19
assert session.db.get("home_timeline")[-1] == 10
assert session.db.get("mentions_timeline")[-1] == 10

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
import pytest
from tweepy.models import Status
@pytest.fixture
def basic_tweet():
data = {'created_at': 'Mon Jan 03 15:03:36 +0000 2022', 'id': 1478019218884857856, 'id_str': '1478019218884857856', 'full_text': 'Changes in projects for next year https://t.co/nW3GS9RmHd', 'truncated': False, 'display_text_range': [0, 57], 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [], 'urls': [{'url': 'https://t.co/nW3GS9RmHd', 'expanded_url': 'https://manuelcortez.net/blog/changes-in-projects-for-next-year/#.YdMQQU6t1FI.twitter', 'display_url': 'manuelcortez.net/blog/changes-i…', 'indices': [34, 57]}]}, 'source': '<a href="https://mobile.twitter.com" rel="nofollow">Twitter Web App</a>', 'in_reply_to_status_id': None, 'in_reply_to_status_id_str': None, 'in_reply_to_user_id': None, 'in_reply_to_user_id_str': None, 'in_reply_to_screen_name': None, 'user': {'id': 258677951, 'id_str': '258677951', 'name': 'Manuel Cortez', 'screen_name': 'manuelcortez00', 'location': 'Nuevo León, México', 'description': 'Python developer, , interested in reading, accessibility, astronomy, physics and science. Я учу русский.', 'url': 'https://t.co/JFRKRA73ZV', 'entities': {'url': {'urls': [{'url': 'https://t.co/JFRKRA73ZV', 'expanded_url': 'https://manuelcortez.net', 'display_url': 'manuelcortez.net', 'indices': [0, 23]}]}, 'description': {'urls': []}}, 'protected': False, 'followers_count': 1453, 'friends_count': 568, 'listed_count': 45, 'created_at': 'Mon Feb 28 06:52:48 +0000 2011', 'favourites_count': 283, 'utc_offset': None, 'time_zone': None, 'geo_enabled': True, 'verified': False, 'statuses_count': 43371, 'lang': None, 'contributors_enabled': False, 'is_translator': False, 'is_translation_enabled': False, 'profile_background_color': 'C0DEED', 'profile_background_image_url': 'http://abs.twimg.com/images/themes/theme1/bg.png', 'profile_background_image_url_https': 'https://abs.twimg.com/images/themes/theme1/bg.png', 'profile_background_tile': False, 'profile_image_url': 'http://pbs.twimg.com/profile_images/442466677645508608/3EBBC-OX_normal.jpeg', 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/442466677645508608/3EBBC-OX_normal.jpeg', 'profile_image_extensions_alt_text': None, 'profile_link_color': '1DA1F2', 'profile_sidebar_border_color': 'C0DEED', 'profile_sidebar_fill_color': 'DDEEF6', 'profile_text_color': '333333', 'profile_use_background_image': True, 'has_extended_profile': False, 'default_profile': True, 'default_profile_image': False, 'following': False, 'follow_request_sent': False, 'notifications': False, 'translator_type': 'regular', 'withheld_in_countries': []}, 'geo': None, 'coordinates': None, 'place': None, 'contributors': None, 'is_quote_status': False, 'retweet_count': 6, 'favorite_count': 2, 'favorited': False, 'retweeted': False, 'possibly_sensitive': False, 'possibly_sensitive_appealable': False, 'lang': 'en'}
yield Status().parse(api=None, json=data)
@pytest.fixture
def basic_tweet_multiple_mentions():
data = {'created_at': 'Mon Dec 27 21:21:25 +0000 2021', 'id': 1475577584947707909, 'id_str': '1475577584947707909', 'full_text': '@tamaranatalia9 @Darkstrings @Chris88171572 @manuelcortez00 Well done, thanks Tamara', 'truncated': False, 'display_text_range': [60, 84], 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [{'screen_name': 'tamaranatalia9', 'name': 'Tamara', 'id': 914114584591597568, 'id_str': '914114584591597568', 'indices': [0, 15]}, {'screen_name': 'Darkstrings', 'name': 'Luc', 'id': 1374154151115042823, 'id_str': '1374154151115042823', 'indices': [16, 28]}, {'screen_name': 'Chris88171572', 'name': 'Chris', 'id': 1323980014799495168, 'id_str': '1323980014799495168', 'indices': [29, 43]}, {'screen_name': 'manuelcortez00', 'name': 'Manuel Cortez', 'id': 258677951, 'id_str': '258677951', 'indices': [44, 59]}], 'urls': []}, 'source': '<a href="http://twitter.com/download/android" rel="nofollow">Twitter for Android</a>', 'in_reply_to_status_id': 1475550502083563526, 'in_reply_to_status_id_str': '1475550502083563526', 'in_reply_to_user_id': 914114584591597568, 'in_reply_to_user_id_str': '914114584591597568', 'in_reply_to_screen_name': 'tamaranatalia9', 'user': {'id': 784837522157436929, 'id_str': '784837522157436929', 'name': 'Paulus', 'screen_name': 'PauloPer01', 'location': '', 'description': '', 'url': None, 'entities': {'description': {'urls': []}}, 'protected': False, 'followers_count': 1082, 'friends_count': 3029, 'listed_count': 2, 'created_at': 'Sat Oct 08 19:27:01 +0000 2016', 'favourites_count': 78862, 'utc_offset': None, 'time_zone': None, 'geo_enabled': False, 'verified': False, 'statuses_count': 4976, 'lang': None, 'contributors_enabled': False, 'is_translator': False, 'is_translation_enabled': False, 'profile_background_color': 'F5F8FA', 'profile_background_image_url': None, 'profile_background_image_url_https': None, 'profile_background_tile': False, 'profile_image_url': 'http://pbs.twimg.com/profile_images/1464572633014587395/246oPPLa_normal.jpg', 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/1464572633014587395/246oPPLa_normal.jpg', 'profile_image_extensions_alt_text': None, 'profile_link_color': '1DA1F2', 'profile_sidebar_border_color': 'C0DEED', 'profile_sidebar_fill_color': 'DDEEF6', 'profile_text_color': '333333', 'profile_use_background_image': True, 'has_extended_profile': True, 'default_profile': True, 'default_profile_image': False, 'following': False, 'follow_request_sent': False, 'notifications': False, 'translator_type': 'none', 'withheld_in_countries': []}, 'geo': None, 'coordinates': None, 'place': None, 'contributors': None, 'is_quote_status': False, 'retweet_count': 1, 'favorite_count': 2, 'favorited': False, 'retweeted': False, 'lang': 'en'}
yield Status().parse(api=None, json=data)

View File

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
import pytest
import gettext
import datetime
gettext.install("test")
from unittest import mock
from sessions.twitter import templates
def test_default_values():
""" Tests wheter default values are the expected ones.
This might be useful so we will have this failing when we update anything from those values.
As TWBlue might be using those from other dialogs.
"""
assert templates.tweet_variables == ["date", "display_name", "screen_name", "source", "lang", "text", "image_descriptions"]
assert templates.dm_variables == ["date", "sender_display_name", "sender_screen_name", "recipient_display_name", "recipient_display_name", "text"]
assert templates.person_variables == ["display_name", "screen_name", "location", "description", "followers", "following", "listed", "likes", "tweets", "created_at"]
@pytest.mark.parametrize("offset, language, expected_result", [
(0, "en_US", "Wednesday, October 10, 2018 20:19:24"),
(-21600, "en_US", "Wednesday, October 10, 2018 14:19:24"),
(7200, "en_US", "Wednesday, October 10, 2018 22:19:24"),
(0, "es_ES", "miércoles, octubre 10, 2018 20:19:24"),
(-21600, "es_ES", "miércoles, octubre 10, 2018 14:19:24"),
(7200, "es_ES", "miércoles, octubre 10, 2018 22:19:24"),
(18000, "es_ES", "jueves, octubre 11, 2018 1:19:24"),
])
def test_process_date_absolute_time(offset, language, expected_result):
""" Tests date processing function for tweets, when relative_times is set to False. """
# Date representation used by twitter, converted to datetime object, as tweepy already does this.
# Original date was Wed Oct 10 20:19:24 +0000 2018
date_field = datetime.datetime(2018, 10, 10, 20, 19, 24)
with mock.patch("languageHandler.curLang", new=language):
processed_date = templates.process_date(date_field, relative_times=False, offset_seconds=offset)
assert processed_date == expected_result
def test_process_date_relative_time():
date_field = datetime.datetime(2018, 10, 10, 20, 19, 24)
with mock.patch("languageHandler.curLang", new="es_ES"):
processed_date = templates.process_date(date_field, relative_times=True, offset_seconds=7200)
# As this depends in relative times and this is subject to change, let's do some light checks here and hope the string is going to be valid.
assert isinstance(processed_date, str)
assert "hace" in processed_date and "años" in processed_date
def test_process_text_basic_tweet(basic_tweet):
expected_result = "Changes in projects for next year https://manuelcortez.net/blog/changes-in-projects-for-next-year/#.YdMQQU6t1FI.twitter"
text = templates.process_text(basic_tweet)
assert text == expected_result
def test_process_text_basic_tweet_multiple_mentions(basic_tweet_multiple_mentions):
expected_result = "@tamaranatalia9, @Darkstrings and 2 more: Well done, thanks Tamara"
text = templates.process_text(basic_tweet_multiple_mentions)
assert text == expected_result

View File

@ -1,200 +0,0 @@
# -*- coding: utf-8 -*-
""" Test case to check some of the scenarios we might face when storing tweets in cache, both loading into memory or rreading from disk. """
import unittest
import os
import paths
import sqlitedict
import shutil
# The base session module requires sound as a dependency, and this needs libVLC to be locatable.
os.environ['PYTHON_VLC_MODULE_PATH']=os.path.abspath(os.path.join(paths.app_path(), "..", "windows-dependencies", "x86"))
os.environ['PYTHON_VLC_LIB_PATH']=os.path.abspath(os.path.join(paths.app_path(), "..", "windows-dependencies", "x86", "libvlc.dll"))
from sessions import base
class cacheTestCase(unittest.TestCase):
def setUp(self):
""" Configures a fake session to check caching objects here. """
self.session = base.baseSession("testing")
if os.path.exists(os.path.join(paths.config_path(), "testing")) == False:
os.mkdir(os.path.join(paths.config_path(), "testing"))
self.session.get_configuration()
def tearDown(self):
""" Removes the previously configured session. """
session_folder = os.path.join(paths.config_path(), "testing")
if os.path.exists(session_folder):
shutil.rmtree(session_folder)
def generate_dataset(self):
""" Generates a sample dataset"""
dataset = dict(home_timeline=["message" for i in range(10000)], mentions_timeline=["mention" for i in range(20000)])
return dataset
### Testing database being read from disk.
def test_cache_in_disk_unlimited_size(self):
""" Tests cache database being read from disk, storing the whole datasets. """
dataset = self.generate_dataset()
self.session.settings["general"]["load_cache_in_memory"] = False
self.session.settings["general"]["persist_size"] = -1
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
self.session.save_persistent_data()
self.assertIsInstance(self.session.db, sqlitedict.SqliteDict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(len(self.session.db.get("home_timeline")), 10000)
self.assertEquals(len(self.session.db.get("mentions_timeline")), 20000)
self.session.db.close()
def test_cache_in_disk_limited_dataset(self):
""" Tests wether the cache stores only the amount of items we ask it to store. """
dataset = self.generate_dataset()
self.session.settings["general"]["load_cache_in_memory"] = False
self.session.settings["general"]["persist_size"] = 100
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
# We need to save and load the db again because we cannot modify buffers' size while the database is opened.
# As TWBlue reads directly from db when reading from disk, an attempt to modify buffers size while Blue is reading the db
# Might cause an out of sync error between the GUI lists and the database.
# So we perform the changes to buffer size when loading data during app startup if the DB is read from disk.
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, sqlitedict.SqliteDict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(len(self.session.db.get("home_timeline")), 100)
self.assertEquals(len(self.session.db.get("mentions_timeline")), 100)
self.session.db.close()
def test_cache_in_disk_limited_dataset_unreversed(self):
"""Test if the cache is saved properly in unreversed buffers, when newest items are at the end of the list. """
dataset = dict(home_timeline=[i for i in range(20)], mentions_timeline=[i for i in range(20)])
self.session.settings["general"]["load_cache_in_memory"] = False
self.session.settings["general"]["persist_size"] = 10
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
# We need to save and load the db again because we cannot modify buffers' size while the database is opened.
# As TWBlue reads directly from db when reading from disk, an attempt to modify buffers size while Blue is reading the db
# Might cause an out of sync error between the GUI lists and the database.
# So we perform the changes to buffer size when loading data during app startup if the DB is read from disk.
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, sqlitedict.SqliteDict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(self.session.db.get("home_timeline")[0], 10)
self.assertEquals(self.session.db.get("mentions_timeline")[0], 10)
self.assertEquals(self.session.db.get("home_timeline")[-1], 19)
self.assertEquals(self.session.db.get("mentions_timeline")[-1], 19)
self.session.db.close()
def test_cache_in_disk_limited_dataset_reversed(self):
"""Test if the cache is saved properly in reversed buffers, when newest items are at the start of the list. """
dataset = dict(home_timeline=[i for i in range(19, -1, -1)], mentions_timeline=[i for i in range(19, -1, -1)])
self.session.settings["general"]["load_cache_in_memory"] = False
self.session.settings["general"]["persist_size"] = 10
self.session.settings["general"]["reverse_timelines"] = True
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
# We need to save and load the db again because we cannot modify buffers' size while the database is opened.
# As TWBlue reads directly from db when reading from disk, an attempt to modify buffers size while Blue is reading the db
# Might cause an out of sync error between the GUI lists and the database.
# So we perform the changes to buffer size when loading data during app startup if the DB is read from disk.
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, sqlitedict.SqliteDict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(self.session.db.get("home_timeline")[0], 19)
self.assertEquals(self.session.db.get("mentions_timeline")[0], 19)
self.assertEquals(self.session.db.get("home_timeline")[-1], 10)
self.assertEquals(self.session.db.get("mentions_timeline")[-1], 10)
self.session.db.close()
### Testing database being loaded into memory. Those tests should give the same results than before
### but as we have different code depending whether we load db into memory or read it from disk,
### We need to test this anyways.
def test_cache_in_memory_unlimited_size(self):
""" Tests cache database being loaded in memory, storing the whole datasets. """
dataset = self.generate_dataset()
self.session.settings["general"]["load_cache_in_memory"] = True
self.session.settings["general"]["persist_size"] = -1
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, dict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(len(self.session.db.get("home_timeline")), 10000)
self.assertEquals(len(self.session.db.get("mentions_timeline")), 20000)
def test_cache_in_memory_limited_dataset(self):
""" Tests wether the cache stores only the amount of items we ask it to store, when loaded in memory. """
dataset = self.generate_dataset()
self.session.settings["general"]["load_cache_in_memory"] = True
self.session.settings["general"]["persist_size"] = 100
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, dict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(len(self.session.db.get("home_timeline")), 100)
self.assertEquals(len(self.session.db.get("mentions_timeline")), 100)
def test_cache_in_memory_limited_dataset_unreversed(self):
"""Test if the cache is saved properly when loaded in memory in unreversed buffers, when newest items are at the end of the list. """
dataset = dict(home_timeline=[i for i in range(20)], mentions_timeline=[i for i in range(20)])
self.session.settings["general"]["load_cache_in_memory"] = True
self.session.settings["general"]["persist_size"] = 10
self.session.load_persistent_data()
self.assertTrue(len(self.session.db)==1)
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, dict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(self.session.db.get("home_timeline")[0], 10)
self.assertEquals(self.session.db.get("mentions_timeline")[0], 10)
self.assertEquals(self.session.db.get("home_timeline")[-1], 19)
self.assertEquals(self.session.db.get("mentions_timeline")[-1], 19)
def test_cache_in_memory_limited_dataset_reversed(self):
"""Test if the cache is saved properly in reversed buffers, when newest items are at the start of the list. This test if for db read into memory. """
dataset = dict(home_timeline=[i for i in range(19, -1, -1)], mentions_timeline=[i for i in range(19, -1, -1)])
self.session.settings["general"]["load_cache_in_memory"] = True
self.session.settings["general"]["persist_size"] = 10
self.session.settings["general"]["reverse_timelines"] = True
self.session.load_persistent_data()
self.session.db["home_timeline"] = dataset["home_timeline"]
self.session.db["mentions_timeline"] = dataset["mentions_timeline"]
self.session.save_persistent_data()
self.session.db = dict()
self.session.load_persistent_data()
self.assertIsInstance(self.session.db, dict)
self.assertTrue(self.session.db.get("home_timeline") != None)
self.assertTrue(self.session.db.get("mentions_timeline") != None)
self.assertEquals(self.session.db.get("home_timeline")[0], 19)
self.assertEquals(self.session.db.get("mentions_timeline")[0], 19)
self.assertEquals(self.session.db.get("home_timeline")[-1], 10)
self.assertEquals(self.session.db.get("mentions_timeline")[-1], 10)
if __name__ == "__main__":
unittest.main()

View File

@ -1,13 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import from . import twitter, mastodon
from __future__ import unicode_literals
from .base import basePanel
from .dm import dmPanel
from .events import eventsPanel
from .favourites import favsPanel
from .lists import listPanel
from .panels import accountPanel, emptyPanel from .panels import accountPanel, emptyPanel
from .people import peoplePanel
from .trends import trendsPanel
from .tweet_searches import searchPanel
from .user_searches import searchUsersPanel

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from .base import basePanel
from .conversationList import conversationListPanel
from .user import userPanel

View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
import wx
from multiplatform_widgets import widgets
class basePanel(wx.Panel):
def set_focus_function(self, f):
self.list.list.Bind(wx.EVT_LIST_ITEM_FOCUSED, f)
def create_list(self):
self.list = widgets.list(self, _(u"User"), _(u"Text"), _(u"Date"), _(u"Client"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
self.list.set_windows_size(0, 60)
self.list.set_windows_size(1, 320)
self.list.set_windows_size(2, 110)
self.list.set_windows_size(3, 84)
self.list.set_size()
def __init__(self, parent, name):
super(basePanel, self).__init__(parent)
self.name = name
self.type = "baseBuffer"
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.create_list()
self.post = wx.Button(self, -1, _("Post"))
self.boost = wx.Button(self, -1, _("Boost"))
self.reply = wx.Button(self, -1, _(u"Reply"))
self.fav = wx.Button(self, wx.ID_ANY, _("Favorite"))
self.bookmark = wx.Button(self, wx.ID_ANY, _("Bookmark"))
self.dm = wx.Button(self, -1, _(u"Direct message"))
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add(self.post, 0, wx.ALL, 5)
btnSizer.Add(self.boost, 0, wx.ALL, 5)
btnSizer.Add(self.reply, 0, wx.ALL, 5)
btnSizer.Add(self.fav, 0, wx.ALL, 5)
btnSizer.Add(self.bookmark, 0, wx.ALL, 5)
btnSizer.Add(self.dm, 0, wx.ALL, 5)
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5)
self.SetSizer(self.sizer)
self.SetClientSize(self.sizer.CalcMin())
def set_position(self, reversed=False):
if reversed == False:
self.list.select_item(self.list.get_count()-1)
else:
self.list.select_item(0)
def set_focus_in_list(self):
self.list.list.SetFocus()

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
import wx
from multiplatform_widgets import widgets
class conversationListPanel(wx.Panel):
def set_focus_function(self, f):
self.list.list.Bind(wx.EVT_LIST_ITEM_FOCUSED, f)
def create_list(self):
self.list = widgets.list(self, _(u"User"), _(u"Text"), _(u"Date"), _(u"Client"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
self.list.set_windows_size(0, 60)
self.list.set_windows_size(1, 320)
self.list.set_windows_size(2, 110)
self.list.set_windows_size(3, 84)
self.list.set_size()
def __init__(self, parent, name):
super(conversationListPanel, self).__init__(parent)
self.name = name
self.type = "baseBuffer"
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.create_list()
self.post = wx.Button(self, -1, _("Post"))
self.reply = wx.Button(self, -1, _(u"Reply"))
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add(self.post, 0, wx.ALL, 5)
btnSizer.Add(self.reply, 0, wx.ALL, 5)
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5)
self.SetSizer(self.sizer)
self.SetClientSize(self.sizer.CalcMin())
def set_position(self, reversed=False):
if reversed == False:
self.list.select_item(self.list.get_count()-1)
else:
self.list.select_item(0)
def set_focus_in_list(self):
self.list.list.SetFocus()

View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
import wx
from multiplatform_widgets import widgets
class userPanel(wx.Panel):
def create_list(self):
self.list = widgets.list(self, _("User"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
self.list.set_windows_size(0, 320)
self.list.set_size()
def __init__(self, parent, name):
super(userPanel, self).__init__(parent)
self.name = name
self.type = "user"
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.create_list()
self.post = wx.Button(self, -1, _("Post"))
self.actions = wx.Button(self, -1, _("Actions"))
self.message = wx.Button(self, -1, _("Message"))
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add(self.post, 0, wx.ALL, 5)
btnSizer.Add(self.actions, 0, wx.ALL, 5)
btnSizer.Add(self.message, 0, wx.ALL, 5)
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5)
self.SetSizer(self.sizer)
self.SetClientSize(self.sizer.CalcMin())
def set_position(self, reversed=False):
if reversed == False:
self.list.select_item(self.list.get_count()-1)
else:
self.list.select_item(0)
def set_focus_in_list(self):
self.list.list.SetFocus()

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
from .base import basePanel
from .dm import dmPanel
from .events import eventsPanel
from .favourites import favsPanel
from .lists import listPanel
from .people import peoplePanel
from .trends import trendsPanel
from .tweet_searches import searchPanel
from .user_searches import searchUsersPanel

View File

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
import wx
import application
def boost_question():
result = False
dlg = wx.MessageDialog(None, _("Would you like to share this post?"), _("Boost"), wx.YES_NO|wx.ICON_QUESTION)
if dlg.ShowModal() == wx.ID_YES:
result = True
dlg.Destroy()
return result
def delete_post_dialog():
result = False
dlg = wx.MessageDialog(None, _("Do you really want to delete this post? It will be deleted from the instance as well."), _("Delete"), wx.ICON_QUESTION|wx.YES_NO)
if dlg.ShowModal() == wx.ID_YES:
result = True
dlg.Destroy()
return result
def clear_list():
result = False
dlg = wx.MessageDialog(None, _("Do you really want to empty this buffer? It's items will be removed from the list but not from the instance"), _(u"Empty buffer"), wx.ICON_QUESTION|wx.YES_NO)
if dlg.ShowModal() == wx.ID_YES:
result = True
dlg.Destroy()
return result
def no_posts():
dlg = wx.MessageDialog(None, _("This user has no posts. {0} can't create a timeline.").format(application.name), _(u"Error"), wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
def no_favs():
dlg = wx.MessageDialog(None, _(u"This user has no favorited posts. {0} can't create a timeline.").format(application.name), _(u"Error"), wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
def no_followers():
dlg = wx.MessageDialog(None, _(u"This user has no followers yet. {0} can't create a timeline.").format(application.name), _(u"Error"), wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
def no_following():
dlg = wx.MessageDialog(None, _("This user is not following anyone. {0} can't create a timeline.").format(application.name), _(u"Error"), wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
import wx
class base(wx.Menu):
def __init__(self):
super(base, self).__init__()
self.boost = wx.MenuItem(self, wx.ID_ANY, _("&Boost"))
self.Append(self.boost)
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
self.Append(self.reply)
self.fav = wx.MenuItem(self, wx.ID_ANY, _(u"&Add to favorites"))
self.Append(self.fav)
self.unfav = wx.MenuItem(self, wx.ID_ANY, _(u"R&emove from favorites"))
self.Append(self.unfav)
self.openUrl = wx.MenuItem(self, wx.ID_ANY, _("&Open URL"))
self.Append(self.openUrl)
self.openInBrowser = wx.MenuItem(self, wx.ID_ANY, _(u"&Open in Twitter"))
self.Append(self.openInBrowser)
self.play = wx.MenuItem(self, wx.ID_ANY, _(u"&Play audio"))
self.Append(self.play)
self.view = wx.MenuItem(self, wx.ID_ANY, _(u"&Show tweet"))
self.Append(self.view)
self.copy = wx.MenuItem(self, wx.ID_ANY, _(u"&Copy to clipboard"))
self.Append(self.copy)
self.remove = wx.MenuItem(self, wx.ID_ANY, _(u"&Delete"))
self.Append(self.remove)
self.userActions = wx.MenuItem(self, wx.ID_ANY, _(u"&User actions..."))
self.Append(self.userActions)

View File

@ -0,0 +1,328 @@
import wx
class Post(wx.Dialog):
def __init__(self, caption=_("Post"), text="", *args, **kwds):
super(Post, self).__init__(parent=None, id=wx.ID_ANY, *args, **kwds)
main_sizer = wx.BoxSizer(wx.VERTICAL)
post_sizer = wx.WrapSizer(wx.VERTICAL)
main_sizer.Add(post_sizer, 1, wx.EXPAND, 0)
post_label = wx.StaticText(self, wx.ID_ANY, caption)
post_sizer.Add(post_label, 0, 0, 0)
self.text = wx.TextCtrl(self, wx.ID_ANY, text, style=wx.TE_MULTILINE)
self.Bind(wx.EVT_CHAR_HOOK, self.handle_keys, self.text)
self.text.SetMinSize((350, -1))
post_sizer.Add(self.text, 0, 0, 0)
lists_sizer = wx.BoxSizer(wx.HORIZONTAL)
main_sizer.Add(lists_sizer, 1, wx.EXPAND, 0)
attachments_sizer = wx.WrapSizer(wx.VERTICAL)
lists_sizer.Add(attachments_sizer, 1, wx.EXPAND, 0)
attachments_label = wx.StaticText(self, wx.ID_ANY, _("Attachments"))
attachments_sizer.Add(attachments_label, 0, 0, 0)
self.attachments = wx.ListCtrl(self, wx.ID_ANY, style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES)
self.attachments.Enable(False)
self.attachments.AppendColumn(_("File"), format=wx.LIST_FORMAT_LEFT, width=-1)
self.attachments.AppendColumn(_("Type"), format=wx.LIST_FORMAT_LEFT, width=-1)
self.attachments.AppendColumn(_("Description"), format=wx.LIST_FORMAT_LEFT, width=-1)
attachments_sizer.Add(self.attachments, 1, wx.EXPAND, 0)
self.remove_attachment = wx.Button(self, wx.ID_ANY, _("Remove Attachment"))
self.remove_attachment.Enable(False)
attachments_sizer.Add(self.remove_attachment, 0, 0, 0)
posts_sizer = wx.WrapSizer(wx.VERTICAL)
lists_sizer.Add(posts_sizer, 1, wx.EXPAND, 0)
posts_label = wx.StaticText(self, wx.ID_ANY, _("Post in the thread"))
posts_sizer.Add(posts_label, 0, 0, 0)
self.posts = wx.ListCtrl(self, wx.ID_ANY, style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES)
self.posts.Enable(False)
self.posts.AppendColumn(_("Text"), format=wx.LIST_FORMAT_LEFT, width=-1)
self.posts.AppendColumn(_("Attachments"), format=wx.LIST_FORMAT_LEFT, width=-1)
posts_sizer.Add(self.posts, 1, wx.EXPAND, 0)
self.remove_post = wx.Button(self, wx.ID_ANY, _("Remove post"))
self.remove_post.Enable(False)
posts_sizer.Add(self.remove_post, 0, 0, 0)
post_actions_sizer = wx.BoxSizer(wx.HORIZONTAL)
main_sizer.Add(post_actions_sizer, 1, wx.EXPAND, 0)
visibility_sizer = wx.BoxSizer(wx.HORIZONTAL)
post_actions_sizer.Add(visibility_sizer, 1, wx.EXPAND, 0)
label_1 = wx.StaticText(self, wx.ID_ANY, _("Visibility"))
visibility_sizer.Add(label_1, 0, 0, 0)
self.visibility = wx.ComboBox(self, wx.ID_ANY, choices=[_("Public"), _("Not listed"), _("Followers only"), _("Direct")], style=wx.CB_DROPDOWN | wx.CB_READONLY | wx.CB_SIMPLE)
self.visibility.SetSelection(0)
visibility_sizer.Add(self.visibility, 0, 0, 0)
self.add = wx.Button(self, wx.ID_ANY, _("A&dd"))
self.sensitive = wx.CheckBox(self, wx.ID_ANY, _("Sensitive content"))
self.sensitive.SetValue(False)
self.sensitive.Bind(wx.EVT_CHECKBOX, self.on_sensitivity_changed)
main_sizer.Add(self.sensitive, 0, wx.ALL, 5)
spoiler_box = wx.BoxSizer(wx.HORIZONTAL)
spoiler_label = wx.StaticText(self, wx.ID_ANY, _("Content warning"))
self.spoiler = wx.TextCtrl(self, wx.ID_ANY)
self.spoiler.Enable(False)
spoiler_box.Add(spoiler_label, 0, wx.ALL, 5)
spoiler_box.Add(self.spoiler, 0, wx.ALL, 10)
main_sizer.Add(spoiler_box, 0, wx.ALL, 5)
post_actions_sizer.Add(self.add, 0, 0, 0)
self.add_post = wx.Button(self, wx.ID_ANY, _("Add p&ost"))
post_actions_sizer.Add(self.add_post, 0, 0, 0)
text_actions_sizer = wx.BoxSizer(wx.HORIZONTAL)
main_sizer.Add(text_actions_sizer, 1, wx.EXPAND, 0)
self.autocomplete_users = wx.Button(self, wx.ID_ANY, _("Auto&complete users"))
text_actions_sizer.Add(self.autocomplete_users, 0, 0, 0)
self.spellcheck = wx.Button(self, wx.ID_ANY, _("Check &spelling"))
text_actions_sizer.Add(self.spellcheck, 0, 0, 0)
self.translate = wx.Button(self, wx.ID_ANY, _("&Translate"))
text_actions_sizer.Add(self.translate, 0, 0, 0)
btn_sizer = wx.StdDialogButtonSizer()
main_sizer.Add(btn_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 4)
self.send = wx.Button(self, wx.ID_OK, "")
self.send.SetDefault()
btn_sizer.AddButton(self.send)
self.close = wx.Button(self, wx.ID_CLOSE, "")
btn_sizer.AddButton(self.close)
btn_sizer.Realize()
self.SetSizer(main_sizer)
main_sizer.Fit(self)
self.SetEscapeId(self.close.GetId())
self.Layout()
def handle_keys(self, event: wx.Event, *args, **kwargs) -> None:
""" Allows to react to certain keyboard events from the text control. """
shift=event.ShiftDown()
if event.GetKeyCode() == wx.WXK_RETURN and shift==False and hasattr(self,'send'):
self.EndModal(wx.ID_OK)
else:
event.Skip()
def on_sensitivity_changed(self, *args, **kwargs):
self.spoiler.Enable(self.sensitive.GetValue())
def set_title(self, chars):
self.SetTitle(_("Post - {} characters").format(chars))
def reset_controls(self):
self.text.ChangeValue("")
self.attachments.DeleteAllItems()
def add_item(self, list_type="attachment", item=[]):
if list_type == "attachment":
self.attachments.Append(item)
else:
self.posts.Append(item)
def remove_item(self, list_type="attachment"):
if list_type == "attachment":
item = self.attachments.GetFocusedItem()
if item > -1:
self.attachments.DeleteItem(item)
else:
item = self.posts.GetFocusedItem()
if item > -1:
self.posts.DeleteItem(item)
def attach_menu(self, event=None, enabled=True, *args, **kwargs):
menu = wx.Menu()
self.add_image = menu.Append(wx.ID_ANY, _("Image"))
self.add_image.Enable(enabled)
self.add_video = menu.Append(wx.ID_ANY, _("Video"))
self.add_video.Enable(enabled)
self.add_audio = menu.Append(wx.ID_ANY, _("Audio"))
self.add_audio.Enable(enabled)
self.add_poll = menu.Append(wx.ID_ANY, _("Poll"))
self.add_poll.Enable(enabled)
return menu
def ask_description(self):
dlg = wx.TextEntryDialog(self, _(u"please provide a description"), _(u"Description"))
dlg.ShowModal()
result = dlg.GetValue()
dlg.Destroy()
return result
def get_image(self):
openFileDialog = wx.FileDialog(self, _(u"Select the picture to be uploaded"), "", "", _("Image files (*.png, *.jpg, *.gif)|*.png; *.jpg; *.gif"), wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
if openFileDialog.ShowModal() == wx.ID_CANCEL:
return (None, None)
dsc = self.ask_description()
return (openFileDialog.GetPath(), dsc)
def get_video(self):
openFileDialog = wx.FileDialog(self, _("Select the video to be uploaded"), "", "", _("Video files (*.mp4, *.mov, *.m4v, *.webm)| *.mp4; *.m4v; *.mov; *.webm"), wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
if openFileDialog.ShowModal() == wx.ID_CANCEL:
return None
return openFileDialog.GetPath()
def get_audio(self):
openFileDialog = wx.FileDialog(self, _("Select the audio file to be uploaded"), "", "", _("Audio files (*.mp3, *.ogg, *.wav, *.flac, *.opus, *.aac, *.m4a, *.3gp)|*.mp3; *.ogg; *.wav; *.flac; *.opus; *.aac; *.m4a; *.3gp"), wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
if openFileDialog.ShowModal() == wx.ID_CANCEL:
return None
return openFileDialog.GetPath()
def unable_to_attach_file(self, *args, **kwargs):
return wx.MessageDialog(self, _("It is not possible to add more attachments. Please take into account that You can add only a maximum of 4 images, or one audio, video or poll per post. Please remove other attachments before continuing."), _("Error adding attachment"), wx.ICON_ERROR).ShowModal()
def unable_to_attach_poll(self, *args, **kwargs):
return wx.MessageDialog(self, _("You can add a poll or media files. In order to add your poll, please remove other attachments first."), _("Error adding poll"), wx.ICON_ERROR).ShowModal()
class viewPost(wx.Dialog):
def set_title(self, lenght):
self.SetTitle(_("Post - %i characters ") % (lenght,))
def __init__(self, text="", boosts_count=0, favs_count=0, source="", date="", privacy="", *args, **kwargs):
super(viewPost, self).__init__(parent=None, id=wx.ID_ANY, size=(850,850))
panel = wx.Panel(self)
label = wx.StaticText(panel, -1, _("Post"))
self.text = wx.TextCtrl(panel, -1, text, style=wx.TE_READONLY|wx.TE_MULTILINE, size=(250, 180))
self.text.SetFocus()
textBox = wx.BoxSizer(wx.HORIZONTAL)
textBox.Add(label, 0, wx.ALL, 5)
textBox.Add(self.text, 1, wx.EXPAND, 5)
mainBox = wx.BoxSizer(wx.VERTICAL)
mainBox.Add(textBox, 0, wx.ALL, 5)
label2 = wx.StaticText(panel, -1, _("Image description"))
self.image_description = wx.TextCtrl(panel, -1, style=wx.TE_READONLY|wx.TE_MULTILINE, size=(250, 180))
self.image_description.Enable(False)
iBox = wx.BoxSizer(wx.HORIZONTAL)
iBox.Add(label2, 0, wx.ALL, 5)
iBox.Add(self.image_description, 1, wx.EXPAND, 5)
mainBox.Add(iBox, 0, wx.ALL, 5)
privacyLabel = wx.StaticText(panel, -1, _("Privacy"))
privacy = wx.TextCtrl(panel, -1, privacy, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE)
privacyBox = wx.BoxSizer(wx.HORIZONTAL)
privacyBox.Add(privacyLabel, 0, wx.ALL, 5)
privacyBox.Add(privacy, 0, wx.ALL, 5)
boostsCountLabel = wx.StaticText(panel, -1, _(u"Boosts: "))
boostsCount = wx.TextCtrl(panel, -1, str(boosts_count), size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE)
boostBox = wx.BoxSizer(wx.HORIZONTAL)
boostBox.Add(boostsCountLabel, 0, wx.ALL, 5)
boostBox.Add(boostsCount, 0, wx.ALL, 5)
favsCountLabel = wx.StaticText(panel, -1, _("Favorites: "))
favsCount = wx.TextCtrl(panel, -1, str(favs_count), size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE)
favsBox = wx.BoxSizer(wx.HORIZONTAL)
favsBox.Add(favsCountLabel, 0, wx.ALL, 5)
favsBox.Add(favsCount, 0, wx.ALL, 5)
sourceLabel = wx.StaticText(panel, -1, _("Source: "))
source = wx.TextCtrl(panel, -1, source, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE)
sourceBox = wx.BoxSizer(wx.HORIZONTAL)
sourceBox.Add(sourceLabel, 0, wx.ALL, 5)
sourceBox.Add(source, 0, wx.ALL, 5)
dateLabel = wx.StaticText(panel, -1, _(u"Date: "))
date = wx.TextCtrl(panel, -1, date, size=wx.DefaultSize, style=wx.TE_READONLY|wx.TE_MULTILINE)
dateBox = wx.BoxSizer(wx.HORIZONTAL)
dateBox.Add(dateLabel, 0, wx.ALL, 5)
dateBox.Add(date, 0, wx.ALL, 5)
infoBox = wx.BoxSizer(wx.HORIZONTAL)
infoBox.Add(privacyBox, 0, wx.ALL, 5)
infoBox.Add(boostBox, 0, wx.ALL, 5)
infoBox.Add(favsBox, 0, wx.ALL, 5)
infoBox.Add(sourceBox, 0, wx.ALL, 5)
mainBox.Add(infoBox, 0, wx.ALL, 5)
mainBox.Add(dateBox, 0, wx.ALL, 5)
self.share = wx.Button(panel, wx.ID_ANY, _("Copy link to clipboard"))
self.share.Enable(False)
self.spellcheck = wx.Button(panel, -1, _("Check &spelling..."), size=wx.DefaultSize)
self.translateButton = wx.Button(panel, -1, _(u"&Translate..."), size=wx.DefaultSize)
cancelButton = wx.Button(panel, wx.ID_CANCEL, _(u"C&lose"), size=wx.DefaultSize)
cancelButton.SetDefault()
buttonsBox = wx.BoxSizer(wx.HORIZONTAL)
buttonsBox.Add(self.share, 0, wx.ALL, 5)
buttonsBox.Add(self.spellcheck, 0, wx.ALL, 5)
buttonsBox.Add(self.translateButton, 0, wx.ALL, 5)
buttonsBox.Add(cancelButton, 0, wx.ALL, 5)
mainBox.Add(buttonsBox, 0, wx.ALL, 5)
selectId = wx.ID_ANY
self.Bind(wx.EVT_MENU, self.onSelect, id=selectId)
self.accel_tbl = wx.AcceleratorTable([
(wx.ACCEL_CTRL, ord('A'), selectId),
])
self.SetAcceleratorTable(self.accel_tbl)
panel.SetSizer(mainBox)
self.SetClientSize(mainBox.CalcMin())
def set_text(self, text):
self.text.ChangeValue(text)
def get_text(self):
return self.text.GetValue()
def text_focus(self):
self.text.SetFocus()
def onSelect(self, ev):
self.text.SelectAll()
def enable_button(self, buttonName):
if hasattr(self, buttonName):
return getattr(self, buttonName).Enable()
class poll(wx.Dialog):
def __init__(self, *args, **kwds):
super(poll, self).__init__(parent=None, id=wx.NewId(), title=_("Add a poll"))
sizer_1 = wx.BoxSizer(wx.VERTICAL)
period_sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer_1.Add(period_sizer, 1, wx.EXPAND, 0)
label_period = wx.StaticText(self, wx.ID_ANY, _("Participation time"))
period_sizer.Add(label_period, 0, 0, 0)
self.period = wx.ComboBox(self, wx.ID_ANY, choices=[_("5 minutes"), _("30 minutes"), _("1 hour"), _("6 hours"), _("1 day"), _("2 days"), _("3 days"), _("4 days"), _("5 days"), _("6 days"), _("7 days")], style=wx.CB_DROPDOWN | wx.CB_READONLY | wx.CB_SIMPLE)
self.period.SetFocus()
self.period.SetSelection(0)
period_sizer.Add(self.period, 0, 0, 0)
sizer_2 = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Choices")), wx.VERTICAL)
sizer_1.Add(sizer_2, 1, wx.EXPAND, 0)
option1_sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer_2.Add(option1_sizer, 1, wx.EXPAND, 0)
label_2 = wx.StaticText(self, wx.ID_ANY, _("Option 1"))
option1_sizer.Add(label_2, 0, 0, 0)
self.option1 = wx.TextCtrl(self, wx.ID_ANY, "")
self.option1.SetMaxLength(25)
option1_sizer.Add(self.option1, 0, 0, 0)
option2_sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer_2.Add(option2_sizer, 1, wx.EXPAND, 0)
label_3 = wx.StaticText(self, wx.ID_ANY, _("Option 2"))
option2_sizer.Add(label_3, 0, 0, 0)
self.option2 = wx.TextCtrl(self, wx.ID_ANY, "")
self.option2.SetMaxLength(25)
option2_sizer.Add(self.option2, 0, 0, 0)
option3_sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer_2.Add(option3_sizer, 1, wx.EXPAND, 0)
label_4 = wx.StaticText(self, wx.ID_ANY, _("Option 3"))
option3_sizer.Add(label_4, 0, 0, 0)
self.option3 = wx.TextCtrl(self, wx.ID_ANY, "")
self.option3.SetMaxLength(25)
option3_sizer.Add(self.option3, 0, 0, 0)
option4_sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer_2.Add(option4_sizer, 1, wx.EXPAND, 0)
label_5 = wx.StaticText(self, wx.ID_ANY, _("Option 4"))
option4_sizer.Add(label_5, 0, 0, 0)
self.option4 = wx.TextCtrl(self, wx.ID_ANY, "")
self.option4.SetMaxLength(25)
option4_sizer.Add(self.option4, 0, 0, 0)
self.multiple = wx.CheckBox(self, wx.ID_ANY, _("Allow multiple votes per user"))
self.multiple.SetValue(False)
sizer_1.Add(self.multiple, 0, wx.ALL, 5)
self.hide_votes = wx.CheckBox(self, wx.ID_ANY, _("Hide votes count until the poll expires"))
self.hide_votes.SetValue(False)
sizer_1.Add(self.hide_votes, 0, wx.ALL, 5)
btn_sizer = wx.StdDialogButtonSizer()
sizer_1.Add(btn_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 4)
self.button_OK = wx.Button(self, wx.ID_OK)
self.button_OK.SetDefault()
self.button_OK.Bind(wx.EVT_BUTTON, self.validate_data)
btn_sizer.AddButton(self.button_OK)
self.button_CANCEL = wx.Button(self, wx.ID_CANCEL, "")
btn_sizer.AddButton(self.button_CANCEL)
btn_sizer.Realize()
self.SetSizer(sizer_1)
sizer_1.Fit(self)
self.SetAffirmativeId(self.button_OK.GetId())
self.SetEscapeId(self.button_CANCEL.GetId())
self.Layout()
def get_options(self):
controls = [self.option1, self.option2, self.option3, self.option4]
options = [option.GetValue() for option in controls if option.GetValue() != ""]
return options
def validate_data(self, *args, **kwargs):
options = self.get_options()
if len(options) < 2:
return wx.MessageDialog(self, _("Please make sure you have provided at least two options for the poll."), _("Not enough information"), wx.ICON_ERROR).ShowModal()
self.EndModal(wx.ID_OK)

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
import wx
class searchDialog(wx.Dialog):
def __init__(self, value=""):
super(searchDialog, self).__init__(parent=None, id=wx.ID_ANY)
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
self.SetTitle(_("Search"))
label = wx.StaticText(panel, wx.ID_ANY, _("&Search"))
self.term = wx.TextCtrl(panel, wx.ID_ANY, value)
self.term.SetFocus()
dc = wx.WindowDC(self.term)
dc.SetFont(self.term.GetFont())
self.term.SetSize(dc.GetTextExtent("0"*40))
sizer.Add(label, 0, wx.ALL, 5)
sizer.Add(self.term, 0, wx.ALL, 5)
self.posts = wx.RadioButton(panel, wx.ID_ANY, _("Posts"), style=wx.RB_GROUP)
self.users = wx.RadioButton(panel, wx.ID_ANY, _("Users"))
radioSizer = wx.BoxSizer(wx.HORIZONTAL)
radioSizer.Add(self.posts, 0, wx.ALL, 5)
radioSizer.Add(self.users, 0, wx.ALL, 5)
sizer.Add(radioSizer, 0, wx.ALL, 5)
ok = wx.Button(panel, wx.ID_OK, _(u"&OK"))
ok.SetDefault()
cancel = wx.Button(panel, wx.ID_CANCEL, _("&Close"))
self.SetEscapeId(cancel.GetId())
btnsizer = wx.BoxSizer()
btnsizer.Add(ok, 0, wx.ALL, 5)
btnsizer.Add(cancel, 0, wx.ALL, 5)
sizer.Add(btnsizer, 0, wx.ALL, 5)
panel.SetSizer(sizer)
self.SetClientSize(sizer.CalcMin())

View File

@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
import wx
class UserActionsDialog(wx.Dialog):
def __init__(self, users=[], default="follow", *args, **kwargs):
super(UserActionsDialog, self).__init__(parent=None, *args, **kwargs)
panel = wx.Panel(self)
userSizer = wx.BoxSizer()
self.SetTitle(_(u"Action"))
userLabel = wx.StaticText(panel, -1, _(u"&User"))
self.cb = wx.ComboBox(panel, -1, choices=users, value=users[0])
self.cb.SetFocus()
self.autocompletion = wx.Button(panel, -1, _(u"&Autocomplete users"))
userSizer.Add(userLabel, 0, wx.ALL, 5)
userSizer.Add(self.cb, 0, wx.ALL, 5)
userSizer.Add(self.autocompletion, 0, wx.ALL, 5)
actionSizer = wx.BoxSizer(wx.VERTICAL)
label2 = wx.StaticText(panel, -1, _(u"Action"))
self.follow = wx.RadioButton(panel, -1, _(u"&Follow"), name=_(u"Action"), style=wx.RB_GROUP)
self.unfollow = wx.RadioButton(panel, -1, _(u"U&nfollow"))
self.mute = wx.RadioButton(panel, -1, _(u"&Mute"))
self.unmute = wx.RadioButton(panel, -1, _(u"Unmu&te"))
self.block = wx.RadioButton(panel, -1, _(u"&Block"))
self.unblock = wx.RadioButton(panel, -1, _(u"Unbl&ock"))
self.setup_default(default)
hSizer = wx.BoxSizer(wx.HORIZONTAL)
hSizer.Add(label2, 0, wx.ALL, 5)
actionSizer.Add(self.follow, 0, wx.ALL, 5)
actionSizer.Add(self.unfollow, 0, wx.ALL, 5)
actionSizer.Add(self.mute, 0, wx.ALL, 5)
actionSizer.Add(self.unmute, 0, wx.ALL, 5)
actionSizer.Add(self.block, 0, wx.ALL, 5)
actionSizer.Add(self.unblock, 0, wx.ALL, 5)
hSizer.Add(actionSizer, 0, wx.ALL, 5)
sizer = wx.BoxSizer(wx.VERTICAL)
ok = wx.Button(panel, wx.ID_OK, _(u"&OK"))
ok.SetDefault()
cancel = wx.Button(panel, wx.ID_CANCEL, _(u"&Close"))
btnsizer = wx.BoxSizer()
btnsizer.Add(ok)
btnsizer.Add(cancel)
sizer.Add(userSizer)
sizer.Add(hSizer, 0, wx.ALL, 5)
sizer.Add(btnsizer)
panel.SetSizer(sizer)
def get_action(self):
if self.follow.GetValue() == True: return "follow"
elif self.unfollow.GetValue() == True: return "unfollow"
elif self.mute.GetValue() == True: return "mute"
elif self.unmute.GetValue() == True: return "unmute"
elif self.block.GetValue() == True: return "block"
elif self.unblock.GetValue() == True: return "unblock"
def setup_default(self, default):
if default == "follow":
self.follow.SetValue(True)
elif default == "unfollow":
self.unfollow.SetValue(True)
elif default == "mute":
self.mute.SetValue(True)
elif default == "unmute":
self.unmute.SetValue(True)
elif default == "block":
self.block.SetValue(True)
elif default == "unblock":
self.unblock.SetValue(True)
def get_response(self):
return self.ShowModal()
def get_user(self):
return self.cb.GetValue()
def get_position(self):
return self.cb.GetPosition()
def popup_menu(self, menu):
self.PopupMenu(menu, self.cb.GetPosition())

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
import wx
class UserTimeline(wx.Dialog):
def __init__(self, users=[], default="posts", *args, **kwargs):
super(UserTimeline, self).__init__(parent=None, *args, **kwargs)
panel = wx.Panel(self)
userSizer = wx.BoxSizer()
self.SetTitle(_("Timeline for %s") % (users[0]))
userLabel = wx.StaticText(panel, -1, _(u"User"))
self.cb = wx.ComboBox(panel, -1, choices=users, value=users[0])
self.cb.SetFocus()
self.autocompletion = wx.Button(panel, -1, _(u"&Autocomplete users"))
userSizer.Add(userLabel, 0, wx.ALL, 5)
userSizer.Add(self.cb, 0, wx.ALL, 5)
userSizer.Add(self.autocompletion, 0, wx.ALL, 5)
actionSizer = wx.BoxSizer(wx.VERTICAL)
label2 = wx.StaticText(panel, -1, _(u"Buffer type"))
self.posts = wx.RadioButton(panel, -1, _(u"&Posts"), style=wx.RB_GROUP)
self.followers = wx.RadioButton(panel, -1, _(u"&Followers"))
self.following = wx.RadioButton(panel, -1, _("Fo&llowing"))
self.setup_default(default)
hSizer = wx.BoxSizer(wx.HORIZONTAL)
hSizer.Add(label2, 0, wx.ALL, 5)
actionSizer.Add(self.posts, 0, wx.ALL, 5)
actionSizer.Add(self.followers, 0, wx.ALL, 5)
actionSizer.Add(self.following, 0, wx.ALL, 5)
hSizer.Add(actionSizer, 0, wx.ALL, 5)
sizer = wx.BoxSizer(wx.VERTICAL)
ok = wx.Button(panel, wx.ID_OK, _(u"&OK"))
ok.SetDefault()
cancel = wx.Button(panel, wx.ID_CANCEL, _(u"&Close"))
btnsizer = wx.BoxSizer()
btnsizer.Add(ok)
btnsizer.Add(cancel)
sizer.Add(userSizer)
sizer.Add(hSizer, 0, wx.ALL, 5)
sizer.Add(btnsizer)
panel.SetSizer(sizer)
def get_action(self):
if self.posts.GetValue() == True: return "posts"
elif self.followers.GetValue() == True: return "followers"
elif self.following.GetValue() == True: return "following"
def setup_default(self, default):
if default == "posts":
self.posts.SetValue(True)
def get_user(self):
return self.cb.GetValue()
def get_position(self):
return self.cb.GetPosition()
def popup_menu(self, menu):
self.PopupMenu(menu, self.cb.GetPosition())