From 10511d3022f1d6fb69c3f2fa5e53e73584f53b45 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 25 Aug 2021 11:13:12 -0500 Subject: [PATCH 1/6] Improvements to reading tweets with many mentions on them --- doc/changelog.md | 1 + src/sessions/twitter/compose.py | 10 +++++----- src/sessions/twitter/utils.py | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index e0b555bd..a68f71c6 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -2,6 +2,7 @@ ## changes in this version +* When reading a tweet, if the tweet contains more than 2 consecutive mentions, TWBlue will announce how many more users the tweet includes, as opposed to read every user in the conversation. You still can display the tweet to read all users. * Added user aliases to TWBlue. This feature allows you to rename user's display names on Twitter, so the next time you'll read an user it will be announced as you configured. For adding an alias to an user, select the "add alias" option in the user menu, located in the menu bar. This feature works only if you have set display screen names unchecked. Users are displayed with their display name in people buffers only. This action is supported in all keymaps, although it is undefined by default. ([#389](https://github.com/manuelcortez/TWBlue/pull/389)) * It is possible to undefine keystrokes in the current keymap in TWBlue. This allows you, for example, to redefine keystrokes completely. * Added a limited version of the Twitter's Streaming API: The Streaming API will work only for tweets, and will receive tweets only by people you follow. Protected users are not possible to be streamed. It is possible that during high tweet traffic, the Stream might get disconnected at times, but TWBlue should be capable of detecting this problem and reconnecting the stream again. ([#385](https://github.com/manuelcortez/TWBlue/pull/385)) diff --git a/src/sessions/twitter/compose.py b/src/sessions/twitter/compose.py index b6ceb4ae..1b761390 100644 --- a/src/sessions/twitter/compose.py +++ b/src/sessions/twitter/compose.py @@ -45,9 +45,9 @@ def compose_tweet(tweet, db, relative_times, show_screen_names=False, session=No else: value = "text" if hasattr(tweet, "retweeted_status") and value != "message": - text = StripChars(getattr(tweet.retweeted_status, value)) + text = utils.clean_mentions(StripChars(getattr(tweet.retweeted_status, value))) else: - text = StripChars(getattr(tweet, value)) + text = utils.clean_mentions(StripChars(getattr(tweet, value))) if show_screen_names: user = session.get_user(tweet.user).screen_name else: @@ -111,7 +111,7 @@ def compose_quoted_tweet(quoted_tweet, original_tweet, show_screen_names=False, value = "full_text" else: value = "text" - text = StripChars(getattr(quoted_tweet, value)) + text = utils.clean_mentions(StripChars(getattr(quoted_tweet, value))) if show_screen_names: quoting_user = session.get_user(quoted_tweet.user).screen_name else: @@ -124,9 +124,9 @@ def compose_quoted_tweet(quoted_tweet, original_tweet, show_screen_names=False, if hasattr(original_tweet, "message"): original_text = original_tweet.message elif hasattr(original_tweet, "full_text"): - original_text = StripChars(original_tweet.full_text) + original_text = utils.clean_mentions(StripChars(original_tweet.full_text)) else: - original_text = StripChars(original_tweet.text) + original_text = utils.clean_mentions(StripChars(original_tweet.text)) quoted_tweet.message = _(u"{0}. Quoted tweet from @{1}: {2}").format( text, original_user, original_text) quoted_tweet = tweets.clear_url(quoted_tweet) if hasattr(original_tweet, "entities") and original_tweet.entities.get("urls"): diff --git a/src/sessions/twitter/utils.py b/src/sessions/twitter/utils.py index 70ebfb1e..05c4c52c 100644 --- a/src/sessions/twitter/utils.py +++ b/src/sessions/twitter/utils.py @@ -244,3 +244,20 @@ def expand_urls(text, entities): if url["url"] in text: text = text.replace(url["url"], url["expanded_url"]) return text + +def clean_mentions(text): + new_text = text + mentionned_people = [u for u in re.finditer("(?<=^|(?<=[^a-zA-Z0-9-\.]))@([A-Za-z0-9_]+)", text)] + if len(mentionned_people) <= 2: + return text + end = -2 + total_users = 0 + for user in mentionned_people: + if abs(user.start()-end) < 3: + new_text = new_text.replace(user.group(0), "") + total_users = total_users+1 + end = user.end() + if total_users < 1: + return text + new_text = _("{user_1}, {user_2} and {all_users} more: {text}").format(user_1=mentionned_people[0].group(0), user_2=mentionned_people[1].group(0), all_users=total_users-2, text=new_text) + return new_text \ No newline at end of file From f4ecf1088543147d9ac00783982e35c37163a0e5 Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 25 Aug 2021 16:30:37 -0500 Subject: [PATCH 2/6] Allow to copy tweet URLS from tweet displayer dialog --- doc/changelog.md | 1 + src/controller/buffers/twitter/base.py | 8 ++++++-- src/controller/buffers/twitter/people.py | 5 ++--- src/controller/mainController.py | 7 +++++-- src/controller/messages.py | 11 ++++++++++- src/wxUI/dialogs/message.py | 8 ++++++-- 6 files changed, 30 insertions(+), 10 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 4f9951ec..6e7ca628 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -3,6 +3,7 @@ ## changes in this version * When reading a tweet, if the tweet contains more than 2 consecutive mentions, TWBlue will announce how many more users the tweet includes, as opposed to read every user in the conversation. You still can display the tweet to read all users. +* In the tweet displayer, It is possible to copy a link to the current tweet or person by pressing a button called "copy link to clipboard". * Added a keymap capable to work under Windows 11. ([#391](https://github.com/manuelcortez/TWBlue/pull/391)) * Added user aliases to TWBlue. This feature allows you to rename user's display names on Twitter, so the next time you'll read an user it will be announced as you configured. For adding an alias to an user, select the "add alias" option in the user menu, located in the menu bar. This feature works only if you have set display screen names unchecked. Users are displayed with their display name in people buffers only. This action is supported in all keymaps, although it is undefined by default. ([#389](https://github.com/manuelcortez/TWBlue/pull/389)) * There are some changes to the autocomplete users feature: diff --git a/src/controller/buffers/twitter/base.py b/src/controller/buffers/twitter/base.py index b7b535e1..fa872844 100644 --- a/src/controller/buffers/twitter/base.py +++ b/src/controller/buffers/twitter/base.py @@ -644,8 +644,12 @@ class BaseBuffer(base.Buffer): original_tweet.text = utils.find_urls_in_text(original_tweet.text, original_tweet.entities) return compose.compose_quoted_tweet(quoted_tweet, original_tweet, self.session.db, self.session.settings["general"]["relative_times"]) - def open_in_browser(self, *args, **kwargs): + def get_item_url(self): tweet = self.get_tweet() - output.speak(_(u"Opening item in web browser...")) url = "https://twitter.com/{screen_name}/status/{tweet_id}".format(screen_name=self.session.get_user(tweet.user).screen_name, tweet_id=tweet.id) + return url + + def open_in_browser(self, *args, **kwargs): + url = self.get_item_url() + output.speak(_(u"Opening item in web browser...")) webbrowser.open(url) \ No newline at end of file diff --git a/src/controller/buffers/twitter/people.py b/src/controller/buffers/twitter/people.py index 09d89c5d..eae6c718 100644 --- a/src/controller/buffers/twitter/people.py +++ b/src/controller/buffers/twitter/people.py @@ -252,8 +252,7 @@ class PeopleBuffer(base.BaseBuffer): 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(_(u"{0} new followers.").format(number_of_items)) - def open_in_browser(self, *args, **kwargs): + def get_item_url(self, *args, **kwargs): tweet = self.get_tweet() - output.speak(_(u"Opening item in web browser...")) url = "https://twitter.com/{screen_name}".format(screen_name=tweet.screen_name) - webbrowser.open(url) \ No newline at end of file + return url \ No newline at end of file diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 0db1a6ca..30fd3f1a 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -831,7 +831,7 @@ class Controller(object): return elif buffer.type == "baseBuffer" or buffer.type == "favourites_timeline" or buffer.type == "list" or buffer.type == "search": tweet, tweetsList = buffer.get_full_tweet() - msg = messages.viewTweet(tweet, tweetsList, utc_offset=buffer.session.db["utc_offset"]) + msg = messages.viewTweet(tweet, tweetsList, utc_offset=buffer.session.db["utc_offset"], item_url=buffer.get_item_url()) elif buffer.type == "dm": non_tweet = buffer.get_formatted_message() item = buffer.get_right_tweet() @@ -839,8 +839,11 @@ class Controller(object): date = original_date.shift(seconds=buffer.session.db["utc_offset"]).format(_(u"MMM D, YYYY. H:m"), locale=languageHandler.getLanguage()) msg = messages.viewTweet(non_tweet, [], False, date=date) else: + item_url = "" + if hasattr(buffer, "get_item_url"): + item_url = buffer.get_item_url() non_tweet = buffer.get_formatted_message() - msg = messages.viewTweet(non_tweet, [], False) + msg = messages.viewTweet(non_tweet, [], False, item_url=item_url) def open_in_browser(self, *args, **kwargs): buffer = self.get_current_buffer() diff --git a/src/controller/messages.py b/src/controller/messages.py index e4a7747c..38082427 100644 --- a/src/controller/messages.py +++ b/src/controller/messages.py @@ -205,7 +205,7 @@ class dm(basicTweet): c.show_menu("dm") class viewTweet(basicTweet): - def __init__(self, tweet, tweetList, is_tweet=True, utc_offset=0, date=""): + def __init__(self, tweet, tweetList, is_tweet=True, utc_offset=0, date="", item_url=""): """ This represents a tweet displayer. However it could be used for showing something wich is not a tweet, like a direct message or an event. param tweet: A dictionary that represents a full tweet or a string for non-tweets. param tweetList: If is_tweet is set to True, this could be a list of quoted tweets. @@ -273,6 +273,10 @@ class viewTweet(basicTweet): text = tweet self.message = message.viewNonTweet(text, date) 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) if self.contain_urls() == True: self.message.enable_button("unshortenButton") @@ -290,3 +294,8 @@ class viewTweet(basicTweet): if "https://twitter.com/" in i: text = text.replace(i, "\n") return text + + def share(self, *args, **kwargs): + if hasattr(self, "item_url"): + output.copy(self.item_url) + output.speak(_("Link copied to clipboard.")) \ No newline at end of file diff --git a/src/wxUI/dialogs/message.py b/src/wxUI/dialogs/message.py index 6a74b32e..093934f5 100644 --- a/src/wxUI/dialogs/message.py +++ b/src/wxUI/dialogs/message.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals -from builtins import str import wx import widgetUtils @@ -356,6 +354,8 @@ class viewTweet(widgetUtils.BaseDialog): infoBox.Add(sourceBox, 0, wx.ALL, 5) mainBox.Add(infoBox, 0, wx.ALL, 5) mainBox.Add(dateBox, 0, wx.ALL, 5) + self.share = wx.Button(panel, wx.ID_ANY, _("Copy link to clipboard")) + self.share.Enable(False) self.spellcheck = wx.Button(panel, -1, _("Check &spelling..."), size=wx.DefaultSize) self.unshortenButton = wx.Button(panel, -1, _(u"&Expand URL"), size=wx.DefaultSize) self.unshortenButton.Disable() @@ -363,6 +363,7 @@ class viewTweet(widgetUtils.BaseDialog): cancelButton = wx.Button(panel, wx.ID_CANCEL, _(u"C&lose"), size=wx.DefaultSize) cancelButton.SetDefault() buttonsBox = wx.BoxSizer(wx.HORIZONTAL) + buttonsBox.Add(self.share, 0, wx.ALL, 5) buttonsBox.Add(self.spellcheck, 0, wx.ALL, 5) buttonsBox.Add(self.unshortenButton, 0, wx.ALL, 5) buttonsBox.Add(self.translateButton, 0, wx.ALL, 5) @@ -429,6 +430,8 @@ class viewNonTweet(widgetUtils.BaseDialog): dateBox.Add(dateLabel, 0, wx.ALL, 5) dateBox.Add(date, 0, wx.ALL, 5) mainBox.Add(dateBox, 0, wx.ALL, 5) + self.share = wx.Button(panel, wx.ID_ANY, _("Copy link to clipboard")) + self.share.Enable(False) self.spellcheck = wx.Button(panel, -1, _("Check &spelling..."), size=wx.DefaultSize) self.unshortenButton = wx.Button(panel, -1, _(u"&Expand URL"), size=wx.DefaultSize) self.unshortenButton.Disable() @@ -436,6 +439,7 @@ class viewNonTweet(widgetUtils.BaseDialog): cancelButton = wx.Button(panel, wx.ID_CANCEL, _(u"C&lose"), size=wx.DefaultSize) cancelButton.SetDefault() buttonsBox = wx.BoxSizer(wx.HORIZONTAL) + buttonsBox.Add(self.share, 0, wx.ALL, 5) buttonsBox.Add(self.spellcheck, 0, wx.ALL, 5) buttonsBox.Add(self.unshortenButton, 0, wx.ALL, 5) buttonsBox.Add(self.translateButton, 0, wx.ALL, 5) From 3d8519313e3e93ec0bbce7d34ea49f17f957f3eb Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Thu, 26 Aug 2021 08:56:51 -0500 Subject: [PATCH 3/6] Switched Geocoding library to OpenStreetMap's Nominatim API. Closes #390 --- doc/changelog.md | 1 + requirements.txt | 2 +- src/controller/mainController.py | 19 ++++++++----------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 6e7ca628..fc178196 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -9,6 +9,7 @@ * There are some changes to the autocomplete users feature: * Now users can search for twitter screen names or display names in the database. * It is possible to undefine keystrokes in the current keymap in TWBlue. This allows you, for example, to redefine keystrokes completely. +* We have changed our Geocoding service to the Nominatim API from OpenStreetMap. Addresses present in tweets are going to be determined by this service, as the Google Maps API now requires an API key. ([#390](https://github.com/manuelcortez/TWBlue/issues/390)) * Added a limited version of the Twitter's Streaming API: The Streaming API will work only for tweets, and will receive tweets only by people you follow. Protected users are not possible to be streamed. It is possible that during high tweet traffic, the Stream might get disconnected at times, but TWBlue should be capable of detecting this problem and reconnecting the stream again. ([#385](https://github.com/manuelcortez/TWBlue/pull/385)) * Fixed an issue that made TWBlue to not show a dialog when attempting to show a profile for a suspended user. ([#387](https://github.com/manuelcortez/TWBlue/issues/387)) * Added support for Twitter audio and videos: Tweets which contains audio or videos will be detected as audio items, and you can playback those with the regular command to play audios. ([#384,](https://github.com/manuelcortez/TWBlue/pull/384)) diff --git a/requirements.txt b/requirements.txt index a3282f7b..8a5c4744 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ oauthlib requests-oauthlib requests-toolbelt pypubsub -pygeocoder +geopy arrow python-dateutil futures diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 30fd3f1a..a3df454d 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -35,17 +35,16 @@ from mysc.repeating_timer import RepeatingTimer from mysc import restart import config import widgetUtils -import pygeocoder -from pygeolib import GeocoderError import logging import webbrowser +from geopy.geocoders import Nominatim from mysc import localization import os import languageHandler log = logging.getLogger("mainController") -geocoder = pygeocoder.Geocoder() +geocoder = Nominatim(user_agent="TWBlue") class Controller(object): @@ -1001,19 +1000,17 @@ class Controller(object): if tweet.coordinates != None: x = tweet.coordinates["coordinates"][0] y = tweet.coordinates["coordinates"][1] - address = geocoder.reverse_geocode(y, x, language = languageHandler.curLang) - if event == None: output.speak(address[0].__str__()) - else: self.view.show_address(address[0].__str__()) + address = geocoder.reverse("{}, {}".format(y, x), language = languageHandler.curLang) + if event == None: output.speak(address.address) + else: self.view.show_address(address.address) else: output.speak(_(u"There are no coordinates in this tweet")) - except GeocoderError: - output.speak(_(u"There are no results for the coordinates in this tweet")) except ValueError: output.speak(_(u"Error decoding coordinates. Try again later.")) - except KeyError: - pass +# except KeyError: +# pass except AttributeError: - pass + output.speak(_("Unable to find address in OpenStreetMap.")) def view_reverse_geocode(self, event=None): try: From f9864a887d97566d0d8c424e3197fa37589cba5f Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Thu, 26 Aug 2021 09:16:02 -0500 Subject: [PATCH 4/6] Fixed a small traceback that was happening when translating a direct message --- src/controller/messages.py | 2 +- src/wxUI/dialogs/message.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controller/messages.py b/src/controller/messages.py index 38082427..a748464d 100644 --- a/src/controller/messages.py +++ b/src/controller/messages.py @@ -102,7 +102,7 @@ class basicTweet(object): else: self.message.disable_button("shortenButton") self.message.disable_button("unshortenButton") - if self.message.get("long_tweet") == False: + if self.message.get("long_tweet") == False and hasattr(self, "max"): text = self.message.get_text() results = parse_tweet(text) self.message.set_title(_(u"%s - %s of %d characters") % (self.title, results.weightedLength, self.max)) diff --git a/src/wxUI/dialogs/message.py b/src/wxUI/dialogs/message.py index 093934f5..8eadfdd9 100644 --- a/src/wxUI/dialogs/message.py +++ b/src/wxUI/dialogs/message.py @@ -467,5 +467,5 @@ class viewNonTweet(widgetUtils.BaseDialog): self.text.SetFocus() def enable_button(self, buttonName): - if getattr(self, buttonName): + if hasattr(self, buttonName): return getattr(self, buttonName).Enable() From 3a5c1c10d3c105d63bd69183f26a66b14b1d3e1a Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Thu, 26 Aug 2021 15:13:34 -0500 Subject: [PATCH 5/6] Released a new snapshot --- src/application.py | 2 +- updates/snapshots.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/application.py b/src/application.py index a7baf074..e4253581 100644 --- a/src/application.py +++ b/src/application.py @@ -9,7 +9,7 @@ if snapshot == False: update_url = 'https://twblue.es/updates/stable.php' mirror_update_url = 'https://raw.githubusercontent.com/manuelcortez/TWBlue/next-gen/updates/stable.json' else: - version = "9" + version = "10" update_url = 'https://twblue.es/updates/snapshot.php' mirror_update_url = 'https://raw.githubusercontent.com/manuelcortez/TWBlue/next-gen/updates/snapshots.json' authors = ["Manuel Cortéz", "José Manuel Delicado"] diff --git a/updates/snapshots.json b/updates/snapshots.json index b3a3ff75..1b0348d7 100644 --- a/updates/snapshots.json +++ b/updates/snapshots.json @@ -1,4 +1,4 @@ -{"current_version": "8", +{"current_version": "10", "description": "Snapshot version.", "date": "unknown", "downloads": From 7c34204d17b2d744ec92da0a06b4273ae79c8a0e Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Fri, 27 Aug 2021 13:45:59 -0500 Subject: [PATCH 6/6] Allow to specify alternative configuration specs for sessions --- src/sessions/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sessions/base.py b/src/sessions/base.py index 63ee5142..9833edd1 100644 --- a/src/sessions/base.py +++ b/src/sessions/base.py @@ -43,6 +43,8 @@ class baseSession(object): self.logged = False self.settings = None self.db={} + # Config specification file. + self.config_spec = "conf.defaults" @property def is_logged(self): @@ -52,7 +54,7 @@ class baseSession(object): """ Get settings for a session.""" file_ = "%s/session.conf" % (self.session_id,) log.debug("Creating config file %s" % (file_,)) - self.settings = config_utils.load_config(os.path.join(paths.config_path(), file_), os.path.join(paths.app_path(), "Conf.defaults")) + self.settings = config_utils.load_config(os.path.join(paths.config_path(), file_), os.path.join(paths.app_path(), self.config_spec)) self.init_sound() self.load_persistent_data()