diff --git a/doc/changelog.md b/doc/changelog.md index db4e9218..eb929fd9 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -1,6 +1,10 @@ TWBlue Changelog ## changes in this version +* We have added Experimental support for templates in the invisible interface. The GUI will remain unchanged for now: + * Each object (tweet, received direct message, sent direct message and people) has its own template in the settings. You can edit those templates from the account settings dialog, in the new "templates" tab. + * Every template is composed of the group of variables you want to display for each object. Each variable will start with a dollar sign ($) and cannot contain spaces or special characters. Templates can include arbitrary text that will not be processed. When editing the example templates, you can get an idea of the variables that are available for each object by using the template editing dialog. When you press enter on a variable from the list of available variables, it will be added to the template automatically. When you try to save a template, TWBlue will warn you if the template is incorrectly formatted or if it includes variables that do not exist in the information provided by objects. It is also possible to return to default values from the same dialog when editing a template. + * TWBlue can display image descriptions within Tweet templates. For that, you can use the $image_description variable in your template. * We have restored conversation and threads support powered by Twitter API V2 thanks to a set of improvements we have done in the application, as well as more generous limits to Tweet monthly cap by Twitter. * In the Windows 11 Keymap, the default shortcut to open the keystrokes editor is now CTRL+Alt+Windows+K to avoid conflicts with the new global mute microphone shortcut. * Fixed issue when uploading attachments (images, videos or gif files) while sending tweets or replies. diff --git a/src/Conf.defaults b/src/Conf.defaults index a2b3760a..e25c5002 100644 --- a/src/Conf.defaults +++ b/src/Conf.defaults @@ -48,6 +48,12 @@ ocr_language = string(default="") braille_reporting = boolean(default=True) speech_reporting = boolean(default=True) +[templates] +tweet = string(default="$display_name, $text $image_descriptions $date. $source") +dm = string(default="$sender_display_name, $text $date") +dm_sent = string(default="Dm to $recipient_display_name, $text $date") +person = string(default="$display_name (@$screen_name). $followers followers, $following following, $tweets tweets. Joined Twitter $created_at.") + [filters] [user-aliases] \ No newline at end of file diff --git a/src/controller/buffers/twitter/base.py b/src/controller/buffers/twitter/base.py index 3ad5b77c..3d7c1767 100644 --- a/src/controller/buffers/twitter/base.py +++ b/src/controller/buffers/twitter/base.py @@ -19,7 +19,7 @@ import languageHandler import logging from audio_services import youtube_utils from controller.buffers.base import base -from sessions.twitter import compose, utils, reduce +from sessions.twitter import compose, utils, reduce, templates from mysc.thread_utils import call_threaded from tweepy.errors import TweepyException from tweepy.cursor import Cursor @@ -100,8 +100,10 @@ class BaseBuffer(base.Buffer): return self.get_message() def get_message(self): + template = self.session.settings["templates"]["tweet"] tweet = self.get_right_tweet() - return " ".join(self.compose_function(tweet, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], self.session)) + t = templates.render_tweet(tweet, template, self.session, relative_times=self.session.settings["general"]["relative_times"], offset_seconds=self.session.db["utc_offset"]) + return t def get_full_tweet(self): tweet = self.get_right_tweet() diff --git a/src/controller/buffers/twitter/directMessages.py b/src/controller/buffers/twitter/directMessages.py index 06cbed9a..776521d0 100644 --- a/src/controller/buffers/twitter/directMessages.py +++ b/src/controller/buffers/twitter/directMessages.py @@ -8,7 +8,7 @@ import config import languageHandler import logging from controller import messages -from sessions.twitter import compose, utils +from sessions.twitter import compose, utils, templates from mysc.thread_utils import call_threaded from tweepy.errors import TweepyException from pubsub import pub @@ -129,6 +129,12 @@ class DirectMessagesBuffer(base.BaseBuffer): def open_in_browser(self, *args, **kwargs): output.speak(_(u"This action is not supported in the buffer yet.")) + def get_message(self): + template = self.session.settings["templates"]["dm"] + dm = self.get_right_tweet() + t = templates.render_dm(dm, template, self.session, relative_times=self.session.settings["general"]["relative_times"], offset_seconds=self.session.db["utc_offset"]) + return t + class SentDirectMessagesBuffer(DirectMessagesBuffer): def __init__(self, *args, **kwargs): @@ -150,4 +156,10 @@ class SentDirectMessagesBuffer(DirectMessagesBuffer): else: for i in items: tweet = self.compose_function(i, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], self.session) - self.buffer.list.insert_item(False, *tweet) \ No newline at end of file + self.buffer.list.insert_item(False, *tweet) + + def get_message(self): + template = self.session.settings["templates"]["dm_sent"] + dm = self.get_right_tweet() + t = templates.render_dm(dm, template, self.session, relative_times=self.session.settings["general"]["relative_times"], offset_seconds=self.session.db["utc_offset"]) + return t diff --git a/src/controller/buffers/twitter/people.py b/src/controller/buffers/twitter/people.py index d29a46dc..6d6c387d 100644 --- a/src/controller/buffers/twitter/people.py +++ b/src/controller/buffers/twitter/people.py @@ -16,7 +16,7 @@ import logging from mysc.thread_utils import call_threaded from tweepy.errors import TweepyException from pubsub import pub -from sessions.twitter import compose +from sessions.twitter import compose, templates from . import base log = logging.getLogger("controller.buffers.twitter.peopleBuffer") @@ -84,7 +84,10 @@ class PeopleBuffer(base.BaseBuffer): pass def get_message(self): - return " ".join(self.compose_function(self.get_tweet(), self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"], self.session)) + template = self.session.settings["templates"]["person"] + user = self.get_right_tweet() + t = templates.render_person(user, template, self.session, relative_times=True, offset_seconds=self.session.db["utc_offset"]) + return t def delete_item(self): pass diff --git a/src/controller/editTemplateController.py b/src/controller/editTemplateController.py new file mode 100644 index 00000000..7251153c --- /dev/null +++ b/src/controller/editTemplateController.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +import re +import wx +from typing import List +from sessions.twitter.templates import tweet_variables, dm_variables, person_variables +from wxUI.dialogs.twitterDialogs import templateDialogs + +class EditTemplate(object): + def __init__(self, template: str, type: str) -> None: + super(EditTemplate, self).__init__() + self.default_template = template + if type == "tweet": + self.variables = tweet_variables + elif type == "dm": + self.variables = dm_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 "" \ No newline at end of file diff --git a/src/controller/mainController.py b/src/controller/mainController.py index ddedb068..a8f4130d 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -334,17 +334,17 @@ class Controller(object): root_position =self.view.search(session.db["user_name"], session.db["user_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=self.view.nb, function="home_timeline", name="home_timeline", sessionObject=session, account=session.db["user_name"], sound="tweet_received.ogg", tweet_mode="extended")) + pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Home"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, function="home_timeline", name="home_timeline", sessionObject=session, account=session.db["user_name"], sound="tweet_received.ogg", include_ext_alt_text=True, tweet_mode="extended")) elif i == 'mentions': - pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Mentions"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, function="mentions_timeline", name="mentions", sessionObject=session, account=session.db["user_name"], sound="mention_received.ogg", tweet_mode="extended")) + pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Mentions"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, function="mentions_timeline", name="mentions", sessionObject=session, account=session.db["user_name"], sound="mention_received.ogg", include_ext_alt_text=True, tweet_mode="extended")) elif i == 'dm': pub.sendMessage("createBuffer", buffer_type="DirectMessagesBuffer", session_type=session.type, buffer_title=_("Direct messages"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, function="get_direct_messages", name="direct_messages", sessionObject=session, account=session.db["user_name"], bufferType="dmPanel", compose_func="compose_direct_message", sound="dm_received.ogg")) elif i == 'sent_dm': pub.sendMessage("createBuffer", buffer_type="SentDirectMessagesBuffer", session_type=session.type, buffer_title=_("Sent direct messages"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, function=None, name="sent_direct_messages", sessionObject=session, account=session.db["user_name"], bufferType="dmPanel", compose_func="compose_direct_message")) elif i == 'sent_tweets': - pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Sent tweets"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, function="user_timeline", name="sent_tweets", sessionObject=session, account=session.db["user_name"], screen_name=session.db["user_name"], tweet_mode="extended")) + pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Sent tweets"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, function="user_timeline", name="sent_tweets", sessionObject=session, account=session.db["user_name"], screen_name=session.db["user_name"], include_ext_alt_text=True, tweet_mode="extended")) elif i == 'favorites': - pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Likes"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, function="get_favorites", name="favourites", sessionObject=session, account=session.db["user_name"], sound="favourite.ogg", tweet_mode="extended")) + pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Likes"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, function="get_favorites", name="favourites", sessionObject=session, account=session.db["user_name"], sound="favourite.ogg", include_ext_alt_text=True, tweet_mode="extended")) elif i == 'followers': pub.sendMessage("createBuffer", buffer_type="PeopleBuffer", session_type=session.type, buffer_title=_("Followers"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, function="get_followers", name="followers", sessionObject=session, account=session.db["user_name"], sound="update_followers.ogg", screen_name=session.db["user_name"])) elif i == 'friends': @@ -356,11 +356,11 @@ class Controller(object): pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, name="timelines", account=session.db["user_name"])) timelines_position =self.view.search("timelines", session.db["user_name"]) for i in session.settings["other_buffers"]["timelines"]: - pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_(u"Timeline for {}").format(i,), parent_tab=timelines_position, start=False, kwargs=dict(parent=self.view.nb, function="user_timeline", name="%s-timeline" % (i,), sessionObject=session, account=session.db["user_name"], sound="tweet_timeline.ogg", bufferType=None, user_id=i, tweet_mode="extended")) + pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_(u"Timeline for {}").format(i,), parent_tab=timelines_position, start=False, kwargs=dict(parent=self.view.nb, function="user_timeline", name="%s-timeline" % (i,), sessionObject=session, account=session.db["user_name"], sound="tweet_timeline.ogg", bufferType=None, user_id=i, include_ext_alt_text=True, tweet_mode="extended")) pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Likes timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, name="favs_timelines", account=session.db["user_name"])) favs_timelines_position =self.view.search("favs_timelines", session.db["user_name"]) for i in session.settings["other_buffers"]["favourites_timelines"]: - pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Likes for {}").format(i,), parent_tab=favs_timelines_position, start=False, kwargs=dict(parent=self.view.nb, function="get_favorites", name="%s-favorite" % (i,), sessionObject=session, account=session.db["user_name"], bufferType=None, sound="favourites_timeline_updated.ogg", user_id=i, tweet_mode="extended")) + pub.sendMessage("createBuffer", buffer_type="BaseBuffer", session_type=session.type, buffer_title=_("Likes for {}").format(i,), parent_tab=favs_timelines_position, start=False, kwargs=dict(parent=self.view.nb, function="get_favorites", name="%s-favorite" % (i,), sessionObject=session, account=session.db["user_name"], bufferType=None, sound="favourites_timeline_updated.ogg", user_id=i, include_ext_alt_text=True, tweet_mode="extended")) pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Followers timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, name="followers_timelines", account=session.db["user_name"])) followers_timelines_position =self.view.search("followers_timelines", session.db["user_name"]) for i in session.settings["other_buffers"]["followers_timelines"]: @@ -372,11 +372,11 @@ class Controller(object): pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Lists"), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, name="lists", account=session.db["user_name"])) lists_position =self.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=self.view.nb, function="list_timeline", name="%s-list" % (i,), sessionObject=session, account=session.db["user_name"], bufferType=None, sound="list_tweet.ogg", list_id=utils.find_list(i, session.db["lists"]), tweet_mode="extended")) + 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=self.view.nb, function="list_timeline", name="%s-list" % (i,), sessionObject=session, account=session.db["user_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=self.view.nb, name="searches", account=session.db["user_name"])) searches_position =self.view.search("searches", session.db["user_name"]) for i in session.settings["other_buffers"]["tweet_searches"]: - pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_(u"Search for {}").format(i), parent_tab=searches_position, start=False, kwargs=dict(parent=self.view.nb, function="search_tweets", name="%s-searchterm" % (i,), sessionObject=session, account=session.db["user_name"], bufferType="searchPanel", sound="search_updated.ogg", q=i, tweet_mode="extended")) + pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=session.type, buffer_title=_(u"Search for {}").format(i), parent_tab=searches_position, start=False, kwargs=dict(parent=self.view.nb, function="search_tweets", name="%s-searchterm" % (i,), sessionObject=session, account=session.db["user_name"], bufferType="searchPanel", sound="search_updated.ogg", q=i, include_ext_alt_text=True, tweet_mode="extended")) for i in session.settings["other_buffers"]["trending_topic_buffers"]: pub.sendMessage("createBuffer", buffer_type="TrendsBuffer", session_type=session.type, buffer_title=_("Trending topics for %s") % (i), parent_tab=root_position, start=False, kwargs=dict(parent=self.view.nb, name="%s_tt" % (i,), sessionObject=session, account=session.db["user_name"], trendsFor=i, sound="trends_updated.ogg")) @@ -423,7 +423,7 @@ class Controller(object): buffer.session.settings["other_buffers"]["tweet_searches"].append(term) buffer.session.settings.write() args = {"lang": dlg.get_language(), "result_type": dlg.get_result_type()} - pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=buffer.session.type, buffer_title=_("Search for {}").format(term), parent_tab=searches_position, start=True, kwargs=dict(parent=self.view.nb, function="search_tweets", name="%s-searchterm" % (term,), sessionObject=buffer.session, account=buffer.session.db["user_name"], bufferType="searchPanel", sound="search_updated.ogg", q=term, tweet_mode="extended", **args)) + pub.sendMessage("createBuffer", buffer_type="SearchBuffer", session_type=buffer.session.type, buffer_title=_("Search for {}").format(term), parent_tab=searches_position, start=True, kwargs=dict(parent=self.view.nb, function="search_tweets", name="%s-searchterm" % (term,), sessionObject=buffer.session, account=buffer.session.db["user_name"], bufferType="searchPanel", sound="search_updated.ogg", q=term, include_ext_alt_text=True, tweet_mode="extended", **args)) else: log.error("A buffer for the %s search term is already created. You can't create a duplicate buffer." % (term,)) return @@ -872,7 +872,7 @@ class Controller(object): if usr.id_str in buff.session.settings["other_buffers"]["timelines"]: commonMessageDialogs.timeline_exist() return - tl = buffers.twitter.BaseBuffer(self.view.nb, "user_timeline", "%s-timeline" % (usr.id_str,), buff.session, buff.session.db["user_name"], bufferType=None, sound="tweet_timeline.ogg", user_id=usr.id_str, tweet_mode="extended") + tl = buffers.twitter.BaseBuffer(self.view.nb, "user_timeline", "%s-timeline" % (usr.id_str,), buff.session, buff.session.db["user_name"], bufferType=None, sound="tweet_timeline.ogg", user_id=usr.id_str, include_ext_alt_text=True, tweet_mode="extended") try: tl.start_stream(play_sound=False) except ValueError: @@ -891,7 +891,7 @@ class Controller(object): if usr.id_str in buff.session.settings["other_buffers"]["favourites_timelines"]: commonMessageDialogs.timeline_exist() return - tl = buffers.twitter.BaseBuffer(self.view.nb, "get_favorites", "%s-favorite" % (usr.id_str,), buff.session, buff.session.db["user_name"], bufferType=None, sound="favourites_timeline_updated.ogg", user_id=usr.id_str, tweet_mode="extended") + tl = buffers.twitter.BaseBuffer(self.view.nb, "get_favorites", "%s-favorite" % (usr.id_str,), buff.session, buff.session.db["user_name"], bufferType=None, sound="favourites_timeline_updated.ogg", user_id=usr.id_str, include_ext_alt_text=True, tweet_mode="extended") try: tl.start_stream(play_sound=False) except ValueError: @@ -1088,10 +1088,10 @@ class Controller(object): if position == page.buffer.list.get_selected(): page.session.sound.play("limit.ogg") - try: - output.speak(page.get_message(), True) - except: - pass +# try: + output.speak(page.get_message(), True) +# except: +# pass def down(self, *args, **kwargs): page = self.get_current_buffer() @@ -1100,16 +1100,16 @@ class Controller(object): return position = page.buffer.list.get_selected() index = position+1 - try: - page.buffer.list.select_item(index) - except: - pass +# try: + page.buffer.list.select_item(index) +# except: +# pass if position == page.buffer.list.get_selected(): page.session.sound.play("limit.ogg") - try: - output.speak(page.get_message(), True) - except: - pass +# try: + output.speak(page.get_message(), True) +# except: +# pass def left(self, *args, **kwargs): buff = self.view.get_current_buffer_pos() @@ -1202,18 +1202,18 @@ class Controller(object): def go_home(self): buffer = self.get_current_buffer() buffer.buffer.list.select_item(0) - try: - output.speak(buffer.get_message(), True) - except: - pass +# try: + output.speak(buffer.get_message(), True) +# except: +# pass def go_end(self): buffer = self.get_current_buffer() buffer.buffer.list.select_item(buffer.buffer.list.get_count()-1) - try: - output.speak(buffer.get_message(), True) - except: - pass +# try: + output.speak(buffer.get_message(), True) +# except: +# pass def go_page_up(self): buffer = self.get_current_buffer() @@ -1222,10 +1222,10 @@ class Controller(object): else: index = buffer.buffer.list.get_selected() - 20 buffer.buffer.list.select_item(index) - try: - output.speak(buffer.get_message(), True) - except: - pass +# try: + output.speak(buffer.get_message(), True) +# except: +# pass def go_page_down(self): buffer = self.get_current_buffer() @@ -1234,10 +1234,10 @@ class Controller(object): else: index = buffer.buffer.list.get_selected() + 20 buffer.buffer.list.select_item(index) - try: - output.speak(buffer.get_message(), True) - except: - pass +# try: + output.speak(buffer.get_message(), True) +# except: +# pass def url(self, *args, **kwargs): buffer = self.get_current_buffer() @@ -1385,7 +1385,7 @@ class Controller(object): buff = self.search_buffer("home_timeline", account) if create == True: if buffer == "favourites": - favourites = buffers.twitter.BaseBuffer(self.view.nb, "get_favorites", "favourites", buff.session, buff.session.db["user_name"], tweet_mode="extended") + favourites = buffers.twitter.BaseBuffer(self.view.nb, "get_favorites", "favourites", buff.session, buff.session.db["user_name"], include_ext_alt_text=True, tweet_mode="extended") self.buffers.append(favourites) self.view.insert_buffer(favourites.buffer, name=_(u"Likes"), pos=self.view.search(buff.session.db["user_name"], buff.session.db["user_name"])) favourites.start_stream(play_sound=False) @@ -1415,7 +1415,7 @@ class Controller(object): if create in buff.session.settings["other_buffers"]["lists"]: output.speak(_(u"This list is already opened"), True) return - tl = buffers.twitter.ListBuffer(self.view.nb, "list_timeline", create+"-list", buff.session, buff.session.db["user_name"], bufferType=None, list_id=utils.find_list(create, buff.session.db["lists"]), tweet_mode="extended") + tl = buffers.twitter.ListBuffer(self.view.nb, "list_timeline", create+"-list", buff.session, buff.session.db["user_name"], bufferType=None, list_id=utils.find_list(create, buff.session.db["lists"]), include_ext_alt_text=True, tweet_mode="extended") buff.session.lists.append(tl) pos=self.view.search("lists", buff.session.db["user_name"]) self.insert_buffer(tl, pos) diff --git a/src/controller/settings.py b/src/controller/settings.py index 84040d2e..d203adc3 100644 --- a/src/controller/settings.py +++ b/src/controller/settings.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os import webbrowser +import logging import sound_lib import paths import widgetUtils @@ -8,17 +9,18 @@ import config import languageHandler import output import application +import config_utils +import keys +from collections import OrderedDict +from pubsub import pub +from mysc import autostart as autostart_windows from wxUI.dialogs import configuration from wxUI import commonMessageDialogs from extra.autocompletionUsers import settings from extra.ocr import OCRSpace -from pubsub import pub -import logging -import config_utils +from .editTemplateController import EditTemplate + log = logging.getLogger("Settings") -import keys -from collections import OrderedDict -from mysc import autostart as autostart_windows class globalSettingsController(object): def __init__(self): @@ -152,6 +154,15 @@ class accountSettingsController(globalSettingsController): self.dialog.create_reporting() self.dialog.set_value("reporting", "speech_reporting", self.config["reporting"]["speech_reporting"]) self.dialog.set_value("reporting", "braille_reporting", self.config["reporting"]["braille_reporting"]) + tweet_template = self.config["templates"]["tweet"] + dm_template = self.config["templates"]["dm"] + sent_dm_template = self.config["templates"]["dm_sent"] + person_template = self.config["templates"]["person"] + self.dialog.create_templates(tweet_template=tweet_template, dm_template=dm_template, sent_dm_template=sent_dm_template, person_template=person_template) + widgetUtils.connect_event(self.dialog.templates.tweet, widgetUtils.BUTTON_PRESSED, self.edit_tweet_template) + widgetUtils.connect_event(self.dialog.templates.dm, widgetUtils.BUTTON_PRESSED, self.edit_dm_template) + widgetUtils.connect_event(self.dialog.templates.sent_dm, widgetUtils.BUTTON_PRESSED, self.edit_sent_dm_template) + widgetUtils.connect_event(self.dialog.templates.person, widgetUtils.BUTTON_PRESSED, self.edit_person_template) self.dialog.create_other_buffers() buffer_values = self.get_buffers_list() self.dialog.buffers.insert_buffers(buffer_values) @@ -160,7 +171,6 @@ class accountSettingsController(globalSettingsController): widgetUtils.connect_event(self.dialog.buffers.up, widgetUtils.BUTTON_PRESSED, self.dialog.buffers.move_up) widgetUtils.connect_event(self.dialog.buffers.down, widgetUtils.BUTTON_PRESSED, self.dialog.buffers.move_down) - self.dialog.create_ignored_clients(self.config["twitter"]["ignored_clients"]) widgetUtils.connect_event(self.dialog.ignored_clients.add, widgetUtils.BUTTON_PRESSED, self.add_ignored_client) widgetUtils.connect_event(self.dialog.ignored_clients.remove, widgetUtils.BUTTON_PRESSED, self.remove_ignored_client) @@ -185,6 +195,42 @@ class accountSettingsController(globalSettingsController): self.dialog.set_title(_(u"Account settings for %s") % (self.user,)) self.response = self.dialog.get_response() + def edit_tweet_template(self, *args, **kwargs): + template = self.config["templates"]["tweet"] + control = EditTemplate(template=template, type="tweet") + result = control.run_dialog() + if result != "": # Template has been saved. + self.config["templates"]["tweet"] = result + self.config.write() + self.dialog.templates.tweet.SetLabel(_("Edit template for tweets. Current template: {}").format(result)) + + def edit_dm_template(self, *args, **kwargs): + template = self.config["templates"]["dm"] + control = EditTemplate(template=template, type="dm") + result = control.run_dialog() + if result != "": # Template has been saved. + self.config["templates"]["dm"] = result + self.config.write() + self.dialog.templates.dm.SetLabel(_("Edit template for direct messages. Current template: {}").format(result)) + + def edit_sent_dm_template(self, *args, **kwargs): + template = self.config["templates"]["dm_sent"] + control = EditTemplate(template=template, type="dm") + result = control.run_dialog() + if result != "": # Template has been saved. + self.config["templates"]["dm_sent"] = result + self.config.write() + self.dialog.templates.sent_dm.SetLabel(_("Edit template for sent direct messages. Current template: {}").format(result)) + + def edit_person_template(self, *args, **kwargs): + template = self.settings["templates"]["person"] + control = EditTemplate(template=template, type="person") + result = control.run_dialog() + if result != "": # Template has been saved. + self.config["templates"]["person"] = result + self.config.write() + self.dialog.templates.person.SetLabel(_("Edit template for persons. Current template: {}").format(result)) + def save_configuration(self): if self.config["general"]["relative_times"] != self.dialog.get_value("general", "relative_time"): self.needs_restart = True diff --git a/src/sessions/twitter/templates.py b/src/sessions/twitter/templates.py new file mode 100644 index 00000000..156a2d67 --- /dev/null +++ b/src/sessions/twitter/templates.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +import re +import arrow +import languageHandler +from string import Template +from . import utils + +# Define variables that would be available for all template objects. +# This will be used for the edit template dialog. +# Available variables for tweet objects. +tweet_variables = ["date", "display_name", "screen_name", "source", "lang", "text", "image_descriptions"] +dm_variables = ["date", "sender_display_name", "sender_screen_name", "recipient_display_name", "recipient_display_name", "text"] +person_variables = ["display_name", "screen_name", "location", "description", "followers", "following", "listed", "likes", "tweets", "created_at"] + +# Default, translatable templates. +tweet_default_template = _("$display_name, $text $image_descriptions $date. $source") +dm_default_template = _("$sender_display_name, $text $date") +dm_sent_default_template = _("Dm to $recipient_display_name, $text $date") +person_default_template = _("$display_name (@$screen_name). $followers followers, $following following, $tweets tweets. Joined Twitter $created_at.") + +def process_date(field, relative_times=True, offset_seconds=0): + original_date = arrow.get(field, locale="en") + if relative_times == True: + ts = original_date.humanize(locale=languageHandler.curLang[:2]) + else: + ts = original_date.shift(seconds=offset_seconds).format(_("dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2]) + return ts + +def process_text(tweet): + if hasattr(tweet, "full_text"): + text = tweet.full_text + elif hasattr(tweet, "text"): + text = tweet.text + # Cleanup mentions, so we'll remove more than 2 mentions to make the tweet easier to read. + text = utils.clean_mentions(text) + # Replace URLS for extended version of those. + if hasattr(tweet, "entities"): + text = utils.expand_urls(text, tweet.entities) + text = re.sub(r"https://twitter.com/\w+/status/\d+", "", text) + return text + +def process_image_descriptions(entities): + """ Attempt to extract information for image descriptions. """ + image_descriptions = [] + for media in entities["media"]: + if media.get("ext_alt_text") != None: + image_descriptions.append(media.get("ext_alt_text")) + idescriptions = "" + for image in image_descriptions: + idescriptions += _("Image description: {}.").format(image) + return idescriptions + +def render_tweet(tweet, template, session, relative_times=False, offset_seconds=0): + """ Renders any given Tweet according to the passed template. + Available data for tweets will be stored in the following variables: + $date: Creation date. + $display_name: User profile name. + $screen_name: User screen name, this is the same name used to reference the user in Twitter. + $ source: Source client from where the current tweet was sent. + $lang: Two letter code for the automatically detected language for the tweet. This detection is performed by Twitter. + $text: Tweet text. + $image_descriptions: Information regarding image descriptions added by twitter users. + """ + available_data = dict() + created_at = process_date(tweet.created_at, relative_times, offset_seconds) + available_data.update(date=created_at) + # user. + available_data.update(display_name=session.get_user(tweet.user).name, screen_name=session.get_user(tweet.user).screen_name) + # Source client from where tweet was originated. + available_data.update(source=tweet.source) + if hasattr(tweet, "retweeted_status"): + if hasattr(tweet.retweeted_status, "quoted_status"): + text = "RT @{}: {} Quote from @{}: {}".format(session.get_user(tweet.retweeted_status.user).screen_name, process_text(tweet.retweeted_status), session.get_user(tweet.retweeted_status.quoted_status.user).screen_name, process_text(tweet.retweeted_status.quoted_status)) + else: + text = "RT @{}: {}".format(session.get_user(tweet.retweeted_status.user).screen_name, process_text(tweet.retweeted_status)) + elif hasattr(tweet, "quoted_status"): + text = "{} Quote from @{}: {}".format(process_text(tweet), session.get_user(tweet.quoted_status.user).screen_name, process_text(tweet.quoted_status)) + else: + text = process_text(tweet) + available_data.update(lang=tweet.lang, text=text) + # process image descriptions + image_descriptions = "" + if hasattr(tweet, "quoted_status") and hasattr(tweet.quoted_status, "extended_entities"): + image_descriptions = process_image_descriptions(tweet.quoted_status.extended_entities) + elif hasattr(tweet, "retweeted_status") and hasattr(tweet.retweeted_status, "quoted_status") and hasattr(tweet.retweeted_status.quoted_status, "extended_entities"): + image_descriptions = process_image_descriptions(tweet.retweeted_status.quoted_status.extended_entities) + elif hasattr(tweet, "extended_entities"): + image_descriptions = process_image_descriptions(tweet.extended_entities) + if image_descriptions != "": + available_data.update(image_descriptions=image_descriptions) + result = Template(_(template)).safe_substitute(**available_data) + result = re.sub(r"\$\w+", "", result) + return result + +def render_dm(dm, template, session, relative_times=False, offset_seconds=0): + """ Renders direct messages by using the provided template. + Available data will be stored in the following variables: + $date: Creation date. + $sender_display_name: User profile name for user who sent the dm. + $sender_screen_name: User screen name for user sending the dm, this is the same name used to reference the user in Twitter. + $recipient_display_name: User profile name for user who received the dm. + $recipient_screen_name: User screen name for user receiving the dm, this is the same name used to reference the user in Twitter. + $text: Text of the direct message. + """ + available_data = dict() + available_data.update(text=utils.expand_urls(dm.message_create["message_data"]["text"], dm.message_create["message_data"]["entities"])) + # Let's remove the last 3 digits in the timestamp string. + # Twitter sends their "epoch" timestamp with 3 digits for milliseconds and arrow doesn't like it. + original_date = arrow.get(int(dm.created_timestamp)) + if relative_times == True: + ts = original_date.humanize(locale=languageHandler.curLang[:2]) + else: + ts = original_date.shift(seconds=offset_seconds) + available_data.update(date=ts) + sender = session.get_user(dm.message_create["sender_id"]) + recipient = session.get_user(dm.message_create["target"]["recipient_id"]) + available_data.update(sender_display_name=sender.name, sender_screen_name=sender.screen_name, recipient_display_name=recipient.name, recipient_screen_name=recipient.screen_name) + result = Template(_(template)).safe_substitute(**available_data) + result = re.sub(r"\$\w+", "", result) + return result + +# Sesion object is not used in this function but we keep compatibility across all rendering functions. +def render_person(user, template, session=None, relative_times=True, offset_seconds=0): + """ Renders persons (any Twitter user) by using the provided template. + Available data will be stored in the following variables: + $display_name: The name of the user, as they’ve defined it. Not necessarily a person’s name. Typically capped at 50 characters, but subject to change. + $screen_name: The screen name, handle, or alias that this user identifies themselves with. + $location: The user-defined location for this account’s profile. Not necessarily a location, nor machine-parseable. + $description: The user-defined UTF-8 string describing their account. + $followers: The number of followers this account currently has. This value might be inaccurate. + $following: The number of users this account is following (AKA their “followings”). This value might be inaccurate. + $listed: The number of public lists that this user is a member of. This value might be inaccurate. + $likes: The number of Tweets this user has liked in the account’s lifetime. This value might be inaccurate. + $tweets: The number of Tweets (including retweets) issued by the user. This value might be inaccurate. + $created_at: The date and time that the user account was created on Twitter. + """ + available_data = dict(display_name=user.name, screen_name=user.screen_name, followers=user.followers_count, following=user.friends_count, likes=user.favourites_count, listed=user.listed_count, tweets=user.statuses_count) + # Nullable values. + nullables = ["location", "description"] + for nullable in nullables: + if hasattr(user, nullable) and getattr(user, nullable) != None: + available_data[nullable] = getattr(user, nullable) + created_at = process_date(user.created_at, relative_times=relative_times, offset_seconds=offset_seconds) + available_data.update(created_at=created_at) + result = Template(_(template)).safe_substitute(**available_data) + result = re.sub(r"\$\w+", "", result) + return result \ No newline at end of file diff --git a/src/wxUI/dialogs/configuration.py b/src/wxUI/dialogs/configuration.py index 1bc5de9c..e8220ae9 100644 --- a/src/wxUI/dialogs/configuration.py +++ b/src/wxUI/dialogs/configuration.py @@ -233,6 +233,20 @@ class other_buffers(wx.Panel): buffers_list.append(self.buffers.get_text_column(i, 0)) return buffers_list +class templates(wx.Panel, baseDialog.BaseWXDialog): + def __init__(self, parent, tweet_template, dm_template, sent_dm_template, person_template): + super(templates, self).__init__(parent) + sizer = wx.BoxSizer(wx.VERTICAL) + self.tweet = wx.Button(self, wx.ID_ANY, _("Edit template for tweets. Current template: {}").format(tweet_template)) + sizer.Add(self.tweet, 0, wx.ALL, 5) + self.dm = wx.Button(self, wx.ID_ANY, _("Edit template for direct messages. Current template: {}").format(dm_template)) + sizer.Add(self.dm, 0, wx.ALL, 5) + self.sent_dm = wx.Button(self, wx.ID_ANY, _("Edit template for sent direct messages. Current template: {}").format(sent_dm_template)) + sizer.Add(self.sent_dm, 0, wx.ALL, 5) + self.person = wx.Button(self, wx.ID_ANY, _("Edit template for persons. Current template: {}").format(person_template)) + sizer.Add(self.person, 0, wx.ALL, 5) + self.SetSizer(sizer) + class ignoredClients(wx.Panel): def __init__(self, parent, choices): super(ignoredClients, self).__init__(parent=parent) @@ -380,6 +394,10 @@ class configurationDialog(baseDialog.BaseWXDialog): self.ignored_clients = ignoredClients(self.notebook, ignored_clients_list) self.notebook.AddPage(self.ignored_clients, _(u"Ignored clients")) + def create_templates(self, tweet_template, dm_template, sent_dm_template, person_template): + self.templates = templates(self.notebook, tweet_template=tweet_template, dm_template=dm_template, sent_dm_template=sent_dm_template, person_template=person_template) + self.notebook.AddPage(self.templates, _("Templates")) + def create_sound(self, output_devices, input_devices, soundpacks): self.sound = sound(self.notebook, output_devices, input_devices, soundpacks) self.notebook.AddPage(self.sound, _(u"Sound")) diff --git a/src/wxUI/dialogs/twitterDialogs/__init__.py b/src/wxUI/dialogs/twitterDialogs/__init__.py index 6a829f06..f067ea11 100644 --- a/src/wxUI/dialogs/twitterDialogs/__init__.py +++ b/src/wxUI/dialogs/twitterDialogs/__init__.py @@ -1 +1,2 @@ -from .tweetDialogs import tweet, reply, dm, viewTweet, viewNonTweet, poll \ No newline at end of file +from .tweetDialogs import tweet, reply, dm, viewTweet, viewNonTweet, poll +from .templateDialogs import EditTemplateDialog \ No newline at end of file diff --git a/src/wxUI/dialogs/twitterDialogs/templateDialogs.py b/src/wxUI/dialogs/twitterDialogs/templateDialogs.py new file mode 100644 index 00000000..9efa96aa --- /dev/null +++ b/src/wxUI/dialogs/twitterDialogs/templateDialogs.py @@ -0,0 +1,52 @@ +# -*- coding: UTF-8 -*- +import wx +import output +from typing import List + +class EditTemplateDialog(wx.Dialog): + def __init__(self, template: str, variables: List[str] = [], default_template: str = "", *args, **kwds) -> None: + super(EditTemplateDialog, self).__init__(parent=None, title=_("Edit Template"), *args, **kwds) + self.default_template = default_template + mainSizer = wx.BoxSizer(wx.VERTICAL) + sizer_1 = wx.BoxSizer(wx.HORIZONTAL) + mainSizer.Add(sizer_1, 1, wx.EXPAND, 0) + label_1 = wx.StaticText(self, wx.ID_ANY, _("Edit template")) + sizer_1.Add(label_1, 0, 0, 0) + self.template = wx.TextCtrl(self, wx.ID_ANY, template) + sizer_1.Add(self.template, 0, 0, 0) + sizer_2 = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Available variables")), wx.HORIZONTAL) + mainSizer.Add(sizer_2, 1, wx.EXPAND, 0) + self.variables = wx.ListBox(self, wx.ID_ANY, choices=["$"+v for v in variables]) + self.variables.Bind(wx.EVT_CHAR_HOOK, self.on_keypress) + sizer_2.Add(self.variables, 0, 0, 0) + sizer_3 = wx.StdDialogButtonSizer() + mainSizer.Add(sizer_3, 0, wx.ALIGN_RIGHT | wx.ALL, 4) + self.button_SAVE = wx.Button(self, wx.ID_SAVE) + self.button_SAVE.SetDefault() + sizer_3.AddButton(self.button_SAVE) + self.button_CANCEL = wx.Button(self, wx.ID_CANCEL) + sizer_3.AddButton(self.button_CANCEL) + self.button_RESTORE = wx.Button(self, wx.ID_ANY, _("Restore template")) + self.button_RESTORE.Bind(wx.EVT_BUTTON, self.on_restore) + sizer_3.AddButton(self.button_CANCEL) + sizer_3.Realize() + self.SetSizer(mainSizer) + mainSizer.Fit(self) + self.SetAffirmativeId(self.button_SAVE.GetId()) + self.SetEscapeId(self.button_CANCEL.GetId()) + self.Layout() + + def on_keypress(self, event, *args, **kwargs): + if event.GetKeyCode() == wx.WXK_RETURN: + self.template.ChangeValue(self.template.GetValue()+self.variables.GetStringSelection()+", ") + output.speak(self.template.GetValue()+self.variables.GetStringSelection()+", ") + return + event.Skip() + + def on_restore(self, *args, **kwargs) -> None: + self.template.ChangeValue(self.default_template) + output.speak(_("Restored template to {}.").format(self.default_template)) + self.template.SetFocus() + +def invalid_template() -> None: + wx.MessageDialog(None, _("the template you have specified include variables that do not exists for the object. Please fix the template and try again. For your reference, you can see a list of all available variables in the variables list while editing your template."), _("Invalid template"), wx.ICON_ERROR).ShowModal() \ No newline at end of file