Eliminando srcantiguo para limpiar el repo

This commit is contained in:
Jesús Pavón Abián
2026-02-01 20:49:00 +01:00
parent 4cd8636e4a
commit 30a7b1a12a
367 changed files with 2 additions and 118899 deletions

View File

@@ -54,7 +54,7 @@ class sessionManagerWindow(wx.Dialog):
mastodon = menu.Append(wx.ID_ANY, _("Mastodon")) mastodon = menu.Append(wx.ID_ANY, _("Mastodon"))
menu.Bind(wx.EVT_MENU, self.on_new_mastodon_account, mastodon) menu.Bind(wx.EVT_MENU, self.on_new_mastodon_account, mastodon)
blueski = menu.Append(wx.ID_ANY, _("Blueski (Bluesky)")) blueski = menu.Append(wx.ID_ANY, _("Bluesky"))
menu.Bind(wx.EVT_MENU, self.on_new_blueski_account, blueski) menu.Bind(wx.EVT_MENU, self.on_new_blueski_account, blueski)
self.PopupMenu(menu, self.new.GetPosition()) self.PopupMenu(menu, self.new.GetPosition())
@@ -67,7 +67,7 @@ class sessionManagerWindow(wx.Dialog):
pub.sendMessage("sessionmanager.new_account", type="mastodon") pub.sendMessage("sessionmanager.new_account", type="mastodon")
def on_new_blueski_account(self, *args, **kwargs): def on_new_blueski_account(self, *args, **kwargs):
dlg = wx.MessageDialog(self, _("You will be prompted for your Blueski (Bluesky) data (user handle and App Password) to authorize TWBlue. Would you like to authorize your account now?"), _(u"Blueski Authorization"), wx.YES_NO) dlg = wx.MessageDialog(self, _("You will be prompted for your Bluesky data (user handle and App Password) to authorize TWBlue. Would you like to authorize your account now?"), _(u"Bluesky Authorization"), wx.YES_NO)
response = dlg.ShowModal() response = dlg.ShowModal()
dlg.Destroy() dlg.Destroy()
if response == wx.ID_YES: if response == wx.ID_YES:

View File

@@ -1,33 +0,0 @@
[sessions]
current_session = string(default="")
sessions = list(default=list())
ignored_sessions = list(default=list())
[app-settings]
language = string(default="system")
update_period = integer(default=2)
hide_gui = boolean(default=False)
voice_enabled = boolean(default=False)
ask_at_exit = boolean(default=True)
read_long_posts_in_gui = boolean(default=True)
use_invisible_keyboard_shorcuts = boolean(default=True)
play_ready_sound = boolean(default=True)
speak_ready_msg = boolean(default=True)
log_level = string(default="error")
load_keymap = string(default="default.keymap")
donation_dialog_displayed = boolean(default=False)
check_for_updates = boolean(default=True)
no_streaming = boolean(default=False)
[proxy]
type = integer(default=0)
server = string(default="")
port = integer(default=8080)
user = string(default="")
password = string(default="")
[translator]
engine=string(default="LibreTranslate")
lt_api_url=string(default="https://translate.nvda.es")
lt_api_key=string(default="")
deepl_api_key = string(default="")

View File

@@ -1,13 +0,0 @@
# -*- coding: utf-8 -*-
name = 'TWBlue'
short_name='twblue'
update_url = 'https://raw.githubusercontent.com/mcv-software/TWBlue/next-gen/updates/updates.json'
authors = ["Manuel Cortéz", "José Manuel Delicado"]
authorEmail = "manuel@manuelcortez.net"
copyright = "Copyright (C) 2013-2024, MCV Software."
description = name+" is an app designed to use Twitter simply and efficiently while using minimal system resources. This app provides access to most Twitter features."
translators = ["Manuel Cortéz (English)", "Mohammed Al Shara, Hatoun Felemban (Arabic)", "Francisco Torres (Catalan)", "Manuel cortéz (Spanish)", "Sukil Etxenike Arizaleta (Basque)", "Jani Kinnunen (finnish)", "Corentin Bacqué-Cazenave (Français)", "Juan Buño (Galician)", "Steffen Schultz (German)", "Zvonimir Stanečić (Croatian)", "Robert Osztolykan (Hungarian)", "Christian Leo Mameli (Italian)", "Riku (Japanese)", "Paweł Masarczyk (Polish)", "Odenilton Júnior Santos (Portuguese)", "Florian Ionașcu, Nicușor Untilă (Romanian)", "Natalia Hedlund, Valeria Kuznetsova (Russian)", "Aleksandar Đurić (Serbian)", "Burak Yüksek (Turkish)"]
url = "https://twblue.mcvsoftware.com"
report_bugs_url = "https://github.com/MCV-Software/TWBlue/issues"
supported_languages = []
version = "11"

View File

@@ -1,23 +0,0 @@
from __future__ import unicode_literals
from functools import wraps
def matches_url(url):
def url_setter(func):
@wraps(func)
def internal_url_setter(*args, **kwargs):
return func(*args, **kwargs)
internal_url_setter.url = url
return internal_url_setter
return url_setter
def find_url_transformer(url):
from audio_services import services
funcs = []
for i in dir(services):
possible = getattr(services, i)
if callable(possible) and hasattr(possible, 'url'):
funcs.append(possible)
for f in funcs:
if url.lower().startswith(f.url.lower()):
return f
return services.convert_generic_audio

View File

@@ -1,41 +0,0 @@
from __future__ import unicode_literals
from audio_services import matches_url
import requests
from . import youtube_utils
@matches_url('https://audioboom.com')
def convert_audioboom(url):
if "audioboom.com" not in url.lower():
raise TypeError('%r is not a valid URL' % url)
audio_id = url.split('.com/')[-1]
return 'https://audioboom.com/%s.mp3' % audio_id
@matches_url ('https://soundcloud.com/')
def convert_soundcloud (url):
client_id = "df8113ca95c157b6c9731f54b105b473"
with requests.get('http://api.soundcloud.com/resolve.json', client_id=client_id, url=url) as permalink:
if permalink.status_code==404:
raise TypeError('%r is not a valid URL' % permalink.url)
else:
resolved_url = permalink.url
with requests.get(resolved_url) as track_url:
track_data = track_url.json()
if track_data ['streamable']:
return track_data ['stream_url'] + "?client_id=%s" %client_id
else:
raise TypeError('%r is not streamable' % url)
@matches_url ('https://www.youtube.com/watch')
def convert_youtube_long (url):
return youtube_utils.get_video_url(url)
@matches_url ('http://anyaudio.net/listen')
def convert_anyaudio(url):
values = url.split("audio=")
if len(values) != 2:
raise TypeError('%r is not streamable' % url)
return "http://anyaudio.net/audiodownload?audio=%s" % (values[1],)
def convert_generic_audio(url):
return url

View File

@@ -1,13 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import youtube_dl
def get_video_url(url):
ydl = youtube_dl.YoutubeDL({'quiet': True, 'format': 'bestaudio/best', 'outtmpl': u'%(id)s%(ext)s'})
with ydl:
result = ydl.extract_info(url, download=False)
if 'entries' in result:
video = result['entries'][0]
else:
video = result
return video["formats"][0]["url"]

View File

@@ -1,12 +0,0 @@
# -*- coding: utf-8 -*-
import argparse
import paths
import logging
import application
log = logging.getLogger("commandlineLauncher")
parser = argparse.ArgumentParser(description=application.name+" command line launcher")
parser.add_argument("-d", "--data-directory", action="store", dest="directory", help="Specifies the directory where " + application.name + " saves userdata.")
args = parser.parse_args()
log.debug("Starting " + application.name + " with the following arguments: directory = %s" % (args.directory))
if args.directory != None: paths.directory = args.directory

View File

@@ -1,32 +0,0 @@
# -*- coding: cp1252 -*-
import os
import sys
import config_utils
import paths
import logging
import platform
log = logging.getLogger("config")
MAINFILE = "twblue.conf"
MAINSPEC = "app-configuration.defaults"
proxyTypes = ["system", "http", "socks4", "socks4a", "socks5", "socks5h"]
app = None
keymap=None
changed_keymap = False
def setup ():
global app
log.debug("Loading global app settings...")
app = config_utils.load_config(os.path.join(paths.config_path(), MAINFILE), os.path.join(paths.app_path(), MAINSPEC))
log.debug("Loading keymap...")
global keymap
if float(platform.version()[:2]) >= 10 and app["app-settings"]["load_keymap"] == "default.keymap":
if sys.getwindowsversion().build > 22000:
app["app-settings"]["load_keymap"] = "Windows11.keymap"
else:
app["app-settings"]["load_keymap"] = "Windows 10.keymap"
app.write()
global changed_keymap
changed_keymap = True
keymap = config_utils.load_config(os.path.join(paths.config_path(), "keymap.keymap"), os.path.join(paths.app_path(), "keymaps/"+app['app-settings']['load_keymap']), copy=False)

View File

@@ -1,79 +0,0 @@
# -*- coding: utf-8 -*-
from configobj import ConfigObj, ParseError
from validate import Validator, ValidateError
import os
import string
from logging import getLogger
from wxUI import commonMessageDialogs
log = getLogger("config_utils")
class ConfigLoadError(Exception): pass
def load_config(config_path, configspec_path=None, copy=True, *args, **kwargs):
spec = ConfigObj(configspec_path, encoding='UTF8', list_values=False, _inspec=True)
try:
config = ConfigObj(infile=config_path, configspec=spec, create_empty=True, encoding='UTF8', *args, **kwargs)
except ParseError:
raise ConfigLoadError("Unable to load %r" % config_path)
validator = Validator()
validated = config.validate(validator, preserve_errors=False, copy=copy)
if validated == True:
config.write()
return config
else:
log.exception("Error in config file: {0}".format(validated,))
commonMessageDialogs.invalid_configuration()
def is_blank(arg):
"Check if a line is blank."
for c in arg:
if c not in string.whitespace:
return False
return True
def get_keys(path):
"Gets the keys of a configobj config file."
res=[]
fin=open(path)
for line in fin:
if not is_blank(line):
res.append(line[0:line.find('=')].strip())
fin.close()
return res
def hist(keys):
"Generates a histogram of an iterable."
res={}
for k in keys:
res[k]=res.setdefault(k,0)+1
return res
def find_problems(hist):
"Takes a histogram and returns a list of items occurring more than once."
res=[]
for k,v in hist.items():
if v>1:
res.append(k)
return res
def clean_config(path):
"Cleans a config file. If duplicate values are found, delete all of them and just use the default."
orig=[]
cleaned=[]
fin=open(path)
for line in fin:
orig.append(line)
fin.close()
for p in find_problems(hist(get_keys(path))):
for o in orig:
o.strip()
if p not in o:
cleaned.append(o)
if len(cleaned) != 0:
cam=open(path,'w')
for c in cleaned:
cam.write(c)
cam.close()
return True
else:
return False

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
from .account import AccountBuffer
from .base import Buffer
from .empty import EmptyBuffer

View File

@@ -1,56 +0,0 @@
# -*- coding: utf-8 -*-
""" Common logic to all buffers in TWBlue."""
import logging
import config
import widgetUtils
from pubsub import pub
from wxUI import buffers
from . import base
log = logging.getLogger("controller.buffers.base.account")
class AccountBuffer(base.Buffer):
def __init__(self, parent, name, account, account_id):
super(AccountBuffer, self).__init__(parent, None, name)
log.debug("Initializing buffer %s, account %s" % (name, account,))
self.buffer = buffers.accountPanel(parent, name)
self.type = self.buffer.type
self.compose_function = None
self.session = None
self.needs_init = False
self.account = account
self.buffer.account = account
self.name = name
self.account_id = account_id
def setup_account(self):
widgetUtils.connect_event(self.buffer, widgetUtils.CHECKBOX, self.autostart, menuitem=self.buffer.autostart_account)
if self.account_id in config.app["sessions"]["ignored_sessions"]:
self.buffer.change_autostart(False)
else:
self.buffer.change_autostart(True)
if not hasattr(self, "logged"):
self.buffer.change_login(login=False)
widgetUtils.connect_event(self.buffer.login, widgetUtils.BUTTON_PRESSED, self.logout)
else:
self.buffer.change_login(login=True)
widgetUtils.connect_event(self.buffer.login, widgetUtils.BUTTON_PRESSED, self.login)
def login(self, *args, **kwargs):
del self.logged
self.setup_account()
pub.sendMessage("login", session_id=self.account_id)
def logout(self, *args, **kwargs):
self.logged = False
self.setup_account()
pub.sendMessage("logout", session_id=self.account_id)
def autostart(self, *args, **kwargs):
if self.account_id in config.app["sessions"]["ignored_sessions"]:
self.buffer.change_autostart(True)
config.app["sessions"]["ignored_sessions"].remove(self.account_id)
else:
self.buffer.change_autostart(False)
config.app["sessions"]["ignored_sessions"].append(self.account_id)
config.app.write()

View File

@@ -1,144 +0,0 @@
# -*- coding: utf-8 -*-
""" Common logic to all buffers in TWBlue."""
import logging
import wx
import output
import sound
import widgetUtils
log = logging.getLogger("controller.buffers.base.base")
class Buffer(object):
""" A basic buffer object. This should be the base class for all other derived buffers."""
def __init__(self, parent=None, function=None, session=None, *args, **kwargs):
"""Inits the main controller for this buffer:
@ 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):
pass
def get_event(self, ev):
""" Catch key presses in the WX interface and generate the corresponding event names."""
if ev.GetKeyCode() == wx.WXK_RETURN and ev.ControlDown(): event = "audio"
elif ev.GetKeyCode() == wx.WXK_RETURN: event = "url"
elif ev.GetKeyCode() == wx.WXK_F5: event = "volume_down"
elif ev.GetKeyCode() == wx.WXK_F6: event = "volume_up"
elif ev.GetKeyCode() == wx.WXK_DELETE and ev.ShiftDown(): event = "clear_list"
elif ev.GetKeyCode() == wx.WXK_DELETE: event = "destroy_status"
# Raise a Special event when pressed Shift+F10 because Wx==4.1.x does not seems to trigger this by itself.
# See https://github.com/manuelcortez/TWBlue/issues/353
elif ev.GetKeyCode() == wx.WXK_F10 and ev.ShiftDown(): event = "show_menu"
else:
event = None
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):
pass
def save_positions(self):
try:
self.session.db[self.name+"_pos"]=self.buffer.list.get_selected()
except AttributeError:
pass
def view_item(self):
pass

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
import logging
from wxUI import buffers
from . import base
log = logging.getLogger("controller.buffers.base.empty")
class EmptyBuffer(base.Buffer):
def __init__(self, parent, name, account):
super(EmptyBuffer, self).__init__(parent=parent)
log.debug("Initializing buffer %s, account %s" % (name, account,))
self.buffer = buffers.emptyPanel(parent, name)
self.type = self.buffer.type
self.compose_function = None
self.account = account
self.buffer.account = account
self.name = name
self.session = None
self.needs_init = True

View File

@@ -1,9 +0,0 @@
# -*- coding: utf-8 -*-
from .base import BaseBuffer
from .mentions import MentionsBuffer
from .conversations import ConversationBuffer, ConversationListBuffer
from .users import UserBuffer
from .notifications import NotificationsBuffer
from .search import SearchBuffer
from .community import CommunityBuffer
from .announcements import AnnouncementsBuffer

View File

@@ -1,165 +0,0 @@
# -*- coding: utf-8 -*-
import time
import logging
import arrow
import widgetUtils
import wx
import output
import languageHandler
import config
from pubsub import pub
from controller.buffers.mastodon.base import BaseBuffer
from sessions.mastodon import compose, templates
from wxUI import buffers
from wxUI.dialogs.mastodon import menus
from mysc.thread_utils import call_threaded
log = logging.getLogger("controller.buffers.mastodon.announcements")
class AnnouncementsBuffer(BaseBuffer):
def __init__(self, *args, **kwargs):
# We enforce compose_func="compose_announcement"
kwargs["compose_func"] = "compose_announcement"
super(AnnouncementsBuffer, self).__init__(*args, **kwargs)
self.type = "announcementsBuffer"
def create_buffer(self, parent, name):
self.buffer = buffers.mastodon.announcementsPanel(parent, name)
def get_buffer_name(self):
return _("Announcements")
def bind_events(self):
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.dismiss_announcement, self.buffer.dismiss)
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 dismiss_announcement(self, event=None, item=None, *args, **kwargs):
index = self.buffer.list.get_selected()
if index == -1: return
item = self.session.db[self.name][index]
# Optimistic UI update or wait for API? Let's wait for API to be safe, but run threaded.
# We need a custom call because 'announcements_dismiss' returns None on success usually.
def _do_dismiss():
try:
self.session.api_call(call_name="announcements_dismiss", id=str(item.id))
# If success, update UI in main thread
wx.CallAfter(self._on_dismiss_success, index)
except Exception as e:
log.exception("Error dismissing announcement")
self.session.sound.play("error.ogg")
call_threaded(_do_dismiss)
def _on_dismiss_success(self, index):
if index < len(self.session.db[self.name]):
self.session.db[self.name].pop(index)
self.buffer.list.remove_item(index)
output.speak(_("Announcement dismissed."))
def show_menu(self, ev, pos=0, *args, **kwargs):
if self.buffer.list.get_count() == 0:
return
# Create a simple menu
menu = wx.Menu()
dismiss_item = menu.Append(wx.ID_ANY, _("Dismiss"))
copy_item = menu.Append(wx.ID_ANY, _("Copy text"))
self.buffer.Bind(wx.EVT_MENU, self.dismiss_announcement, dismiss_item)
self.buffer.Bind(wx.EVT_MENU, self.copy, copy_item)
if pos != 0:
self.buffer.PopupMenu(menu, pos)
else:
self.buffer.PopupMenu(menu, self.buffer.list.list.GetPosition())
def url(self, *args, **kwargs):
self.dismiss_announcement()
def get_more_items(self): output.speak(_("This buffer does not support loading more items."), True)
# Disable social interactions not applicable to announcements
def reply(self, *args, **kwargs):
pass
def share_item(self, *args, **kwargs):
pass
def toggle_favorite(self, *args, **kwargs):
pass
def add_to_favorites(self, *args, **kwargs):
pass
def remove_from_favorites(self, *args, **kwargs):
pass
def toggle_bookmark(self, *args, **kwargs):
pass
def mute_conversation(self, *args, **kwargs):
pass
def vote(self, *args, **kwargs):
pass
def send_message(self, *args, **kwargs):
pass
def user_details(self, *args, **kwargs):
pass
def view_item(self, *args, **kwargs):
# We could implement a specific viewer for announcements if needed,
# but the default one expects a status object structure.
pass
def copy(self, event=None):
item = self.get_item()
if item:
pub.sendMessage("execute-action", action="copy_to_clipboard")
def onFocus(self, *args, **kwargs):
# Similar logic to BaseBuffer but adapted if needed.
# BaseBuffer.onFocus handles reading long posts.
if config.app["app-settings"]["read_long_posts_in_gui"] == True and self.buffer.list.list.HasFocus():
wx.CallLater(40, output.speak, self.get_message(), interrupt=True)
def get_message(self):
# Override to use announcement template
announcement = self.get_item()
if announcement == None:
return
template = self.session.settings.get("templates", {}).get("announcement", templates.announcement_default_template)
t = templates.render_announcement(announcement, template, self.session.settings, 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 >= 300 or mandatory==True:
self.execution_time = current_time
log.debug("Starting stream for announcements buffer")
try:
# The announcements API does not accept min_id or limit parameters
results = self.session.api.announcements()
# Reverse the list so order_buffer processes them according to user preference
results.reverse()
except Exception as e:
log.exception("Error retrieving announcements: %s" % (str(e)))
return 0
# order_buffer handles duplication filtering by ID internally
number_of_items = self.session.order_buffer(self.name, results)
log.debug("Number of new announcements retrieved: %d" % (number_of_items,))
self.put_items_on_list(number_of_items)
if number_of_items > 0 and play_sound == True and self.sound != None and self.session.settings["sound"]["session_mute"] == False:
self.session.sound.play(self.sound)
return number_of_items
return 0

View File

@@ -1,772 +0,0 @@
# -*- coding: utf-8 -*-
import time
import wx
import widgetUtils
import arrow
import webbrowser
import output
import config
import sound
import languageHandler
import logging
from mastodon import MastodonNotFoundError
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, commonMessageDialogs
from wxUI.dialogs.mastodon import menus
from wxUI.dialogs.mastodon import dialogs as mastodon_dialogs
from wxUI.dialogs.mastodon.postDialogs import attachedPoll
from wxUI.dialogs import urlList
log = logging.getLogger("controller.buffers.mastodon.base")
class BaseBuffer(base.Buffer):
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
pub.subscribe(self.on_mute_cleanup, "mastodon.mute_cleanup")
if "-timeline" in self.name or "-followers" in self.name or "-following" in self.name or "searchterm" in self.name:
self.finished_timeline = False
def on_mute_cleanup(self, conversation_id, session_name):
if self.name != "home_timeline":
return
if session_name != self.session.get_name():
return
items_to_remove = []
for index, item in enumerate(self.session.db[self.name]):
c_id = None
if hasattr(item, "conversation_id"):
c_id = item.conversation_id
elif isinstance(item, dict):
c_id = item.get("conversation_id")
if c_id == conversation_id:
items_to_remove.append(index)
items_to_remove.sort(reverse=True)
for index in items_to_remove:
self.session.db[self.name].pop(index)
self.buffer.list.remove_item(index)
def create_buffer(self, parent, name):
self.buffer = buffers.mastodon.basePanel(parent, name)
def get_buffer_name(self):
""" Get buffer name from a set of different techniques."""
# firstly let's take the easier buffers.
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"))
if self.name in list(basic_buffers.keys()):
return basic_buffers[self.name]
# Check user timelines
elif hasattr(self, "username"):
if "-timeline" in self.name:
return _(u"{username}'s timeline").format(username=self.username,)
elif "-followers" in self.name:
return _(u"{username}'s followers").format(username=self.username,)
elif "-following" in self.name:
return _(u"{username}'s following").format(username=self.username,)
log.error("Error getting name for buffer %s" % (self.name,))
return _(u"Unknown buffer")
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(), language=post.get_language(), **kwargs)
if hasattr(post.message, "destroy"):
post.message.destroy()
def get_formatted_message(self):
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
return self.compose_function(self.get_item(), self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)[1]
def get_message(self):
post = self.get_item()
if post == None:
return
template = self.session.settings["templates"]["post"]
# If template is set to hide sensitive media by default, let's change it according to user preferences.
if self.session.settings["general"]["read_preferences_from_instance"] == True:
if self.session.expand_spoilers == True and "$safe_text" in template:
template = template.replace("$safe_text", "$text")
elif self.session.expand_spoilers == False and "$text" in template:
template = template.replace("$text", "$safe_text")
t = templates.render_post(post, template, self.session.settings, 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:
# We use the maximum ID present in the buffer to ensure we only request posts
# that are newer than our most recent chronological post.
# This prevents old pinned posts from pulling in hundreds of previous statuses.
min_id = max(item.id for item in self.session.db[self.name])
# loads pinned posts from user accounts.
# Load those posts only when there are no items previously loaded.
if "-timeline" in self.name and "account_statuses" in self.function and len(self.session.db.get(self.name, [])) == 0:
pinned_posts = self.session.api.account_statuses(pinned=True, limit=count, *self.args, **self.kwargs)
for p in pinned_posts:
p["pinned"] = True
pinned_posts.reverse()
else:
pinned_posts = None
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
if self.session.settings["general"]["reverse_timelines"]:
if pinned_posts != None and len(pinned_posts) > 0:
amount_of_pinned_posts = self.session.order_buffer(self.name, pinned_posts)
number_of_items = self.session.order_buffer(self.name, results)
if self.session.settings["general"]["reverse_timelines"] == False:
if pinned_posts != None and len(pinned_posts) > 0:
amount_of_pinned_posts = self.session.order_buffer(self.name, pinned_posts)
if pinned_posts != None and len(pinned_posts) > 0:
number_of_items = amount_of_pinned_posts+number_of_items
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
pub.sendMessage("core.change_buffer_title", name=self.session.get_name(), buffer=self.name, title=_("Timeline for {}").format(self.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()))
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
output.speak(" ".join(self.compose_function(post, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)))
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 len(self.session.db[self.name]) == 0:
return
# We use the minimum ID in the buffer to correctly request the next page of older items.
# This prevents old pinned posts from causing us to skip chronological posts.
# We try to exclude pinned posts from this calculation as they are usually outliers at the top.
unpinned_ids = [item.id for item in self.session.db[self.name] if not getattr(item, "pinned", False)]
if unpinned_ids:
max_id = min(unpinned_ids)
else:
max_id = min(item.id for item in self.session.db[self.name])
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:
filter_status = utils.evaluate_filters(post=i, current_context=utils.get_current_context(self.name))
if filter_status == "hide":
continue
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))
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(True, *post)
else:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
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,))
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
if self.buffer.list.get_count() == 0:
for i in list_to_use:
post = self.compose_function(i, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
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, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
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, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
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):
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
post = self.compose_function(item, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
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):
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
post = self.compose_function(item, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
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)
# Enable/disable edit based on whether the post belongs to the user
item = self.get_item()
if item and item.account.id == self.session.db["user_id"] and item.reblog == None:
widgetUtils.connect_event(menu, widgetUtils.MENU, self.edit_status, menuitem=menu.edit)
else:
menu.edit.Enable(False)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
if self.can_share() == True:
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.quote, menuitem=menu.quote)
else:
menu.boost.Enable(False)
menu.quote.Enable(False)
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.mute_conversation, menuitem=menu.mute)
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, item=None):
if item == None:
item = self.get_item()
if item.visibility == "direct":
return False
return True
def reply(self, event=None, item=None, *args, **kwargs):
if item == None:
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")
# Set unlisted by default, so we will not clutter other user's buffers with replies.
# see https://github.com/MCV-Software/TWBlue/issues/504
visibility = "unlisted"
if item.reblog != None:
users = ["@{} ".format(user.acct) for user in item.reblog.mentions if user.id != self.session.db["user_id"]]
language = item.reblog.language
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"]]
language = item.language
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))
post.set_language(language)
# 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(), language=post.get_language())
if hasattr(post.message, "destroy"):
post.message.destroy()
def send_message(self, event=None, item=None, *args, **kwargs):
if item == None:
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, language=post.get_language())
if hasattr(post.message, "destroy"):
post.message.destroy()
def share_item(self, event=None, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
if self.can_share(item=item) == False:
return output.speak(_("This action is not supported on conversations."))
id = item.id
if self.session.settings["general"]["boost_mode"] == "ask":
answer = mastodon_dialogs.boost_question()
if answer == 1:
self._direct_boost(id)
elif answer == 2:
self.quote(item=item)
else:
self._direct_boost(id)
def quote(self, event=None, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
if self.can_share(item=item) == False:
return output.speak(_("This action is not supported on conversations."))
title = _("Quote post")
caption = _("Write your comment 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, quote_id=item.id, posts=post_data, visibility=post.get_visibility(), language=post.get_language(), **kwargs)
if hasattr(post.message, "destroy"):
post.message.destroy()
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 config.app["app-settings"]["read_long_posts_in_gui"] == True and self.buffer.list.list.HasFocus():
wx.CallLater(40, output.speak, self.get_message(), interrupt=True)
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, event=None, item=None, *args, **kwargs):
if sound.URLPlayer.player.is_playing():
return sound.URLPlayer.stop_audio()
if item == None:
item = self.get_item()
urls = utils.get_media_urls(item)
if len(urls) == 1:
url=urls[0]
elif len(urls) > 1:
urls_list = 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, announce=True, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
if item.reblog != None:
urls = utils.find_urls(item.reblog)
else:
urls = utils.find_urls(item)
if len(urls) == 1:
url=urls[0]
elif len(urls) > 1:
urls_list = 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")
log.exception("")
self.session.db[self.name] = items
def edit_status(self, event=None, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
# Check if the post belongs to the current user
if item.account.id != self.session.db["user_id"] or item.reblog != None:
output.speak(_("You can only edit your own posts."))
return
# Check if post has a poll with votes - warn user before proceeding
if hasattr(item, 'poll') and item.poll is not None:
votes_count = item.poll.votes_count if hasattr(item.poll, 'votes_count') else 0
if votes_count > 0:
# Show confirmation dialog
warning_title = _("Warning: Poll with votes")
warning_message = _("This post contains a poll with {votes} votes.\n\n"
"According to Mastodon's API, editing this post will reset ALL votes to zero, "
"even if you don't modify the poll itself.\n\n"
"Do you want to continue editing?").format(votes=votes_count)
dialog = wx.MessageDialog(self.buffer, warning_message, warning_title,
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_WARNING)
result = dialog.ShowModal()
dialog.Destroy()
if result != wx.ID_YES:
output.speak(_("Edit cancelled"))
return
# Log item info for debugging
log.debug("Editing status: id={}, has_media_attachments={}, media_count={}".format(
item.id,
hasattr(item, 'media_attachments'),
len(item.media_attachments) if hasattr(item, 'media_attachments') else 0
))
# Create edit dialog with existing post data
title = _("Edit post")
caption = _("Edit your post here")
post = messages.editPost(session=self.session, item=item, title=title, caption=caption)
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
# Call edit_post method in session
# Note: visibility and language cannot be changed when editing per Mastodon API
call_threaded(self.session.edit_post, post_id=post.post_id, posts=post_data)
if hasattr(post.message, "destroy"):
post.message.destroy()
def user_details(self):
item = self.get_item()
pass
def get_item_url(self, item=None):
if item == None:
item = self.get_item()
if item.reblog != None:
return item.reblog.url
return item.url
def open_in_browser(self, event=None, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
url = self.get_item_url(item=item)
output.speak(_("Opening item in web browser..."))
webbrowser.open(url)
def add_to_favorites(self, item=None):
if item == None:
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=None):
if item == None:
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, event=None, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
if item.reblog != None:
item = item.reblog
try:
item = self.session.api.status(item.id)
except MastodonNotFoundError:
output.speak(_("No status found with that ID"))
return
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, event=None, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
if item.reblog != None:
item = item.reblog
try:
item = self.session.api.status(item.id)
except MastodonNotFoundError:
output.speak(_("No status found with that ID"))
return
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 mute_conversation(self, event=None, item=None, *args, **kwargs):
if item == None:
item = self.get_item()
if item.reblog != None:
item = item.reblog
try:
item = self.session.api.status(item.id)
except MastodonNotFoundError:
output.speak(_("No status found with that ID"))
return
if item.muted == False:
call_threaded(self.session.api_call, call_name="status_mute", preexec_message=_("Muting conversation..."), _sound="favourite.ogg", id=item.id)
pub.sendMessage("mastodon.mute_cleanup", conversation_id=item.conversation_id, session_name=self.session.get_name())
else:
call_threaded(self.session.api_call, call_name="status_unmute", preexec_message=_("Unmuting conversation..."), _sound="favourite.ogg", id=item.id)
def view_item(self, item=None):
if item == None:
item = self.get_item()
# Update object so we can retrieve newer stats
try:
item = self.session.api.status(id=item.id)
except MastodonNotFoundError:
output.speak(_("No status found with that ID"))
return
msg = messages.viewPost(self.session, item, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url(item=item))
def ocr_image(self):
post = self.get_item()
media_list = []
if post.reblog != None:
post = post.reblog
for media in post.get("media_attachments"):
if media.get("type", "") == "image":
media_list.append(media)
if len(media_list) > 1:
image_list = [_(u"Picture {0}").format(i+1,) for i in range(0, len(media_list))]
dialog = urlList.urlList(title=_(u"Select the picture"))
dialog.populate_list(image_list)
if dialog.get_response() == widgetUtils.OK:
img = media_list[dialog.get_item()]
else:
return
elif len(media_list) == 1:
img = media_list[0]
else:
return
if self.session.settings["mysc"]["ocr_language"] != "":
ocr_lang = self.session.settings["mysc"]["ocr_language"]
else:
ocr_lang = ocr.OCRSpace.short_langs.index(post.language)
ocr_lang = ocr.OCRSpace.OcrLangs[ocr_lang]
if img["remote_url"] != None:
url = img["remote_url"]
else:
url = img["url"]
api = ocr.OCRSpace.OCRSpaceAPI()
try:
text = api.OCR_URL(url)
except ocr.OCRSpace.APIError as er:
output.speak(_(u"Unable to extract text"))
return
viewer = messages.text(title=_("OCR Result"), text=text["ParsedText"])
response = viewer.message.ShowModal()
viewer.message.Destroy()
def vote(self, item=None):
if item == None:
post = self.get_item()
else:
post = item
if not hasattr(post, "poll") or post.poll == None:
return
poll = post.poll
try:
poll = self.session.api.poll(id=poll.id)
except MastodonNotFoundError:
output.speak(_("this poll no longer exists."))
return
if poll.expired:
output.speak(_("This poll has already expired."))
return
if poll.voted:
output.speak(_("You have already voted on this poll."))
return
options = poll.options
dlg = attachedPoll(poll_options=[option.title for option in options], multiple=poll.multiple)
answer = dlg.ShowModal()
options = dlg.get_selected()
dlg.Destroy()
if answer != wx.ID_OK:
return
poll = self.session.api_call(call_name="poll_vote", id=poll.id, choices=options, preexec_message=_("Sending vote..."))
def post_from_error(self, visibility, reply_to, data, lang):
title = _("Post")
caption = _("Write your post here")
post = messages.post(session=self.session, title=title, caption=caption)
post.set_post_data(visibility=visibility, data=data, language=language)
response = post.message.ShowModal()
if response == wx.ID_OK:
post_data = post.get_data()
call_threaded(self.session.send_post, posts=post_data, reply_to=reply_to, visibility=post.get_visibility(), language=post.get_language())
if hasattr(post.message, "destroy"):
post.message.destroy()

View File

@@ -1,162 +0,0 @@
# -*- coding: utf-8 -*-
import time
import logging
import mastodon
import widgetUtils
import output
from wxUI import commonMessageDialogs
from sessions.mastodon import utils
from . import base
log = logging.getLogger("controller.buffers.mastodon.community")
class CommunityBuffer(base.BaseBuffer):
def __init__(self, community_url, *args, **kwargs):
super(CommunityBuffer, self).__init__(*args, **kwargs)
self.community_url = community_url
self.community_api = mastodon.Mastodon(api_base_url=self.community_url)
self.timeline = kwargs.get("timeline", "local")
self.kwargs.pop("timeline")
def get_buffer_name(self):
type = _("Local") if self.timeline == "local" else _("Federated")
instance = self.community_url.replace("https://", "")
return _(f"{type} timeline for {instance}")
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 in self.session.db and len(self.session.db[self.name]) > 0:
min_id = max(item.id for item in self.session.db[self.name])
try:
results = self.community_api.timeline(timeline=self.timeline, 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,))
self.put_items_on_list(number_of_items)
if number_of_items > 0 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 len(self.session.db[self.name]) == 0:
return
unpinned_ids = [item.id for item in self.session.db[self.name] if not getattr(item, "pinned", False)]
if unpinned_ids:
max_id = min(unpinned_ids)
else:
max_id = min(item.id for item in self.session.db[self.name])
try:
items = self.community_api.timeline(timeline=self.timeline, 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))
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(True, *post)
else:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
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 force == False:
dlg = commonMessageDialogs.remove_buffer()
else:
dlg = widgetUtils.YES
if dlg == widgetUtils.YES:
tl_info = f"{self.timeline}@{self.community_url}"
self.session.settings["other_buffers"]["communities"].remove(tl_info)
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
def get_item_from_instance(self, *args, **kwargs):
item = self.get_item()
try:
results = self.session.api.search(q=item.url, resolve=True, result_type="statuses")
except Exception as e:
log.exception("Error when searching for remote post.")
return None
item = results["statuses"][0]
return item
def reply(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).reply(item=item)
def send_message(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).send_message(item=item)
def share_item(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).share_item(item=item)
def add_to_favorites(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).add_to_favorite(item=item)
def remove_from_favorites(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).remove_from_favorites(item=item)
def toggle_favorite(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).toggle_favorite(item=item)
def toggle_bookmark(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).toggle_bookmark(item=item)
def vote(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).vote(item=item)
def view_item(self, *args, **kwargs):
item = self.get_item_from_instance()
if item != None:
super(CommunityBuffer, self).view_item(item=item)

View File

@@ -1,249 +0,0 @@
# -*- coding: utf-8 -*-
import time
import logging
import wx
import widgetUtils
import output
import config
from mastodon import MastodonNotFoundError
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, 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, settings=self.session.settings, 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
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 loading %s with args of %r and kwargs of %r" % (str(e), self.function, self.args, self.kwargs))
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, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *conversation)
else:
for i in elements:
conversation = self.compose_function(i, self.session.db, self.session.settings, 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:
# Deleted conversations handling.
if i.last_status == None:
continue
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 config.app["app-settings"]["read_long_posts_in_gui"] == True and self.buffer.list.list.HasFocus():
wx.CallLater(40, output.speak, self.get_message(), interrupt=True)
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, language=post.get_language())
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))
try:
self.post = self.session.api.status(id=self.post.id)
except MastodonNotFoundError:
output.speak(_("No status found with that ID"))
return
# 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

@@ -1,133 +0,0 @@
# -*- coding: utf-8 -*-
import time
import logging
import output
from controller.buffers.mastodon.base import BaseBuffer
from sessions.mastodon import utils
log = logging.getLogger("controller.buffers.mastodon.mentions")
class MentionsBuffer(BaseBuffer):
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]["status"]
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
try:
items = getattr(self.session.api, self.function)(min_id=min_id, limit=count, types=["mention"], *self.args, **self.kwargs)
items.reverse()
except Exception as e:
log.exception("Error %s" % (str(e)))
return
# Attempt to remove items with no statuses attached to them as it might happen when blocked accounts have notifications.
items = [item for item in items if item.status != None]
number_of_items = self.session.order_buffer(self.name, items)
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 len(self.session.db[self.name]) == 0:
return
# In mentions buffer, items are notification objects which don't have 'pinned' attribute directly.
# But we check the status attached to the notification if it exists.
# However, notifications are strictly chronological usually. Pinned mentions don't exist?
# But let's stick to the safe ID extraction.
# The logic here is tricky because self.session.db stores notification objects, but sometimes just dicts?
# Let's assume they are objects with 'id' attribute.
# Notifications don't have 'pinned', so we just take the min ID.
# But wait, did I change this file previously to use min()? Yes.
# Is there any case where a notification ID is "pinned" (old)? No.
# So min() should be fine here. But for consistency with other buffers if any weird logic exists...
# Actually, let's keep min() as notifications don't support pinning.
max_id = min(item.id for item in self.session.db[self.name])
try:
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], types=["mention"], *self.args, **self.kwargs)
except Exception as e:
log.exception("Error %s" % (str(e)))
return
# Attempt to remove items with no statuses attached to them as it might happen when blocked accounts have notifications.
items = [item for item in items if item.status != None]
items_db = self.session.db[self.name]
for i in items:
if utils.find_item(i, self.session.db[self.name]) == None:
filter_status = utils.evaluate_filters(post=i, current_context=utils.get_current_context(self.name))
if filter_status == "hide":
continue
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))
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
if self.session.settings["general"]["reverse_timelines"] == False:
for i in elements:
post = self.compose_function(i.status, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(True, *post)
else:
for i in elements:
post = self.compose_function(i.status, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
self.buffer.list.insert_item(False, *post)
self.buffer.list.select_item(selection)
output.speak(_(u"%s items retrieved") % (str(len(elements))), True)
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,))
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
if self.buffer.list.get_count() == 0:
for i in list_to_use:
post = self.compose_function(i.status, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
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.status, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
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.status, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
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):
safe = True
if self.session.settings["general"]["read_preferences_from_instance"]:
safe = self.session.expand_spoilers == False
post = self.compose_function(item.status, self.session.db, self.session.settings, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], safe=safe)
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"])

View File

@@ -1,188 +0,0 @@
# -*- coding: utf-8 -*-
import time
import logging
import arrow
import widgetUtils
import wx
import output
import languageHandler
import config
from pubsub import pub
from controller.buffers.mastodon.base import BaseBuffer
from controller.mastodon import messages
from sessions.mastodon import compose, templates
from wxUI import buffers
from wxUI.dialogs.mastodon import dialogs as mastodon_dialogs
from wxUI.dialogs.mastodon import menus
from mysc.thread_utils import call_threaded
log = logging.getLogger("controller.buffers.mastodon.notifications")
class NotificationsBuffer(BaseBuffer):
def __init__(self, *args, **kwargs):
super(NotificationsBuffer, self).__init__(*args, **kwargs)
self.type = "notificationsBuffer"
def get_message(self):
notification = self.get_item()
if notification == None:
return
template = self.session.settings["templates"]["notification"]
post_template = self.session.settings["templates"]["post"]
t = templates.render_notification(notification, template, post_template, self.session.settings, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"])
return t
def create_buffer(self, parent, name):
self.buffer = buffers.mastodon.notificationsPanel(parent, name)
def onFocus(self, *args, **kwargs):
item = 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(), 1, ts)
if config.app["app-settings"]["read_long_posts_in_gui"] == True and self.buffer.list.list.HasFocus():
wx.CallLater(40, output.speak, self.get_message(), interrupt=True)
def bind_events(self):
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.destroy_status, self.buffer.dismiss)
def vote(self):
pass
def can_share(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
return super(NotificationsBuffer, self).can_share(item=item.status)
return False
def add_to_favorites(self):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).add_to_favorites(item=item.status)
def remove_from_favorites(self):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).remove_from_favorites(item=item.status)
def toggle_favorite(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).toggle_favorite(item=item.status)
def toggle_bookmark(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).toggle_bookmark(item=item.status)
def reply(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).reply(item=item.status)
def share_item(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).share_item(item=item.status)
def url(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).url(item=item.status, *args, **kwargs)
def audio(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).audio(item=item.status)
def view_item(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).view_item(item=item.status)
else:
pub.sendMessage("execute-action", action="user_details")
def open_in_browser(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).open_in_browser(item=item.status)
def send_message(self, *args, **kwargs):
if self.is_post():
item = self.get_item()
super(NotificationsBuffer, self).send_message(item=item.status)
else:
item = self.get_item()
title = _("New conversation with {}").format(item.account.username)
caption = _("Write your message here")
users_str = "@{} ".format(item.account.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", language=post.get_language())
if hasattr(post.message, "destroy"):
post.message.destroy()
def is_post(self):
post_types = ["status", "mention", "reblog", "favourite", "update", "poll"]
item = self.get_item()
if item.type in post_types:
return True
return False
def destroy_status(self, *args, **kwargs):
index = self.buffer.list.get_selected()
item = self.session.db[self.name][index]
answer = mastodon_dialogs.delete_notification_dialog()
if answer == False:
return
items = self.session.db[self.name]
try:
self.session.api.notifications_dismiss(id=item.id)
items.pop(index)
self.buffer.list.remove_item(index)
output.speak(_("Notification dismissed."))
except Exception as e:
self.session.sound.play("error.ogg")
log.exception("")
self.session.db[self.name] = items
def show_menu(self, ev, pos=0, *args, **kwargs):
if self.buffer.list.get_count() == 0:
return
notification = self.get_item()
menu = menus.notification(notification.type)
if self.is_post():
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
# Enable/disable edit based on whether the post belongs to the user
if hasattr(menu, 'edit'):
status = self.get_post()
if status and status.account.id == self.session.db["user_id"] and status.reblog == None:
widgetUtils.connect_event(menu, widgetUtils.MENU, self.edit_status, menuitem=menu.edit)
else:
menu.edit.Enable(False)
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
if self.can_share() == True:
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
else:
menu.boost.Enable(False)
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())

View File

@@ -1,78 +0,0 @@
# -*- coding: utf-8 -*-
"""
Implements searching functionality for mastodon
Used for searching for statuses (posts) or possibly hashtags
"""
import logging
import time
from pubsub import pub
from .base import BaseBuffer
import output
import widgetUtils
from wxUI import commonMessageDialogs
log = logging.getLogger("controller.buffers.mastodon.search")
class SearchBuffer(BaseBuffer):
"""Search buffer
There are some methods of the Base Buffer that can't be used here
"""
def start_stream(self, mandatory: bool=False, play_sound: bool=True, avoid_autoreading: bool=False) -> None:
"""Start streaming
Parameters:
- mandatory [bool]: Force start stream if True
- play_sound [bool]: Specifies whether to play sound after receiving posts
avoid_autoreading [bool]: Reads the posts if set to True
returns [None | int]: Number of posts received
"""
log.debug(f"Starting streamd for buffer {self.name} account {self.account} and type {self.type}")
log.debug(f"Args: {self.args}, Kwargs: {self.kwargs}")
current_time = time.time()
if self.execution_time == 0 or current_time-self.execution_time >= 180 or mandatory==True:
self.execution_time = current_time
min_id = None
if self.name in self.session.db and len(self.session.db[self.name]) > 0:
min_id = max(item.id for item in self.session.db[self.name])
try:
results = getattr(self.session.api, self.function)(min_id=min_id, **self.kwargs)
except Exception as mess:
log.exception(f"Error while receiving search posts {mess}")
return
results = results.statuses
results.reverse()
num_of_items = self.session.order_buffer(self.name, results)
log.debug(f"Number of items retrieved: {num_of_items}")
self.put_items_on_list(num_of_items)
# playsound and autoread
if num_of_items > 0:
if 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)
if avoid_autoreading == False and mandatory == True and self.name in self.session.settings["other_buffers"]["autoread_buffers"]:
self.auto_read(num_of_items)
return num_of_items
def remove_buffer(self, force: bool=False) -> bool:
"""Performs clean-up tasks before removing buffer
Parameters:
- force [bool]: Force removes buffer if true
Returns [bool]: True proceed with removing buffer or False abort
removing buffer
"""
# Ask user
if not force:
response = commonMessageDialogs.remove_buffer()
else:
response = widgetUtils.YES
if response == widgetUtils.NO:
return False
# remove references of this buffer in db and settings
if self.name in self.session.db:
self.session.db.pop(self.name)
if self.kwargs.get('q') in self.session.settings['other_buffers']['post_searches']:
self.session.settings['other_buffers']['post_searches'].remove(self.kwargs['q'])
return True
def get_more_items(self):
output.speak(_(u"This action is not supported for this buffer"), True)

View File

@@ -1,207 +0,0 @@
# -*- coding: utf-8 -*-
import time
import logging
import wx
import widgetUtils
import output
from pubsub import pub
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, settings=self.session.settings, 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"]
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
if "-followers" in self.name:
title=_("Followers for {}").format(self.username)
else:
title=_("Following for {}").format(self.username)
pub.sendMessage("core.change_buffer_title", name=self.session.get_name(), buffer=self.name, title=title)
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, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])
self.buffer.list.insert_item(True, *post)
else:
for i in elements:
post = self.compose_function(i, self.session.db, self.session.settings, 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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,112 +0,0 @@
# -*- coding: utf-8 -*-
import widgetUtils
from wxUI.dialogs.mastodon.filters import create_filter as dialog
from mastodon import MastodonAPIError
class CreateFilterController(object):
def __init__(self, session, filter_data=None):
super(CreateFilterController, self).__init__()
self.session = session
self.filter_data = filter_data
self.dialog = dialog.CreateFilterDialog(parent=None)
if self.filter_data is not None:
self.keywords = self.filter_data.get("keywords")
self.load_filter_data()
else:
self.keywords = []
widgetUtils.connect_event(self.dialog.keyword_panel.add_button, widgetUtils.BUTTON_PRESSED, self.on_add_keyword)
widgetUtils.connect_event(self.dialog.keyword_panel.remove_button, widgetUtils.BUTTON_PRESSED, self.on_remove_keyword)
def on_add_keyword(self, event):
""" Adds a keyword to the list. """
keyword = self.dialog.keyword_panel.keyword_text.GetValue().strip()
whole_word = self.dialog.keyword_panel.whole_word_checkbox.GetValue()
if keyword:
for idx, kw in enumerate(self.keywords):
if kw['keyword'] == keyword:
return
keyword_data = {
'keyword': keyword,
'whole_word': whole_word
}
self.keywords.append(keyword_data)
self.dialog.keyword_panel.add_keyword(keyword, whole_word)
def on_remove_keyword(self, event):
removed = self.dialog.keyword_panel.remove_keyword()
if removed is not None:
self.keywords.pop(removed)
def get_expires_in_seconds(self, selection, value):
if selection == 0:
return None
if selection == 1:
return value * 3600
elif selection == 2:
return value * 86400
elif selection == 3:
return value * 604800
elif selection == 4:
return value * 2592000
return None
def set_expires_in(self, seconds):
if seconds is None:
self.dialog.expiration_choice.SetSelection(0)
self.dialog.expiration_value.Enable(False)
return
if seconds % 2592000 == 0 and seconds >= 2592000:
self.dialog.expiration_choice.SetSelection(4)
self.dialog.expiration_value.SetValue(seconds // 2592000)
elif seconds % 604800 == 0 and seconds >= 604800:
self.dialog.expiration_choice.SetSelection(3)
self.dialog.expiration_value.SetValue(seconds // 604800)
elif seconds % 86400 == 0 and seconds >= 86400:
self.dialog.expiration_choice.SetSelection(2)
self.dialog.expiration_value.SetValue(seconds // 86400)
else:
self.dialog.expiration_choice.SetSelection(1)
self.dialog.expiration_value.SetValue(max(1, seconds // 3600))
self.dialog.expiration_value.Enable(True)
def load_filter_data(self):
if 'title' in self.filter_data:
self.dialog.name_ctrl.SetValue(self.filter_data['title'])
self.dialog.SetTitle(_("Update Filter: {}").format(self.filter_data['title']))
if 'context' in self.filter_data:
for context in self.filter_data['context']:
if context in self.dialog.context_checkboxes:
self.dialog.context_checkboxes[context].SetValue(True)
if 'filter_action' in self.filter_data:
action_index = self.dialog.actions.index(self.filter_data['filter_action']) if self.filter_data['filter_action'] in self.dialog.actions else 0
self.dialog.action_choice.SetSelection(action_index)
if 'expires_in' in self.filter_data:
self.set_expires_in(self.filter_data['expires_in'])
print(self.filter_data)
if 'keywords' in self.filter_data:
self.keywords = self.filter_data['keywords']
self.dialog.keyword_panel.set_keywords(self.filter_data['keywords'])
def get_filter_data(self):
filter_data = {
'title': self.dialog.name_ctrl.GetValue(),
'context': [],
'filter_action': self.dialog.actions[self.dialog.action_choice.GetSelection()],
'expires_in': self.get_expires_in_seconds(selection=self.dialog.expiration_choice.GetSelection(), value=self.dialog.expiration_value.GetValue()),
'keywords_attributes': self.keywords
}
for context, checkbox in self.dialog.context_checkboxes.items():
if checkbox.GetValue():
filter_data['context'].append(context)
return filter_data
def get_response(self):
response = self.dialog.ShowModal()
if response == widgetUtils.OK:
filter_data = self.get_filter_data()
if self.filter_data == None:
result = self.session.api.create_filter_v2(**filter_data)
else:
result = self.session.api.update_filter_v2(filter_id=self.filter_data['id'], **filter_data)
return result
return None

View File

@@ -1,99 +0,0 @@
# -*- coding: utf-8 -*-
import datetime
import wx
import widgetUtils
from wxUI import commonMessageDialogs
from wxUI.dialogs.mastodon.filters import manage_filters as dialog
from . import create_filter
from mastodon import MastodonError
class ManageFiltersController(object):
def __init__(self, session):
super(ManageFiltersController, self).__init__()
self.session = session
self.selected_filter_idx = -1
self.error_loading = False
self.dialog = dialog.ManageFiltersDialog(parent=None)
self.dialog.filter_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_filter_selected)
self.dialog.filter_list.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.on_filter_deselected)
widgetUtils.connect_event(self.dialog.add_button, wx.EVT_BUTTON, self.on_add_filter)
widgetUtils.connect_event(self.dialog.edit_button, wx.EVT_BUTTON, self.on_edit_filter)
widgetUtils.connect_event(self.dialog.remove_button, wx.EVT_BUTTON, self.on_remove_filter)
self.load_filter_data()
def on_filter_selected(self, event):
"""Handle filter selection event."""
self.selected_filter_idx = event.GetIndex()
self.dialog.edit_button.Enable()
self.dialog.remove_button.Enable()
def on_filter_deselected(self, event):
"""Handle filter deselection event."""
self.selected_filter_idx = -1
self.dialog.edit_button.Disable()
self.dialog.remove_button.Disable()
def get_selected_filter_id(self):
"""Get the ID of the currently selected filter."""
if self.selected_filter_idx != -1:
return self.dialog.filter_list.GetItemData(self.selected_filter_idx)
return None
def load_filter_data(self):
try:
filters = self.session.api.filters_v2()
self.dialog.filter_list.DeleteAllItems()
self.on_filter_deselected(None)
for i, filter_obj in enumerate(filters):
index = self.dialog.filter_list.InsertItem(i, filter_obj.title)
keyword_count = len(filter_obj.keywords)
self.dialog.filter_list.SetItem(index, 1, str(keyword_count))
contexts = ", ".join(filter_obj.context)
self.dialog.filter_list.SetItem(index, 2, contexts)
self.dialog.filter_list.SetItem(index, 3, filter_obj.filter_action)
if filter_obj.expires_at:
expiry_str = filter_obj.expires_at.strftime("%Y-%m-%d %H:%M")
else:
expiry_str = _("Never")
self.dialog.filter_list.SetItem(index, 4, expiry_str)
self.dialog.filter_list.SetItemData(index, int(filter_obj.id) if isinstance(filter_obj.id, (int, str)) else 0)
except MastodonError as e:
commonMessageDialogs.error_loading_filters()
self.error_loading = True
def on_add_filter(self, *args, **kwargs):
filterController = create_filter.CreateFilterController(self.session)
try:
filter = filterController.get_response()
self.load_filter_data()
except MastodonError as error:
commonMessageDialogs.error_adding_filter()
return self.on_add_filter()
def on_edit_filter(self, *args, **kwargs):
filter_id = self.get_selected_filter_id()
if filter_id == None:
return
try:
filter_data = self.session.api.filter_v2(filter_id)
filterController = create_filter.CreateFilterController(self.session, filter_data=filter_data)
filterController.get_response()
self.load_filter_data()
except MastodonError as error:
commonMessageDialogs.error_adding_filter()
def on_remove_filter(self, *args, **kwargs):
filter_id = self.get_selected_filter_id()
if filter_id == None:
return
dlg = commonMessageDialogs.remove_filter()
if dlg == widgetUtils.NO:
return
try:
self.session.api.delete_filter_v2(filter_id)
self.load_filter_data()
except MastodonError as error:
commonMessageDialogs.error_removing_filter()
def get_response(self):
return self.dialog.ShowModal() == wx.ID_OK

View File

@@ -1,425 +0,0 @@
# -*- coding: utf-8 -*-
import wx
import logging
import mastodon
import output
from mastodon import MastodonError
from pubsub import pub
from mysc import restart
from mysc.thread_utils import call_threaded
from wxUI.dialogs.mastodon import search as search_dialogs
from wxUI.dialogs.mastodon import dialogs
from wxUI.dialogs import userAliasDialogs
from wxUI import commonMessageDialogs
from wxUI.dialogs.mastodon import updateProfile as update_profile_dialogs
from wxUI.dialogs.mastodon import showUserProfile, communityTimeline
from sessions.mastodon.utils import html_filter
from . import userActions, settings
from .filters import create_filter, manage_filters
log = logging.getLogger("controller.mastodon.handler")
class Handler(object):
def __init__(self):
super(Handler, self).__init__()
# Structure to hold names for menu bar items.
# empty names mean the item will be Disabled.
self.menus = dict(
# In application menu.
updateProfile=_("Update Profile"),
menuitem_search=_("&Search"),
lists=None,
manageAliases=_("Manage user aliases"),
# In item menu.
compose=_("&Post"),
reply=_("Re&ply"),
share=_("&Boost"),
quote=_("&Quote"),
fav=_("&Add to favorites"),
unfav=_("Remove from favorites"),
view=_("&Show post"),
view_conversation=_("View conversa&tion"),
ocr=_("Read text in picture"),
delete=_("&Delete"),
# In user menu.
follow=_("&Actions..."),
timeline=_("&View timeline..."),
dm=_("Direct me&ssage"),
addAlias=_("Add a&lias"),
addToList=None,
removeFromList=None,
details=_("S&how user profile"),
favs=None,
# In buffer Menu.
community_timeline =_("Create c&ommunity timeline"),
filter=_("Create a &filter"),
manage_filters=_("&Manage filters")
)
# Name for the "tweet" menu in the menu bar.
self.item_menu = _("&Post")
def create_buffers(self, session, createAccounts=True, controller=None):
session.get_user_info()
name = session.get_name()
controller.accounts.append(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))
elif i == 'notifications':
pub.sendMessage("createBuffer", buffer_type="NotificationsBuffer", session_type=session.type, buffer_title=_("Notifications"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_notification", function="notifications", name="notifications", sessionObject=session, account=name))
elif i == 'announcements':
pub.sendMessage("createBuffer", buffer_type="AnnouncementsBuffer", session_type=session.type, buffer_title=_("Announcements"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="announcements", name="announcements", sessionObject=session, account=name, sound="new_event.ogg"))
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=_("Timeline for {}").format(i), parent_tab=timelines_position, start=False, kwargs=dict(parent=controller.view.nb, function="account_statuses", name="{}-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="{}-followers".format(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="{}-following".format(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", name)
for term in session.settings["other_buffers"]["post_searches"]:
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, compose_func="compose_post", function="search", name="%s-searchterm" % (term,), sessionObject=session, account=session.get_name(), sound="search_updated.ogg", q=term, result_type="statuses"))
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Communities"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="communities", account=name))
communities_position =controller.view.search("communities", name)
for community in session.settings["other_buffers"]["communities"]:
bufftype = _("Local") if community.split("@")[0] == "local" else _("federated")
community_name = community.split("@")[1].replace("https://", "")
title = _(f"{bufftype} timeline for {community_name}")
pub.sendMessage("createBuffer", buffer_type="CommunityBuffer", session_type=session.type, buffer_title=title, parent_tab=communities_position, start=True, kwargs=dict(parent=controller.view.nb, function="timeline", compose_func="compose_post", name=community, sessionObject=session, community_url=community.split("@")[1], account=session.get_name(), sound="search_updated.ogg", timeline=community.split("@")[0]))
# 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):
# detect if we are in a community buffer.
# Community buffers are special because we'll need to retrieve the object locally at first.
if hasattr(buffer, "community_url"):
post = buffer.get_item_from_instance()
else:
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
# Community buffers are special because we'll need to retrieve the object locally at first.
if hasattr(buffer, "community_url"):
item = buffer.get_item_from_instance()
else:
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)
elif buffer.type == "notificationsBuffer":
if buffer.is_post():
status = item.status
if status.reblog != None:
users = [user.acct for user in status.reblog.mentions if user.id != buffer.session.db["user_id"]]
if status.reblog.account.acct not in users and status.account.id != buffer.session.db["user_id"]:
users.insert(0, status.reblog.account.acct)
else:
users = [user.acct for user in status.mentions if user.id != buffer.session.db["user_id"]]
if hasattr(item, "account"):
acct = item.account.acct
else:
acct = item.acct
if 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, compose_func="compose_post", function="search", name="%s-searchterm" % (term,), sessionObject=session, account=session.get_name(), sound="search_updated.ogg", q=term, result_type="statuses"))
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
if hasattr(buffer, "community_url"):
item = buffer.get_item_from_instance()
else:
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":
self.openPostTimeline(controller, buffer, user)
elif action == "followers":
self.openFollowersTimeline(controller, buffer, user)
elif action == "following":
self.openFollowingTimeline(controller, buffer, user)
def openPostTimeline(self, controller, buffer, user):
"""Opens post timeline for user"""
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")
buffer.session.settings.write()
def openFollowersTimeline(self, controller, buffer, user):
"""Open followers timeline for user"""
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")
buffer.session.settings.write()
def openFollowingTimeline(self, controller, buffer, user):
"""Open following timeline for user"""
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()
def account_settings(self, buffer, controller):
d = settings.accountSettingsController(buffer, controller)
if d.response == wx.ID_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 add_alias(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)
dlg = userAliasDialogs.addAliasDialog(_("Add an user alias"), users)
if dlg.get_response() == wx.ID_OK:
user, alias = dlg.get_user()
if user == "" or alias == "":
return
try:
full_user = buffer.session.api.account_lookup(user)
except Exception as e:
log.exception("Error adding alias to user {}.".format(user))
return
buffer.session.settings["user-aliases"][str(full_user.id)] = alias
buffer.session.settings.write()
output.speak(_("Alias has been set correctly for {}.").format(user))
pub.sendMessage("alias-added")
def update_profile(self, session):
"""Updates the users dialog"""
profile = session.api.me()
data = {
'display_name': profile.display_name,
'note': html_filter(profile.note),
'header': profile.header,
'avatar': profile.avatar,
'fields': [(field.name, html_filter(field.value)) for field in profile.fields],
'locked': profile.locked,
'bot': profile.bot,
# discoverable could be None, set it to False
'discoverable': profile.discoverable if profile.discoverable else False,
}
log.debug(f"Received data_ {data['fields']}")
dialog = update_profile_dialogs.UpdateProfileDialog(**data)
if dialog.ShowModal() != wx.ID_OK:
log.debug("User canceled dialog")
return
updated_data = dialog.data
if updated_data == data:
log.debug("No profile info was changed.")
return
# remove data that hasn't been updated
for key in data:
if data[key] == updated_data[key]:
del updated_data[key]
log.debug(f"Updating users profile with: {updated_data}")
call_threaded(session.api_call, "account_update_credentials", _("Update profile"), report_success=True, **updated_data)
def user_details(self, buffer):
"""Displays user profile in a dialog.
This works as long as the focused item hass a 'account' key."""
if not hasattr(buffer, 'get_item'):
return # Tell user?
item = buffer.get_item()
if not item:
return # empty buffer
log.debug(f"Opening user profile. dictionary: {item}")
mentionedUsers = list()
holdUser = item.account if item.get('account') else None
if hasattr(item, "type") and item.type in ["status", "mention", "reblog", "favourite", "update", "poll"]: # statuses in Notification buffers
item = item.status
if item.get('username'): # account dict
holdUser = item
elif isinstance(item.get('mentions'), list):
# mentions in statuses
if item.reblog:
item = item.reblog
mentionedUsers = [(user.acct, user.id) for user in item.mentions]
holdUser = item.account
if not holdUser:
dialogs.no_user()
return
if len(mentionedUsers) == 0:
user = holdUser
else:
mentionedUsers.insert(0, (holdUser.display_name, holdUser.username, holdUser.id))
mentionedUsers = list(set(mentionedUsers))
selectedUser = showUserProfile.selectUserDialog(mentionedUsers)
if not selectedUser:
return # Canceled selection
elif selectedUser[-1] == holdUser.id:
user = holdUser
else: # We don't have this user's dictionary, get it!
user = buffer.session.api.account(selectedUser[-1])
dlg = showUserProfile.ShowUserProfile(user)
dlg.ShowModal()
def community_timeline(self, controller, buffer):
dlg = communityTimeline.CommunityTimeline()
if dlg.ShowModal() != wx.ID_OK:
return
url = dlg.url.GetValue()
bufftype = dlg.get_action()
local_api = mastodon.Mastodon(api_base_url=url)
try:
instance = local_api.instance()
except MastodonError:
commonMessageDialogs.invalid_instance()
return
if bufftype == "local":
title = _(f"Local timeline for {url.replace('https://', '')}")
else:
title = _(f"Federated timeline for {url}")
bufftype = "public"
dlg.Destroy()
tl_info = f"{bufftype}@{url}"
if tl_info in buffer.session.settings["other_buffers"]["communities"]:
return # buffer already exists.
buffer.session.settings["other_buffers"]["communities"].append(tl_info)
buffer.session.settings.write()
communities_position =controller.view.search("communities", buffer.session.get_name())
pub.sendMessage("createBuffer", buffer_type="CommunityBuffer", session_type=buffer.session.type, buffer_title=title, parent_tab=communities_position, start=True, kwargs=dict(parent=controller.view.nb, function="timeline", name=tl_info, sessionObject=buffer.session, account=buffer.session.get_name(), sound="tweet_timeline.ogg", community_url=url, timeline=bufftype))
def create_filter(self, controller, buffer):
filterController = create_filter.CreateFilterController(buffer.session)
try:
filter = filterController.get_response()
except MastodonError as error:
log.exception("Error adding filter.")
commonMessageDialogs.error_adding_filter()
return self.create_filter(controller=controller, buffer=buffer)
def manage_filters(self, controller, buffer):
manageFiltersController = manage_filters.ManageFiltersController(buffer.session)
manageFiltersController.get_response()

View File

@@ -1,459 +0,0 @@
# -*- coding: utf-8 -*-
import os
import re
import wx
import logging
import widgetUtils
import config
import output
import languageHandler
from twitter_text import parse_tweet, config
from mastodon import MastodonError
from controller import messages
from sessions.mastodon import templates, utils
from wxUI.dialogs.mastodon import postDialogs
from extra.autocompletionUsers import completion
from . import userList
log = logging.getLogger("controller.mastodon.messages")
def character_count(post_text, post_cw, character_limit=500):
# We will use text for counting character limit only.
full_text = post_text+post_cw
# find remote users as Mastodon doesn't count the domain in char limit.
users = re.findall("@[\w\.-]+@[\w\.-]+", full_text)
for user in users:
domain = user.split("@")[-1]
full_text = full_text.replace("@"+domain, "")
options = config.config.get("defaults")
options.update(max_weighted_tweet_length=character_limit, default_weight=100)
parsed = parse_tweet(full_text, options=options)
return parsed.weightedLength
class post(messages.basicMessage):
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
langs = self.session.supported_languages
display_langs = [l.name for l in langs]
self.message = postDialogs.Post(caption=caption, text=text, languages=display_langs, *args, **kwargs)
self.message.SetTitle(title)
self.message.text.SetInsertionPoint(len(self.message.text.GetValue()))
self.set_language(self.session.default_language)
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.spoiler, 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)
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 autocomplete_users(self, *args, **kwargs):
c = completion.autocompletionUsers(self.message, self.session.session_id)
c.show_menu()
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())
# Check for scheduled post
if hasattr(self.message, 'get_scheduled_at'):
scheduled_at = self.message.get_scheduled_at()
if scheduled_at:
postdata['scheduled_at'] = scheduled_at
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 set_language(self, language=None):
""" Attempt to set the default language for a post. """
# language can be provided in a post (replying or recovering from errors).
# Also it can be provided in user preferences (retrieved in the session).
# If no language is provided, let's fallback to TWBlue's user language.
if language != None:
language_code = language
else:
# Let's cut langcode_VARIANT to ISO-639 two letter code only.
language_code = languageHandler.curLang[:2]
for lang in self.session.supported_languages:
if lang.code == language_code:
self.message.language.SetStringSelection(lang.name)
def set_post_data(self, visibility, data, language):
if len(data) == 0:
return
if len(data) > 1:
self.thread = data[:-1]
for p in self.thread:
self.message.add_item(item=[p.get("text") or "", len(p.get("attachments") or [])], list_type="post")
post = data[-1]
self.attachments = post.get("attachments") or []
self.message.text.SetValue(post.get("text") or "")
self.message.sensitive.SetValue(post.get("sensitive") or False)
self.message.spoiler.SetValue(post.get("spoiler_text") or "")
visibility_settings = dict(public=0, unlisted=1, private=2, direct=3)
self.message.visibility.SetSelection(visibility_settings.get(visibility))
self.message.on_sensitivity_changed()
for attachment in self.attachments:
self.message.add_item(item=[attachment["file"], attachment["type"], attachment["description"]])
self.set_language(language)
self.text_processor()
def text_processor(self, *args, **kwargs):
text = self.message.text.GetValue()
cw = self.message.spoiler.GetValue()
results = character_count(text, cw, character_limit=self.max)
self.message.SetTitle(_("%s - %s of %d characters") % (self.title, results, self.max))
if results > self.max:
self.session.sound.play("max_length.ogg")
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, description = self.message.get_video()
if video != None:
videoInfo = {"type": "video", "file": video, "description": 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, description = self.message.get_audio()
if audio != None:
audioInfo = {"type": "audio", "file": audio, "description": 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()]
def get_language(self):
langs = self.session.supported_languages
lang = self.message.language.GetSelection()
if lang >= 0:
return langs[lang].code
return None
def set_visibility(self, setting):
visibility_settings = ["public", "unlisted", "private", "direct"]
visibility_setting = visibility_settings.index(setting)
self.message.visibility.SetSelection(setting)
class editPost(post):
def __init__(self, session, item, title, caption, *args, **kwargs):
""" Initialize edit dialog with existing post data.
Note: Per Mastodon API, visibility and language cannot be changed when editing.
These fields will be displayed but disabled in the UI.
"""
# Extract text from post
if item.reblog != None:
item = item.reblog
text = utils.html_filter(item.content)
# Initialize parent class
super(editPost, self).__init__(session, title, caption, text=text, *args, **kwargs)
# Store the post ID for editing
self.post_id = item.id
# Set visibility (read-only, cannot be changed)
visibility_settings = dict(public=0, unlisted=1, private=2, direct=3)
self.message.visibility.SetSelection(visibility_settings.get(item.visibility, 0))
self.message.visibility.Enable(False) # Disable as it cannot be edited
# Set language (read-only, cannot be changed)
if item.language:
self.set_language(item.language)
self.message.language.Enable(False) # Disable as it cannot be edited
# Set sensitive content and spoiler
if item.sensitive:
self.message.sensitive.SetValue(True)
if item.spoiler_text:
self.message.spoiler.ChangeValue(item.spoiler_text)
self.message.on_sensitivity_changed()
# Load existing poll (if any)
# Note: You cannot have both media and a poll, so check poll first
if hasattr(item, 'poll') and item.poll is not None:
log.debug("Loading existing poll for post {}".format(self.post_id))
poll = item.poll
# Extract poll options (just the text, not the votes)
poll_options = [option.title for option in poll.options]
# Calculate expires_in based on current time and expires_at
# For editing, we need to provide a new expiration time
# Since we can't get the original expires_in, use a default or let user configure
# For now, use 1 day (86400 seconds) as default
expires_in = 86400
if hasattr(poll, 'expires_at') and poll.expires_at and not poll.expired:
# Calculate remaining time if poll hasn't expired
from dateutil import parser as date_parser
import datetime
try:
expires_at = poll.expires_at
if isinstance(expires_at, str):
expires_at = date_parser.parse(expires_at)
now = datetime.datetime.now(datetime.timezone.utc)
remaining = (expires_at - now).total_seconds()
if remaining > 0:
expires_in = int(remaining)
except Exception as e:
log.warning("Could not calculate poll expiration: {}".format(e))
poll_info = {
"type": "poll",
"file": "",
"description": _("Poll with {} options").format(len(poll_options)),
"options": poll_options,
"expires_in": expires_in,
"multiple": poll.multiple if hasattr(poll, 'multiple') else False,
"hide_totals": poll.voters_count == 0 if hasattr(poll, 'voters_count') else False
}
self.attachments.append(poll_info)
self.message.add_item(item=[poll_info["file"], poll_info["type"], poll_info["description"]])
log.debug("Loaded poll with {} options. WARNING: Editing will reset all votes!".format(len(poll_options)))
# Load existing media attachments (only if no poll)
elif hasattr(item, 'media_attachments'):
log.debug("Loading existing media attachments for post {}".format(self.post_id))
log.debug("Item has media_attachments attribute, count: {}".format(len(item.media_attachments)))
if len(item.media_attachments) > 0:
for media in item.media_attachments:
log.debug("Processing media: id={}, type={}, url={}".format(media.id, media.type, media.url))
media_info = {
"id": media.id, # Keep the existing media ID
"type": media.type,
"file": media.url, # URL of existing media
"description": media.description or ""
}
# Include focus point if available
if hasattr(media, 'meta') and media.meta and 'focus' in media.meta:
focus = media.meta['focus']
media_info["focus"] = (focus.get('x'), focus.get('y'))
log.debug("Added focus point: {}".format(media_info["focus"]))
self.attachments.append(media_info)
# Display in the attachment list
display_name = media.url.split('/')[-1]
log.debug("Adding item to UI: name={}, type={}, desc={}".format(display_name, media.type, media.description or ""))
self.message.add_item(item=[display_name, media.type, media.description or ""])
log.debug("Total attachments loaded: {}".format(len(self.attachments)))
else:
log.debug("media_attachments list is empty")
else:
log.debug("Item has no poll or media attachments")
# Update text processor to reflect the loaded content
self.text_processor()
class viewPost(post):
def __init__(self, session, post, offset_hours=0, date="", item_url=""):
self.session = session
if post.reblog != None:
post = post.reblog
self.post_id = post.id
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)
participants = [post.account.id] + [account.id for account in post.mentions]
if self.session.db["user_id"] in participants:
self.message.mute.Enable(True)
if post.muted:
self.message.mute.SetLabel(_("Unmute conversation"))
widgetUtils.connect_event(self.message.mute, widgetUtils.BUTTON_PRESSED, self.mute_unmute)
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)
widgetUtils.connect_event(self.message.boosts_button, widgetUtils.BUTTON_PRESSED, self.on_boosts)
widgetUtils.connect_event(self.message.favorites_button, widgetUtils.BUTTON_PRESSED, self.on_favorites)
self.message.ShowModal()
# We won't need text_processor in this dialog, so let's avoid it.
def text_processor(self):
pass
def mute_unmute(self, *args, **kwargs):
post = self.session.api.status(self.post_id)
if post.muted == True:
action = "status_unmute"
new_label = _("Mute conversation")
msg = _("Conversation unmuted.")
else:
action = "status_mute"
new_label = _("Unmute conversation")
msg = _("Conversation muted.")
try:
getattr(self.session.api, action)(self.post_id)
self.message.mute.SetLabel(new_label)
output.speak(msg)
except MastodonError:
return
def on_boosts(self, *args, **kwargs):
users = self.session.api.status_reblogged_by(self.post_id)
title = _("people who boosted this post")
user_list = userList.MastodonUserList(session=self.session, users=users, title=title)
def on_favorites(self, *args, **kwargs):
users = self.session.api.status_favourited_by(self.post_id)
title = _("people who favorited this post")
user_list = userList.MastodonUserList(session=self.session, users=users, title=title)
def share(self, *args, **kwargs):
if hasattr(self, "item_url"):
output.copy(self.item_url)
output.speak(_("Link copied to clipboard."))
class text(messages.basicMessage):
def __init__(self, title, text="", *args, **kwargs):
self.title = title
self.message = postDialogs.viewText(title=title, text=text, *args, **kwargs)
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.translateButton, widgetUtils.BUTTON_PRESSED, self.translate)

View File

@@ -1,236 +0,0 @@
# -*- 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 wxUI.dialogs.mastodon import configuration
from extra.autocompletionUsers import manage
from extra.autocompletionUsers.mastodon import scan
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
self.dialog = configuration.configurationDialog()
self.create_config()
self.needs_restart = False
self.is_started = True
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", "disable_streaming", self.config["general"]["disable_streaming"])
self.dialog.set_value("general", "relative_time", self.config["general"]["relative_times"])
self.dialog.set_value("general", "read_preferences_from_instance", self.config["general"]["read_preferences_from_instance"])
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_posts_per_call"])
self.dialog.set_value("general", "reverse_timelines", self.config["general"]["reverse_timelines"])
boost_mode = self.config["general"]["boost_mode"]
if boost_mode == "ask":
self.dialog.set_value("general", "ask_before_boost", True)
else:
self.dialog.set_value("general", "ask_before_boost", False)
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"])
post_template = self.config["templates"]["post"]
conversation_template = self.config["templates"]["conversation"]
person_template = self.config["templates"]["person"]
announcement_template = self.config.get("templates", {}).get("announcement", "$text. Published $published_at. $read")
self.dialog.create_templates(post_template=post_template, conversation_template=conversation_template, person_template=person_template, announcement_template=announcement_template)
widgetUtils.connect_event(self.dialog.templates.post, widgetUtils.BUTTON_PRESSED, self.edit_post_template)
widgetUtils.connect_event(self.dialog.templates.conversation, widgetUtils.BUTTON_PRESSED, self.edit_conversation_template)
widgetUtils.connect_event(self.dialog.templates.person, widgetUtils.BUTTON_PRESSED, self.edit_person_template)
widgetUtils.connect_event(self.dialog.templates.announcement, widgetUtils.BUTTON_PRESSED, self.edit_announcement_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.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_img", self.config["sound"]["indicate_img"])
self.dialog.create_extras(OCRSpace.translatable_langs)
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(_("Account settings for %s") % (self.user,))
self.response = self.dialog.get_response()
def edit_post_template(self, *args, **kwargs):
template = self.config["templates"]["post"]
control = EditTemplate(template=template, type="post")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["post"] = result
self.config.write()
self.dialog.templates.post.SetLabel(_("Edit template for posts. Current template: {}").format(result))
def edit_conversation_template(self, *args, **kwargs):
template = self.config["templates"]["conversation"]
control = EditTemplate(template=template, type="conversation")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["conversation"] = result
self.config.write()
self.dialog.templates.conversation.SetLabel(_("Edit template for conversations. 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 edit_announcement_template(self, *args, **kwargs):
template = self.config.get("templates", {}).get("announcement", "$text. Published $published_at. $read")
control = EditTemplate(template=template, type="announcement")
result = control.run_dialog()
if result != "": # Template has been saved.
self.config["templates"]["announcement"] = result
self.config.write()
self.dialog.templates.announcement.SetLabel(_("Edit template for announcements. 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")
if self.config["general"]["disable_streaming"] != self.dialog.get_value("general", "disable_streaming"):
self.needs_restart = True
log.debug("Triggered app restart due to change in streaming settings.")
self.config["general"]["disable_streaming"] = self.dialog.get_value("general", "disable_streaming")
self.config["general"]["read_preferences_from_instance"] = self.dialog.get_value("general", "read_preferences_from_instance")
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_posts_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")
ask_before_boost = self.dialog.get_value("general", "ask_before_boost")
if ask_before_boost == True:
self.config["general"]["boost_mode"] = "ask"
else:
self.config["general"]["boost_mode"] = "direct"
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_img"] = self.dialog.get_value("sound", "indicate_img")
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 get_buffers_list(self):
all_buffers=OrderedDict()
all_buffers['home']=_("Home")
all_buffers['local'] = _("Local")
all_buffers['federated'] = _("Federated")
all_buffers['mentions']=_("Mentions")
all_buffers['direct_messages']=_("Direct Messages")
all_buffers['sent']=_("Sent")
all_buffers['favorites']=_("Favorites")
all_buffers['bookmarks']=_("Bookmarks")
all_buffers['followers']=_("Followers")
all_buffers['following']=_("Following")
all_buffers['blocked']=_("Blocked users")
all_buffers['muted']=_("Muted users")
all_buffers['notifications']=_("Notifications")
all_buffers['announcements']=_("Announcements")
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

@@ -1,42 +0,0 @@
# -*- coding: utf-8 -*-
import re
import wx
from typing import List
from sessions.mastodon.templates import post_variables, conversation_variables, person_variables, announcement_variables
from wxUI.dialogs import templateDialogs
class EditTemplate(object):
def __init__(self, template: str, type: str) -> None:
super(EditTemplate, self).__init__()
self.default_template = template
if type == "post":
self.variables = post_variables
elif type == "conversation":
self.variables = conversation_variables
elif type == "announcement":
self.variables = announcement_variables
else:
self.variables = person_variables
self.template: str = template
def validate_template(self, template: str) -> bool:
used_variables: List[str] = re.findall("\$\w+", template)
validated: bool = True
for var in used_variables:
if var[1:] not in self.variables:
validated = False
return validated
def run_dialog(self) -> str:
dialog = templateDialogs.EditTemplateDialog(template=self.template, variables=self.variables, default_template=self.default_template)
response = dialog.ShowModal()
if response == wx.ID_SAVE:
validated: bool = self.validate_template(dialog.template.GetValue())
if validated == False:
templateDialogs.invalid_template()
self.template = dialog.template.GetValue()
return self.run_dialog()
else:
return dialog.template.GetValue()
else:
return ""

View File

@@ -1,101 +0,0 @@
# -*- 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 +0,0 @@
# -*- coding: utf-8 -*-
from mastodon import MastodonError
from wxUI.dialogs.mastodon import showUserProfile
from controller.userList import UserListController
from . import userActions
class MastodonUserList(UserListController):
def process_users(self, users):
return [dict(id=user.id, display_name=f"{user.display_name} (@{user.acct})", acct=user.acct) for user in users]
def on_actions(self, *args, **kwargs):
user = self.dialog.user_list.GetSelection()
user_account = self.users[user]
u = userActions.userActions(self.session, [user_account.get("acct")])
def on_details(self, *args, **kwargs):
user = self.dialog.user_list.GetSelection()
user_id = self.users[user].get("id")
try:
user_object = self.session.api.account(user_id)
except MastodonError:
return
dlg = showUserProfile.ShowUserProfile(user_object)
dlg.ShowModal()

View File

@@ -1,37 +0,0 @@
# -*- coding: utf-8 -*-
import widgetUtils
import output
import config
from extra import SpellChecker
from extra.translator import TranslatorController
class basicMessage(object):
def translate(self, event=None):
t = TranslatorController(self.message.text.GetValue())
if t.response == False:
return
msg = t.translate()
self.message.text.ChangeValue(msg)
self.message.text.SetInsertionPoint(len(self.message.text.GetValue()))
self.text_processor()
self.message.text.SetFocus()
output.speak(_(u"Translated"))
def text_processor(self, *args, **kwargs):
pass
def spellcheck(self, event=None):
text = self.message.text.GetValue()
checker = SpellChecker.spellchecker.spellChecker(text, "")
if hasattr(checker, "fixed_text"):
self.message.text.ChangeValue(checker.fixed_text)
self.text_processor()
self.message.text.SetFocus()
def remove_attachment(self, *args, **kwargs):
attachment = self.message.attachments.GetFocusedItem()
if attachment > -1 and len(self.attachments) > attachment:
self.attachments.pop(attachment)
self.message.remove_item(list_type="attachment")
self.text_processor()
self.message.text.SetFocus()

View File

@@ -1,119 +0,0 @@
# -*- coding: utf-8 -*-
import os
import logging
import paths
import config
import languageHandler
import application
from pubsub import pub
from wxUI.dialogs import configuration
from wxUI import commonMessageDialogs
log = logging.getLogger("Settings")
class globalSettingsController(object):
def __init__(self):
super(globalSettingsController, self).__init__()
self.dialog = configuration.configurationDialog()
self.create_config()
self.needs_restart = False
self.is_started = True
def make_kmmap(self):
res={}
for i in os.listdir(os.path.join(paths.app_path(), 'keymaps')):
if ".keymap" not in i:
continue
try:
res[i[:-7]] =i
except:
log.exception("Exception while loading keymap " + i)
return res
def create_config(self):
self.kmmap=self.make_kmmap()
self.langs = languageHandler.getAvailableLanguages()
langs = []
[langs.append(i[1]) for i in self.langs]
self.codes = []
[self.codes.append(i[0]) for i in self.langs]
id = self.codes.index(config.app["app-settings"]["language"])
self.kmfriendlies=[]
self.kmnames=[]
for k,v in list(self.kmmap.items()):
self.kmfriendlies.append(k)
self.kmnames.append(v)
self.kmid=self.kmnames.index(config.app['app-settings']['load_keymap'])
self.dialog.create_general(langs,self.kmfriendlies)
self.dialog.general.language.SetSelection(id)
self.dialog.general.km.SetSelection(self.kmid)
self.dialog.set_value("general", "ask_at_exit", config.app["app-settings"]["ask_at_exit"])
self.dialog.set_value("general", "no_streaming", config.app["app-settings"]["no_streaming"])
self.dialog.set_value("general", "play_ready_sound", config.app["app-settings"]["play_ready_sound"])
self.dialog.set_value("general", "speak_ready_msg", config.app["app-settings"]["speak_ready_msg"])
self.dialog.set_value("general", "read_long_posts_in_gui", config.app["app-settings"]["read_long_posts_in_gui"])
self.dialog.set_value("general", "use_invisible_shorcuts", config.app["app-settings"]["use_invisible_keyboard_shorcuts"])
self.dialog.set_value("general", "disable_sapi5", config.app["app-settings"]["voice_enabled"])
self.dialog.set_value("general", "hide_gui", config.app["app-settings"]["hide_gui"])
self.dialog.set_value("general", "update_period", config.app["app-settings"]["update_period"])
self.dialog.set_value("general", "check_for_updates", config.app["app-settings"]["check_for_updates"])
proxyTypes = [_("System default"), _("HTTP"), _("SOCKS v4"), _("SOCKS v4 with DNS support"), _("SOCKS v5"), _("SOCKS v5 with DNS support")]
self.dialog.create_proxy(proxyTypes)
try:
self.dialog.proxy.type.SetSelection(config.app["proxy"]["type"])
except:
self.dialog.proxy.type.SetSelection(0)
self.dialog.set_value("proxy", "server", config.app["proxy"]["server"])
self.dialog.set_value("proxy", "port", config.app["proxy"]["port"])
self.dialog.set_value("proxy", "user", config.app["proxy"]["user"])
self.dialog.set_value("proxy", "password", config.app["proxy"]["password"])
self.dialog.create_translator_panel()
self.dialog.set_value("translator_panel", "libre_api_url", config.app["translator"]["lt_api_url"])
self.dialog.set_value("translator_panel", "libre_api_key", config.app["translator"]["lt_api_key"])
self.dialog.set_value("translator_panel", "deepL_api_key", config.app["translator"]["deepl_api_key"])
self.dialog.realize()
self.response = self.dialog.get_response()
def save_configuration(self):
if self.codes[self.dialog.general.language.GetSelection()] != config.app["app-settings"]["language"]:
config.app["app-settings"]["language"] = self.codes[self.dialog.general.language.GetSelection()]
languageHandler.setLanguage(config.app["app-settings"]["language"])
self.needs_restart = True
log.debug("Triggered app restart due to interface language changes.")
if self.kmnames[self.dialog.general.km.GetSelection()] != config.app["app-settings"]["load_keymap"]:
config.app["app-settings"]["load_keymap"] =self.kmnames[self.dialog.general.km.GetSelection()]
kmFile = open(os.path.join(paths.config_path(), "keymap.keymap"), "w")
kmFile.close()
log.debug("Triggered app restart due to a keymap change.")
self.needs_restart = True
if config.app["app-settings"]["use_invisible_keyboard_shorcuts"] != self.dialog.get_value("general", "use_invisible_shorcuts"):
config.app["app-settings"]["use_invisible_keyboard_shorcuts"] = self.dialog.get_value("general", "use_invisible_shorcuts")
pub.sendMessage("invisible-shorcuts-changed", registered=self.dialog.get_value("general", "use_invisible_shorcuts"))
if config.app["app-settings"]["no_streaming"] != self.dialog.get_value("general", "no_streaming"):
config.app["app-settings"]["no_streaming"] = self.dialog.get_value("general", "no_streaming")
self.needs_restart = True
log.debug("Triggered app restart due to change in streaming availability.")
if config.app["app-settings"]["update_period"] != self.dialog.get_value("general", "update_period"):
config.app["app-settings"]["update_period"] = self.dialog.get_value("general", "update_period")
self.needs_restart = True
log.debug("Triggered app restart due to changes in update period.")
config.app["app-settings"]["voice_enabled"] = self.dialog.get_value("general", "disable_sapi5")
config.app["app-settings"]["hide_gui"] = self.dialog.get_value("general", "hide_gui")
config.app["app-settings"]["ask_at_exit"] = self.dialog.get_value("general", "ask_at_exit")
config.app["app-settings"]["read_long_posts_in_gui"] = self.dialog.get_value("general", "read_long_posts_in_gui")
config.app["app-settings"]["play_ready_sound"] = self.dialog.get_value("general", "play_ready_sound")
config.app["app-settings"]["speak_ready_msg"] = self.dialog.get_value("general", "speak_ready_msg")
config.app["app-settings"]["check_for_updates"] = self.dialog.get_value("general", "check_for_updates")
if config.app["proxy"]["type"]!=self.dialog.get_value("proxy", "type") or config.app["proxy"]["server"] != self.dialog.get_value("proxy", "server") or config.app["proxy"]["port"] != self.dialog.get_value("proxy", "port") or config.app["proxy"]["user"] != self.dialog.get_value("proxy", "user") or config.app["proxy"]["password"] != self.dialog.get_value("proxy", "password"):
if self.is_started == True:
self.needs_restart = True
log.debug("Triggered app restart due to change in proxy settings.")
config.app["proxy"]["type"] = self.dialog.proxy.type.Selection
config.app["proxy"]["server"] = self.dialog.get_value("proxy", "server")
config.app["proxy"]["port"] = self.dialog.get_value("proxy", "port")
config.app["proxy"]["user"] = self.dialog.get_value("proxy", "user")
config.app["proxy"]["password"] = self.dialog.get_value("proxy", "password")
config.app["translator"]["lt_api_url"] = self.dialog.get_value("translator_panel", "libre_api_url")
config.app["translator"]["lt_api_key"] = self.dialog.get_value("translator_panel", "libre_api_key")
config.app["translator"]["deepl_api_key"] = self.dialog.get_value("translator_panel", "deepL_api_key")
config.app.write()

View File

@@ -1,52 +0,0 @@
# -*- coding: utf-8 -*-
import widgetUtils
from pubsub import pub
from wxUI.dialogs import userAliasDialogs
class userAliasController(object):
def __init__(self, settings):
super(userAliasController, self).__init__()
self.settings = settings
self.dialog = userAliasDialogs.userAliasEditorDialog()
self.update_aliases_manager()
widgetUtils.connect_event(self.dialog.add, widgetUtils.BUTTON_PRESSED, self.on_add)
widgetUtils.connect_event(self.dialog.edit, widgetUtils.BUTTON_PRESSED, self.on_edit)
widgetUtils.connect_event(self.dialog.remove, widgetUtils.BUTTON_PRESSED, self.on_remove)
pub.subscribe(self.update_aliases_manager, "alias-added")
self.dialog.ShowModal()
def update_aliases_manager(self):
self.dialog.users.Clear()
aliases = [self.settings["user-aliases"].get(k) for k in self.settings["user-aliases"].keys()]
if len(aliases) > 0:
self.dialog.users.InsertItems(aliases, 0)
self.dialog.on_selection_changes()
def on_add(self, *args, **kwargs):
pub.sendMessage("execute-action", action="add_alias")
def on_edit(self, *args, **kwargs):
selection = self.dialog.get_selected_user()
if selection != "":
edited = self.dialog.edit_alias_dialog(_("Edit alias for {}").format(selection))
if edited == None or edited == "":
return
for user_key in self.settings["user-aliases"].keys():
if self.settings["user-aliases"][user_key] == selection:
self.settings["user-aliases"][user_key] = edited
self.settings.write()
self.update_aliases_manager()
break
def on_remove(self, *args, **kwargs):
selection = self.dialog.get_selected_user()
if selection == None or selection == "":
return
should_remove = self.dialog.remove_alias_dialog()
if should_remove:
for user_key in self.settings["user-aliases"].keys():
if self.settings["user-aliases"][user_key] == selection:
self.settings["user-aliases"].pop(user_key)
self.settings.write()
self.update_aliases_manager()
break

View File

@@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
import widgetUtils
from pubsub import pub
from wxUI.dialogs import userList
class UserListController(object):
def __init__(self, users, session, title):
super(UserListController, self).__init__()
self.session = session
self.users = self.process_users(users)
self.dialog = userList.UserListDialog(title=title, users=[user.get("display_name", user.get("acct")) for user in self.users])
widgetUtils.connect_event(self.dialog.actions_button, widgetUtils.BUTTON_PRESSED, self.on_actions)
widgetUtils.connect_event(self.dialog.details_button, widgetUtils.BUTTON_PRESSED, self.on_details)
self.dialog.ShowModal()
def process_users(self, users):
return {}
def on_actions(self):
pass
def on_details(self, *args, **kwargs):
pass

View File

@@ -1 +0,0 @@
from .soundsTutorial import soundsTutorial

View File

@@ -1,11 +0,0 @@
#Reverse sort, by Bill Dengler <codeofdusk@gmail.com> for use in TWBlue http://twblue.es
def invert_tuples(t):
"Invert a list of tuples, so that the 0th element becomes the -1th, and the -1th becomes the 0th."
res=[]
for i in t:
res.append(i[::-1])
return res
def reverse_sort(t):
"Sorts a list of tuples/lists by their last elements, not their first."
return invert_tuples(sorted(invert_tuples(t)))

View File

@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
import platform
import widgetUtils
import os
import paths
import logging
log = logging.getLogger("extra.SoundsTutorial.soundsTutorial")
from . import soundsTutorial_constants
from . import wx_ui as UI
class soundsTutorial(object):
def __init__(self, sessionObject):
log.debug("Creating sounds tutorial object...")
super(soundsTutorial, self).__init__()
self.session = sessionObject
self.actions = []
log.debug("Loading actions for sounds tutorial...")
[self.actions.append(i[1]) for i in soundsTutorial_constants.actions]
self.files = []
log.debug("Searching sound files...")
[self.files.append(i[0]) for i in soundsTutorial_constants.actions]
log.debug("Creating dialog...")
self.dialog = UI.soundsTutorialDialog(self.actions)
widgetUtils.connect_event(self.dialog.play, widgetUtils.BUTTON_PRESSED, self.on_play)
self.dialog.get_response()
def on_play(self, *args, **kwargs):
try:
self.session.sound.play(self.files[self.dialog.get_selection()]+".ogg")
except:
log.exception("Error playing the %s sound" % (self.files[self.dialog.items.GetSelection()],))

View File

@@ -1,28 +0,0 @@
#-*- coding: utf-8 -*-
from . import reverse_sort
import application
actions = reverse_sort.reverse_sort([ ("audio", _(u"Audio tweet.")),
("create_timeline", _(u"User timeline buffer created.")),
("delete_timeline", _(u"Buffer destroied.")),
("dm_received", _(u"Direct message received.")),
("dm_sent", _(u"Direct message sent.")),
("error", _(u"Error.")),
("favourite", _(u"Tweet liked.")),
("favourites_timeline_updated", _(u"Likes buffer updated.")),
("geo", _(u"Geotweet.")),
("image", _("Tweet contains one or more images")),
("limit", _(u"Boundary reached.")),
("list_tweet", _(u"List updated.")),
("max_length", _(u"Too many characters.")),
("mention_received", _(u"Mention received.")),
("new_event", _(u"New event.")),
("ready", _(u"{0} is ready.").format(application.name,)),
("reply_send", _(u"Mention sent.")),
("retweet_send", _(u"Tweet retweeted.")),
("search_updated", _(u"Search buffer updated.")),
("tweet_received", _(u"Tweet received.")),
("tweet_send", _(u"Tweet sent.")),
("trends_updated", _(u"Trending topics buffer updated.")),
("tweet_timeline", _(u"New tweet in user timeline buffer.")),
("update_followers", _(u"New follower.")),
("volume_changed", _(u"Volume changed."))])

View File

@@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
import wx
import widgetUtils
class soundsTutorialDialog(widgetUtils.BaseDialog):
def __init__(self, actions):
super(soundsTutorialDialog, self).__init__(None, -1)
self.SetTitle(_(u"Sounds tutorial"))
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
label = wx.StaticText(panel, -1, _(u"Press enter to listen to the sound for the selected event"))
self.items = wx.ListBox(panel, 1, choices=actions, style=wx.LB_SINGLE)
self.items.SetSelection(0)
listBox = wx.BoxSizer(wx.HORIZONTAL)
listBox.Add(label)
listBox.Add(self.items)
self.play = wx.Button(panel, 1, (u"Play"))
self.play.SetDefault()
close = wx.Button(panel, wx.ID_CANCEL)
btnBox = wx.BoxSizer(wx.HORIZONTAL)
btnBox.Add(self.play)
btnBox.Add(close)
sizer.Add(listBox)
sizer.Add(btnBox)
panel.SetSizer(sizer)
self.SetClientSize(sizer.CalcMin())
def get_selection(self):
return self.items.GetSelection()

View File

@@ -1,6 +0,0 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from . import spellchecker
import platform
if platform.system() == "Windows":
from .wx_ui import *

View File

@@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
import os
import logging
from . import wx_ui
import widgetUtils
import output
import config
import languageHandler
import enchant
import paths
from . import twitterFilter
from enchant.checker import SpellChecker
from enchant.errors import DictNotFoundError
from enchant import tokenize
log = logging.getLogger("extra.SpellChecker.spellChecker")
class spellChecker(object):
def __init__(self, text, dictionary):
super(spellChecker, self).__init__()
# Set Dictionary path if not set in a previous call to this method.
# Dictionary path will be located in user config, see https://github.com/manuelcortez/twblue/issues/208
# dict_path = enchant.get_param("enchant.myspell.dictionary.path")
# if dict_path == None:
# enchant.set_param("enchant.myspell.dictionary.path", os.path.join(paths.config_path(), "dicts"))
# log.debug("Dictionary path set to %s" % (os.path.join(paths.config_path(), "dicts"),))
log.debug("Creating the SpellChecker object. Dictionary: %s" % (dictionary,))
self.active = True
try:
if config.app["app-settings"]["language"] == "system":
log.debug("Using the system language")
self.dict = enchant.DictWithPWL(languageHandler.curLang[:2], os.path.join(paths.config_path(), "wordlist.dict"))
else:
log.debug("Using language: %s" % (languageHandler.getLanguage(),))
self.dict = enchant.DictWithPWL(languageHandler.getLanguage()[:2], os.path.join(paths.config_path(), "wordlist.dict"))
except DictNotFoundError:
log.exception("Dictionary for language %s not found." % (dictionary,))
wx_ui.dict_not_found_error()
self.active = False
self.checker = SpellChecker(self.dict, filters=[twitterFilter.TwitterFilter, tokenize.EmailFilter, tokenize.URLFilter])
self.checker.set_text(text)
if self.active == True:
log.debug("Creating dialog...")
self.dialog = wx_ui.spellCheckerDialog()
widgetUtils.connect_event(self.dialog.ignore, widgetUtils.BUTTON_PRESSED, self.ignore)
widgetUtils.connect_event(self.dialog.ignoreAll, widgetUtils.BUTTON_PRESSED, self.ignoreAll)
widgetUtils.connect_event(self.dialog.replace, widgetUtils.BUTTON_PRESSED, self.replace)
widgetUtils.connect_event(self.dialog.replaceAll, widgetUtils.BUTTON_PRESSED, self.replaceAll)
widgetUtils.connect_event(self.dialog.add, widgetUtils.BUTTON_PRESSED, self.add)
self.check()
self.dialog.get_response()
self.fixed_text = self.checker.get_text()
def check(self):
try:
next(self.checker)
textToSay = _(u"Misspelled word: %s") % (self.checker.word,)
context = u"... %s %s %s" % (self.checker.leading_context(10), self.checker.word, self.checker.trailing_context(10))
self.dialog.set_title(textToSay)
output.speak(textToSay)
self.dialog.set_word_and_suggestions(word=self.checker.word, context=context, suggestions=self.checker.suggest())
except StopIteration:
log.debug("Process finished.")
wx_ui.finished()
self.dialog.Destroy()
def ignore(self, ev):
self.check()
def ignoreAll(self, ev):
self.checker.ignore_always(word=self.checker.word)
self.check()
def replace(self, ev):
self.checker.replace(self.dialog.get_selected_suggestion())
self.check()
def replaceAll(self, ev):
self.checker.replace_always(self.dialog.get_selected_suggestion())
self.check()
def add(self, ev):
self.checker.add()
self.check()

View File

@@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
import re
from enchant.tokenize import Filter
class TwitterFilter(Filter):
"""Filter skipping over twitter usernames and hashtags.
This filter skips any words matching the following regular expression:
^[#@](\S){1, }$
That is, any words that resemble users and hashtags.
"""
_pattern = re.compile(r"^[#@](\S){1,}$")
def _skip(self,word):
if self._pattern.match(word):
return True
return False

View File

@@ -1,82 +0,0 @@
# -*- coding: utf-8 -*-
############################################################
# Copyright (c) 2013, 2014 Manuel Eduardo Cortéz Vallejo <manuel@manuelcortez.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
############################################################
import wx
import application
class spellCheckerDialog(wx.Dialog):
def __init__(self):
super(spellCheckerDialog, self).__init__(None, 1)
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
word = wx.StaticText(panel, -1, _(u"Misspelled word"))
self.word = wx.TextCtrl(panel, -1)
wordBox = wx.BoxSizer(wx.HORIZONTAL)
wordBox.Add(word, 0, wx.ALL, 5)
wordBox.Add(self.word, 0, wx.ALL, 5)
context = wx.StaticText(panel, -1, _(u"Context"))
self.context = wx.TextCtrl(panel, -1)
contextBox = wx.BoxSizer(wx.HORIZONTAL)
contextBox.Add(context, 0, wx.ALL, 5)
contextBox.Add(self.context, 0, wx.ALL, 5)
suggest = wx.StaticText(panel, -1, _(u"Suggestions"))
self.suggestions = wx.ListBox(panel, -1, choices=[], style=wx.LB_SINGLE)
suggestionsBox = wx.BoxSizer(wx.HORIZONTAL)
suggestionsBox.Add(suggest, 0, wx.ALL, 5)
suggestionsBox.Add(self.suggestions, 0, wx.ALL, 5)
self.ignore = wx.Button(panel, -1, _(u"&Ignore"))
self.ignoreAll = wx.Button(panel, -1, _(u"I&gnore all"))
self.replace = wx.Button(panel, -1, _(u"&Replace"))
self.replaceAll = wx.Button(panel, -1, _(u"R&eplace all"))
self.add = wx.Button(panel, -1, _(u"&Add to personal dictionary"))
close = wx.Button(panel, wx.ID_CANCEL)
btnBox = wx.BoxSizer(wx.HORIZONTAL)
btnBox.Add(self.ignore, 0, wx.ALL, 5)
btnBox.Add(self.ignoreAll, 0, wx.ALL, 5)
btnBox.Add(self.replace, 0, wx.ALL, 5)
btnBox.Add(self.replaceAll, 0, wx.ALL, 5)
btnBox.Add(self.add, 0, wx.ALL, 5)
btnBox.Add(close, 0, wx.ALL, 5)
sizer.Add(wordBox, 0, wx.ALL, 5)
sizer.Add(contextBox, 0, wx.ALL, 5)
sizer.Add(suggestionsBox, 0, wx.ALL, 5)
sizer.Add(btnBox, 0, wx.ALL, 5)
panel.SetSizer(sizer)
self.SetClientSize(sizer.CalcMin())
def get_response(self):
return self.ShowModal()
def set_title(self, title):
return self.SetTitle(title)
def set_word_and_suggestions(self, word, context, suggestions):
self.word.SetValue(word)
self.context.ChangeValue(context)
self.suggestions.Set(suggestions)
self.suggestions.SetFocus()
def get_selected_suggestion(self):
return self.suggestions.GetStringSelection()
def dict_not_found_error():
wx.MessageDialog(None, _(u"An error has occurred. There are no dictionaries available for the selected language in {0}").format(application.name,), _(u"Error"), wx.ICON_ERROR).ShowModal()
def finished():
wx.MessageDialog(None, _(u"Spell check complete."), application.name, style=wx.OK).ShowModal()

View File

@@ -1,2 +0,0 @@
# -*- coding: utf-8 -*-
""" Autocompletion users for TWBlue. This package contains all needed code to support this feature, including automatic addition of users, management and code to show the autocompletion menu when an user is composing a post. """

View File

@@ -1,66 +0,0 @@
# -*- coding: utf-8 -*-
""" Module to display the user autocompletion menu in post dialogs. """
import output
from . import storage
from . import wx_menu
class autocompletionUsers(object):
def __init__(self, window, session_id):
""" Class constructor. Displays a menu with users matching the specified pattern for autocompletion.
:param window: A wx control where the menu should be displayed. Normally this is going to be the wx.TextCtrl indicating the tweet's text or direct message recipient.
:type window: wx.Dialog
:param session_id: Session ID which calls this class. We will load the users database from this session.
:type session_id: str.
"""
super(autocompletionUsers, self).__init__()
self.window = window
self.db = storage.storage(session_id)
def show_menu(self, mode="mastodon"):
""" displays a menu with possible users matching the specified pattern.
this menu can be displayed in dialogs where an username is expected. For Mastodon's post dialogs, the string should start with an at symbol (@), otherwise it won't match the pattern.
Of course, users must be already loaded in database before attempting this.
If no users are found, an error message will be spoken.
:param mode: this controls how the dialog will behave. Possible values are 'mastodon' and 'free'. In mastodon mode, the matching pattern will be @user (@ is required), while in 'free' mode the matching pattern will be anything written in the text control.
:type mode: str
"""
if mode == "mastodon":
position = self.window.text.GetInsertionPoint()
text = self.window.text.GetValue()
text = text[:position]
try:
pattern = text.split()[-1]
except IndexError:
output.speak(_(u"You have to start writing"))
return
if pattern.startswith("@") == True:
menu = wx_menu.menu(self.window.text, pattern[1:], mode=mode)
users = self.db.get_users(pattern[1:])
if len(users) > 0:
menu.append_options(users)
self.window.PopupMenu(menu, self.window.text.GetPosition())
menu.destroy()
else:
output.speak(_(u"There are no results in your users database"))
else:
output.speak(_(u"Autocompletion only works for users."))
elif mode == "free":
text = self.window.cb.GetValue()
try:
pattern = text.split()[-1]
except IndexError:
output.speak(_(u"You have to start writing"))
return
menu = wx_menu.menu(self.window.cb, pattern, mode=mode)
users = self.db.get_users(pattern)
if len(users) > 0:
menu.append_options(users)
self.window.PopupMenu(menu, self.window.cb.GetPosition())
menu.destroy()
else:
output.speak(_(u"There are no results in your users database"))

View File

@@ -1,57 +0,0 @@
# -*- coding: utf-8 -*-
""" Management of users in the local database for autocompletion. """
import time
import widgetUtils
from wxUI import commonMessageDialogs
from . import storage, wx_manage
from .mastodon import scan as mastodon
class autocompletionManage(object):
def __init__(self, session):
""" class constructor. Manages everything related to user autocompletion.
:param session: Sessiom where the autocompletion management has been requested.
:type session: sessions.base.Session.
"""
super(autocompletionManage, self).__init__()
self.session = session
# Instantiate database so we can perform modifications on it.
self.database = storage.storage(self.session.session_id)
def show_settings(self):
""" display user management dialog and connect events associated to it. """
self.dialog = wx_manage.autocompletionManageDialog()
self.users = self.database.get_all_users()
self.dialog.put_users(self.users)
widgetUtils.connect_event(self.dialog.add, widgetUtils.BUTTON_PRESSED, self.add_user)
widgetUtils.connect_event(self.dialog.remove, widgetUtils.BUTTON_PRESSED, self.remove_user)
self.dialog.get_response()
def update_list(self):
""" update users list in management dialog. This function is normallhy used after we modify the database in any way, so we can reload all users in the autocompletion user management list. """
item = self.dialog.users.get_selected()
self.dialog.users.clear()
self.users = self.database.get_all_users()
self.dialog.put_users(self.users)
self.dialog.users.select_item(item)
def add_user(self, *args, **kwargs):
""" Add a new username to the autocompletion database. """
usr = self.dialog.get_user()
if usr == False:
return
user_added = False
if self.session.type == "mastodon":
user_added = mastodon.add_user(session=self.session, database=self.database, user=usr)
if user_added == False:
self.dialog.show_invalid_user_error()
return
self.update_list()
def remove_user(self, *args, **kwargs):
""" Remove focused user from the autocompletion database. """
if commonMessageDialogs.delete_user_from_db() == widgetUtils.YES:
item = self.dialog.users.get_selected()
user = self.users[item]
self.database.remove_user(user[0])
self.update_list()

View File

@@ -1,103 +0,0 @@
# -*- coding: utf-8 -*-
""" Scanning code for autocompletion feature on TWBlue. This module can retrieve user objects from the selected Mastodon account automatically. """
import time
import wx
import widgetUtils
import output
from pubsub import pub
from . import wx_scan
from extra.autocompletionUsers import manage, storage
class autocompletionScan(object):
def __init__(self, config, buffer, window):
""" Class constructor. This class will take care of scanning the selected Mastodon account to populate the database with users automatically upon request.
:param config: Config for the session that will be scanned in search for users.
:type config: dict
:param buffer: home buffer for the focused session.
:type buffer: controller.buffers.mastodon.base.baseBuffer
:param window: Main Window of TWBlue.
:type window:wx.Frame
"""
super(autocompletionScan, self).__init__()
self.config = config
self.buffer = buffer
self.window = window
def show_dialog(self):
""" displays a dialog to confirm which buffers should be scanned (followers or following users). """
self.dialog = wx_scan.autocompletionScanDialog()
self.dialog.set("friends", self.config["mysc"]["save_friends_in_autocompletion_db"])
self.dialog.set("followers", self.config["mysc"]["save_followers_in_autocompletion_db"])
if self.dialog.get_response() == widgetUtils.OK:
confirmation = wx_scan.confirm()
return confirmation
def prepare_progress_dialog(self):
self.progress_dialog = wx_scan.autocompletionScanProgressDialog()
# connect method to update progress dialog
pub.subscribe(self.on_update_progress, "on-update-progress")
self.progress_dialog.Show()
def on_update_progress(self):
wx.CallAfter(self.progress_dialog.progress_bar.Pulse)
def scan(self):
""" Attempts to add all users selected by current user to the autocomplete database. """
self.config["mysc"]["save_friends_in_autocompletion_db"] = self.dialog.get("friends")
self.config["mysc"]["save_followers_in_autocompletion_db"] = self.dialog.get("followers")
output.speak(_("Updating database... You can close this window now. A message will tell you when the process finishes."))
database = storage.storage(self.buffer.session.session_id)
percent = 0
users = []
if self.dialog.get("friends") == True:
first_page = self.buffer.session.api.account_following(id=self.buffer.session.db["user_id"], limit=80)
pub.sendMessage("on-update-progress")
if first_page != None:
for user in first_page:
users.append(user)
next_page = first_page
while next_page != None:
next_page = self.buffer.session.api.fetch_next(next_page)
pub.sendMessage("on-update-progress")
if next_page == None:
break
for user in next_page:
users.append(user)
# same step, but for followers.
if self.dialog.get("followers") == True:
first_page = self.buffer.session.api.account_followers(id=self.buffer.session.db["user_id"], limit=80)
pub.sendMessage("on-update-progress")
if first_page != None:
for user in first_page:
if user not in users:
users.append(user)
next_page = first_page
while next_page != None:
next_page = self.buffer.session.api.fetch_next(next_page)
pub.sendMessage("on-update-progress")
if next_page == None:
break
for user in next_page:
if user not in users:
users.append(user)
# except TweepyException:
# wx.CallAfter(wx_scan.show_error)
# return self.done()
for user in users:
name = user.display_name if user.display_name != None and user.display_name != "" else user.username
database.set_user(user.acct, name, 1)
wx.CallAfter(wx_scan .show_success, len(users))
self.done()
def done(self):
wx.CallAfter(self.progress_dialog.Destroy)
wx.CallAfter(self.dialog.Destroy)
pub.unsubscribe(self.on_update_progress, "on-update-progress")
def add_user(session, database, user):
""" Adds an user to the database. """
user = session.api.account_lookup(user)
if user != None:
name = user.display_name if user.display_name != None and user.display_name != "" else user.username
database.set_user(user.acct, name, 1)

View File

@@ -1,45 +0,0 @@
# -*- coding: utf-8 -*-
import wx
import widgetUtils
import application
class autocompletionScanDialog(widgetUtils.BaseDialog):
def __init__(self):
super(autocompletionScanDialog, self).__init__(parent=None, id=-1, title=_(u"Autocomplete users' settings"))
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
self.followers = wx.CheckBox(panel, -1, _("Add &followers to database"))
self.friends = wx.CheckBox(panel, -1, _("Add f&ollowing to database"))
sizer.Add(self.followers, 0, wx.ALL, 5)
sizer.Add(self.friends, 0, wx.ALL, 5)
ok = wx.Button(panel, wx.ID_OK)
cancel = wx.Button(panel, wx.ID_CANCEL)
sizerBtn = wx.BoxSizer(wx.HORIZONTAL)
sizerBtn.Add(ok, 0, wx.ALL, 5)
sizer.Add(cancel, 0, wx.ALL, 5)
sizer.Add(sizerBtn, 0, wx.ALL, 5)
panel.SetSizer(sizer)
self.SetClientSize(sizer.CalcMin())
class autocompletionScanProgressDialog(widgetUtils.BaseDialog):
def __init__(self, *args, **kwargs):
super(autocompletionScanProgressDialog, self).__init__(parent=None, id=wx.ID_ANY, title=_("Updating autocompletion database"), *args, **kwargs)
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
self.progress_bar = wx.Gauge(parent=panel)
sizer.Add(self.progress_bar)
panel.SetSizerAndFit(sizer)
def confirm():
with wx.MessageDialog(None, _("This process will retrieve the users you selected from your Mastodon account, and add them to the user autocomplete database. Please note that if there are many users or you have tried to perform this action less than 15 minutes ago, TWBlue may reach a limit in API calls when trying to load the users into the database. If this happens, we will show you an error, in which case you will have to try this process again in a few minutes. If this process ends with no error, you will be redirected back to the account settings dialog. Do you want to continue?"), _("Attention"), style=wx.ICON_QUESTION|wx.YES_NO) as result:
if result.ShowModal() == wx.ID_YES:
return True
return False
def show_success(users):
with wx.MessageDialog(None, _("TWBlue has imported {} users successfully.").format(users), _("Done")) as dlg:
dlg.ShowModal()
def show_error():
with wx.MessageDialog(None, _("Error adding users from Mastodon. Please try again in about 15 minutes."), _("Error"), style=wx.ICON_ERROR) as dlg:
dlg.ShowModal()

View File

@@ -1,52 +0,0 @@
# -*- coding: utf-8 -*-
import os, sqlite3, paths
class storage(object):
def __init__(self, session_id):
self.connection = sqlite3.connect(os.path.join(paths.config_path(), "%s/autocompletionUsers.dat" % (session_id)))
self.cursor = self.connection.cursor()
if self.table_exist("users") == False:
self.create_table()
def table_exist(self, table):
ask = self.cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='%s'" % (table))
answer = ask.fetchone()
if answer == None:
return False
else:
return True
def get_all_users(self):
self.cursor.execute("""select * from users""")
return self.cursor.fetchall()
def get_users(self, term):
self.cursor.execute("""SELECT * FROM users WHERE UPPER(user) LIKE :term OR UPPER(name) LIKE :term""", {"term": "%{}%".format(term.upper())})
return self.cursor.fetchall()
def set_user(self, screen_name, user_name, from_a_buffer):
self.cursor.execute("""insert or ignore into users values(?, ?, ?)""", (screen_name, user_name, from_a_buffer))
self.connection.commit()
def remove_user(self, user):
self.cursor.execute("""DELETE FROM users WHERE user = ?""", (user,))
self.connection.commit()
return self.cursor.fetchone()
def remove_by_buffer(self, bufferType):
""" Removes all users saved on a buffer. BufferType is 0 for no buffer, 1 for friends and 2 for followers"""
self.cursor.execute("""DELETE FROM users WHERE from_a_buffer = ?""", (bufferType,))
self.connection.commit()
return self.cursor.fetchone()
def create_table(self):
self.cursor.execute("""
create table users(
user TEXT UNIQUE,
name TEXT,
from_a_buffer INTEGER
)""")
def __del__(self):
self.cursor.close()
self.connection.close()

View File

@@ -1,44 +0,0 @@
# -*- coding: utf-8 -*-
import wx
import widgetUtils
from multiplatform_widgets import widgets
import application
class autocompletionManageDialog(widgetUtils.BaseDialog):
def __init__(self):
super(autocompletionManageDialog, self).__init__(parent=None, id=-1, title=_(u"Manage Autocompletion database"))
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
label = wx.StaticText(panel, -1, _(u"Editing {0} users database").format(application.name,))
self.users = widgets.list(panel, _(u"Username"), _(u"Name"), style=wx.LC_REPORT)
sizer.Add(label, 0, wx.ALL, 5)
sizer.Add(self.users.list, 0, wx.ALL, 5)
self.add = wx.Button(panel, -1, _(u"&Add user"))
self.remove = wx.Button(panel, -1, _(u"&Remove user"))
optionsBox = wx.BoxSizer(wx.HORIZONTAL)
optionsBox.Add(self.add, 0, wx.ALL, 5)
optionsBox.Add(self.remove, 0, wx.ALL, 5)
sizer.Add(optionsBox, 0, wx.ALL, 5)
ok = wx.Button(panel, wx.ID_OK)
cancel = wx.Button(panel, wx.ID_CANCEL)
sizerBtn = wx.BoxSizer(wx.HORIZONTAL)
sizerBtn.Add(ok, 0, wx.ALL, 5)
sizer.Add(cancel, 0, wx.ALL, 5)
sizer.Add(sizerBtn, 0, wx.ALL, 5)
panel.SetSizer(sizer)
self.SetClientSize(sizer.CalcMin())
def put_users(self, users):
for i in users:
j = [i[0], i[1]]
self.users.insert_item(False, *j)
def get_user(self):
usr = False
userDlg = wx.TextEntryDialog(None, _(u"Twitter username"), _(u"Add user to database"))
if userDlg.ShowModal() == wx.ID_OK:
usr = userDlg.GetValue()
return usr
def show_invalid_user_error(self):
wx.MessageDialog(None, _(u"The user does not exist"), _(u"Error!"), wx.ICON_ERROR).ShowModal()

View File

@@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
import wx
class menu(wx.Menu):
def __init__(self, window, pattern, mode):
super(menu, self).__init__()
self.window = window
self.pattern = pattern
self.mode = mode
def append_options(self, options):
for i in options:
item = wx.MenuItem(self, wx.ID_ANY, "%s (@%s)" % (i[1], i[0]))
self.Append(item)
self.Bind(wx.EVT_MENU, lambda evt, temp=i[0]: self.select_text(evt, temp), item)
def select_text(self, ev, text):
if self.mode == "mastodon":
self.window.ChangeValue(self.window.GetValue().replace("@"+self.pattern, "@"+text+" "))
elif self.mode == "free":
self.window.SetValue(self.window.GetValue().replace(self.pattern, text))
self.window.SetInsertionPointEnd()
def destroy(self):
self.Destroy()

View File

@@ -1,45 +0,0 @@
# -*- coding: utf-8 -*-
""" original module taken and modified from https://github.com/ctoth/cloudOCR"""
from __future__ import unicode_literals
from builtins import object
import requests
translatable_langs = [_(u"Detect automatically"), _(u"Danish"), _(u"Dutch"), _(u"English"), _(u"Finnish"), _(u"French"), _(u"German"), _(u"Hungarian"), _(u"Korean"), _(u"Italian"), _(u"Japanese"), _(u"Polish"), _(u"Portuguese"), _(u"Russian"), _(u"Spanish"), _(u"Turkish")]
short_langs = ["", "da", "du", "en", "fi", "fr", "de", "hu", "ko", "it", "ja", "pl", "pt", "ru", "es", "tr"]
OcrLangs = ["", "dan", "dut", "eng", "fin", "fre", "ger", "hun", "kor", "ita", "jpn", "pol", "por", "rus", "spa", "tur"]
class APIError(Exception):
pass
class OCRSpaceAPI(object):
def __init__(self, key="4e72ae996f88957", url='https://api.ocr.space/parse/image'):
self.key = key
self.url = url
def OCR_URL(self, url, overlay=False, lang=None):
payload = {
'url': url,
'isOverlayRequired': overlay,
'apikey': self.key,
}
if lang != None:
payload.update(language=lang)
r = requests.post(self.url, data=payload)
result = r.json()['ParsedResults'][0]
if result['ErrorMessage']:
raise APIError(result['ErrorMessage'])
return result
def OCR_file(self, fileobj, overlay=False):
payload = {
'isOverlayRequired': overlay,
'apikey': self.key,
'lang': 'es',
}
r = requests.post(self.url, data=payload, files={'file': fileobj})
results = r.json()['ParsedResults']
if results[0]['ErrorMessage']:
raise APIError(results[0]['ErrorMessage'])
return results

View File

@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
# -*- coding: utf-8 -*-
from . import OCRSpace

View File

@@ -1,2 +0,0 @@
# -*- coding: utf-8 -*-
from .translator import TranslatorController

View File

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

View File

@@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
import config
from deepl import Translator
def translate(text: str, target_language: str) -> str:
key = config.app["translator"]["deepl_api_key"]
t = Translator(key)
return t.translate_text(text, target_lang=target_language).text
def languages():
key = config.app["translator"]["deepl_api_key"]
t = Translator(key)
langs = t.get_target_languages()
return langs

View File

@@ -1,45 +0,0 @@
# -*- coding: utf-8 -*-
""" Modified Libretranslatepy module which adds an user agent for making requests against more instances. """
import json
from typing import Any, Dict
from urllib import request, parse
from libretranslatepy import LibreTranslateAPI
class CustomLibreTranslateAPI(LibreTranslateAPI):
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
def _create_request(self, url: str, method: str, data: Dict[str, str]) -> request.Request:
url_params = parse.urlencode(data)
req = request.Request(url, method=method, data=url_params.encode())
req.add_header("User-Agent", self.USER_AGENT)
return req
def translate(self, q: str, source: str = "en", target: str = "es", timeout: int | None = None) -> Any:
url = self.url + "translate"
params: Dict[str, str] = {"q": q, "source": source, "target": target}
if self.api_key is not None:
params["api_key"] = self.api_key
req = self._create_request(url=url, method="POST", data=params)
response = request.urlopen(req, timeout=timeout)
response_str = response.read().decode()
return json.loads(response_str)["translatedText"]
def detect(self, q: str, timeout: int | None = None) -> Any:
url = self.url + "detect"
params: Dict[str, str] = {"q": q}
if self.api_key is not None:
params["api_key"] = self.api_key
req = self._create_request(url=url, method="POST", data=params)
response = request.urlopen(req, timeout=timeout)
response_str = response.read().decode()
return json.loads(response_str)
def languages(self, timeout: int | None = None) -> Any:
url = self.url + "languages"
params: Dict[str, str] = dict()
if self.api_key is not None:
params["api_key"] = self.api_key
req = self._create_request(url=url, method="GET", data=params)
response = request.urlopen(req, timeout=timeout)
response_str = response.read().decode()
return json.loads(response_str)

View File

@@ -1,58 +0,0 @@
# -*- coding: utf-8 -*-
import logging
import threading
import wx
import config
from pubsub import pub
from . engines import libre_translate, deep_l
from .wx_ui import translateDialog
log = logging.getLogger("extras.translator")
class TranslatorController(object):
def __init__(self, text):
super(TranslatorController, self).__init__()
self.text = text
self.languages = []
self.response = False
self.dialog = translateDialog()
pub.subscribe(self.on_engine_changed, "translator.engine_changed")
if config.app["translator"]["engine"] == "LibreTranslate":
self.dialog.engine_select.SetSelection(0)
elif config.app["translator"]["engine"] == "DeepL":
self.dialog.engine_select.SetSelection(1)
threading.Thread(target=self.load_languages).start()
if self.dialog.ShowModal() == wx.ID_OK:
self.response = True
for k in self.language_dict:
if self.language_dict[k] == self.dialog.dest_lang.GetStringSelection():
self.target_language= k
pub.unsubscribe(self.on_engine_changed, "translator.engine_changed")
def load_languages(self):
self.language_dict = self.get_languages()
self.languages = [self.language_dict[k] for k in self.language_dict]
self.dialog.set_languages(self.languages)
def on_engine_changed(self, engine):
config.app["translator"]["engine"] = engine
config.app.write()
threading.Thread(target=self.load_languages).start()
def translate(self):
log.debug("Received translation request for language %s, text=%s" % (self.target_language, self.text))
if config.app["translator"].get("engine") == "LibreTranslate":
translator = libre_translate.CustomLibreTranslateAPI(config.app["translator"]["lt_api_url"], config.app["translator"]["lt_api_key"])
vars = dict(q=self.text, target=self.target_language)
return translator.translate(**vars)
elif config.app["translator"]["engine"] == "DeepL" and config.app["translator"]["deepl_api_key"] != "":
return deep_l.translate(text=self.text, target_language=self.target_language)
def get_languages(self):
languages = {}
if config.app["translator"].get("engine") == "LibreTranslate":
translator = libre_translate.CustomLibreTranslateAPI(config.app["translator"]["lt_api_url"], config.app["translator"]["lt_api_key"])
languages = {l.get("code"): l.get("name") for l in translator.languages()}
elif config.app["translator"]["engine"] == "DeepL" and config.app["translator"]["deepl_api_key"] != "":
languages = {language.code: language.name for language in deep_l.languages()}
return dict(sorted(languages.items(), key=lambda x: x[1]))

View File

@@ -1,56 +0,0 @@
# -*- coding: utf-8 -*-
############################################################
# Copyright (c) 2013, 2014 Manuel Eduardo Cortéz Vallejo <manuel@manuelcortez.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
############################################################
import wx
from pubsub import pub
from wxUI.dialogs import baseDialog
class translateDialog(baseDialog.BaseWXDialog):
def __init__(self):
super(translateDialog, self).__init__(None, -1, title=_(u"Translate message"))
self.engines = ["LibreTranslate", "DeepL"]
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
staticEngine = wx.StaticText(panel, -1, _(u"Translation engine"))
self.engine_select = wx.ComboBox(panel, -1, choices=self.engines, style=wx.CB_READONLY)
self.engine_select.Bind(wx.EVT_COMBOBOX, lambda event: pub.sendMessage("translator.engine_changed", engine=self.engine_select.GetValue()))
staticDest = wx.StaticText(panel, -1, _(u"Target language"))
self.dest_lang = wx.ComboBox(panel, -1, style = wx.CB_READONLY)
self.dest_lang.SetFocus()
self.dest_lang.SetSelection(0)
engineSizer = wx.BoxSizer(wx.HORIZONTAL)
engineSizer.Add(staticEngine)
engineSizer.Add(self.engine_select)
listSizer = wx.BoxSizer(wx.HORIZONTAL)
listSizer.Add(staticDest)
listSizer.Add(self.dest_lang)
ok = wx.Button(panel, wx.ID_OK)
ok.SetDefault()
cancel = wx.Button(panel, wx.ID_CANCEL)
self.SetEscapeId(wx.ID_CANCEL)
sizer.Add(engineSizer, 0, wx.EXPAND | wx.ALL, 5)
sizer.Add(listSizer, 0, wx.EXPAND | wx.ALL, 5)
sizer.Add(ok, 0, wx.ALIGN_CENTER | wx.ALL, 5)
sizer.Add(cancel, 0, wx.ALIGN_CENTER | wx.ALL, 5)
panel.SetSizer(sizer)
def set_languages(self, languages):
wx.CallAfter(self.dest_lang.SetItems, languages)
def get(self, control):
return getattr(self, control).GetSelection()

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
""" This module contains some bugfixes for packages used in TWBlue."""
from __future__ import absolute_import
from __future__ import unicode_literals
import sys
from . import fix_arrow # A few new locales for Three languages in arrow.
#from . import fix_libloader # Regenerates comcache properly.
from . import fix_urllib3_warnings # Avoiding some SSL warnings related to Twython.
#from . import fix_win32com
#from . import fix_requests #fix cacert.pem location for TWBlue binary copies
def setup():
fix_arrow.fix()
# if hasattr(sys, "frozen"):
# fix_libloader.fix()
# fix_win32com.fix()
# fix_requests.fix()
# else:
# fix_requests.fix(False)
fix_urllib3_warnings.fix()

View File

@@ -1,49 +0,0 @@
# -*- coding: utf-8 -*-
from arrow import locales
from arrow.locales import Locale
def fix():
# insert a modified function so if there is no language available in arrow, returns English locale.
locales.get_locale = get_locale
def get_locale(name):
locale_cls = locales._locale_map.get(name.lower())
if locale_cls is None:
name = name[:2]
locale_cls = locales._locale_map.get(name.lower())
if locale_cls == None:
return locales.EnglishLocale()
return locale_cls()
class GalicianLocale(object):
names = ['gl', 'gl_es', 'gl_gl']
past = 'Hai {0}'
future = 'En {0}'
and_word = "e"
timeframes = {
'now': 'Agora',
"second": "un segundo",
'seconds': '{0} segundos',
'minute': 'un minuto',
'minutes': '{0} minutos',
'hour': 'unha hora',
'hours': '{0} horas',
'day': 'un día',
'days': '{0} días',
"week": "unha semana",
"weeks": "{0} semanas",
'month': 'un mes',
'months': '{0} meses',
'year': 'un ano',
'years': '{0} anos',
}
meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"}
month_names = ['', 'xaneiro', 'febreiro', 'marzo', 'abril', 'maio', 'xuño', 'xullo', 'agosto', 'setembro', 'outubro', 'novembro', 'decembro']
month_abbreviations = ['', 'xan', 'feb', 'mar', 'abr', 'mai', 'xun', 'xul', 'ago', 'set', 'out', 'nov', 'dec']
day_names = ['', 'luns', 'martes', 'mércores', 'xoves', 'venres', 'sábado', 'domingo']
day_abbreviations = ['', 'lun', 'mar', 'mer', 'xov', 'ven', 'sab', 'dom']
ordinal_day_re = r"((?P<value>[1-3]?[0-9](?=[ºª]))[ºª])"

View File

@@ -1,42 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import win32com
import paths
win32com.__build_path__=paths.com_path()
import sys
import os
sys.path.append(os.path.join(win32com.__gen_path__, "."))
from win32com.client import gencache
from pywintypes import com_error
from libloader import com
log = logging.getLogger("fixes.fix_libloader")
fixed=False
def patched_getmodule(modname):
mod=__import__(modname)
return sys.modules[modname]
def load_com(*names):
global fixed
if fixed==False:
gencache._GetModule=patched_getmodule
com.prepare_gencache()
fixed=True
result = None
for name in names:
try:
result = gencache.EnsureDispatch(name)
break
except com_error:
continue
if result is None:
raise com_error("Unable to load any of the provided com objects.")
return result
def fix():
log.debug("Applying fix for Libloader...")
com.load_com = load_com
log.debug("Load_com has been mapped correctly.")

View File

@@ -1,12 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import requests
import paths
import os
import logging
log = logging.getLogger("fixes.fix_requests")
def fix():
log.debug("Applying fix for requests...")
os.environ["REQUESTS_CA_BUNDLE"] = os.path.join(paths.app_path(), "certifi", "cacert.pem")#.encode(paths.fsencoding)
# log.debug("Changed CA path to %s" % (os.environ["REQUESTS_CA_BUNDLE"]))#.decode(paths.fsencoding)))

View File

@@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from future import standard_library
standard_library.install_aliases()
from requests.packages import urllib3
from requests.packages.urllib3 import fields
import six
import urllib.request, urllib.parse, urllib.error
def fix():
urllib3.disable_warnings()
fields.format_header_param=patched_format_header_param
def patched_format_header_param(name, value):
if not any(ch in value for ch in '"\\\r\n'):
result = '%s="%s"' % (name, value)
try:
result.encode('ascii')
except (UnicodeEncodeError, UnicodeDecodeError):
pass
else:
return result
if not six.PY3 and isinstance(value, six.text_type): # Python 2:
value = value.encode('utf-8')
value=urllib.parse.quote(value, safe='')
value = '%s=%s' % (name, value)
return value

View File

@@ -1,6 +0,0 @@
from __future__ import unicode_literals
import win32com.client
def fix():
if win32com.client.gencache.is_readonly == True:
win32com.client.gencache.is_readonly = False
win32com.client.gencache.Rebuild()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

View File

@@ -1,3 +0,0 @@
from __future__ import absolute_import
from .main import KeyboardHandler, KeyboardHandlerError
__all__ = ["KeyboardHandler", "KeyboardHandlerError", ]

View File

@@ -1,8 +0,0 @@
from __future__ import absolute_import
import platform
if platform.system() == 'Linux':
from .linux import LinuxKeyboardHandler as GlobalKeyboardHandler
else:
from .wx_handler import WXKeyboardHandler as GlobalKeyboardHandler
#elif platform.system() == 'Darwin':
#from osx import OSXKeyboardHandler as GlobalKeyboardHandler

View File

@@ -1,128 +0,0 @@
keys = {
'accept': 30,
'add': 107,
'apps': 93,
'attn': 246,
'back': 8,
'browser_back': 166,
'browser_forward': 167,
'cancel': 3,
'capital': 20,
'clear': 12,
'control': 17,
'convert': 28,
'crsel': 247,
'decimal': 110,
'delete': 46,
'divide': 111,
'down': 40,
'end': 35,
'ereof': 249,
'escape': 27,
'execute': 43,
'exsel': 248,
'f1': 112,
'f10': 121,
'f11': 122,
'f12': 123,
'f13': 124,
'f14': 125,
'f15': 126,
'f16': 127,
'f17': 128,
'f18': 129,
'f19': 130,
'f2': 113,
'f20': 131,
'f21': 132,
'f22': 133,
'f23': 134,
'f24': 135,
'f3': 114,
'f4': 115,
'f5': 116,
'f6': 117,
'f7': 118,
'f8': 119,
'f9': 120,
'final': 24,
'hangeul': 21,
'hangul': 21,
'hanja': 25,
'help': 47,
'home': 36,
'insert': 45,
'junja': 23,
'kana': 21,
'kanji': 25,
'lbutton': 1,
'lcontrol': 162,
'left': 37,
'lmenu': 164,
'lshift': 160,
'lwin': 91,
'mbutton': 4,
'media_next_track': 176,
'media_play_pause': 179,
'media_prev_track': 177,
'menu': 18,
'modechange': 31,
'multiply': 106,
'next': 34,
'noname': 252,
'nonconvert': 29,
'numlock': 144,
'numpad0': 96,
'numpad1': 97,
'numpad2': 98,
'numpad3': 99,
'numpad4': 100,
'numpad5': 101,
'numpad6': 102,
'numpad7': 103,
'numpad8': 104,
'numpad9': 105,
'oem_clear': 254,
'pa1': 253,
'pagedown': 34,
'pageup': 33,
'pause': 19,
'play': 250,
'print': 42,
'prior': 33,
'processkey': 229,
'rbutton': 2,
'rcontrol': 163,
'return': 13,
'right': 39,
'rmenu': 165,
'rshift': 161,
'rwin': 92,
'scroll': 145,
'select': 41,
'separator': 108,
'shift': 16,
'snapshot': 44,
'space': 32,
'subtract': 109,
'tab': 9,
'up': 38,
'volume_down': 174,
'volume_mute': 173,
'volume_up': 175,
'xbutton1': 5,
'xbutton2': 6,
'zoom': 251,
'/': 191,
';': 218,
'[': 219,
'\\': 220,
']': 221,
'\'': 222,
'=': 187,
'-': 189,
';': 186,
}
modifiers = {'alt': 1, 'control': 2, 'shift': 4, 'win': 8}

View File

@@ -1,58 +0,0 @@
from main import KeyboardHandler
import threading
import thread
import pyatspi
def parse(s):
"""parse a string like control+f into (modifier, key).
Unknown modifiers will return ValueError."""
m = 0
lst = s.split('+')
if not len(lst): return (0, s)
#Are these right?
d = {
"shift": 1<<pyatspi.MODIFIER_SHIFT,
"control": 1<<pyatspi.MODIFIER_CONTROL,
"alt": 1<<pyatspi.MODIFIER_ALT,
"win":1<<pyatspi.MODIFIER_META3,
}
for item in lst:
if item in d:
m|=d[item]
lst.remove(item)
#end if
if len(lst) > 1: #more than one key, parse error
raise ValueError('unknown modifier %s' % lst[0])
return (m, lst[0].lower())
class AtspiThread(threading.Thread):
def run(self):
pyatspi.Registry.registerKeystrokeListener(handler, kind=(pyatspi.KEY_PRESSED_EVENT,),
mask=pyatspi.allModifiers())
pyatspi.Registry.start()
#the keys we registered
keys = {}
def handler(e):
m,k = e.modifiers,e.event_string.lower()
#not sure why we can't catch control+f. Try to fix it.
if (not e.is_text) and e.id >= 97 <= 126:
k = chr(e.id)
if (m,k) not in keys: return False
thread.start_new(keys[(m,k)], ())
return True #don't pass it on
class LinuxKeyboardHandler(KeyboardHandler):
def __init__(self, *args, **kwargs):
KeyboardHandler.__init__(self, *args, **kwargs)
t = AtspiThread()
t.start()
def register_key(self, key, function):
"""key will be a string, such as control+shift+f.
We need to convert that, using parse_key,
into modifier and key to put into our dictionary."""
#register key so we know if we have it on event receive.
t = parse(key)
keys[t] = function
#if we got this far, the key is valid.
KeyboardHandler.register_key(self, key, function)
def unregister_key (self, key, function):
KeyboardHandler.unregister_key(self, key, function)
del keys[parse(key)]

View File

@@ -1,88 +0,0 @@
import platform
import time
class KeyboardHandlerError (Exception): pass
class KeyboardHandler(object):
def __init__(self, repeat_rate=0.0, *args, **kwargs):
self.repeat_rate = repeat_rate #How long between accepting the same keystroke?
self._last_key = None
self._last_keypress_time = 0
super(KeyboardHandler, self).__init__(*args, **kwargs)
self.active_keys = {}
if not hasattr(self, 'replacement_mods'):
self.replacement_mods = {}
if not hasattr(self, 'replacement_keys'):
self.replacement_keys = {}
def register_key (self, key, function):
if key in self.active_keys:
raise KeyboardHandlerError("Key %s is already registered to a function" % key)
if not callable(function):
raise TypeError("Must provide a callable to be invoked upon keypress")
self.active_keys[key] = function
def unregister_key (self, key, function):
try:
if self.active_keys[key] != function:
raise KeyboardHandlerError("key %s is not registered to that function" % key)
except KeyError:
raise KeyboardHandlerError("Key %s not currently registered" % key)
del(self.active_keys[key])
def unregister_all_keys(self):
for key in list(self.active_keys):
self.unregister_key(key, self.active_keys[key])
def handle_key (self, key):
if self.repeat_rate and key == self._last_key and time.time() - self._last_keypress_time < self.repeat_rate:
return
try:
function = self.active_keys[key]
except KeyError:
return
self._last_key = key
self._last_keypress_time = time.time()
return function()
def register_keys(self, keys):
"""Given a mapping of keystrokes to functions, registers all keystrokes"""
for k in keys:
self.register_key(k, keys[k])
def unregister_keys(self, keys):
"""Given a mapping of keys to their functions, unregisters all provided keys."""
for k in keys:
self.unregister_key(k, keys[k])
def standardize_key(self, key):
"""Takes a keystroke and places it in a standard case and order in a list."""
working = key.split('+')
working = [i.lower() for i in working]
answer = []
if "control" in working:
answer.append("control")
if "win" in working:
answer.append("win")
if "alt" in working:
answer.append("alt")
if "shift" in working:
answer.append("shift")
if working[-1] not in answer:
answer.append(working[-1])
return answer
def standardize_keymap(self, keymap):
"""Given a keymap, returns the keymap standardized."""
full = {}
for i in keymap:
answer = ""
new = self.standardize_key(keymap[i])
for (c, j) in enumerate(new):
if c < len(new)-1:
answer = "%s%s+" % (answer, j)
else:
answer = "%s%s" % (answer, j)
full[i] = answer
return full

View File

@@ -1,56 +0,0 @@
from AppKit import *
from PyObjCTools import AppHelper
from Carbon.CarbonEvt import RegisterEventHotKey, GetApplicationEventTarget
from Carbon.Events import cmdKey, controlKey
import struct
from threading import Thread
from main import KeyboardHandler
kEventHotKeyPressedSubtype = 6
kEventHotKeyReleasedSubtype = 9
class OSXKeyboardHandler(KeyboardHandler):
def __init__(self):
super(OSXKeyboardHandler, self).__init__()
self.replacement_keys = dict()
self.app = KeyboardCapturingNSApplication.alloc().init()
self._event_thread = Thread(target=AppHelper.runEventLoop)
self._event_thread.start()
def register_key (self, key, function):
super(OSXKeyboardHandler, self).register_key(key, function)
k, m = self.parse_key(key)
key_id = RegisterEventHotKey(k, m, (0, 0), GetApplicationEventTarget(), 0)
self.key_ids[key] = key_id
def unregister_key (self, key, function):
super(OSXKeyboardHandler, self).unregister_key(key, function)
key_id = self.key_ids[key]
raise NotImplementedError
def parse_key (self, key):
key=key.split("+")
#replacements
#Modifier keys:
for index, item in enumerate(key[0:-1]):
if self.replacement_mods.has_key(item):
key[index] = self.replacement_mods[item]
if self.replacement_keys.has_key(key[-1]):
key[-1] = self.replacement_keys[key[-1]]
elif len(key[-1])==1:
key[-1] = ord(str(key[-1]))-36
mods = 0
for i in key[:-1]:
mods = mods|i
return [key[-1], mods]
class KeyboardCapturingNSApplication(NSApplication):
def sendEvent_(self, theEvent):
if theEvent.type() == NSSystemDefined and theEvent.subtype() == kEventHotKeyPressedSubtype:
self.activateIgnoringOtherApps_(True)
NSRunAlertPanel(u'Hot Key Pressed', u'Hot Key Pressed', None, None, None)
super(NSApplication, self).sendEvent_(theEvent)

View File

@@ -1,40 +0,0 @@
import win32api
import win32con
from main import KeyboardHandler
class WindowsKeyboardHandler(KeyboardHandler):
def __init__ (self, *args, **kwargs):
super(WindowsKeyboardHandler, self).__init__(*args, **kwargs)
#Setup the replacement dictionaries.
for i in dir(win32con):
if i.startswith("VK_"):
key = i[3:].lower()
self.replacement_keys[key] = getattr(win32con, i)
elif i.startswith("MOD_"):
key = i[4:].lower()
self.replacement_mods[key] = getattr(win32con, i)
self.replacement_keys .update(dict(pageup=win32con.VK_PRIOR, pagedown=win32con.VK_NEXT))
def parse_key (self, keystroke, separator="+"):
keystroke = str(keystroke) #We don't want unicode
keystroke = [self.keycode_from_key(i) for i in keystroke.split(separator)]
mods = 0
for i in keystroke[:-1]:
mods = mods | i #or everything together
return (mods, keystroke[-1])
def keycode_from_key(self, key):
if key in self.replacement_mods:
return self.replacement_mods[key]
if key in self.replacement_keys:
return self.replacement_keys[key]
if len(key) == 1:
return win32api.VkKeyScanEx(key, win32api.GetKeyboardLayout())
def is_key_pressed(self, key):
"""Returns if the given key was pressed. Requires an active message loop or will simply give if the key was pressed recently."""
key = self.keycode_from_key(key)
return win32api.GetAsyncKeyState(key)

View File

@@ -1,130 +0,0 @@
from __future__ import absolute_import
import functools
import logging
logger = logging.getLogger("keyboard_handler")
import wx
from .main import KeyboardHandler, KeyboardHandlerError
from . import key_constants
__all__ = ['WXKeyboardHandler', 'WXControlKeyboardHandler']
def call_after(func):
def wrapper(*args, **kwargs):
wx.CallAfter(func, *args, **kwargs)
functools.update_wrapper(wrapper, func)
return wrapper
class BaseWXKeyboardHandler(KeyboardHandler):
def __init__(self, *args, **kwargs):
super(BaseWXKeyboardHandler, self).__init__(*args, **kwargs)
#Setup the replacement dictionaries.
for i in dir(wx):
if i.startswith('WXK_'):
key = i[4:].lower()
self.replacement_keys[key] = getattr(wx, i)
elif i.startswith('MOD_'):
key = i[4:].lower()
self.replacement_mods[key] = getattr(wx, i)
def parse_key (self, keystroke, separator="+"):
keystroke = [self.keycode_from_key(i) for i in keystroke.split(separator)]
mods = 0
for i in keystroke[:-1]:
mods = mods | i #or everything together
return (mods, keystroke[-1])
def keycode_from_key(self, key):
result = None
if key in self.replacement_mods:
result = self.replacement_mods[key]
elif key in self.replacement_keys:
result = self.replacement_keys[key]
if result >= 277:
result -= 277
elif len(key) == 1:
result = ord(key.upper())
if result is None:
raise KeyboardHandlerError("Could not translate key %r into a valid keycode." % key)
return result
class WXKeyboardHandler(BaseWXKeyboardHandler):
def __init__ (self, parent, *args, **kwargs):
super(WXKeyboardHandler, self).__init__(*args, **kwargs)
self.parent = parent
self.key_ids = {}
self.replacement_keys = key_constants.keys
self.replacement_mods = key_constants.modifiers
@call_after
def register_key(self, key, function):
super(WXKeyboardHandler, self).register_key(key, function)
key_id = wx.NewId()
parsed = self.parse_key(key)
res = self.parent.RegisterHotKey(key_id, *parsed)
if not res:
logger.warn("Failed to register hotkey: %s for function %r", key, function)
self.parent.Bind(wx.EVT_HOTKEY, lambda evt: self.process_key(evt, key_id), id=key_id)
self.key_ids[key] = key_id
return res
def parse_key (self, keystroke, separator="+"):
keystroke = str(keystroke) #We don't want unicode
keystroke = [self.keycode_from_key(i) for i in keystroke.split(separator)]
mods = 0
for i in keystroke[:-1]:
mods = mods | i #or everything together
return (mods, keystroke[-1])
@call_after
def unregister_key (self, key, function):
super(WXKeyboardHandler, self).unregister_key(key, function)
if key not in self.key_ids:
return #there's nothing we can do.
key_id = self.key_ids[key]
self.parent.UnregisterHotKey(key_id)
self.parent.Unbind( wx.EVT_HOTKEY, id=key_id)
self.key_ids.pop(key)
def process_key (self, evt, id):
evt.Skip()
key_ids = self.key_ids.keys()
for i in key_ids:
if self.key_ids.get(i) == id:
self.handle_key(i)
class WXControlKeyboardHandler(wx.StaticText, KeyboardHandler):
def __init__(self, parent=None, *a, **k):
wx.StaticText.__init__(self, parent=parent)
KeyboardHandler.__init__(self, *a, **k)
self.wx_replacements = {}
for i in [d for d in dir(wx) if d.startswith('WXK_')]:
self.wx_replacements[getattr(wx, i)] = i[4:].lower()
self.Bind(wx.EVT_KEY_DOWN, self.process_key, self)
self.SetFocus()
def process_key(self, evt):
keycode = evt.GetKeyCode()
keyname = self.wx_replacements.get(keycode, None)
modifiers = ""
replacements = ( (evt.ControlDown(), 'control+'),
(evt.AltDown(), 'alt+'),
(evt.ShiftDown(), 'shift+'),
(evt.MetaDown(), 'win+')
)
for mod, ch in (replacements):
if mod:
modifiers += ch
if keyname is None:
if 27 < keycode < 256:
keyname = chr(keycode).lower()
else:
keyname = "(%s)unknown" % keycode
key = modifiers + keyname
self.handle_key(key)

View File

@@ -1,48 +0,0 @@
[info]
name = string(default="Chicken Nugget")
desc = string(default="Remaps TWBlue shortcuts to their equivalents in Christopher Toth's Chicken Nugget Twitter client.")
author = string(default="Bill Dengler <codeofdusk@gmail.com>")
[keymap]
up = string(default="control+win+up")
down = string(default="control+win+down")
left = string(default="control+win+left")
right = string(default="control+win+right")
open_conversation = string(default="control+win+c")
show_hide = string(default="control+win+w")
post_tweet = string(default="control+win+t")
post_reply = string(default="control+win+r")
post_retweet = string(default="control+win+shift+t")
send_dm = string(default="control+win+d")
user_details = string(default="control+win+shift+u")
exit = string(default="control+win+q")
open_timeline = string(default="control+win+u")
remove_buffer = string(default="control+win+back")
audio = string(default="control+win+return")
url = string(default="control+win+b")
go_home = string(default="control+win+home")
go_end = string(default="control+win+end")
delete = string(default="control+win+delete")
clear_buffer = string(default="control+win+shift+delete")
repeat_item = string(default="control+win+space")
copy_to_clipboard = string(default="control+win+shift+c")
search = string(default="control+win+/")
find = string(default="control+win+shift+/")
check_for_updates = string(default="alt+win+u")
list_manager = string(default="control+win+shift+l")
configuration = string(default="control+win+o")
accountConfiguration = string(default="control+win+shift+o")
update_buffer = string(default="control+win+shift+u")
ocr_image = string(default="win+alt+o")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
vote=string(default="alt+win+shift+v")
edit_post=string(default="")
open_favs_timeline=string(default="")
community_timeline=string(default="")
seekLeft=string(default="")
seekRight=string(default="")
manage_aliases=string(default="")
create_filter=string(default="")
manage_filters=string(default="")
manage_accounts=string(default="")

View File

@@ -1,67 +0,0 @@
[info]
name = string(default="Qwitter")
desc = string(default="A keymap which emulates Qwitter as closely as possible.")
author = string(default="Bill Dengler <codeofdusk@gmail.com>")
[keymap]
up = string(default="control+win+up")
down = string(default="control+win+down")
left = string(default="control+win+left")
right = string(default="control+win+right")
next_account = string(default="control+win+shift+right")
previous_account = string(default="control+win+shift+left")
show_hide = string(default="control+win+w")
post_tweet = string(default="control+win+n")
post_reply = string(default="control+win+r")
post_retweet = string(default="control+win+shift+r")
send_dm = string(default="control+win+d")
add_to_favourites = string(default="alt+win+f")
remove_from_favourites = string(default="alt+shift+win+f")
follow = string(default="control+win+l")
user_details = string(default="control+win+shift+;")
view_item = string(default="control+win+v")
exit = string(default="control+win+f4")
open_timeline = string(default="control+win+i")
remove_buffer = '''string(default="control+win+'")'''
url = string(default="control+win+return")
audio = string(default="control+win+shift+return")
volume_up = string(default="control+win+alt+up")
volume_down = string(default="control+win+alt+down")
go_home = string(default="control+win+home")
go_end = string(default="control+win+end")
go_page_up = string(default="control+win+pageup")
go_page_down = string(default="control+win+pagedown")
update_profile = string(default="control+win+shift+p")
delete = string(default="control+win+delete")
clear_buffer = string(default="control+win+shift+delete")
repeat_item = string(default="control+win+space")
copy_to_clipboard = string(default="control+win+shift+c")
add_to_list = string(default="control+win+alt+l")
remove_from_list = string(default="control+win+alt+shift+l")
toggle_buffer_mute = string(default="control+win+alt+m")
toggle_session_mute = string(default="control+win+m")
search = string(default="control+win+/")
find = string(default="control+win+shift+/")
edit_keystrokes = string(default="control+win+k")
view_user_lists = string(default="win+alt+shift+l")
reverse_geocode = string(default="control+win+g")
view_reverse_geocode = string(default="control+win+shift+g")
get_trending_topics = string(default="control+win+shift+t")
check_for_updates = string(default="control+win+u")
list_manager = string(default="control+win+shift+l")
configuration = string(default="control+win+o")
accountConfiguration = string(default="control+win+shift+o")
update_buffer = string(default="control+win+shift+u")
ocr_image = string(default="win+alt+o")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
vote=string(default="alt+win+shift+v")
edit_post=string(default="")
open_favs_timeline=string(default="")
community_timeline=string(default="")
seekLeft=string(default="")
seekRight=string(default="")
manage_aliases=string(default="")
create_filter=string(default="")
manage_filters=string(default="")
manage_accounts=string(default="")

View File

@@ -1,70 +0,0 @@
[info]
name = string(default="Windows 10")
desc = string(default="A keymap with remapped modifiers for Windows 10 compatibility.")
author = string(default="Bill Dengler <codeofdusk@gmail.com>")
[keymap]
up = string(default="alt+win+up")
down = string(default="alt+win+down")
left = string(default="alt+win+left")
right = string(default="alt+win+right")
next_account = string(default="alt+win+shift+right")
previous_account = string(default="alt+win+shift+left")
open_conversation = string(default="alt+win+c")
show_hide = string(default="control+win+w")
post_tweet = string(default="alt+win+n")
post_reply = string(default="control+win+r")
post_retweet = string(default="alt+win+shift+r")
send_dm = string(default="alt+win+shift+d")
toggle_like = string(default="control+alt+win+f")
follow = string(default="alt+win+shift+s")
user_details = string(default="alt+win+shift+n")
view_item = string(default="alt+win+v")
exit = string(default="alt+win+f4")
open_timeline = string(default="alt+win+i")
remove_buffer = string(default="alt+win+shift+i")
url = string(default="alt+win+return")
audio = string(default="alt+shift+win+return")
volume_up = string(default="alt+win+shift+up")
go_home = string(default="alt+win+home")
volume_down = string(default="alt+win+shift+down")
go_end = string(default="alt+win+end")
go_page_up = string(default="control+win+pageup")
go_page_down = string(default="control+win+pagedown")
update_profile = string(default="alt+win+p")
delete = string(default="alt+win+delete")
clear_buffer = string(default="alt+win+shift+delete")
repeat_item = string(default="alt+win+space")
copy_to_clipboard = string(default="alt+win+shift+c")
add_to_list = string(default="alt+win+a")
remove_from_list = string(default="alt+win+shift+a")
toggle_buffer_mute = string(default="alt+win+shift+m")
toggle_session_mute = string(default="control+alt+win+m")
toggle_autoread = string(default="alt+win+e")
search = string(default="alt+win+-")
edit_keystrokes = string(default="alt+win+k")
view_user_lists = string(default="alt+win+l")
get_more_items = string(default="alt+win+pageup")
reverse_geocode = string(default="control+win+g")
view_reverse_geocode = string(default="alt+win+shift+g")
get_trending_topics = string(default="control+win+t")
check_for_updates = string(default="alt+win+u")
list_manager = string(default="alt+win+shift+l")
configuration = string(default="control+win+alt+o")
accountConfiguration = string(default="control+win+shift+o")
update_buffer = string(default="control+alt+shift+u")
ocr_image = string(default="win+alt+o")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
mute_conversation=string(default="control+alt+win+back")
find = string(default="control+win+{")
vote=string(default="alt+win+shift+v")
edit_post=string(default="")
open_favs_timeline=string(default="")
community_timeline=string(default="")
seekLeft=string(default="")
seekRight=string(default="")
manage_aliases=string(default="")
create_filter=string(default="")
manage_filters=string(default="")
manage_accounts=string(default="")

View File

@@ -1,70 +0,0 @@
[info]
name = string(default="Windows 11")
desc = string(default="A keymap with remapped modifiers for Windows 11 compatibility.")
author = string(default="Bill Jesús <galorasd@gmail.com>")
[keymap]
up = string(default="control+alt+win+up")
down = string(default="control+alt+win+down")
left = string(default="control+alt+win+left")
right = string(default="control+alt+win+right")
next_account = string(default="control+alt+win+shift+right")
previous_account = string(default="control+alt+win+shift+left")
open_conversation = string(default="control+alt+win+c")
show_hide = string(default="control+win+w")
post_tweet = string(default="alt+win+n")
post_reply = string(default="control+win+r")
post_retweet = string(default="alt+win+shift+r")
send_dm = string(default="alt+win+shift+d")
toggle_like = string(default="control+alt+win+f")
follow = string(default="alt+win+shift+s")
user_details = string(default="alt+win+shift+n")
view_item = string(default="alt+win+v")
exit = string(default="alt+win+f4")
open_timeline = string(default="alt+win+i")
remove_buffer = string(default="alt+win+shift+i")
url = string(default="alt+win+return")
audio = string(default="alt+shift+win+return")
volume_up = string(default="control+alt+win+shift+up")
go_home = string(default="control+alt+win+home")
volume_down = string(default="control+alt+win+shift+down")
go_end = string(default="control+alt+win+end")
go_page_up = string(default="control+win+pageup")
go_page_down = string(default="control+win+pagedown")
update_profile = string(default="alt+win+p")
delete = string(default="alt+win+delete")
clear_buffer = string(default="alt+win+shift+delete")
repeat_item = string(default="control+alt+win+space")
copy_to_clipboard = string(default="alt+win+shift+c")
add_to_list = string(default="alt+win+a")
remove_from_list = string(default="alt+win+shift+a")
toggle_buffer_mute = string(default="alt+win+shift+m")
toggle_session_mute = string(default="control+alt+win+m")
toggle_autoread = string(default="alt+win+e")
search = string(default="alt+win+-")
edit_keystrokes = string(default="control+alt+win+k")
view_user_lists = string(default="alt+win+l")
get_more_items = string(default="alt+win+pageup")
reverse_geocode = string(default="control+win+g")
view_reverse_geocode = string(default="alt+win+shift+g")
get_trending_topics = string(default="control+win+t")
check_for_updates = string(default="alt+win+u")
list_manager = string(default="alt+win+shift+l")
configuration = string(default="control+win+alt+o")
accountConfiguration = string(default="control+win+shift+o")
update_buffer = string(default="control+alt+shift+u")
ocr_image = string(default="win+alt+o")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
mute_conversation=string(default="control+alt+win+back")
find = string(default="control+win+{")
vote=string(default="alt+win+shift+v")
edit_post=string(default="")
open_favs_timeline=string(default="")
community_timeline=string(default="")
seekLeft=string(default="")
seekRight=string(default="")
manage_aliases=string(default="")
create_filter=string(default="")
manage_filters=string(default="")
manage_accounts=string(default="")

View File

@@ -1,70 +0,0 @@
[info]
name = string(default="Untitled Keymap")
desc = string(default="A fairly nondescript keymap. Nothing noteworthy.")
author = string(default="Nobody")
[keymap]
up = string(default="control+win+up")
down = string(default="control+win+down")
left = string(default="control+win+left")
right = string(default="control+win+right")
next_account = string(default="control+win+shift+right")
previous_account = string(default="control+win+shift+left")
open_conversation = string(default="control+win+c")
show_hide = string(default="control+win+m")
post_tweet = string(default="control+win+n")
post_reply = string(default="control+win+r")
post_retweet = string(default="control+win+shift+r")
send_dm = string(default="control+win+d")
add_to_favourites = string(default="alt+win+f")
remove_from_favourites = string(default="alt+shift+win+f")
follow = string(default="control+win+s")
user_details = string(default="control+win+alt+n")
view_item = string(default="control+win+v")
exit = string(default="control+win+f4")
open_timeline = string(default="control+win+i")
remove_buffer = string(default="control+win+shift+i")
audio = string(default="control+win+return")
secondary_interact = string(default="control+win+alt+return")
volume_up = string(default="control+win+alt+up")
volume_down = string(default="control+win+alt+down")
go_home = string(default="control+win+home")
go_end = string(default="control+win+end")
go_page_up = string(default="control+win+pageup")
go_page_down = string(default="control+win+pagedown")
update_profile = string(default="alt+win+p")
delete = string(default="control+win+delete")
clear_buffer = string(default="control+win+shift+delete")
repeat_item = string(default="control+win+space")
copy_to_clipboard = string(default="control+win+shift+c")
add_to_list = string(default="control+win+a")
remove_from_list = string(default="control+win+shift+a")
toggle_buffer_mute = string(default="control+win+shift+m")
toggle_session_mute = string(default="alt+win+m")
toggle_autoread = string(default="control+win+e")
search = string(default="control+win+-")
find = string(default="control+win+/")
edit_keystrokes = string(default="control+win+k")
view_user_lists = string(default="control+win+l")
get_more_items = string(default="alt+win+pageup")
reverse_geocode = string(default="control+win+g")
view_reverse_geocode = string(default="control+win+shift+g")
get_trending_topics = string(default="control+win+t")
check_for_updates = string(default="control+win+u")
list_manager = string(default="control+win+shift+l")
configuration = string(default="control+win+o")
accountConfiguration = string(default="control+win+shift+o")
update_buffer = string(default="control+win+shift+u")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
mute_conversation=string(default="alt+win+shift+delete")
vote=string(default="alt+win+shift+v")
edit_post=string(default="")
open_favs_timeline=string(default="")
community_timeline=string(default="")
seekLeft=string(default="")
seekRight=string(default="")
manage_aliases=string(default="")
create_filter=string(default="")
manage_filters=string(default="")
manage_accounts=string(default="")

View File

@@ -1,71 +0,0 @@
[info]
name = string(default="Default")
desc = string(default="TWBlue's default keymap.")
author = string(default="Bill Dengler <codeofdusk@gmail.com>")
[keymap]
up = string(default="control+win+up")
down = string(default="control+win+down")
left = string(default="control+win+left")
right = string(default="control+win+right")
next_account = string(default="control+win+shift+right")
previous_account = string(default="control+win+shift+left")
open_conversation = string(default="control+win+c")
show_hide = string(default="control+win+m")
post_tweet = string(default="control+win+n")
post_reply = string(default="control+win+r")
post_retweet = string(default="control+win+shift+r")
send_dm = string(default="control+win+d")
add_to_favourites = string(default="alt+win+f")
remove_from_favourites = string(default="alt+shift+win+f")
follow = string(default="control+win+s")
user_details = string(default="control+win+alt+n")
view_item = string(default="control+win+v")
exit = string(default="control+win+f4")
open_timeline = string(default="control+win+i")
remove_buffer = string(default="control+win+shift+i")
audio = string(default="control+alt+win+return")
url = string(default="control+win+return")
volume_up = string(default="control+win+alt+up")
volume_down = string(default="control+win+alt+down")
go_home = string(default="control+win+home")
go_end = string(default="control+win+end")
go_page_up = string(default="control+win+pageup")
go_page_down = string(default="control+win+pagedown")
update_profile = string(default="alt+win+p")
delete = string(default="control+win+delete")
clear_buffer = string(default="control+win+shift+delete")
repeat_item = string(default="control+win+space")
copy_to_clipboard = string(default="control+win+shift+c")
add_to_list = string(default="control+win+a")
remove_from_list = string(default="control+win+shift+a")
toggle_buffer_mute = string(default="control+win+shift+m")
toggle_session_mute = string(default="alt+win+m")
toggle_autoread = string(default="control+win+e")
search = string(default="control+win+-")
edit_keystrokes = string(default="control+win+k")
view_user_lists = string(default="control+win+l")
get_more_items = string(default="alt+win+pageup")
reverse_geocode = string(default="control+win+g")
view_reverse_geocode = string(default="control+win+shift+g")
get_trending_topics = string(default="control+win+t")
find = string(default="control+win+{")
check_for_updates = string(default="control+win+u")
list_manager = string(default="control+win+shift+l")
configuration = string(default="control+win+o")
accountConfiguration = string(default="control+win+shift+o")
update_buffer = string(default="control+win+shift+u")
ocr_image = string(default="win+alt+o")
open_in_browser = string(default="alt+control+win+return")
add_alias=string(default="")
mute_conversation=string(default="alt+win+shift+delete")
vote=string(default="alt+win+shift+v")
edit_post=string(default="")
open_favs_timeline=string(default="")
community_timeline=string(default="")
seekLeft=string(default="")
seekRight=string(default="")
manage_aliases=string(default="")
create_filter=string(default="")
manage_filters=string(default="")
manage_accounts=string(default="")

View File

@@ -1 +0,0 @@
from .keystrokeEditor import KeystrokeEditor

View File

@@ -1 +0,0 @@
from . import mastodon

View File

@@ -1,68 +0,0 @@
# -*- coding: utf-8 -*-
actions = {
"up": _(u"Go up in the current buffer"),
"down": _(u"Go down in the current buffer"),
"left": _(u"Go to the previous buffer"),
"right": _(u"Go to the next buffer"),
"next_account": _(u"Focus the next session"),
"previous_account": _(u"Focus the previous session"),
"show_hide": _(u"Show or hide the GUI"),
"post_tweet": _("Make a new post"),
"post_reply": _(u"Reply"),
"post_retweet": _(u"Boost"),
"send_dm": _(u"Send direct message"),
"add_to_favourites": _("Add post to favorites"),
"remove_from_favourites": _(u"Remove post from favorites"),
"toggle_like": _("Add/remove post from favorites"),
"follow": _(u"Open the user actions dialogue"),
"user_details": _(u"See user details"),
"view_item": _(u"Show post"),
"exit": _(u"Quit"),
"open_timeline": _(u"Open user timeline"),
"remove_buffer": _(u"Destroy buffer"),
"interact": _(u"Interact with the currently focused post."),
"url": _(u"Open URL"),
"open_in_browser": _(u"View in browser"),
"volume_up": _(u"Increase volume by 5%"),
"volume_down": _(u"Decrease volume by 5%"),
"go_home": _(u"Jump to the first element of a buffer"),
"go_end": _(u"Jump to the last element of the current buffer"),
"go_page_up": _(u"Jump 20 elements up in the current buffer"),
"go_page_down": _(u"Jump 20 elements down in the current buffer"),
"update_profile": _(u"Edit profile"),
"delete": _("Delete post"),
"clear_buffer": _(u"Empty the current buffer"),
"repeat_item": _(u"Repeat last item"),
"copy_to_clipboard": _(u"Copy to clipboard"),
# "add_to_list": _(u"Add to list"),
# "remove_from_list": _(u"Remove from list"),
"toggle_buffer_mute": _(u"Mute/unmute the active buffer"),
"toggle_session_mute": _(u"Mute/unmute the current session"),
"toggle_autoread": _(u"toggle the automatic reading of incoming tweets in the active buffer"),
"search": _(u"Search on instance"),
"find": _(u"Find a string in the currently focused buffer"),
"edit_keystrokes": _(u"Show the keystroke editor"),
# "view_user_lists": _(u"Show lists for a specified user"),
"get_more_items": _(u"load previous items"),
# "get_trending_topics": _(u"Create a trending topics buffer"),
"open_conversation": _(u"View conversation"),
"check_for_updates": _(u"Check and download updates"),
"configuration": _(u"Opens the global settings dialogue"),
# "list_manager": _(u"Opens the list manager"),
"accountConfiguration": _(u"Opens the account settings dialogue"),
"audio": _(u"Try to play a media file"),
"update_buffer": _(u"Updates the buffer and retrieves possible lost items there."),
"ocr_image": _(u"Extracts the text from a picture and displays the result in a dialog."),
"add_alias": _("Adds an alias to an user"),
"mute_conversation": _("Mute/Unmute conversation"),
"edit_post": _(u"Edit the selected post"),
"vote": _(u"Vote in the selected poll"),
"open_favs_timeline": _(u"Open favorites timeline"),
"community_timeline": _(u"Open local/federated timeline"),
"seekLeft": _(u"Seek media backward"),
"seekRight": _(u"Seek media forward"),
"manage_aliases": _(u"Manage user aliases"),
"create_filter": _(u"Create a new filter"),
"manage_filters": _(u"Manage filters"),
"manage_accounts": _(u"Manage accounts"),
}

View File

@@ -1,71 +0,0 @@
# -*- coding: utf-8 -*-
import widgetUtils
import config
from . import wx_ui
from . import actions
from pubsub import pub
class KeystrokeEditor(object):
def __init__(self, session_type="twitter"):
super(KeystrokeEditor, self).__init__()
self.actions = getattr(actions, session_type).actions
self.changed = False # Change it if the keyboard shorcuts are reassigned.
self.dialog = wx_ui.keystrokeEditorDialog()
self.map = config.keymap["keymap"]
# we need to copy the keymap before modify it, for unregistering the old keystrokes if is needed.
self.hold_map = self.map.copy()
self.dialog.put_keystrokes(self.actions, self.map)
widgetUtils.connect_event(self.dialog.edit, widgetUtils.BUTTON_PRESSED, self.edit_keystroke)
widgetUtils.connect_event(self.dialog.undefine, widgetUtils.BUTTON_PRESSED, self.undefine_keystroke)
widgetUtils.connect_event(self.dialog.execute, widgetUtils.BUTTON_PRESSED, self.execute_action)
self.dialog.get_response()
def edit_keystroke(self, *args, **kwargs):
action = self.dialog.actions[self.dialog.get_action()]
edit_dialog = wx_ui.editKeystrokeDialog()
self.set_keystroke(self.map[action], edit_dialog)
answer = edit_dialog.get_response()
if answer == widgetUtils.OK:
new_keystroke = self.get_edited_keystroke(edit_dialog)
if new_keystroke != self.map[action]:
self.changed = True
self.map[action] = new_keystroke
self.dialog.put_keystrokes(self.actions, self.map)
def undefine_keystroke(self, *args, **kwargs):
action = self.dialog.actions[self.dialog.get_action()]
keystroke = self.map.get(action)
if keystroke == None:
return
answer = self.dialog.undefine_keystroke_confirmation()
if answer == widgetUtils.YES:
self.map[action] = ""
self.changed = True
self.dialog.put_keystrokes(self.actions, self.map)
def set_keystroke(self, keystroke, dialog):
for i in keystroke.split("+"):
if hasattr(dialog, i):
dialog.set(i, True)
dialog.set("key", keystroke.split("+")[-1])
def get_edited_keystroke(self, dialog):
keys = []
if dialog.get("control") == True:
keys.append("control")
if dialog.get("win") == True:
keys.append("win")
if dialog.get("alt") == True:
keys.append("alt")
if dialog.get("shift") == True:
keys.append("shift")
if dialog.get("key") != "":
keys.append(dialog.get("key"))
else:
wx_ui.no_key()
return
return "+".join(keys)
def execute_action(self, *args, **kwargs):
action = self.dialog.actions[self.dialog.get_action()]
pub.sendMessage("execute-action", action=action)

View File

@@ -1,86 +0,0 @@
# -*- coding: utf-8 -*-
import wx
from multiplatform_widgets import widgets
from wxUI.dialogs import baseDialog
class keystrokeEditorDialog(baseDialog.BaseWXDialog):
def __init__(self):
super(keystrokeEditorDialog, self).__init__(parent=None, id=-1, title=_(u"Keystroke editor"))
self.actions = []
sizer = wx.BoxSizer(wx.VERTICAL)
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.list.SetFocus()
firstSizer = wx.BoxSizer(wx.HORIZONTAL)
firstSizer.Add(keysText, 0, wx.ALL, 5)
firstSizer.Add(self.keys.list, 0, wx.ALL, 5)
self.edit = wx.Button(self, -1, _(u"Edit"))
self.edit.SetDefault()
self.undefine = wx.Button(self, -1, _("Undefine keystroke"))
self.execute = wx.Button(self, -1, _(u"Execute action"))
close = wx.Button(self, wx.ID_CANCEL, _(u"Close"))
secondSizer = wx.BoxSizer(wx.HORIZONTAL)
secondSizer.Add(self.edit, 0, wx.ALL, 5)
secondSizer.Add(self.execute, 0, wx.ALL, 5)
secondSizer.Add(close, 0, wx.ALL, 5)
sizer.Add(firstSizer, 0, wx.ALL, 5)
sizer.Add(secondSizer, 0, wx.ALL, 5)
self.SetSizer(sizer)
self.SetClientSize(sizer.CalcMin())
def put_keystrokes(self, actions, keystrokes):
selection = self.keys.get_selected()
self.keys.clear()
for i in keystrokes:
if (i in actions) == False:
continue
action = actions[i]
self.actions.append(i)
keystroke = keystrokes.get(i)
if keystroke == "":
keystroke = _("Undefined")
self.keys.insert_item(False, *[action, keystroke])
self.keys.select_item(selection)
def get_action(self):
return self.keys.get_selected()
def undefine_keystroke_confirmation(self):
return wx.MessageDialog(self, _("Are you sure you want to undefine this keystroke?"), _("Undefine keystroke"), wx.YES_NO|wx.CANCEL|wx.ICON_QUESTION).ShowModal()
class editKeystrokeDialog(baseDialog.BaseWXDialog):
def __init__(self):
super(editKeystrokeDialog, self).__init__(parent=None, id=-1, title=_(u"Editing keystroke"))
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
self.control = wx.CheckBox(panel, -1, _(u"Control"))
self.alt = wx.CheckBox(panel, -1, _(u"Alt"))
self.shift = wx.CheckBox(panel, -1, _(u"Shift"))
self.win = wx.CheckBox(panel, -1, _(u"Windows"))
sizer1 = wx.BoxSizer(wx.HORIZONTAL)
sizer1.Add(self.control)
sizer1.Add(self.alt)
sizer1.Add(self.shift)
sizer1.Add(self.win)
charLabel = wx.StaticText(panel, -1, _(u"Key"))
self.key = wx.TextCtrl(panel, -1)
sizer2 = wx.BoxSizer(wx.HORIZONTAL)
sizer2.Add(charLabel)
sizer2.Add(self.key)
ok = wx.Button(panel, wx.ID_OK, _(u"OK"))
ok.SetDefault()
cancel = wx.Button(panel, wx.ID_CANCEL)
sizer3 = wx.BoxSizer(wx.HORIZONTAL)
sizer3.Add(ok)
sizer3.Add(cancel)
sizer.Add(sizer1)
sizer.Add(sizer2)
sizer.Add(sizer3)
panel.SetSizerAndFit(sizer)
def no_win_message():
return wx.MessageDialog(None, _(u"You need to use the Windows key"), _(u"Invalid keystroke"), wx.OK|wx.ICON_ERROR).ShowModal()
def no_key():
return wx.MessageDialog(None, _(u"You must provide a character for the keystroke"), _(u"Invalid keystroke"), wx.ICON_ERROR).ShowModal()

View File

@@ -1,214 +0,0 @@
from __future__ import unicode_literals
from future import standard_library
standard_library.install_aliases()
from builtins import zip
from builtins import str
import builtins
import os
import sys
import ctypes
import locale
import gettext
import paths
import platform
import application
#a few Windows locale constants
LOCALE_SLANGUAGE=0x2
LOCALE_SLANGDISPLAYNAME=0x6f
curLang="en"
def localeNameToWindowsLCID(localeName):
"""Retreave the Windows locale identifier (LCID) for the given locale name
@param localeName: a string of 2letterLanguage_2letterCountry or or just 2letterLanguage
@type localeName: string
@returns: a Windows LCID
@rtype: integer
"""
#Windows Vista is able to convert locale names to LCIDs
func_LocaleNameToLCID=getattr(ctypes.windll.kernel32,'LocaleNameToLCID',None)
if func_LocaleNameToLCID is not None:
localeName=localeName.replace('_','-')
LCID=func_LocaleNameToLCID(str(localeName),0)
else: #Windows doesn't have this functionality, manually search Python's windows_locale dictionary for the LCID
localeName=locale.normalize(localeName)
if '.' in localeName:
localeName=localeName.split('.')[0]
LCList=[x[0] for x in locale.windows_locale.items() if x[1]==localeName]
if len(LCList)>0:
LCID=LCList[0]
else:
LCID=0
return LCID
def getLanguageDescription(language):
"""Finds out the description (localized full name) of a given local name"""
desc=None
if platform.system() == "Windows":
LCID=localeNameToWindowsLCID(language)
if LCID!=0:
buf=ctypes.create_unicode_buffer(1024)
if '_' not in language:
res=ctypes.windll.kernel32.GetLocaleInfoW(LCID,LOCALE_SLANGDISPLAYNAME,buf,1024)
else:
res=0
if res==0:
res=ctypes.windll.kernel32.GetLocaleInfoW(LCID,LOCALE_SLANGUAGE,buf,1024)
desc=buf.value
elif platform.system() == "Linux" or not desc:
desc={
"am":pgettext("languageName","Amharic"),
"an":pgettext("languageName","Aragonese"),
"es":pgettext("languageName","Spanish"),
"pt":pgettext("languageName","Portuguese"),
"ru":pgettext("languageName","Russian"),
"it":pgettext("languageName","italian"),
"tr":pgettext("languageName","Turkey"),
"gl":pgettext("languageName","Galician"),
"ca":pgettext("languageName","Catala"),
"eu":pgettext("languageName","Vasque"),
"pl":pgettext("languageName","polish"),
"ar":pgettext("languageName","Arabic"),
"ne":pgettext("languageName","Nepali"),
"sr":pgettext("languageName","Serbian (Latin)"),
"ja":pgettext("languageName","Japanese"),
}.get(language,None)
return desc
def getAvailableLanguages():
"""generates a list of locale names, plus their full localized language and country names.
@rtype: list of tuples
"""
#Make a list of all the locales found in NVDA's locale dir
l=[x for x in os.listdir(paths.locale_path()) if not x.startswith('.')]
l=[x for x in l if os.path.isfile(os.path.join(paths.locale_path(), '%s/LC_MESSAGES/%s.po' % (x, application.short_name)))]
#Make sure that en (english) is in the list as it may not have any locale files, but is default
if 'en' not in l:
l.append('en')
l.sort()
#For each locale, ask Windows for its human readable display name
d=[]
for i in l:
desc=getLanguageDescription(i)
label="%s, %s"%(desc,i) if desc else i
d.append(label)
#include a 'user default, windows' language, which just represents the default language for this user account
l.append("system")
# Translators: the label for the Windows default NVDA interface language.
d.append(_("User default"))
#return a zipped up version of both the lists (a list with tuples of locale,label)
return list(zip(l,d))
def makePgettext(translations):
"""Obtaina pgettext function for use with a gettext translations instance.
pgettext is used to support message contexts,
but Python 2.7's gettext module doesn't support this,
so NVDA must provide its own implementation.
"""
if isinstance(translations, gettext.GNUTranslations):
def pgettext(context, message):
message = str(message)
try:
# Look up the message with its context.
return translations._catalog["%s\x04%s" % (context, message)]
except KeyError:
return message
else:
def pgettext(context, message):
return str(message)
return pgettext
def setLanguage(lang):
system = platform.system()
global curLang
try:
if lang=="system":
if system == "Windows":
windowsLCID=ctypes.windll.kernel32.GetUserDefaultUILanguage()
localeName=locale.windows_locale[windowsLCID]
elif system == "Darwin":
import Foundation
localeName = Foundation.NSLocale.currentLocale().identifier()
elif system == "Linux":
localeName = locale.getdefaultlocale()[0]
trans=gettext.translation(application.short_name, localedir=paths.locale_path(), languages=[localeName])
curLang=localeName
# else:
# localeName=locale.getdefaultlocale()[0]
# trans=gettext.translation('twblue', localedir=paths.locale_path(), languages=[localeName])
# curLang=localeName
else:
trans=gettext.translation(application.short_name, localedir=paths.locale_path(), languages=[lang])
curLang=lang
localeChanged=False
#Try setting Python's locale to lang
# try:
if system == "Windows":
locale.setlocale(locale.LC_ALL, langToWindowsLocale(lang))
localeChanged=True
else:
locale.setlocale(locale.LC_ALL, lang)
localeChanged=True
# except:
# pass
if not localeChanged and '_' in lang:
#Python couldn'tsupport the language_country locale, just try language.
try:
locale.setlocale(locale.LC_ALL, lang.split('_')[0])
except:
pass
#Set the windows locale for this thread (NVDA core) to this locale.
if system == "Windows":
LCID=localeNameToWindowsLCID(lang)
ctypes.windll.kernel32.SetThreadLocale(LCID)
except IOError:
trans=gettext.translation(application.short_name, fallback=True)
curLang="en"
if sys.version[0] == "3":
trans.install()
else:
trans.install(unicode=True)
# Install our pgettext function.
# __builtin__.__dict__["pgettext"] = makePgettext(trans)
def getLanguage():
return curLang
def normalizeLanguage(lang):
"""
Normalizes a language-dialect string in to a standard form we can deal with.
Converts any dash to underline, and makes sure that language is lowercase and dialect is upercase.
"""
lang=lang.replace('-','_')
ld=lang.split('_')
ld[0]=ld[0].lower()
#Filter out meta languages such as x-western
if ld[0]=='x':
return None
if len(ld)>=2:
ld[1]=ld[1].upper()
return "_".join(ld)
def langToWindowsLocale(lang):
languages = {"en": "eng",
"ar": "ara",
"ca": "cat",
"de": "deu",
"es": "esp",
"fi": "fin",
"fr": "fre_FRA",
"gl": "glc",
"eu": "euq",
"hu": "hun",
"hr": "hrv",
"it": "ita",
"ja": "jpn",
"pl": "plk",
"pt": "ptb",
"ru": "rus",
"tr": "trk",
"sr": "eng",
}
return languages[lang]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More