diff --git a/doc/changelog.md b/doc/changelog.md index 912468e3..ecf61aec 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -11,6 +11,7 @@ TWBlue Changelog * Fixed an issue that was preventing TWBlue to create more than one user timeline during startup. * TWBlue will display properly new paragraphs in mastodon posts. * In the session manager, Mastodon sessions are now displayed including the instance to avoid confusion. + * TWBlue will now read default visibility preferences when posting new statuses, and display sensitive content. These preferences can be set on the mastodon instance, in the account's preferences section. If you wish to change TWBlue's behavior and have it not read those preferences from your instance, but instead set the default public visibility and hide sensitive content, you can uncheck the Read preferences from instance checkbox in the account options. ## Changes in version 2022.12.13 diff --git a/src/controller/buffers/mastodon/base.py b/src/controller/buffers/mastodon/base.py index 9fa5ade8..25510090 100644 --- a/src/controller/buffers/mastodon/base.py +++ b/src/controller/buffers/mastodon/base.py @@ -9,6 +9,7 @@ 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 @@ -72,14 +73,22 @@ class BaseBuffer(base.Buffer): post.message.destroy() def get_formatted_message(self): - return self.compose_function(self.get_item(), self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"])[1] + 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["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, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"]) return t @@ -124,7 +133,10 @@ class BaseBuffer(base.Buffer): else: post = self.session.db[self.name][0] output.speak(_("New post in {0}").format(self.get_buffer_name())) - output.speak(" ".join(self.compose_function(post, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"]))) + 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["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())) @@ -150,13 +162,16 @@ class BaseBuffer(base.Buffer): 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["general"]["relative_times"], self.session.settings["general"]["show_screen_names"]) + post = self.compose_function(i, self.session.db, 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["general"]["relative_times"], self.session.settings["general"]["show_screen_names"]) + post = self.compose_function(i, self.session.db, 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) @@ -185,27 +200,33 @@ class BaseBuffer(base.Buffer): 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["general"]["relative_times"], self.session.settings["general"]["show_screen_names"]) + post = self.compose_function(i, self.session.db, 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["general"]["relative_times"], self.session.settings["general"]["show_screen_names"]) + post = self.compose_function(i, self.session.db, 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["general"]["relative_times"], self.session.settings["general"]["show_screen_names"]) + post = self.compose_function(i, self.session.db, 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): - post = self.compose_function(item, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"]) + 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["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: @@ -214,7 +235,10 @@ class BaseBuffer(base.Buffer): output.speak(" ".join(post[:2]), speech=self.session.settings["reporting"]["speech_reporting"], braille=self.session.settings["reporting"]["braille_reporting"]) def update_item(self, item, position): - post = self.compose_function(item, self.session.db, self.session.settings["general"]["relative_times"], self.session.settings["general"]["show_screen_names"]) + 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["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): @@ -439,6 +463,7 @@ class BaseBuffer(base.Buffer): 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 user_details(self): @@ -472,7 +497,11 @@ class BaseBuffer(base.Buffer): item = self.get_item() if item.reblog != None: item = item.reblog - item = self.session.api.status(item.id) + 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: @@ -482,7 +511,11 @@ class BaseBuffer(base.Buffer): item = self.get_item() if item.reblog != None: item = item.reblog - item = self.session.api.status(item.id) + 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: @@ -491,7 +524,11 @@ class BaseBuffer(base.Buffer): def view_item(self): post = self.get_item() # Update object so we can retrieve newer stats - post = self.session.api.status(id=post.id) + try: + post = self.session.api.status(id=post.id) + except MastodonNotFoundError: + output.speak(_("No status found with that ID")) + return # print(post) msg = messages.viewPost(post, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url()) diff --git a/src/controller/buffers/mastodon/conversations.py b/src/controller/buffers/mastodon/conversations.py index e00a2c02..06b5c8ef 100644 --- a/src/controller/buffers/mastodon/conversations.py +++ b/src/controller/buffers/mastodon/conversations.py @@ -4,6 +4,7 @@ import logging import wx import widgetUtils import output +from mastodon import MastodonNotFoundError from controller.mastodon import messages from controller.buffers.mastodon.base import BaseBuffer from mysc.thread_utils import call_threaded @@ -200,7 +201,11 @@ class ConversationBuffer(BaseBuffer): self.execution_time = current_time log.debug("Starting stream for buffer %s, account %s and type %s" % (self.name, self.account, self.type)) log.debug("args: %s, kwargs: %s" % (self.args, self.kwargs)) - self.post = self.session.api.status(id=self.post.id) + 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 = [] diff --git a/src/controller/buffers/mastodon/mentions.py b/src/controller/buffers/mastodon/mentions.py index 1e6402c8..23659d3b 100644 --- a/src/controller/buffers/mastodon/mentions.py +++ b/src/controller/buffers/mastodon/mentions.py @@ -56,13 +56,16 @@ class MentionsBuffer(BaseBuffer): 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["general"]["relative_times"], self.session.settings["general"]["show_screen_names"]) + post = self.compose_function(i, self.session.db, 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["general"]["relative_times"], self.session.settings["general"]["show_screen_names"]) + post = self.compose_function(i, self.session.db, 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) \ No newline at end of file diff --git a/src/controller/mastodon/messages.py b/src/controller/mastodon/messages.py index 1a43d162..cd41dfe0 100644 --- a/src/controller/mastodon/messages.py +++ b/src/controller/mastodon/messages.py @@ -185,6 +185,11 @@ class post(messages.basicTweet): visibility_settings = ["public", "unlisted", "private", "direct"] return visibility_settings[self.message.visibility.GetSelection()] + def set_visibility(self, setting): + visibility_settings = ["public", "unlisted", "private", "direct"] + visibility_setting = visibility_settings.index(setting) + self.message.visibility.SetSelection(setting) + class viewPost(post): def __init__(self, post, offset_hours=0, date="", item_url=""): if post.reblog != None: diff --git a/src/controller/mastodon/settings.py b/src/controller/mastodon/settings.py index 104a2631..be4d5f93 100644 --- a/src/controller/mastodon/settings.py +++ b/src/controller/mastodon/settings.py @@ -32,6 +32,7 @@ class accountSettingsController(globalSettingsController): # widgetUtils.connect_event(self.dialog.general.userAutocompletionScan, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_scan) # widgetUtils.connect_event(self.dialog.general.userAutocompletionManage, widgetUtils.BUTTON_PRESSED, self.on_autocompletion_manage) self.dialog.set_value("general", "relative_time", self.config["general"]["relative_times"]) + self.dialog.set_value("general", "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"]) @@ -111,6 +112,7 @@ class accountSettingsController(globalSettingsController): self.needs_restart = True log.debug("Triggered app restart due to change in relative times.") self.config["general"]["relative_times"] = self.dialog.get_value("general", "relative_time") + self.config["general"]["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") diff --git a/src/mastodon.defaults b/src/mastodon.defaults index a49cf471..618e0cd3 100644 --- a/src/mastodon.defaults +++ b/src/mastodon.defaults @@ -6,6 +6,7 @@ ignored_clients = list(default=list()) [general] relative_times = boolean(default=True) +read_preferences_from_instance = boolean(default=True) max_posts_per_call = integer(default=40) reverse_timelines = boolean(default=False) persist_size = integer(default=0) diff --git a/src/sessions/mastodon/compose.py b/src/sessions/mastodon/compose.py index db368bda..d6d60a63 100644 --- a/src/sessions/mastodon/compose.py +++ b/src/sessions/mastodon/compose.py @@ -3,7 +3,7 @@ import arrow import languageHandler from . import utils, templates -def compose_post(post, db, relative_times, show_screen_names): +def compose_post(post, db, relative_times, show_screen_names, safe=True): if show_screen_names == False: user = post.account.get("display_name") if user == "": @@ -16,9 +16,9 @@ def compose_post(post, db, relative_times, show_screen_names): else: ts = original_date.shift(hours=db["utc_offset"]).format(_("dddd, MMMM D, YYYY H:m"), locale=languageHandler.curLang[:2]) if post.reblog != None: - text = _("Boosted from @{}: {}").format(post.reblog.account.acct, templates.process_text(post.reblog)) + text = _("Boosted from @{}: {}").format(post.reblog.account.acct, templates.process_text(post.reblog, safe=safe)) else: - text = templates.process_text(post) + text = templates.process_text(post, safe=safe) source = post.get("application", "") # "" means remote user, None for legacy apps so we should cover both sides. if source != None and source != "": @@ -27,7 +27,7 @@ def compose_post(post, db, relative_times, show_screen_names): source = "" return [user+", ", text, ts+", ", source] -def compose_user(user, db, relative_times=True, show_screen_names=False): +def compose_user(user, db, relative_times=True, show_screen_names=False, safe=False): original_date = arrow.get(user.created_at) if relative_times: ts = original_date.humanize(locale=languageHandler.curLang[:2]) @@ -38,7 +38,7 @@ def compose_user(user, db, relative_times=True, show_screen_names=False): name = user.get("username") return [_("%s (@%s). %s followers, %s following, %s posts. Joined %s") % (name, user.acct, user.followers_count, user.following_count, user.statuses_count, ts)] -def compose_conversation(conversation, db, relative_times, show_screen_names): +def compose_conversation(conversation, db, relative_times, show_screen_names, safe=False): users = [] for account in conversation.accounts: if account.display_name != "": @@ -50,7 +50,7 @@ def compose_conversation(conversation, db, relative_times, show_screen_names): text = _("Last message from {}: {}").format(last_post[0], last_post[1]) return [users, text, last_post[-2], last_post[-1]] -def compose_notification(notification, db, relative_times, show_screen_names): +def compose_notification(notification, db, relative_times, show_screen_names, safe=False): if show_screen_names == False: user = notification.account.get("display_name") if user == "": @@ -64,15 +64,15 @@ def compose_notification(notification, db, relative_times, show_screen_names): ts = original_date.shift(hours=db["utc_offset"]).format(_("dddd, MMMM D, YYYY H:m"), locale=languageHandler.curLang[:2]) text = "Unknown: %r" % (notification) if notification.type == "mention": - text = _("{username} has mentionned you: {status}").format(username=user, status=",".join(compose_post(notification.status, db, relative_times, show_screen_names))) + text = _("{username} has mentionned you: {status}").format(username=user, status=",".join(compose_post(notification.status, db, relative_times, show_screen_names, safe=safe))) elif notification.type == "reblog": - text = _("{username} has boosted: {status}").format(username=user, status=",".join(compose_post(notification.status, db, relative_times, show_screen_names))) + text = _("{username} has boosted: {status}").format(username=user, status=",".join(compose_post(notification.status, db, relative_times, show_screen_names, safe=safe))) elif notification.type == "favourite": - text = _("{username} has added to favorites: {status}").format(username=user, status=",".join(compose_post(notification.status, db, relative_times, show_screen_names))) + text = _("{username} has added to favorites: {status}").format(username=user, status=",".join(compose_post(notification.status, db, relative_times, show_screen_names, safe=safe))) elif notification.type == "follow": text = _("{username} has followed you.").format(username=user) elif notification.type == "poll": - text = _("A poll in which you have voted has expired: {status}").format(status=",".join(compose_post(notification.status, db, relative_times, show_screen_names))) + text = _("A poll in which you have voted has expired: {status}").format(status=",".join(compose_post(notification.status, db, relative_times, show_screen_names, safe=safe))) elif notification.type == "follow_request": text = _("{username} wants to follow you.").format(username=user) return [user, text, ts] \ No newline at end of file diff --git a/src/sessions/mastodon/session.py b/src/sessions/mastodon/session.py index e0f21f88..2a15eb07 100644 --- a/src/sessions/mastodon/session.py +++ b/src/sessions/mastodon/session.py @@ -29,6 +29,8 @@ class Session(base.baseSession): self.type = "mastodon" self.db["pagination_info"] = dict() self.char_limit = 500 + self.post_visibility = "public" + self.expand_spoilers = False pub.subscribe(self.on_status, "mastodon.status_received") pub.subscribe(self.on_status_updated, "mastodon.status_updated") pub.subscribe(self.on_notification, "mastodon.notification_received") @@ -98,14 +100,18 @@ class Session(base.baseSession): offset = time.timezone if (time.localtime().tm_isdst == 0) else time.altzone offset = offset / 60 / 60 * -1 self.db["utc_offset"] = offset + instance = self.api.instance() if len(self.supported_languages) == 0: - self.supported_languages = self.api.instance().languages + self.supported_languages = instance.languages self.get_lists() self.get_muted_users() # determine instance custom characters limit. - instance = self.api.instance() if hasattr(instance, "configuration") and hasattr(instance.configuration, "statuses") and hasattr(instance.configuration.statuses, "max_characters"): self.char_limit = instance.configuration.statuses.max_characters + # User preferences for some things. + preferences = self.api.preferences() + self.post_visibility = preferences.get("posting:default:visibility") + self.expand_spoilers = preferences.get("reading:expand:spoilers") self.settings.write() def get_lists(self): diff --git a/src/wxUI/dialogs/mastodon/configuration.py b/src/wxUI/dialogs/mastodon/configuration.py index 9b0af7c5..5c219c4b 100644 --- a/src/wxUI/dialogs/mastodon/configuration.py +++ b/src/wxUI/dialogs/mastodon/configuration.py @@ -22,6 +22,8 @@ class generalAccount(wx.Panel, baseDialog.BaseWXDialog): sizer.Add(autocompletionSizer, 0, wx.ALL, 5) self.relative_time = wx.CheckBox(self, wx.ID_ANY, _("Relative timestamps")) sizer.Add(self.relative_time, 0, wx.ALL, 5) + self.read_preferences_from_instance = wx.CheckBox(self, wx.ID_ANY, _("Read preferences from instance (default visibility when publishing and displaying sensitive content)")) + sizer.Add(self.read_preferences_from_instance, 0, wx.ALL, 5) itemsPerCallBox = wx.BoxSizer(wx.HORIZONTAL) itemsPerCallBox.Add(wx.StaticText(self, -1, _("Items on each API call")), 0, wx.ALL, 5) self.itemsPerApiCall = wx.SpinCtrl(self, wx.ID_ANY)