mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-03-06 01:17:32 +01:00
Avance
This commit is contained in:
@@ -1,104 +0,0 @@
|
|||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
# Add src to path
|
|
||||||
sys.path.insert(0, os.path.join(os.getcwd(), 'src'))
|
|
||||||
|
|
||||||
import config_utils
|
|
||||||
from configobj import ConfigObj
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# Setup simple logging
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
def test_config_save():
|
|
||||||
print("Beginning Config Save Test")
|
|
||||||
|
|
||||||
# 1. Setup paths
|
|
||||||
config_dir = os.path.join(os.getcwd(), 'test_config_dir')
|
|
||||||
if os.path.exists(config_dir):
|
|
||||||
shutil.rmtree(config_dir)
|
|
||||||
os.mkdir(config_dir)
|
|
||||||
|
|
||||||
session_id = "test_session"
|
|
||||||
session_dir = os.path.join(config_dir, session_id)
|
|
||||||
os.mkdir(session_dir)
|
|
||||||
|
|
||||||
config_path = os.path.join(session_dir, "session.conf")
|
|
||||||
# We use the ACTUAL atproto.defaults from src
|
|
||||||
spec_path = os.path.join(os.getcwd(), 'src', 'atproto.defaults')
|
|
||||||
|
|
||||||
print(f"Config Path: {config_path}")
|
|
||||||
print(f"Spec Path: {spec_path}")
|
|
||||||
|
|
||||||
if not os.path.exists(spec_path):
|
|
||||||
print("ERROR: Spec file not found at", spec_path)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 2. Simulate Load & Create
|
|
||||||
print("\n--- Loading Config (create empty) ---")
|
|
||||||
try:
|
|
||||||
# Mimic session.get_configuration
|
|
||||||
config = config_utils.load_config(config_path, spec_path)
|
|
||||||
except Exception as e:
|
|
||||||
print("Error loading config:", e)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 3. Modify Values
|
|
||||||
print("\n--- Modifying Values ---")
|
|
||||||
|
|
||||||
# Check if section exists, if not, create it
|
|
||||||
if 'atproto' not in config:
|
|
||||||
print("Section 'atproto' missing (expected for new file). Using defaults from spec?")
|
|
||||||
# ConfigObj with spec should automatically have sections if create_empty=True?
|
|
||||||
# Actually config_utils.load_config sets create_empty=True
|
|
||||||
|
|
||||||
# Let's inspect what we have
|
|
||||||
print("Current Config Keys:", config.keys())
|
|
||||||
|
|
||||||
# If section is missing (it might be if file was empty and defaults didn't force creation yet?), force create
|
|
||||||
if 'atproto' not in config:
|
|
||||||
print("Creating 'atproto' section manually (simulating what might happen if defaults don't auto-create structure)")
|
|
||||||
config['atproto'] = {}
|
|
||||||
|
|
||||||
config['atproto']['handle'] = "test_user.bsky.social"
|
|
||||||
config['atproto']['session_string'] = "fake_session_string_12345"
|
|
||||||
|
|
||||||
print(f"Set handle: {config['atproto']['handle']}")
|
|
||||||
print(f"Set session_string: {config['atproto']['session_string']}")
|
|
||||||
|
|
||||||
# 4. Write
|
|
||||||
print("\n--- Writing Config ---")
|
|
||||||
config.write()
|
|
||||||
print("Write called.")
|
|
||||||
|
|
||||||
# 5. Read Back from Disk (Raw)
|
|
||||||
print("\n--- Reading Back (Raw Text) ---")
|
|
||||||
if os.path.exists(config_path):
|
|
||||||
with open(config_path, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
print("File Content:")
|
|
||||||
print(content)
|
|
||||||
|
|
||||||
if "session_string = fake_session_string_12345" in content:
|
|
||||||
print("SUCCESS: Session string found in file.")
|
|
||||||
else:
|
|
||||||
print("FAILURE: Session string NOT found in file.")
|
|
||||||
else:
|
|
||||||
print("FAILURE: File does not exist.")
|
|
||||||
|
|
||||||
# 6. Read Back (using config_utils again)
|
|
||||||
print("\n--- Reading Back (config_utils) ---")
|
|
||||||
config2 = config_utils.load_config(config_path, spec_path)
|
|
||||||
val = config2['atproto']['session_string']
|
|
||||||
print(f"Read session_string: {val}")
|
|
||||||
|
|
||||||
if val == "fake_session_string_12345":
|
|
||||||
print("SUCCESS: Read back correct value.")
|
|
||||||
else:
|
|
||||||
print("FAILURE: Read back mismatched value.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_config_save()
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Simple example of using Blueski session programmatically.
|
|
||||||
|
|
||||||
This is a minimal example showing how to use the Blueski session.
|
|
||||||
For full testing with wx dialogs, use test_atproto_session.py instead.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add src to path
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
|
||||||
|
|
||||||
from sessions.blueski import session
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# Setup basic logging
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("Blueski Session Simple Example")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Create session
|
|
||||||
print("\n1. Creating session...")
|
|
||||||
s = session.Session(session_id="example_blueski")
|
|
||||||
|
|
||||||
# Try to get configuration (will create folder if needed)
|
|
||||||
print("2. Loading configuration...")
|
|
||||||
s.get_configuration()
|
|
||||||
|
|
||||||
# Try to login (will fail if no stored credentials)
|
|
||||||
print("3. Attempting login...")
|
|
||||||
try:
|
|
||||||
s.login()
|
|
||||||
print(f" ✓ Logged in as: {s.get_name()}")
|
|
||||||
print(f" User DID: {s.db.get('user_id', 'unknown')}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Login failed: {e}")
|
|
||||||
print("\n To authorize a new session:")
|
|
||||||
print(" - Run test_atproto_session.py for GUI-based auth")
|
|
||||||
print(" - Or manually call s.authorise() after importing wx")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Show session info
|
|
||||||
print("\n4. Session information:")
|
|
||||||
print(f" Logged: {s.logged}")
|
|
||||||
print(f" Handle: {s.settings['blueski']['handle']}")
|
|
||||||
print(f" Service: {s.settings['blueski'].get('service_url', '')}")
|
|
||||||
print(f" Has session_string: {bool(s.settings['blueski']['session_string'])}")
|
|
||||||
|
|
||||||
# Test logout
|
|
||||||
print("\n5. Testing logout...")
|
|
||||||
s.logout()
|
|
||||||
print(f" Logged: {s.logged}")
|
|
||||||
print(f" Session string cleared: {not s.settings['blueski']['session_string']}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
print("Example complete!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -56,4 +56,4 @@ winpaths==0.2
|
|||||||
wxPython==4.2.4
|
wxPython==4.2.4
|
||||||
youtube-dl==2021.12.17
|
youtube-dl==2021.12.17
|
||||||
zipp==3.23.0
|
zipp==3.23.0
|
||||||
atproto>=0.0.45
|
atproto>=0.0.65
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import wx
|
||||||
|
import output
|
||||||
|
from wxUI.dialogs.blueski.showUserProfile import ShowUserProfileDialog
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import languageHandler # Ensure _() injection
|
import languageHandler # Ensure _() injection
|
||||||
|
|
||||||
@@ -19,10 +22,16 @@ class Handler:
|
|||||||
|
|
||||||
def create_buffers(self, session, createAccounts=True, controller=None):
|
def create_buffers(self, session, createAccounts=True, controller=None):
|
||||||
name = session.get_name()
|
name = session.get_name()
|
||||||
controller.accounts.append(name)
|
|
||||||
if createAccounts:
|
if createAccounts:
|
||||||
from pubsub import pub
|
from pubsub import pub
|
||||||
pub.sendMessage("core.create_account", name=name, session_id=session.session_id, logged=True)
|
pub.sendMessage("core.create_account", name=name, session_id=session.session_id, logged=session.logged)
|
||||||
|
|
||||||
|
if not session.logged:
|
||||||
|
logger.debug(f"Session {session.session_id} is not logged in, skipping timeline buffer creation.")
|
||||||
|
return
|
||||||
|
if name not in controller.accounts:
|
||||||
|
controller.accounts.append(name)
|
||||||
|
|
||||||
root_position = controller.view.search(name, name)
|
root_position = controller.view.search(name, name)
|
||||||
# Discover/home timeline
|
# Discover/home timeline
|
||||||
from pubsub import pub
|
from pubsub import pub
|
||||||
@@ -45,6 +54,66 @@ class Handler:
|
|||||||
start=False,
|
start=False,
|
||||||
kwargs=dict(parent=controller.view.nb, name="following_timeline", session=session)
|
kwargs=dict(parent=controller.view.nb, name="following_timeline", session=session)
|
||||||
)
|
)
|
||||||
|
# Notifications
|
||||||
|
pub.sendMessage(
|
||||||
|
"createBuffer",
|
||||||
|
buffer_type="notifications",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=_("Notifications"),
|
||||||
|
parent_tab=root_position,
|
||||||
|
start=False,
|
||||||
|
kwargs=dict(parent=controller.view.nb, name="notifications", session=session)
|
||||||
|
)
|
||||||
|
# Likes
|
||||||
|
pub.sendMessage(
|
||||||
|
"createBuffer",
|
||||||
|
buffer_type="likes",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=_("Likes"),
|
||||||
|
parent_tab=root_position,
|
||||||
|
start=False,
|
||||||
|
kwargs=dict(parent=controller.view.nb, name="likes", session=session)
|
||||||
|
)
|
||||||
|
# Followers
|
||||||
|
pub.sendMessage(
|
||||||
|
"createBuffer",
|
||||||
|
buffer_type="FollowersBuffer",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=_("Followers"),
|
||||||
|
parent_tab=root_position,
|
||||||
|
start=False,
|
||||||
|
kwargs=dict(parent=controller.view.nb, name="followers", session=session)
|
||||||
|
)
|
||||||
|
# Following (Users)
|
||||||
|
pub.sendMessage(
|
||||||
|
"createBuffer",
|
||||||
|
buffer_type="FollowingBuffer",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=_("Following (Users)"),
|
||||||
|
parent_tab=root_position,
|
||||||
|
start=False,
|
||||||
|
kwargs=dict(parent=controller.view.nb, name="following", session=session)
|
||||||
|
)
|
||||||
|
# Blocks
|
||||||
|
pub.sendMessage(
|
||||||
|
"createBuffer",
|
||||||
|
buffer_type="BlocksBuffer",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=_("Blocked Users"),
|
||||||
|
parent_tab=root_position,
|
||||||
|
start=False,
|
||||||
|
kwargs=dict(parent=controller.view.nb, name="blocked", session=session)
|
||||||
|
)
|
||||||
|
# Chats
|
||||||
|
pub.sendMessage(
|
||||||
|
"createBuffer",
|
||||||
|
buffer_type="ConversationListBuffer",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=_("Chats"),
|
||||||
|
parent_tab=root_position,
|
||||||
|
start=False,
|
||||||
|
kwargs=dict(parent=controller.view.nb, name="direct_messages", session=session)
|
||||||
|
)
|
||||||
|
|
||||||
def start_buffer(self, controller, buffer):
|
def start_buffer(self, controller, buffer):
|
||||||
"""Start a newly created Bluesky buffer."""
|
"""Start a newly created Bluesky buffer."""
|
||||||
@@ -86,6 +155,45 @@ class Handler:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Error opening Bluesky account settings dialog")
|
logger.exception("Error opening Bluesky account settings dialog")
|
||||||
|
|
||||||
|
def user_details(self, buffer):
|
||||||
|
"""Show user profile dialog for the selected user/post."""
|
||||||
|
session = getattr(buffer, "session", None)
|
||||||
|
if not session:
|
||||||
|
output.speak(_("No active session to view user details."), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
item = buffer.get_item() if hasattr(buffer, "get_item") else None
|
||||||
|
if not item:
|
||||||
|
output.speak(_("No user selected or identified to view details."), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
user_ident = None
|
||||||
|
|
||||||
|
# If we're in a user list, the item itself is the user profile dict/model.
|
||||||
|
if g(item, "did") or g(item, "handle"):
|
||||||
|
user_ident = g(item, "did") or g(item, "handle")
|
||||||
|
else:
|
||||||
|
author = g(item, "author")
|
||||||
|
if not author:
|
||||||
|
post = g(item, "post") or g(item, "record")
|
||||||
|
author = g(post, "author") if post else None
|
||||||
|
if author:
|
||||||
|
user_ident = g(author, "did") or g(author, "handle")
|
||||||
|
|
||||||
|
if not user_ident:
|
||||||
|
output.speak(_("No user selected or identified to view details."), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
parent = getattr(buffer, "buffer", None) or wx.GetApp().GetTopWindow()
|
||||||
|
dialog = ShowUserProfileDialog(parent, session, user_ident)
|
||||||
|
dialog.ShowModal()
|
||||||
|
dialog.Destroy()
|
||||||
|
|
||||||
async def handle_action(self, action_name: str, user_id: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
async def handle_action(self, action_name: str, user_id: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
logger.debug("handle_action stub: %s %s %s", action_name, user_id, payload)
|
logger.debug("handle_action stub: %s %s %s", action_name, user_id, payload)
|
||||||
return None
|
return None
|
||||||
@@ -97,3 +205,156 @@ class Handler:
|
|||||||
async def handle_user_command(self, command: str, user_id: str, target_user_id: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
async def handle_user_command(self, command: str, user_id: str, target_user_id: str, payload: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
logger.debug("handle_user_command stub: %s %s %s %s", command, user_id, target_user_id, payload)
|
logger.debug("handle_user_command stub: %s %s %s %s", command, user_id, target_user_id, payload)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def add_to_favourites(self, buffer):
|
||||||
|
"""Standard action for Alt+Win+F"""
|
||||||
|
if hasattr(buffer, "add_to_favorites"):
|
||||||
|
buffer.add_to_favorites()
|
||||||
|
elif hasattr(buffer, "on_like"):
|
||||||
|
# Fallback
|
||||||
|
buffer.on_like(None)
|
||||||
|
|
||||||
|
def remove_from_favourites(self, buffer):
|
||||||
|
"""Standard action for Alt+Shift+Win+F"""
|
||||||
|
if hasattr(buffer, "remove_from_favorites"):
|
||||||
|
buffer.remove_from_favorites()
|
||||||
|
elif hasattr(buffer, "on_like"):
|
||||||
|
buffer.on_like(None)
|
||||||
|
|
||||||
|
def follow(self, buffer):
|
||||||
|
"""Standard action for Ctrl+Win+S"""
|
||||||
|
session = getattr(buffer, "session", None)
|
||||||
|
if not session:
|
||||||
|
output.speak(_("No active session."), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
user_ident = None
|
||||||
|
item = buffer.get_item() if hasattr(buffer, "get_item") else None
|
||||||
|
if item:
|
||||||
|
if g(item, "handle") or g(item, "did"):
|
||||||
|
user_ident = g(item, "handle") or g(item, "did")
|
||||||
|
else:
|
||||||
|
author = g(item, "author")
|
||||||
|
if not author:
|
||||||
|
post = g(item, "post") or g(item, "record")
|
||||||
|
author = g(post, "author") if post else None
|
||||||
|
if author:
|
||||||
|
user_ident = g(author, "handle") or g(author, "did")
|
||||||
|
|
||||||
|
users = [user_ident] if user_ident else []
|
||||||
|
from controller.blueski import userActions as user_actions_controller
|
||||||
|
user_actions_controller.userActions(session, users)
|
||||||
|
|
||||||
|
def open_conversation(self, controller, buffer):
|
||||||
|
"""Standard action for Control+Win+C"""
|
||||||
|
item = buffer.get_item()
|
||||||
|
if not item:
|
||||||
|
return
|
||||||
|
|
||||||
|
uri = None
|
||||||
|
if hasattr(buffer, "get_selected_item_id"):
|
||||||
|
uri = buffer.get_selected_item_id()
|
||||||
|
if not uri:
|
||||||
|
uri = getattr(item, "uri", None) or (item.get("post", {}).get("uri") if isinstance(item, dict) else None)
|
||||||
|
if not uri: return
|
||||||
|
|
||||||
|
# Buffer Title
|
||||||
|
author = getattr(item, "author", None) or (item.get("post", {}).get("author") if isinstance(item, dict) else None)
|
||||||
|
handle = getattr(author, "handle", "unknown") if author else "unknown"
|
||||||
|
title = _("Conversation with {0}").format(handle)
|
||||||
|
|
||||||
|
from pubsub import pub
|
||||||
|
pub.sendMessage(
|
||||||
|
"createBuffer",
|
||||||
|
buffer_type="conversation",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=title,
|
||||||
|
parent_tab=controller.view.search(buffer.session.get_name(), buffer.session.get_name()) if hasattr(buffer.session, "get_name") else None,
|
||||||
|
start=True,
|
||||||
|
kwargs=dict(parent=controller.view.nb, name=title, session=buffer.session, uri=uri)
|
||||||
|
)
|
||||||
|
|
||||||
|
def open_followers_timeline(self, main_controller, session, user_payload=None):
|
||||||
|
actor, handle = self._resolve_actor(session, user_payload)
|
||||||
|
if not actor:
|
||||||
|
output.speak(_("No user selected."), True)
|
||||||
|
return
|
||||||
|
self._open_user_list(main_controller, session, actor, handle, list_type="followers")
|
||||||
|
|
||||||
|
def open_following_timeline(self, main_controller, session, user_payload=None):
|
||||||
|
actor, handle = self._resolve_actor(session, user_payload)
|
||||||
|
if not actor:
|
||||||
|
output.speak(_("No user selected."), True)
|
||||||
|
return
|
||||||
|
self._open_user_list(main_controller, session, actor, handle, list_type="following")
|
||||||
|
|
||||||
|
def _resolve_actor(self, session, user_payload):
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
actor = None
|
||||||
|
handle = None
|
||||||
|
if user_payload:
|
||||||
|
actor = g(user_payload, "did") or g(user_payload, "handle")
|
||||||
|
handle = g(user_payload, "handle") or g(user_payload, "did")
|
||||||
|
if not actor:
|
||||||
|
actor = session.db.get("user_id") or session.db.get("user_name")
|
||||||
|
handle = session.db.get("user_name") or actor
|
||||||
|
return actor, handle
|
||||||
|
|
||||||
|
def _open_user_list(self, main_controller, session, actor, handle, list_type):
|
||||||
|
account_name = session.get_name()
|
||||||
|
own_actor = session.db.get("user_id") or session.db.get("user_name")
|
||||||
|
own_handle = session.db.get("user_name")
|
||||||
|
if actor == own_actor or (own_handle and actor == own_handle):
|
||||||
|
name = "followers" if list_type == "followers" else "following"
|
||||||
|
index = main_controller.view.search(name, account_name)
|
||||||
|
if index is not None:
|
||||||
|
main_controller.view.change_buffer(index)
|
||||||
|
return
|
||||||
|
|
||||||
|
list_name = f"{handle}-{list_type}"
|
||||||
|
if main_controller.search_buffer(list_name, account_name):
|
||||||
|
index = main_controller.view.search(list_name, account_name)
|
||||||
|
if index is not None:
|
||||||
|
main_controller.view.change_buffer(index)
|
||||||
|
return
|
||||||
|
|
||||||
|
title = _("Followers for {user}").format(user=handle) if list_type == "followers" else _("Following for {user}").format(user=handle)
|
||||||
|
from pubsub import pub
|
||||||
|
pub.sendMessage(
|
||||||
|
"createBuffer",
|
||||||
|
buffer_type="FollowersBuffer" if list_type == "followers" else "FollowingBuffer",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=title,
|
||||||
|
parent_tab=main_controller.view.search(account_name, account_name),
|
||||||
|
start=True,
|
||||||
|
kwargs=dict(parent=main_controller.view.nb, name=list_name, session=session, actor=actor)
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, buffer, controller):
|
||||||
|
"""Standard action for delete key / menu item"""
|
||||||
|
item = buffer.get_item()
|
||||||
|
if not item: return
|
||||||
|
|
||||||
|
uri = getattr(item, "uri", None) or (item.get("post", {}).get("uri") if isinstance(item, dict) else None)
|
||||||
|
if not uri: return
|
||||||
|
|
||||||
|
import wx
|
||||||
|
if wx.MessageBox(_("Are you sure you want to delete this post?"), _("Delete post"), wx.YES_NO | wx.ICON_QUESTION) == wx.YES:
|
||||||
|
if buffer.session.delete_post(uri):
|
||||||
|
import output
|
||||||
|
output.speak(_("Post deleted."))
|
||||||
|
# Refresh buffer
|
||||||
|
if hasattr(buffer, "start_stream"):
|
||||||
|
buffer.start_stream(mandatory=True, play_sound=False)
|
||||||
|
else:
|
||||||
|
import output
|
||||||
|
output.speak(_("Failed to delete post."))
|
||||||
|
|||||||
@@ -1,75 +1,98 @@
|
|||||||
from __future__ import annotations
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
import widgetUtils
|
||||||
|
import output
|
||||||
|
from wxUI.dialogs.blueski import userActions as userActionsDialog
|
||||||
|
import languageHandler
|
||||||
|
|
||||||
fromapprove.translation import translate as _
|
log = logging.getLogger("controller.blueski.userActions")
|
||||||
# fromapprove.controller.mastodon import userActions as mastodon_user_actions # If adapting
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
fromapprove.sessions.blueski.session import Session as BlueskiSession # Adjusted
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# This file defines user-specific actions that can be performed on Blueski entities,
|
|
||||||
# typically represented as buttons or links in the UI, often on user profiles or posts.
|
|
||||||
|
|
||||||
# For Blueski, actions might include:
|
|
||||||
# - Viewing a user's profile on Bluesky/Blueski instance.
|
|
||||||
# - Following/Unfollowing a user.
|
|
||||||
# - Muting/Blocking a user.
|
|
||||||
# - Reporting a user.
|
|
||||||
# - Fetching a user's latest posts.
|
|
||||||
|
|
||||||
# These actions are often presented in a context menu or as direct buttons.
|
|
||||||
# The `get_user_actions` method in the BlueskiSession class would define these.
|
|
||||||
# This file would contain the implementation or further handling logic if needed,
|
|
||||||
# or if actions are too complex for simple lambda/method calls in the session class.
|
|
||||||
|
|
||||||
# Example structure for defining an action:
|
|
||||||
# (This might be more detailed if actions require forms or multi-step processes)
|
|
||||||
|
|
||||||
# def view_profile_action(session: BlueskiSession, user_id: str) -> dict[str, Any]:
|
|
||||||
# """
|
|
||||||
# Generates data for a "View Profile on Blueski" action.
|
|
||||||
# user_id here would be the Blueski DID or handle.
|
|
||||||
# """
|
|
||||||
# # profile_url = f"https://bsky.app/profile/{user_id}" # Example, construct from handle or DID
|
|
||||||
# # This might involve resolving DID to handle or vice-versa if only one is known.
|
|
||||||
# # handle = await session.util.get_username_from_user_id(user_id) or user_id
|
|
||||||
# # profile_url = f"https://bsky.app/profile/{handle}"
|
|
||||||
|
|
||||||
# return {
|
|
||||||
# "id": "blueski_view_profile",
|
|
||||||
# "label": _("View Profile on Bluesky"),
|
|
||||||
# "icon": "external-link-alt", # FontAwesome icon name
|
|
||||||
# "action_type": "link", # "link", "modal", "api_call"
|
|
||||||
# "url": profile_url, # For "link" type
|
|
||||||
# # "api_endpoint": "/api/blueski/user_action", # For "api_call"
|
|
||||||
# # "payload": {"action": "view_profile", "target_user_id": user_id},
|
|
||||||
# "confirmation_required": False,
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
# async def follow_user_action_handler(session: BlueskiSession, target_user_id: str) -> dict[str, Any]:
|
class BasicUserSelector(object):
|
||||||
# """
|
def __init__(self, session, users=None):
|
||||||
# Handles the 'follow_user' action for Blueski.
|
super(BasicUserSelector, self).__init__()
|
||||||
# target_user_id should be the DID of the user to follow.
|
self.session = session
|
||||||
# """
|
self.create_dialog(users=users or [])
|
||||||
# # success = await session.util.follow_user(target_user_id)
|
|
||||||
# # if success:
|
def create_dialog(self, users):
|
||||||
# # return {"status": "success", "message": _("User {target_user_id} followed.").format(target_user_id=target_user_id)}
|
pass
|
||||||
# # else:
|
|
||||||
# # return {"status": "error", "message": _("Failed to follow user {target_user_id}.").format(target_user_id=target_user_id)}
|
def resolve_profile(self, actor):
|
||||||
# return {"status": "pending", "message": "Follow action not implemented yet."}
|
try:
|
||||||
|
return self.session.get_profile(actor)
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error resolving Bluesky profile for %s.", actor)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# The list of available actions is typically defined in the Session class,
|
class userActions(BasicUserSelector):
|
||||||
# e.g., BlueskiSession.get_user_actions(). That method would return a list
|
def __init__(self, *args, **kwargs):
|
||||||
# of dictionaries, and this file might provide handlers for more complex actions
|
super(userActions, self).__init__(*args, **kwargs)
|
||||||
# if they aren't simple API calls defined directly in the session's util.
|
if self.dialog.get_response() == widgetUtils.OK:
|
||||||
|
self.process_action()
|
||||||
|
|
||||||
# For now, this file can be a placeholder if most actions are simple enough
|
def create_dialog(self, users):
|
||||||
# to be handled directly by the session.util methods or basic handler routes.
|
self.dialog = userActionsDialog.UserActionsDialog(users)
|
||||||
|
|
||||||
logger.info("Blueski userActions module loaded (placeholders).")
|
def process_action(self):
|
||||||
|
action = self.dialog.get_action()
|
||||||
|
actor = self.dialog.get_user().strip()
|
||||||
|
if not actor:
|
||||||
|
output.speak(_("No user specified."), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
profile = self.resolve_profile(actor)
|
||||||
|
if not profile:
|
||||||
|
output.speak(_("User not found."), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
did = g(profile, "did")
|
||||||
|
viewer = g(profile, "viewer") or {}
|
||||||
|
|
||||||
|
if not did:
|
||||||
|
output.speak(_("User identifier not available."), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if action == "follow":
|
||||||
|
if self.session.follow_user(did):
|
||||||
|
output.speak(_("Followed."))
|
||||||
|
else:
|
||||||
|
output.speak(_("Failed to follow user."), True)
|
||||||
|
elif action == "unfollow":
|
||||||
|
follow_uri = g(viewer, "following")
|
||||||
|
if not follow_uri:
|
||||||
|
output.speak(_("Follow information not available."), True)
|
||||||
|
return
|
||||||
|
if self.session.unfollow_user(follow_uri):
|
||||||
|
output.speak(_("Unfollowed."))
|
||||||
|
else:
|
||||||
|
output.speak(_("Failed to unfollow user."), True)
|
||||||
|
elif action == "mute":
|
||||||
|
if self.session.mute_user(did):
|
||||||
|
output.speak(_("Muted."))
|
||||||
|
else:
|
||||||
|
output.speak(_("Failed to mute user."), True)
|
||||||
|
elif action == "unmute":
|
||||||
|
if self.session.unmute_user(did):
|
||||||
|
output.speak(_("Unmuted."))
|
||||||
|
else:
|
||||||
|
output.speak(_("Failed to unmute user."), True)
|
||||||
|
elif action == "block":
|
||||||
|
if self.session.block_user(did):
|
||||||
|
output.speak(_("Blocked."))
|
||||||
|
else:
|
||||||
|
output.speak(_("Failed to block user."), True)
|
||||||
|
elif action == "unblock":
|
||||||
|
block_uri = g(viewer, "blocking")
|
||||||
|
if not block_uri:
|
||||||
|
output.speak(_("Block information not available."), True)
|
||||||
|
return
|
||||||
|
if self.session.unblock_user(block_uri):
|
||||||
|
output.speak(_("Unblocked."))
|
||||||
|
else:
|
||||||
|
output.speak(_("Failed to unblock user."), True)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from . import base
|
|||||||
log = logging.getLogger("controller.buffers.base.account")
|
log = logging.getLogger("controller.buffers.base.account")
|
||||||
|
|
||||||
class AccountBuffer(base.Buffer):
|
class AccountBuffer(base.Buffer):
|
||||||
def __init__(self, parent, name, account, account_id):
|
def __init__(self, parent, name, account, account_id, session=None):
|
||||||
super(AccountBuffer, self).__init__(parent, None, name)
|
super(AccountBuffer, self).__init__(parent, None, name)
|
||||||
log.debug("Initializing buffer %s, account %s" % (name, account,))
|
log.debug("Initializing buffer %s, account %s" % (name, account,))
|
||||||
self.buffer = buffers.accountPanel(parent, name)
|
self.buffer = buffers.accountPanel(parent, name)
|
||||||
@@ -53,4 +53,4 @@ class AccountBuffer(base.Buffer):
|
|||||||
else:
|
else:
|
||||||
self.buffer.change_autostart(False)
|
self.buffer.change_autostart(False)
|
||||||
config.app["sessions"]["ignored_sessions"].append(self.account_id)
|
config.app["sessions"]["ignored_sessions"].append(self.account_id)
|
||||||
config.app.write()
|
config.app.write()
|
||||||
|
|||||||
4
src/controller/buffers/blueski/__init__.py
Normal file
4
src/controller/buffers/blueski/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from .timeline import HomeTimeline, FollowingTimeline, NotificationBuffer, Conversation
|
||||||
|
from .user import FollowersBuffer, FollowingBuffer, BlocksBuffer
|
||||||
|
from .chat import ConversationListBuffer, ChatBuffer as ChatMessageBuffer
|
||||||
579
src/controller/buffers/blueski/base.py
Normal file
579
src/controller/buffers/blueski/base.py
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
import wx
|
||||||
|
import output
|
||||||
|
import sound
|
||||||
|
import config
|
||||||
|
import widgetUtils
|
||||||
|
from pubsub import pub
|
||||||
|
from controller.buffers.base import base
|
||||||
|
from sessions.blueski import compose
|
||||||
|
from wxUI.buffers.blueski import panels as BlueskiPanels
|
||||||
|
|
||||||
|
log = logging.getLogger("controller.buffers.blueski.base")
|
||||||
|
|
||||||
|
class BaseBuffer(base.Buffer):
|
||||||
|
def __init__(self, parent=None, name=None, session=None, *args, **kwargs):
|
||||||
|
# Adapt params to BaseBuffer
|
||||||
|
# BaseBuffer expects (parent, function, name, sessionObject, account)
|
||||||
|
function = "timeline" # Dummy
|
||||||
|
sessionObject = session
|
||||||
|
account = session.get_name() if session else "Unknown"
|
||||||
|
|
||||||
|
super(BaseBuffer, self).__init__(parent, function, name=name, sessionObject=sessionObject, account=account, *args, **kwargs)
|
||||||
|
|
||||||
|
self.session = sessionObject
|
||||||
|
self.account = account
|
||||||
|
self.name = name
|
||||||
|
self.create_buffer(parent, name)
|
||||||
|
self.buffer.account = account
|
||||||
|
self.invisible = True
|
||||||
|
compose_func = kwargs.get("compose_func", "compose_post")
|
||||||
|
self.compose_function = getattr(compose, compose_func)
|
||||||
|
self.sound = sound
|
||||||
|
|
||||||
|
# Initialize DB list if needed
|
||||||
|
if self.name not in self.session.db:
|
||||||
|
self.session.db[self.name] = []
|
||||||
|
|
||||||
|
self.bind_events()
|
||||||
|
|
||||||
|
def create_buffer(self, parent, name):
|
||||||
|
# Default to HomePanel, can be overridden
|
||||||
|
self.buffer = BlueskiPanels.HomePanel(parent, name, account=self.account)
|
||||||
|
self.buffer.session = self.session
|
||||||
|
|
||||||
|
def bind_events(self):
|
||||||
|
# Bind essential events
|
||||||
|
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
if hasattr(self.buffer, "post"):
|
||||||
|
self.buffer.post.Bind(wx.EVT_BUTTON, self.on_post)
|
||||||
|
if hasattr(self.buffer, "reply"):
|
||||||
|
self.buffer.reply.Bind(wx.EVT_BUTTON, self.on_reply)
|
||||||
|
if hasattr(self.buffer, "repost"):
|
||||||
|
self.buffer.repost.Bind(wx.EVT_BUTTON, self.on_repost)
|
||||||
|
if hasattr(self.buffer, "like"):
|
||||||
|
self.buffer.like.Bind(wx.EVT_BUTTON, self.on_like)
|
||||||
|
if hasattr(self.buffer, "dm"):
|
||||||
|
self.buffer.dm.Bind(wx.EVT_BUTTON, self.on_dm)
|
||||||
|
if hasattr(self.buffer, "actions"):
|
||||||
|
self.buffer.actions.Bind(wx.EVT_BUTTON, self.user_actions)
|
||||||
|
|
||||||
|
def on_post(self, evt):
|
||||||
|
from wxUI.dialogs.blueski import postDialogs
|
||||||
|
dlg = postDialogs.Post(caption=_("New Post"))
|
||||||
|
if dlg.ShowModal() == wx.ID_OK:
|
||||||
|
text, files, cw, langs = dlg.get_payload()
|
||||||
|
self.session.send_message(message=text, files=files, cw_text=cw, langs=langs)
|
||||||
|
output.speak(_("Sending..."))
|
||||||
|
dlg.Destroy()
|
||||||
|
|
||||||
|
def on_reply(self, evt):
|
||||||
|
item = self.get_item()
|
||||||
|
if not item: return
|
||||||
|
|
||||||
|
# item is a feed object or dict.
|
||||||
|
# We need its URI.
|
||||||
|
uri = self.get_selected_item_id()
|
||||||
|
if not uri:
|
||||||
|
uri = item.get("uri") if isinstance(item, dict) else getattr(item, "uri", None)
|
||||||
|
# Attempt to get CID if present for consistency, though send_message handles it
|
||||||
|
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
author = g(item, "author")
|
||||||
|
if not author:
|
||||||
|
post = g(item, "post") or g(item, "record")
|
||||||
|
author = g(post, "author") if post else None
|
||||||
|
handle = g(author, "handle", "")
|
||||||
|
initial_text = f"@{handle} " if handle and not handle.startswith("@") else (f"{handle} " if handle else "")
|
||||||
|
|
||||||
|
from wxUI.dialogs.blueski import postDialogs
|
||||||
|
dlg = postDialogs.Post(caption=_("Reply"), text=initial_text)
|
||||||
|
if dlg.ShowModal() == wx.ID_OK:
|
||||||
|
text, files, cw, langs = dlg.get_payload()
|
||||||
|
self.session.send_message(message=text, files=files, reply_to=uri, cw_text=cw, langs=langs)
|
||||||
|
output.speak(_("Sending reply..."))
|
||||||
|
dlg.Destroy()
|
||||||
|
|
||||||
|
def on_repost(self, evt):
|
||||||
|
self.share_item(confirm=True)
|
||||||
|
|
||||||
|
def share_item(self, confirm=False, *args, **kwargs):
|
||||||
|
item = self.get_item()
|
||||||
|
if not item: return
|
||||||
|
uri = item.get("uri") if isinstance(item, dict) else getattr(item, "uri", None)
|
||||||
|
|
||||||
|
if confirm:
|
||||||
|
if wx.MessageBox(_("Repost this?"), _("Confirm"), wx.YES_NO | wx.ICON_QUESTION) != wx.YES:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.session.repost(uri)
|
||||||
|
output.speak(_("Reposted."))
|
||||||
|
|
||||||
|
def on_like(self, evt):
|
||||||
|
self.toggle_favorite(confirm=True)
|
||||||
|
|
||||||
|
def toggle_favorite(self, confirm=False, *args, **kwargs):
|
||||||
|
item = self.get_item()
|
||||||
|
if not item: return
|
||||||
|
uri = item.get("uri") if isinstance(item, dict) else getattr(item, "uri", None)
|
||||||
|
|
||||||
|
if confirm:
|
||||||
|
if wx.MessageBox(_("Like this post?"), _("Confirm"), wx.YES_NO | wx.ICON_QUESTION) != wx.YES:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.session.like(uri)
|
||||||
|
output.speak(_("Liked."))
|
||||||
|
|
||||||
|
def add_to_favorites(self, *args, **kwargs):
|
||||||
|
self.toggle_favorite(confirm=False)
|
||||||
|
|
||||||
|
def remove_from_favorites(self, *args, **kwargs):
|
||||||
|
# We need unlike support in session
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_dm(self, evt):
|
||||||
|
self.send_message()
|
||||||
|
|
||||||
|
def send_message(self, *args, **kwargs):
|
||||||
|
# Global shortcut for DM
|
||||||
|
item = self.get_item()
|
||||||
|
if not item:
|
||||||
|
output.speak(_("No user selected to message."), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
author = getattr(item, "author", None) or (item.get("post", {}).get("author") if isinstance(item, dict) else None)
|
||||||
|
if not author:
|
||||||
|
# Try item itself if it's a user object (UserBuffer)
|
||||||
|
author = item
|
||||||
|
|
||||||
|
did = getattr(author, "did", None) or author.get("did")
|
||||||
|
handle = getattr(author, "handle", "unknown") or (author.get("handle") if isinstance(author, dict) else "unknown")
|
||||||
|
|
||||||
|
if not did:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.showing == False:
|
||||||
|
dlg = wx.TextEntryDialog(None, _("Message to {0}:").format(handle), _("Send Message"))
|
||||||
|
if dlg.ShowModal() == wx.ID_OK:
|
||||||
|
text = dlg.GetValue()
|
||||||
|
if text:
|
||||||
|
try:
|
||||||
|
api = self.session._ensure_client()
|
||||||
|
# Get or create conversation
|
||||||
|
res = api.chat.bsky.convo.get_convo_for_members({"members": [did]})
|
||||||
|
convo_id = res.convo.id
|
||||||
|
self.session.send_chat_message(convo_id, text)
|
||||||
|
output.speak(_("Message sent."), True)
|
||||||
|
except:
|
||||||
|
log.exception("Error sending Bluesky DM (invisible)")
|
||||||
|
output.speak(_("Failed to send message."), True)
|
||||||
|
dlg.Destroy()
|
||||||
|
return
|
||||||
|
|
||||||
|
# If showing, we'll just open the chat buffer for now as it's more structured
|
||||||
|
self.view_chat_with_user(did, handle)
|
||||||
|
|
||||||
|
def user_actions(self, *args, **kwargs):
|
||||||
|
pub.sendMessage("execute-action", action="follow")
|
||||||
|
|
||||||
|
def view_chat_with_user(self, did, handle):
|
||||||
|
try:
|
||||||
|
api = self.session._ensure_client()
|
||||||
|
res = api.chat.bsky.convo.get_convo_for_members({"members": [did]})
|
||||||
|
convo_id = res.convo.id
|
||||||
|
|
||||||
|
import application
|
||||||
|
title = _("Chat: {0}").format(handle)
|
||||||
|
application.app.controller.create_buffer(
|
||||||
|
buffer_type="chat_messages",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=title,
|
||||||
|
kwargs={"session": self.session, "convo_id": convo_id, "name": title},
|
||||||
|
start=True
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
output.speak(_("Could not open chat."), True)
|
||||||
|
|
||||||
|
def block_user(self, *args, **kwargs):
|
||||||
|
item = self.get_item()
|
||||||
|
if not item: return
|
||||||
|
author = getattr(item, "author", None) or (item.get("post", {}).get("author") if isinstance(item, dict) else item)
|
||||||
|
did = getattr(author, "did", None) or (author.get("did") if isinstance(author, dict) else None)
|
||||||
|
handle = getattr(author, "handle", "unknown") or (author.get("handle") if isinstance(author, dict) else "unknown")
|
||||||
|
|
||||||
|
if wx.MessageBox(_("Are you sure you want to block {0}?").format(handle), _("Block"), wx.YES_NO | wx.ICON_WARNING) == wx.YES:
|
||||||
|
if self.session.block_user(did):
|
||||||
|
output.speak(_("User blocked."))
|
||||||
|
else:
|
||||||
|
output.speak(_("Failed to block user."))
|
||||||
|
|
||||||
|
def unblock_user(self, *args, **kwargs):
|
||||||
|
# Unblocking usually needs the block record URI.
|
||||||
|
# In a UserBuffer (Blocks), it might be present.
|
||||||
|
item = self.get_item()
|
||||||
|
if not item: return
|
||||||
|
|
||||||
|
# Check if item itself is a block record or user object with viewer.blocking
|
||||||
|
block_uri = None
|
||||||
|
if isinstance(item, dict):
|
||||||
|
block_uri = item.get("viewer", {}).get("blocking")
|
||||||
|
else:
|
||||||
|
viewer = getattr(item, "viewer", None)
|
||||||
|
block_uri = getattr(viewer, "blocking", None) if viewer else None
|
||||||
|
|
||||||
|
if not block_uri:
|
||||||
|
output.speak(_("Could not find block information for this user."), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.session.unblock_user(block_uri):
|
||||||
|
output.speak(_("User unblocked."))
|
||||||
|
else:
|
||||||
|
output.speak(_("Failed to unblock user."))
|
||||||
|
|
||||||
|
def put_items_on_list(self, number_of_items):
|
||||||
|
list_to_use = self.session.db[self.name]
|
||||||
|
count = self.buffer.list.get_count()
|
||||||
|
reverse = False
|
||||||
|
try:
|
||||||
|
reverse = self.session.settings["general"].get("reverse_timelines", False)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
if number_of_items == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
safe = True
|
||||||
|
relative_times = self.session.settings["general"].get("relative_times", False)
|
||||||
|
show_screen_names = self.session.settings["general"].get("show_screen_names", False)
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
for i in list_to_use:
|
||||||
|
post = self.compose_function(i, self.session.db, self.session.settings, relative_times=relative_times, show_screen_names=show_screen_names, safe=safe)
|
||||||
|
self.buffer.list.insert_item(False, *post)
|
||||||
|
# Set selection
|
||||||
|
total = self.buffer.list.get_count()
|
||||||
|
if total > 0:
|
||||||
|
if not reverse:
|
||||||
|
self.buffer.list.select_item(total - 1) # Bottom
|
||||||
|
else:
|
||||||
|
self.buffer.list.select_item(0) # Top
|
||||||
|
|
||||||
|
elif count > 0 and number_of_items > 0:
|
||||||
|
if not reverse:
|
||||||
|
items = list_to_use[:number_of_items] # If we prepended items for normal (oldest first) timeline... wait.
|
||||||
|
# Standard flow: "New items" come from API.
|
||||||
|
# If standard timeline (oldest at top, newest at bottom): new items appended to DB.
|
||||||
|
# UI: append to bottom.
|
||||||
|
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, relative_times=relative_times, show_screen_names=show_screen_names, safe=safe)
|
||||||
|
self.buffer.list.insert_item(False, *post)
|
||||||
|
else:
|
||||||
|
# Reverse timeline (Newest at top).
|
||||||
|
# New items appended to DB? Or inserted at 0?
|
||||||
|
# Mastodon BaseBuffer:
|
||||||
|
# if reverse_timelines == False: items_db.insert(0, i) (Wait, insert at 0?)
|
||||||
|
# Actually let's look at `get_more_items` in Mastodon BaseBuffer again.
|
||||||
|
# "if self.session.settings["general"]["reverse_timelines"] == False: items_db.insert(0, i)"
|
||||||
|
# This means for standard timeline, new items (newer time) go to index 0?
|
||||||
|
# No, standard timeline usually has oldest at top. Retrieve "more items" usually means "newer items" or "older items" depending on context (streaming vs styling).
|
||||||
|
|
||||||
|
# Let's trust that we just need to insert based on how we updated DB in start_stream.
|
||||||
|
|
||||||
|
# For now, simplistic approach:
|
||||||
|
items = list_to_use[0:number_of_items] # Assuming we inserted at 0 in DB
|
||||||
|
# items.reverse() if needed?
|
||||||
|
for i in items:
|
||||||
|
post = self.compose_function(i, self.session.db, self.session.settings, relative_times=relative_times, show_screen_names=show_screen_names, safe=safe)
|
||||||
|
self.buffer.list.insert_item(True, *post) # Insert at 0 (True)
|
||||||
|
|
||||||
|
def reply(self, *args, **kwargs):
|
||||||
|
self.on_reply(None)
|
||||||
|
|
||||||
|
def post_status(self, *args, **kwargs):
|
||||||
|
self.on_post(None)
|
||||||
|
|
||||||
|
def share_item(self, *args, **kwargs):
|
||||||
|
self.on_repost(None)
|
||||||
|
|
||||||
|
def destroy_status(self, *args, **kwargs):
|
||||||
|
# Delete post
|
||||||
|
item = self.get_item()
|
||||||
|
if not item: return
|
||||||
|
uri = self.get_selected_item_id()
|
||||||
|
if not uri:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
uri = item.get("uri") or item.get("post", {}).get("uri")
|
||||||
|
else:
|
||||||
|
post = getattr(item, "post", None)
|
||||||
|
uri = getattr(item, "uri", None) or getattr(post, "uri", None)
|
||||||
|
if not uri:
|
||||||
|
output.speak(_("Could not find the post identifier."), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if author is self
|
||||||
|
# Implementation depends on parsing URI or checking active user DID vs author DID
|
||||||
|
# For now, just try and handle error
|
||||||
|
if wx.MessageBox(_("Delete this post?"), _("Confirm"), wx.YES_NO | wx.ICON_QUESTION) == wx.YES:
|
||||||
|
try:
|
||||||
|
ok = self.session.delete_post(uri)
|
||||||
|
if not ok:
|
||||||
|
output.speak(_("Could not delete."), True)
|
||||||
|
return
|
||||||
|
index = self.buffer.list.get_selected()
|
||||||
|
if index > -1 and self.session.db.get(self.name):
|
||||||
|
try:
|
||||||
|
self.session.db[self.name].pop(index)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.buffer.list.remove_item(index)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
output.speak(_("Deleted."))
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error deleting Bluesky post")
|
||||||
|
output.speak(_("Could not delete."), True)
|
||||||
|
|
||||||
|
def url(self, *args, **kwargs):
|
||||||
|
item = self.get_item()
|
||||||
|
if not item: return
|
||||||
|
|
||||||
|
uri = item.get("uri") if isinstance(item, dict) else getattr(item, "uri", None)
|
||||||
|
# Convert at:// uri to https://bsky.app link
|
||||||
|
if uri and "at://" in uri and "app.bsky.feed.post" in uri:
|
||||||
|
parts = uri.split("/")
|
||||||
|
# at://did:plc:xxx/app.bsky.feed.post/rkey
|
||||||
|
did = parts[2]
|
||||||
|
rkey = parts[-1]
|
||||||
|
|
||||||
|
# Need handle for prettier url, but did works? bluesky web supports profile/did/post/rkey?
|
||||||
|
# Let's try to find handle if possible
|
||||||
|
handle = None
|
||||||
|
if isinstance(item, dict):
|
||||||
|
handle = item.get("handle")
|
||||||
|
else:
|
||||||
|
handle = getattr(getattr(item, "author", None), "handle", None)
|
||||||
|
|
||||||
|
target = handle if handle else did
|
||||||
|
link = f"https://bsky.app/profile/{target}/post/{rkey}"
|
||||||
|
|
||||||
|
import webbrowser
|
||||||
|
webbrowser.open(link)
|
||||||
|
|
||||||
|
def audio(self, *args, **kwargs):
|
||||||
|
output.speak(_("Audio playback not supported for Bluesky yet."))
|
||||||
|
|
||||||
|
# Helper to map standard keys if they don't invoke the methods above via get_event
|
||||||
|
# But usually get_event is enough.
|
||||||
|
|
||||||
|
# Also implement "view_item" if standard keymap uses it
|
||||||
|
def get_formatted_message(self):
|
||||||
|
return self.compose_function(self.get_item(), self.session.db, self.session.settings, self.session.settings["general"].get("relative_times", False), self.session.settings["general"].get("show_screen_names", False))[1]
|
||||||
|
|
||||||
|
def get_message(self):
|
||||||
|
item = self.get_item()
|
||||||
|
if item is None:
|
||||||
|
return
|
||||||
|
# Use the compose function to get the full formatted text
|
||||||
|
# Bluesky compose returns [user, text, date, source]
|
||||||
|
composed = self.compose_function(item, self.session.db, self.session.settings, self.session.settings["general"].get("relative_times", False), self.session.settings["general"].get("show_screen_names", False))
|
||||||
|
# Join them for a full readout similar to Mastodon's template render
|
||||||
|
return " ".join(composed)
|
||||||
|
|
||||||
|
def view_item(self, *args, **kwargs):
|
||||||
|
self.view_conversation()
|
||||||
|
|
||||||
|
def view_conversation(self, *args, **kwargs):
|
||||||
|
item = self.get_item()
|
||||||
|
if not item: return
|
||||||
|
|
||||||
|
uri = item.get("uri") if isinstance(item, dict) else getattr(item, "uri", None)
|
||||||
|
if not uri: return
|
||||||
|
|
||||||
|
import application
|
||||||
|
controller = application.app.controller
|
||||||
|
|
||||||
|
handle = "Unknown"
|
||||||
|
if isinstance(item, dict):
|
||||||
|
handle = item.get("author", {}).get("handle", "Unknown")
|
||||||
|
else:
|
||||||
|
handle = getattr(getattr(item, "author", None), "handle", "Unknown")
|
||||||
|
|
||||||
|
title = _("Conversation: {0}").format(handle)
|
||||||
|
|
||||||
|
controller.create_buffer(
|
||||||
|
buffer_type="conversation",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=title,
|
||||||
|
kwargs={"session": self.session, "uri": uri, "name": title},
|
||||||
|
start=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_item(self):
|
||||||
|
index = self.buffer.list.get_selected()
|
||||||
|
if index > -1 and self.session.db.get(self.name) is not None:
|
||||||
|
# Logic implies DB order matches UI order
|
||||||
|
return self.session.db[self.name][index]
|
||||||
|
|
||||||
|
def get_selected_item_id(self):
|
||||||
|
item = self.get_item()
|
||||||
|
if not item:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(item, dict):
|
||||||
|
uri = item.get("uri")
|
||||||
|
if uri:
|
||||||
|
return uri
|
||||||
|
post = item.get("post") or item.get("record")
|
||||||
|
if isinstance(post, dict):
|
||||||
|
return post.get("uri")
|
||||||
|
return getattr(post, "uri", None)
|
||||||
|
|
||||||
|
return getattr(item, "uri", None) or getattr(getattr(item, "post", None), "uri", None)
|
||||||
|
|
||||||
|
def get_selected_item_author_details(self):
|
||||||
|
item = self.get_item()
|
||||||
|
if not item:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
author = None
|
||||||
|
if g(item, "did") or g(item, "handle"):
|
||||||
|
author = item
|
||||||
|
else:
|
||||||
|
author = g(item, "author")
|
||||||
|
if not author:
|
||||||
|
post = g(item, "post") or g(item, "record")
|
||||||
|
author = g(post, "author") if post else None
|
||||||
|
|
||||||
|
if not author:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"did": g(author, "did"),
|
||||||
|
"handle": g(author, "handle"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def process_items(self, items, play_sound=True):
|
||||||
|
"""
|
||||||
|
Process list of items (FeedViewPost objects), update DB, and update UI.
|
||||||
|
Returns number of new items.
|
||||||
|
"""
|
||||||
|
if not items:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Identify new items
|
||||||
|
new_items = []
|
||||||
|
current_uris = set()
|
||||||
|
# Create a set of keys from existing db to check duplicates
|
||||||
|
def get_key(it):
|
||||||
|
if isinstance(it, dict):
|
||||||
|
post = it.get("post")
|
||||||
|
if isinstance(post, dict) and post.get("uri"):
|
||||||
|
return post.get("uri")
|
||||||
|
if it.get("uri"):
|
||||||
|
return it.get("uri")
|
||||||
|
if it.get("id"):
|
||||||
|
return it.get("id")
|
||||||
|
if it.get("did"):
|
||||||
|
return it.get("did")
|
||||||
|
if it.get("handle"):
|
||||||
|
return it.get("handle")
|
||||||
|
author = it.get("author")
|
||||||
|
if isinstance(author, dict):
|
||||||
|
return author.get("did") or author.get("handle")
|
||||||
|
return None
|
||||||
|
post = getattr(it, "post", None)
|
||||||
|
if post is not None:
|
||||||
|
return getattr(post, "uri", None)
|
||||||
|
for attr in ("uri", "id", "did", "handle"):
|
||||||
|
val = getattr(it, attr, None)
|
||||||
|
if val:
|
||||||
|
return val
|
||||||
|
author = getattr(it, "author", None)
|
||||||
|
if author is not None:
|
||||||
|
return getattr(author, "did", None) or getattr(author, "handle", None)
|
||||||
|
return None
|
||||||
|
|
||||||
|
for item in self.session.db[self.name]:
|
||||||
|
key = get_key(item)
|
||||||
|
if key:
|
||||||
|
current_uris.add(key)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
key = get_key(item)
|
||||||
|
if key:
|
||||||
|
if key in current_uris:
|
||||||
|
continue
|
||||||
|
current_uris.add(key)
|
||||||
|
new_items.append(item)
|
||||||
|
|
||||||
|
if not new_items:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Add to DB
|
||||||
|
# Reverse timeline setting
|
||||||
|
reverse = False
|
||||||
|
try: reverse = self.session.settings["general"].get("reverse_timelines", False)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# If reverse (newest at top), we insert new items at index 0?
|
||||||
|
# Typically API returns newest first.
|
||||||
|
# If DB is [Newest ... Oldest] (Reverse order)
|
||||||
|
# Then we insert new items at 0.
|
||||||
|
# If DB is [Oldest ... Newest] (Normal order)
|
||||||
|
# Then we append new items at end.
|
||||||
|
|
||||||
|
# But traditionally APIs return [Newest ... Oldest].
|
||||||
|
# So 'items' list is [Newest ... Oldest].
|
||||||
|
|
||||||
|
if reverse: # Newest at top
|
||||||
|
# DB: [Newest (Index 0) ... Oldest]
|
||||||
|
# We want to insert 'new_items' at 0.
|
||||||
|
# But 'new_items' are also [Newest...Oldest]
|
||||||
|
# So duplicates check handled.
|
||||||
|
# We insert the whole block at 0?
|
||||||
|
for it in reversed(new_items): # Insert oldest of new first, so newest ends up at 0
|
||||||
|
self.session.db[self.name].insert(0, it)
|
||||||
|
else: # Oldest at top
|
||||||
|
# DB: [Oldest ... Newest]
|
||||||
|
# APIs return [Newest ... Oldest]
|
||||||
|
# We want to append them.
|
||||||
|
# So we append reversed(new_items)?
|
||||||
|
for it in reversed(new_items):
|
||||||
|
self.session.db[self.name].append(it)
|
||||||
|
|
||||||
|
# Update UI
|
||||||
|
self.put_items_on_list(len(new_items))
|
||||||
|
|
||||||
|
# Play sound
|
||||||
|
if play_sound and self.sound and not self.session.settings["sound"]["session_mute"]:
|
||||||
|
self.session.sound.play(self.sound)
|
||||||
|
|
||||||
|
return len(new_items)
|
||||||
|
|
||||||
|
def save_positions(self):
|
||||||
|
try:
|
||||||
|
self.session.db[self.name+"_pos"] = self.buffer.list.get_selected()
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
def remove_buffer(self, force=False):
|
||||||
|
if self.type in ("conversation", "chat_messages") or self.name.lower().startswith("conversation"):
|
||||||
|
try:
|
||||||
|
self.session.db.pop(self.name, None)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
117
src/controller/buffers/blueski/chat.py
Normal file
117
src/controller/buffers/blueski/chat.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
import wx
|
||||||
|
import output
|
||||||
|
from .base import BaseBuffer
|
||||||
|
from wxUI.buffers.blueski import panels as BlueskiPanels
|
||||||
|
from sessions.blueski import compose
|
||||||
|
|
||||||
|
log = logging.getLogger("controller.buffers.blueski.chat")
|
||||||
|
|
||||||
|
class ConversationListBuffer(BaseBuffer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs["compose_func"] = "compose_convo"
|
||||||
|
super(ConversationListBuffer, self).__init__(*args, **kwargs)
|
||||||
|
self.type = "chat"
|
||||||
|
|
||||||
|
def create_buffer(self, parent, name):
|
||||||
|
self.buffer = BlueskiPanels.ChatPanel(parent, name)
|
||||||
|
self.buffer.session = self.session
|
||||||
|
|
||||||
|
def start_stream(self, mandatory=False, play_sound=True):
|
||||||
|
count = self.session.settings["general"].get("max_posts_per_call", 50)
|
||||||
|
try:
|
||||||
|
res = self.session.list_convos(limit=count)
|
||||||
|
items = res.get("items", [])
|
||||||
|
|
||||||
|
# Clear to avoid list weirdness on refreshes?
|
||||||
|
# Chat list usually replaces content on fetch
|
||||||
|
self.session.db[self.name] = []
|
||||||
|
self.buffer.list.clear()
|
||||||
|
|
||||||
|
return self.process_items(items, play_sound)
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error fetching conversations")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def url(self, *args, **kwargs):
|
||||||
|
# In chat list, Enter (URL) should open the chat conversation buffer
|
||||||
|
self.view_chat()
|
||||||
|
|
||||||
|
def send_message(self, *args, **kwargs):
|
||||||
|
# Global shortcut for DM
|
||||||
|
self.view_chat()
|
||||||
|
|
||||||
|
def view_chat(self):
|
||||||
|
item = self.get_item()
|
||||||
|
if not item: return
|
||||||
|
|
||||||
|
convo_id = getattr(item, "id", None) or item.get("id")
|
||||||
|
if not convo_id: return
|
||||||
|
|
||||||
|
# Determine participants names for title
|
||||||
|
members = getattr(item, "members", []) or item.get("members", [])
|
||||||
|
others = [m for m in members if (getattr(m, "did", None) or m.get("did")) != self.session.db["user_id"]]
|
||||||
|
if not others: others = members
|
||||||
|
names = ", ".join([getattr(m, "handle", "unknown") or m.get("handle") for m in others])
|
||||||
|
|
||||||
|
title = _("Chat: {0}").format(names)
|
||||||
|
|
||||||
|
import application
|
||||||
|
application.app.controller.create_buffer(
|
||||||
|
buffer_type="chat_messages",
|
||||||
|
session_type="blueski",
|
||||||
|
buffer_title=title,
|
||||||
|
kwargs={"session": self.session, "convo_id": convo_id, "name": title},
|
||||||
|
start=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class ChatBuffer(BaseBuffer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs["compose_func"] = "compose_chat_message"
|
||||||
|
super(ChatBuffer, self).__init__(*args, **kwargs)
|
||||||
|
self.type = "chat_messages"
|
||||||
|
self.convo_id = kwargs.get("convo_id")
|
||||||
|
|
||||||
|
def create_buffer(self, parent, name):
|
||||||
|
self.buffer = BlueskiPanels.ChatMessagePanel(parent, name)
|
||||||
|
self.buffer.session = self.session
|
||||||
|
|
||||||
|
def start_stream(self, mandatory=False, play_sound=True):
|
||||||
|
if not self.convo_id: return 0
|
||||||
|
count = self.session.settings["general"].get("max_posts_per_call", 50)
|
||||||
|
try:
|
||||||
|
res = self.session.get_convo_messages(self.convo_id, limit=count)
|
||||||
|
items = res.get("items", [])
|
||||||
|
# Message order in API is often Oldest...Newest or vice versa.
|
||||||
|
# We want them in order and only new ones.
|
||||||
|
# For chat, let's just clear and show last N messages for simplicity now.
|
||||||
|
self.session.db[self.name] = []
|
||||||
|
self.buffer.list.clear()
|
||||||
|
|
||||||
|
# API usually returns newest first. We want newest at bottom.
|
||||||
|
items = list(reversed(items))
|
||||||
|
|
||||||
|
return self.process_items(items, play_sound)
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error fetching chat messages")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def on_reply(self, evt):
|
||||||
|
# Open a text entry chat box
|
||||||
|
dlg = wx.TextEntryDialog(None, _("Message:"), _("Send Message"), style=wx.TE_MULTILINE | wx.OK | wx.CANCEL)
|
||||||
|
if dlg.ShowModal() == wx.ID_OK:
|
||||||
|
text = dlg.GetValue()
|
||||||
|
if text:
|
||||||
|
try:
|
||||||
|
self.session.send_chat_message(self.convo_id, text)
|
||||||
|
output.speak(_("Message sent."))
|
||||||
|
# Refresh
|
||||||
|
self.start_stream(mandatory=True, play_sound=False)
|
||||||
|
except:
|
||||||
|
output.speak(_("Failed to send message."))
|
||||||
|
dlg.Destroy()
|
||||||
|
|
||||||
|
def send_message(self, *args, **kwargs):
|
||||||
|
# Global shortcut for DM
|
||||||
|
self.on_reply(None)
|
||||||
209
src/controller/buffers/blueski/timeline.py
Normal file
209
src/controller/buffers/blueski/timeline.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
from .base import BaseBuffer
|
||||||
|
from wxUI.buffers.blueski import panels as BlueskiPanels
|
||||||
|
from pubsub import pub
|
||||||
|
|
||||||
|
log = logging.getLogger("controller.buffers.blueski.timeline")
|
||||||
|
|
||||||
|
class HomeTimeline(BaseBuffer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(HomeTimeline, self).__init__(*args, **kwargs)
|
||||||
|
self.type = "home_timeline"
|
||||||
|
self.feed_uri = None
|
||||||
|
|
||||||
|
def create_buffer(self, parent, name):
|
||||||
|
# Override to use HomePanel
|
||||||
|
self.buffer = BlueskiPanels.HomePanel(parent, name)
|
||||||
|
self.buffer.session = self.session
|
||||||
|
|
||||||
|
def start_stream(self, mandatory=False, play_sound=True):
|
||||||
|
count = 50
|
||||||
|
try:
|
||||||
|
count = self.session.settings["general"].get("max_posts_per_call", 50)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
api = self.session._ensure_client()
|
||||||
|
|
||||||
|
# Discover Logic
|
||||||
|
if not self.feed_uri:
|
||||||
|
self.feed_uri = self._resolve_discover_feed(api)
|
||||||
|
|
||||||
|
items = []
|
||||||
|
try:
|
||||||
|
res = None
|
||||||
|
if self.feed_uri:
|
||||||
|
# Fetch feed
|
||||||
|
res = api.app.bsky.feed.get_feed({"feed": self.feed_uri, "limit": count})
|
||||||
|
else:
|
||||||
|
# Fallback to standard timeline
|
||||||
|
res = api.app.bsky.feed.get_timeline({"limit": count})
|
||||||
|
|
||||||
|
feed = getattr(res, "feed", [])
|
||||||
|
items = list(feed)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
log.exception("Failed to fetch home timeline")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return self.process_items(items, play_sound)
|
||||||
|
|
||||||
|
def _resolve_discover_feed(self, api):
|
||||||
|
# Reuse logic from panels.py
|
||||||
|
try:
|
||||||
|
cached = self.session.db.get("discover_feed_uri")
|
||||||
|
if cached: return cached
|
||||||
|
|
||||||
|
# Simple fallback: Suggested feeds
|
||||||
|
try:
|
||||||
|
res = api.app.bsky.feed.get_suggested_feeds({"limit": 50})
|
||||||
|
feeds = getattr(res, "feeds", [])
|
||||||
|
for feed in feeds:
|
||||||
|
dn = getattr(feed, "displayName", "") or getattr(feed, "display_name", "")
|
||||||
|
if "discover" in dn.lower():
|
||||||
|
uri = getattr(feed, "uri", "")
|
||||||
|
self.session.db["discover_feed_uri"] = uri
|
||||||
|
try: self.session.save_persistent_data()
|
||||||
|
except: pass
|
||||||
|
return uri
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
class FollowingTimeline(BaseBuffer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(FollowingTimeline, self).__init__(*args, **kwargs)
|
||||||
|
self.type = "following_timeline"
|
||||||
|
|
||||||
|
def create_buffer(self, parent, name):
|
||||||
|
self.buffer = BlueskiPanels.HomePanel(parent, name) # Reuse HomePanel layout
|
||||||
|
self.buffer.session = self.session
|
||||||
|
|
||||||
|
def start_stream(self, mandatory=False, play_sound=True):
|
||||||
|
count = 50
|
||||||
|
try: count = self.session.settings["general"].get("max_posts_per_call", 50)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
api = self.session._ensure_client()
|
||||||
|
try:
|
||||||
|
# Force reverse-chronological
|
||||||
|
res = api.app.bsky.feed.get_timeline({"limit": count, "algorithm": "reverse-chronological"})
|
||||||
|
feed = getattr(res, "feed", [])
|
||||||
|
items = list(feed)
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error fetching following timeline")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return self.process_items(items, play_sound)
|
||||||
|
|
||||||
|
class NotificationBuffer(BaseBuffer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(NotificationBuffer, self).__init__(*args, **kwargs)
|
||||||
|
self.type = "notifications"
|
||||||
|
|
||||||
|
def create_buffer(self, parent, name):
|
||||||
|
self.buffer = BlueskiPanels.NotificationPanel(parent, name)
|
||||||
|
self.buffer.session = self.session
|
||||||
|
|
||||||
|
def start_stream(self, mandatory=False, play_sound=True):
|
||||||
|
count = 50
|
||||||
|
api = self.session._ensure_client()
|
||||||
|
try:
|
||||||
|
res = api.app.bsky.notification.list_notifications({"limit": count})
|
||||||
|
notifs = getattr(res, "notifications", [])
|
||||||
|
items = []
|
||||||
|
# Notifications are not FeedViewPost. They have different structure.
|
||||||
|
# self.compose_function expects FeedViewPost-like structure (post, author, etc).
|
||||||
|
# We need to map them or have a different compose function.
|
||||||
|
# For now, let's skip items to avoid crash
|
||||||
|
# Or attempt to map.
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
|
return 0
|
||||||
|
|
||||||
|
class Conversation(BaseBuffer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(Conversation, self).__init__(*args, **kwargs)
|
||||||
|
self.type = "conversation"
|
||||||
|
# We need the root URI or the URI of the post to show thread for
|
||||||
|
self.root_uri = kwargs.get("uri")
|
||||||
|
|
||||||
|
def create_buffer(self, parent, name):
|
||||||
|
self.buffer = BlueskiPanels.HomePanel(parent, name)
|
||||||
|
self.buffer.session = self.session
|
||||||
|
|
||||||
|
def start_stream(self, mandatory=False, play_sound=True):
|
||||||
|
if not self.root_uri: return 0
|
||||||
|
|
||||||
|
api = self.session._ensure_client()
|
||||||
|
try:
|
||||||
|
params = {"uri": self.root_uri, "depth": 100, "parentHeight": 100}
|
||||||
|
try:
|
||||||
|
res = api.app.bsky.feed.get_post_thread(params)
|
||||||
|
except Exception:
|
||||||
|
res = api.app.bsky.feed.get_post_thread({"uri": self.root_uri})
|
||||||
|
thread = getattr(res, "thread", None)
|
||||||
|
if not thread:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
# Find the root of the thread tree
|
||||||
|
curr = thread
|
||||||
|
while g(curr, "parent"):
|
||||||
|
curr = g(curr, "parent")
|
||||||
|
|
||||||
|
final_items = []
|
||||||
|
|
||||||
|
def traverse(node):
|
||||||
|
if not node:
|
||||||
|
return
|
||||||
|
post = g(node, "post")
|
||||||
|
if post:
|
||||||
|
final_items.append(post)
|
||||||
|
replies = g(node, "replies") or []
|
||||||
|
for r in replies:
|
||||||
|
traverse(r)
|
||||||
|
|
||||||
|
traverse(curr)
|
||||||
|
|
||||||
|
# Clear existing items to avoid duplication when refreshing a thread view (which changes structure little)
|
||||||
|
self.session.db[self.name] = []
|
||||||
|
self.buffer.list.clear() # Clear UI too
|
||||||
|
|
||||||
|
return self.process_items(final_items, play_sound)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error fetching thread")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
class LikesBuffer(BaseBuffer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(LikesBuffer, self).__init__(*args, **kwargs)
|
||||||
|
self.type = "likes"
|
||||||
|
|
||||||
|
def create_buffer(self, parent, name):
|
||||||
|
self.buffer = BlueskiPanels.HomePanel(parent, name)
|
||||||
|
self.buffer.session = self.session
|
||||||
|
|
||||||
|
def start_stream(self, mandatory=False, play_sound=True):
|
||||||
|
count = 50
|
||||||
|
try:
|
||||||
|
count = self.session.settings["general"].get("max_posts_per_call", 50)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
api = self.session._ensure_client()
|
||||||
|
try:
|
||||||
|
res = api.app.bsky.feed.get_actor_likes({"actor": api.me.did, "limit": count})
|
||||||
|
items = getattr(res, "feed", None) or getattr(res, "items", None) or []
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error fetching likes")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return self.process_items(list(items), play_sound)
|
||||||
63
src/controller/buffers/blueski/user.py
Normal file
63
src/controller/buffers/blueski/user.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
from .base import BaseBuffer
|
||||||
|
from wxUI.buffers.blueski import panels as BlueskiPanels
|
||||||
|
from sessions.blueski import compose
|
||||||
|
|
||||||
|
log = logging.getLogger("controller.buffers.blueski.user")
|
||||||
|
|
||||||
|
class UserBuffer(BaseBuffer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# We need compose_user for this buffer
|
||||||
|
kwargs["compose_func"] = "compose_user"
|
||||||
|
super(UserBuffer, self).__init__(*args, **kwargs)
|
||||||
|
self.type = "user"
|
||||||
|
|
||||||
|
def create_buffer(self, parent, name):
|
||||||
|
self.buffer = BlueskiPanels.UserPanel(parent, name)
|
||||||
|
self.buffer.session = self.session
|
||||||
|
|
||||||
|
def start_stream(self, mandatory=False, play_sound=True):
|
||||||
|
api_method = self.kwargs.get("api_method")
|
||||||
|
if not api_method: return 0
|
||||||
|
|
||||||
|
count = self.session.settings["general"].get("max_posts_per_call", 50)
|
||||||
|
actor = (
|
||||||
|
self.kwargs.get("actor")
|
||||||
|
or self.kwargs.get("did")
|
||||||
|
or self.kwargs.get("handle")
|
||||||
|
or self.kwargs.get("id")
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# We call the method in session. API methods return {"items": [...], "cursor": ...}
|
||||||
|
if api_method in ("get_followers", "get_follows"):
|
||||||
|
res = getattr(self.session, api_method)(actor=actor, limit=count)
|
||||||
|
else:
|
||||||
|
res = getattr(self.session, api_method)(limit=count)
|
||||||
|
items = res.get("items", [])
|
||||||
|
|
||||||
|
# Clear existing items for these lists to start fresh?
|
||||||
|
# Or append? Standard lists in TWBlue usually append.
|
||||||
|
# But followers/blocks are often full-sync or large jumps.
|
||||||
|
# For now, append like timelines.
|
||||||
|
|
||||||
|
return self.process_items(items, play_sound)
|
||||||
|
except Exception:
|
||||||
|
log.exception(f"Error fetching user list for {self.name}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
class FollowersBuffer(UserBuffer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs["api_method"] = "get_followers"
|
||||||
|
super(FollowersBuffer, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
class FollowingBuffer(UserBuffer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs["api_method"] = "get_follows"
|
||||||
|
super(FollowingBuffer, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
class BlocksBuffer(UserBuffer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs["api_method"] = "get_blocks"
|
||||||
|
super(BlocksBuffer, self).__init__(*args, **kwargs)
|
||||||
@@ -5,6 +5,7 @@ import logging
|
|||||||
import webbrowser
|
import webbrowser
|
||||||
import wx
|
import wx
|
||||||
import requests
|
import requests
|
||||||
|
import asyncio
|
||||||
import keystrokeEditor
|
import keystrokeEditor
|
||||||
import sessions
|
import sessions
|
||||||
import widgetUtils
|
import widgetUtils
|
||||||
@@ -293,9 +294,52 @@ class Controller(object):
|
|||||||
pub.sendMessage("core.create_account", name=session.get_name(), session_id=session.session_id)
|
pub.sendMessage("core.create_account", name=session.get_name(), session_id=session.session_id)
|
||||||
|
|
||||||
def login_account(self, session_id):
|
def login_account(self, session_id):
|
||||||
|
session = None
|
||||||
for i in sessions.sessions:
|
for i in sessions.sessions:
|
||||||
if sessions.sessions[i].session_id == session_id: session = sessions.sessions[i]
|
if sessions.sessions[i].session_id == session_id:
|
||||||
session.login()
|
session = sessions.sessions[i]
|
||||||
|
break
|
||||||
|
if not session:
|
||||||
|
return
|
||||||
|
|
||||||
|
old_name = session.get_name()
|
||||||
|
try:
|
||||||
|
session.login()
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Login failed for session %s", session_id)
|
||||||
|
output.speak(_("Login failed for {0}: {1}").format(old_name, str(e)), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not session.logged:
|
||||||
|
output.speak(_("Login failed for {0}. Please check your credentials.").format(old_name), True)
|
||||||
|
return
|
||||||
|
|
||||||
|
new_name = session.get_name()
|
||||||
|
if old_name != new_name:
|
||||||
|
log.info(f"Account name changed from {old_name} to {new_name} after login")
|
||||||
|
if self.current_account == old_name:
|
||||||
|
self.current_account = new_name
|
||||||
|
if old_name in self.accounts:
|
||||||
|
idx = self.accounts.index(old_name)
|
||||||
|
self.accounts[idx] = new_name
|
||||||
|
else:
|
||||||
|
self.accounts.append(new_name)
|
||||||
|
|
||||||
|
# Update root buffer name and account
|
||||||
|
for b in self.buffers:
|
||||||
|
if b.account == old_name:
|
||||||
|
b.account = new_name
|
||||||
|
if hasattr(b, "buffer"):
|
||||||
|
b.buffer.account = new_name
|
||||||
|
# If this is the root node, its name matches old_name (e.g. "Bluesky")
|
||||||
|
if b.name == old_name:
|
||||||
|
b.name = new_name
|
||||||
|
if hasattr(b, "buffer"):
|
||||||
|
b.buffer.name = new_name
|
||||||
|
|
||||||
|
# Update tree node label
|
||||||
|
self.change_buffer_title(old_name, old_name, new_name)
|
||||||
|
|
||||||
handler = self.get_handler(type=session.type)
|
handler = self.get_handler(type=session.type)
|
||||||
if handler != None and hasattr(handler, "create_buffers"):
|
if handler != None and hasattr(handler, "create_buffers"):
|
||||||
try:
|
try:
|
||||||
@@ -329,60 +373,35 @@ class Controller(object):
|
|||||||
try:
|
try:
|
||||||
buffer_panel_class = None
|
buffer_panel_class = None
|
||||||
if session_type == "blueski":
|
if session_type == "blueski":
|
||||||
from wxUI.buffers.blueski import panels as BlueskiPanels # Import new panels
|
from controller.buffers.blueski import timeline as BlueskiTimelines
|
||||||
if buffer_type == "home_timeline":
|
from controller.buffers.blueski import user as BlueskiUsers
|
||||||
buffer_panel_class = BlueskiPanels.BlueskiHomeTimelinePanel
|
from controller.buffers.blueski import chat as BlueskiChats
|
||||||
# kwargs for HomeTimelinePanel: parent, name, session
|
|
||||||
# 'name' is buffer_title, 'parent' is self.view.nb
|
if "user_id" in kwargs and "session" not in kwargs:
|
||||||
# 'session' needs to be fetched based on user_id in kwargs
|
kwargs["session"] = sessions.sessions.get(kwargs["user_id"])
|
||||||
if "user_id" in kwargs and "session" not in kwargs: # Ensure session is passed
|
|
||||||
kwargs["session"] = sessions.sessions.get(kwargs["user_id"])
|
if "name" not in kwargs: kwargs["name"] = buffer_title
|
||||||
# Clean unsupported kwarg for panel ctor
|
|
||||||
if "user_id" in kwargs:
|
|
||||||
kwargs.pop("user_id", None)
|
|
||||||
if "name" not in kwargs: kwargs["name"] = buffer_title
|
|
||||||
|
|
||||||
elif buffer_type == "user_timeline":
|
buffer_map = {
|
||||||
buffer_panel_class = BlueskiPanels.BlueskiUserTimelinePanel
|
"home_timeline": BlueskiTimelines.HomeTimeline,
|
||||||
# kwargs for UserTimelinePanel: parent, name, session, target_user_did, target_user_handle
|
"following_timeline": BlueskiTimelines.FollowingTimeline,
|
||||||
if "user_id" in kwargs and "session" not in kwargs:
|
"notifications": BlueskiTimelines.NotificationBuffer,
|
||||||
kwargs["session"] = sessions.sessions.get(kwargs["user_id"])
|
"conversation": BlueskiTimelines.Conversation,
|
||||||
kwargs.pop("user_id", None)
|
"likes": BlueskiTimelines.LikesBuffer,
|
||||||
if "name" not in kwargs: kwargs["name"] = buffer_title
|
"UserBuffer": BlueskiUsers.UserBuffer,
|
||||||
# target_user_did and target_user_handle must be in kwargs from blueski.Handler
|
"FollowersBuffer": BlueskiUsers.FollowersBuffer,
|
||||||
|
"FollowingBuffer": BlueskiUsers.FollowingBuffer,
|
||||||
|
"BlocksBuffer": BlueskiUsers.BlocksBuffer,
|
||||||
|
"ConversationListBuffer": BlueskiChats.ConversationListBuffer,
|
||||||
|
"ChatMessageBuffer": BlueskiChats.ChatBuffer,
|
||||||
|
"chat_messages": BlueskiChats.ChatBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
elif buffer_type == "notifications":
|
buffer_panel_class = buffer_map.get(buffer_type)
|
||||||
buffer_panel_class = BlueskiPanels.BlueskiNotificationPanel
|
if buffer_panel_class is None:
|
||||||
if "user_id" in kwargs and "session" not in kwargs:
|
# Fallback for others including user_timeline to HomeTimeline for now
|
||||||
kwargs["session"] = sessions.sessions.get(kwargs["user_id"])
|
log.warning(f"Unsupported Blueski buffer type: {buffer_type}. Falling back to HomeTimeline.")
|
||||||
kwargs.pop("user_id", None)
|
buffer_panel_class = BlueskiTimelines.HomeTimeline
|
||||||
if "name" not in kwargs: kwargs["name"] = buffer_title
|
|
||||||
# target_user_did and target_user_handle must be in kwargs from blueski.Handler
|
|
||||||
|
|
||||||
elif buffer_type == "notifications":
|
|
||||||
buffer_panel_class = BlueskiPanels.BlueskiNotificationPanel
|
|
||||||
if "user_id" in kwargs and "session" not in kwargs:
|
|
||||||
kwargs["session"] = sessions.sessions.get(kwargs["user_id"])
|
|
||||||
kwargs.pop("user_id", None)
|
|
||||||
if "name" not in kwargs: kwargs["name"] = buffer_title
|
|
||||||
elif buffer_type == "user_list_followers" or buffer_type == "user_list_following":
|
|
||||||
buffer_panel_class = BlueskiPanels.BlueskiUserListPanel
|
|
||||||
elif buffer_type == "following_timeline":
|
|
||||||
buffer_panel_class = BlueskiPanels.BlueskiFollowingTimelinePanel
|
|
||||||
# Clean stray keys that this panel doesn't accept
|
|
||||||
kwargs.pop("user_id", None)
|
|
||||||
kwargs.pop("list_type", None)
|
|
||||||
if "name" not in kwargs: kwargs["name"] = buffer_title
|
|
||||||
else:
|
|
||||||
log.warning(f"Unsupported Blueski buffer type: {buffer_type}. Falling back to generic.")
|
|
||||||
# Fallback to trying to find it in generic buffers or error
|
|
||||||
available_buffers = getattr(buffers, "base", None) # Or some generic panel module
|
|
||||||
if available_buffers and hasattr(available_buffers, buffer_type):
|
|
||||||
buffer_panel_class = getattr(available_buffers, buffer_type)
|
|
||||||
elif available_buffers and hasattr(available_buffers, "TimelinePanel"): # Example generic
|
|
||||||
buffer_panel_class = getattr(available_buffers, "TimelinePanel")
|
|
||||||
else:
|
|
||||||
raise AttributeError(f"Blueski buffer type {buffer_type} not found in blueski.panels or base panels.")
|
|
||||||
else: # Existing logic for other session types
|
else: # Existing logic for other session types
|
||||||
available_buffers = getattr(buffers, session_type)
|
available_buffers = getattr(buffers, session_type)
|
||||||
if not hasattr(available_buffers, buffer_type):
|
if not hasattr(available_buffers, buffer_type):
|
||||||
@@ -722,6 +741,12 @@ class Controller(object):
|
|||||||
|
|
||||||
session = buffer.session
|
session = buffer.session
|
||||||
if getattr(session, "type", "") == "blueski":
|
if getattr(session, "type", "") == "blueski":
|
||||||
|
author_handle = ""
|
||||||
|
if hasattr(buffer, "get_selected_item_author_details"):
|
||||||
|
details = buffer.get_selected_item_author_details()
|
||||||
|
if details:
|
||||||
|
author_handle = details.get("handle", "") or details.get("did", "")
|
||||||
|
initial_text = f"@{author_handle} " if author_handle and not author_handle.startswith("@") else (f"{author_handle} " if author_handle else "")
|
||||||
if self.showing == False:
|
if self.showing == False:
|
||||||
dlg = wx.TextEntryDialog(None, _("Write your reply:"), _("Reply"))
|
dlg = wx.TextEntryDialog(None, _("Write your reply:"), _("Reply"))
|
||||||
if dlg.ShowModal() == wx.ID_OK:
|
if dlg.ShowModal() == wx.ID_OK:
|
||||||
@@ -742,7 +767,7 @@ class Controller(object):
|
|||||||
dlg.Destroy()
|
dlg.Destroy()
|
||||||
return
|
return
|
||||||
from wxUI.dialogs.blueski.postDialogs import Post as ATPostDialog
|
from wxUI.dialogs.blueski.postDialogs import Post as ATPostDialog
|
||||||
dlg = ATPostDialog(caption=_("Reply"))
|
dlg = ATPostDialog(caption=_("Reply"), text=initial_text)
|
||||||
if dlg.ShowModal() == wx.ID_OK:
|
if dlg.ShowModal() == wx.ID_OK:
|
||||||
text, files, cw_text, langs = dlg.get_payload()
|
text, files, cw_text, langs = dlg.get_payload()
|
||||||
dlg.Destroy()
|
dlg.Destroy()
|
||||||
@@ -1432,13 +1457,10 @@ class Controller(object):
|
|||||||
def update_buffers(self):
|
def update_buffers(self):
|
||||||
for i in self.buffers[:]:
|
for i in self.buffers[:]:
|
||||||
if i.session != None and i.session.is_logged == True:
|
if i.session != None and i.session.is_logged == True:
|
||||||
# For Blueski, initial load is in session.start() or manual.
|
try:
|
||||||
# Periodic updates would need a separate timer or manual refresh via update_buffer.
|
i.start_stream(mandatory=True)
|
||||||
if i.session.KIND != "blueski":
|
except Exception as err:
|
||||||
try:
|
log.exception("Error %s starting buffer %s on account %s, with args %r and kwargs %r." % (str(err), i.name, i.account, i.args, i.kwargs))
|
||||||
i.start_stream(mandatory=True) # This is likely for streaming connections or timed polling within buffer
|
|
||||||
except Exception as err:
|
|
||||||
log.exception("Error %s starting buffer %s on account %s, with args %r and kwargs %r." % (str(err), i.name, i.account, i.args, i.kwargs))
|
|
||||||
|
|
||||||
def update_buffer(self, *args, **kwargs):
|
def update_buffer(self, *args, **kwargs):
|
||||||
"""Handles the 'Update buffer' menu command to fetch newest items."""
|
"""Handles the 'Update buffer' menu command to fetch newest items."""
|
||||||
@@ -1454,50 +1476,27 @@ class Controller(object):
|
|||||||
new_ids = []
|
new_ids = []
|
||||||
try:
|
try:
|
||||||
if session.KIND == "blueski":
|
if session.KIND == "blueski":
|
||||||
if bf.name == f"{session.label} Home": # Assuming buffer name indicates type
|
if hasattr(bf, "start_stream"):
|
||||||
# Its panel's load_initial_posts calls session.fetch_home_timeline
|
count = bf.start_stream(mandatory=True)
|
||||||
if hasattr(bf, "load_initial_posts"): # Generic for timeline panels
|
if count: new_ids = [str(x) for x in range(count)]
|
||||||
await bf.load_initial_posts(limit=config.app["app-settings"].get("items_per_request", 20))
|
|
||||||
new_ids = getattr(bf, "item_uris", [])
|
|
||||||
else: # Should not happen if panel is correctly typed
|
|
||||||
logger.warning(f"Home timeline panel for {session.KIND} missing load_initial_posts")
|
|
||||||
elif bf.type == "notifications" and hasattr(bf, "refresh_notifications"):
|
|
||||||
await bf.refresh_notifications(limit=config.app["app-settings"].get("items_per_request", 20))
|
|
||||||
new_ids = []
|
|
||||||
elif bf.type == "user_timeline" and hasattr(bf, "load_initial_posts"):
|
|
||||||
await bf.load_initial_posts(limit=config.app["app-settings"].get("items_per_request", 20))
|
|
||||||
new_ids = getattr(bf, "item_uris", [])
|
|
||||||
elif bf.type in ["user_list_followers", "user_list_following"] and hasattr(bf, "load_initial_users"):
|
|
||||||
await bf.load_initial_users(limit=config.app["app-settings"].get("items_per_request", 30))
|
|
||||||
new_ids = [u.get("did") for u in getattr(bf, "user_list_data", []) if isinstance(u,dict)]
|
|
||||||
else:
|
else:
|
||||||
if hasattr(bf, "start_stream"): # Fallback for non-Blueski panels or unhandled types
|
output.speak(_(u"This buffer type cannot be updated."), True)
|
||||||
count = bf.start_stream(mandatory=True, avoid_autoreading=True)
|
return
|
||||||
if count is not None: new_ids = [str(x) for x in range(count)] # Dummy IDs for count
|
else: # Generic fallback for other sessions
|
||||||
else:
|
|
||||||
output.speak(_(u"This buffer type cannot be updated in this way."), True)
|
|
||||||
return
|
|
||||||
else: # For other session types (e.g. Mastodon)
|
|
||||||
if hasattr(bf, "start_stream"):
|
if hasattr(bf, "start_stream"):
|
||||||
count = bf.start_stream(mandatory=True, avoid_autoreading=True)
|
count = bf.start_stream(mandatory=True, avoid_autoreading=True)
|
||||||
if count is not None: new_ids = [str(x) for x in range(count)] # Dummy IDs for count
|
if count: new_ids = [str(x) for x in range(count)]
|
||||||
else:
|
else:
|
||||||
output.speak(_(u"Unable to update this buffer."), True)
|
output.speak(_(u"Unable to update this buffer."), True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Generic feedback based on new_ids for timelines or user lists
|
# Generic feedback
|
||||||
if bf.type in ["home_timeline", "user_timeline"]:
|
if bf.type in ["home_timeline", "user_timeline"]:
|
||||||
output.speak(_("{0} posts retrieved").format(len(new_ids)), True)
|
output.speak(_("{0} posts retrieved").format(len(new_ids)), True)
|
||||||
elif bf.type in ["user_list_followers", "user_list_following"]:
|
|
||||||
output.speak(_("{0} users retrieved").format(len(new_ids)), True)
|
|
||||||
elif bf.type == "notifications":
|
elif bf.type == "notifications":
|
||||||
output.speak(_("Notifications updated."), True)
|
output.speak(_("Notifications updated."), True)
|
||||||
# else, original start_stream might have given feedback
|
except Exception as e:
|
||||||
|
log.exception("Error updating buffer %s", bf.name)
|
||||||
except NotificationError as e:
|
|
||||||
output.speak(str(e), True) # Ensure output.speak is on main thread if called from here
|
|
||||||
except Exception as e_general:
|
|
||||||
logger.error(f"Error updating buffer {bf.name}: {e_general}", exc_info=True)
|
|
||||||
output.speak(_("An error occurred while updating the buffer."), True)
|
output.speak(_("An error occurred while updating the buffer."), True)
|
||||||
|
|
||||||
wx.CallAfter(asyncio.create_task, do_update())
|
wx.CallAfter(asyncio.create_task, do_update())
|
||||||
@@ -1674,10 +1673,9 @@ class Controller(object):
|
|||||||
# The handler's user_details method is responsible for extracting context
|
# The handler's user_details method is responsible for extracting context
|
||||||
# (e.g., selected user) from the buffer and displaying the profile.
|
# (e.g., selected user) from the buffer and displaying the profile.
|
||||||
# For Blueski, handler.user_details calls the ShowUserProfileDialog.
|
# For Blueski, handler.user_details calls the ShowUserProfileDialog.
|
||||||
# It's an async method, so needs to be called appropriately.
|
result = handler.user_details(buffer)
|
||||||
async def _show_details():
|
if asyncio.iscoroutine(result):
|
||||||
await handler.user_details(buffer)
|
call_threaded(asyncio.run, result)
|
||||||
wx.CallAfter(asyncio.create_task, _show_details())
|
|
||||||
else:
|
else:
|
||||||
output.speak(_("This session type does not support viewing user details in this way."), True)
|
output.speak(_("This session type does not support viewing user details in this way."), True)
|
||||||
|
|
||||||
@@ -1737,9 +1735,9 @@ class Controller(object):
|
|||||||
if author_details: user = author_details
|
if author_details: user = author_details
|
||||||
|
|
||||||
if handler and hasattr(handler, 'open_followers_timeline'):
|
if handler and hasattr(handler, 'open_followers_timeline'):
|
||||||
async def _open_followers():
|
result = handler.open_followers_timeline(main_controller=self, session=session_to_use, user_payload=user)
|
||||||
await handler.open_followers_timeline(main_controller=self, session=session_to_use, user_payload=user)
|
if asyncio.iscoroutine(result):
|
||||||
wx.CallAfter(asyncio.create_task, _open_followers())
|
call_threaded(asyncio.run, result)
|
||||||
elif handler and hasattr(handler, 'openFollowersTimeline'): # Fallback
|
elif handler and hasattr(handler, 'openFollowersTimeline'): # Fallback
|
||||||
handler.openFollowersTimeline(self, current_buffer, user)
|
handler.openFollowersTimeline(self, current_buffer, user)
|
||||||
else:
|
else:
|
||||||
@@ -1768,9 +1766,9 @@ class Controller(object):
|
|||||||
if author_details: user = author_details
|
if author_details: user = author_details
|
||||||
|
|
||||||
if handler and hasattr(handler, 'open_following_timeline'):
|
if handler and hasattr(handler, 'open_following_timeline'):
|
||||||
async def _open_following():
|
result = handler.open_following_timeline(main_controller=self, session=session_to_use, user_payload=user)
|
||||||
await handler.open_following_timeline(main_controller=self, session=session_to_use, user_payload=user)
|
if asyncio.iscoroutine(result):
|
||||||
wx.CallAfter(asyncio.create_task, _open_following())
|
call_threaded(asyncio.run, result)
|
||||||
elif handler and hasattr(handler, 'openFollowingTimeline'): # Fallback
|
elif handler and hasattr(handler, 'openFollowingTimeline'): # Fallback
|
||||||
handler.openFollowingTimeline(self, current_buffer, user)
|
handler.openFollowingTimeline(self, current_buffer, user)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -205,6 +205,10 @@ class sessionManagerController(object):
|
|||||||
# But for immediate use if not restarting, it might need to be added to sessions.sessions
|
# But for immediate use if not restarting, it might need to be added to sessions.sessions
|
||||||
sessions.sessions[location] = s # Make it globally available immediately
|
sessions.sessions[location] = s # Make it globally available immediately
|
||||||
self.new_sessions[location] = s
|
self.new_sessions[location] = s
|
||||||
|
# Sync with global config
|
||||||
|
if location not in config.app["sessions"]["sessions"]:
|
||||||
|
config.app["sessions"]["sessions"].append(location)
|
||||||
|
config.app.write()
|
||||||
|
|
||||||
|
|
||||||
else: # Authorise returned False or None
|
else: # Authorise returned False or None
|
||||||
@@ -232,6 +236,9 @@ class sessionManagerController(object):
|
|||||||
self.view.remove_session(index)
|
self.view.remove_session(index)
|
||||||
self.removed_sessions.append(selected_account.get("id"))
|
self.removed_sessions.append(selected_account.get("id"))
|
||||||
self.sessions.remove(selected_account)
|
self.sessions.remove(selected_account)
|
||||||
|
if selected_account.get("id") in config.app["sessions"]["sessions"]:
|
||||||
|
config.app["sessions"]["sessions"].remove(selected_account.get("id"))
|
||||||
|
config.app.write()
|
||||||
shutil.rmtree(path=os.path.join(paths.config_path(), selected_account.get("id")), ignore_errors=True)
|
shutil.rmtree(path=os.path.join(paths.config_path(), selected_account.get("id")), ignore_errors=True)
|
||||||
|
|
||||||
def configuration(self):
|
def configuration(self):
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ class baseSession(object):
|
|||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
log.debug("Creating %s path" % (os.path.join(paths.config_path(), path),))
|
log.debug("Creating %s path" % (os.path.join(paths.config_path(), path),))
|
||||||
os.mkdir(path)
|
os.mkdir(path)
|
||||||
config.app["sessions"]["sessions"].append(id)
|
if self.session_id not in config.app["sessions"]["sessions"]:
|
||||||
|
config.app["sessions"]["sessions"].append(self.session_id)
|
||||||
|
config.app.write()
|
||||||
|
|
||||||
def get_configuration(self):
|
def get_configuration(self):
|
||||||
""" Get settings for a session."""
|
""" Get settings for a session."""
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import arrow
|
||||||
from approve.translation import translate as _
|
import languageHandler
|
||||||
from approve.util import parse_iso_datetime # For parsing ISO timestamps
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from approve.sessions.blueski.session import Session as BlueskiSession
|
from sessions.blueski.session import Session as BlueskiSession
|
||||||
from atproto.xrpc_client import models # For type hinting ATProto models
|
from atproto.xrpc_client import models # For type hinting ATProto models
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -94,12 +93,23 @@ class BlueskiCompose:
|
|||||||
|
|
||||||
post_text = getattr(record, 'text', '') if not isinstance(record, dict) else record.get('text', '')
|
post_text = getattr(record, 'text', '') if not isinstance(record, dict) else record.get('text', '')
|
||||||
|
|
||||||
|
reason = post_data.get("reason")
|
||||||
|
if reason:
|
||||||
|
rtype = getattr(reason, "$type", "") if not isinstance(reason, dict) else reason.get("$type", "")
|
||||||
|
if not rtype and not isinstance(reason, dict):
|
||||||
|
rtype = getattr(reason, "py_type", "")
|
||||||
|
if rtype and "reasonRepost" in rtype:
|
||||||
|
by = getattr(reason, "by", None) if not isinstance(reason, dict) else reason.get("by")
|
||||||
|
by_handle = getattr(by, "handle", "") if by and not isinstance(by, dict) else (by.get("handle", "") if by else "")
|
||||||
|
reason_line = _("Reposted by @{handle}").format(handle=by_handle) if by_handle else _("Reposted")
|
||||||
|
post_text = f"{reason_line}\n{post_text}" if post_text else reason_line
|
||||||
|
|
||||||
created_at_str = getattr(record, 'createdAt', '') if not isinstance(record, dict) else record.get('createdAt', '')
|
created_at_str = getattr(record, 'createdAt', '') if not isinstance(record, dict) else record.get('createdAt', '')
|
||||||
timestamp_str = ""
|
timestamp_str = ""
|
||||||
if created_at_str:
|
if created_at_str:
|
||||||
try:
|
try:
|
||||||
dt_obj = parse_iso_datetime(created_at_str)
|
ts = arrow.get(created_at_str)
|
||||||
timestamp_str = dt_obj.strftime("%I:%M %p - %b %d, %Y") if dt_obj else created_at_str
|
timestamp_str = ts.format(_("dddd, MMMM D, YYYY H:m"), locale=languageHandler.curLang[:2])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Could not parse timestamp {created_at_str}: {e}")
|
logger.debug(f"Could not parse timestamp {created_at_str}: {e}")
|
||||||
timestamp_str = created_at_str
|
timestamp_str = created_at_str
|
||||||
@@ -143,8 +153,10 @@ class BlueskiCompose:
|
|||||||
if alt_texts_present: embed_display += _(" (Alt text available)")
|
if alt_texts_present: embed_display += _(" (Alt text available)")
|
||||||
embed_display += "]"
|
embed_display += "]"
|
||||||
|
|
||||||
elif embed_type in ['app.bsky.embed.record#view', 'app.bsky.embed.record']:
|
elif embed_type in ['app.bsky.embed.record#view', 'app.bsky.embed.record', 'app.bsky.embed.recordWithMedia#view', 'app.bsky.embed.recordWithMedia']:
|
||||||
record_embed_data = getattr(embed_data, 'record', None) if hasattr(embed_data, 'record') else embed_data.get('record', None)
|
record_embed_data = getattr(embed_data, 'record', None) if hasattr(embed_data, 'record') else embed_data.get('record', None)
|
||||||
|
if record_embed_data and isinstance(record_embed_data, dict):
|
||||||
|
record_embed_data = record_embed_data.get("record") or record_embed_data
|
||||||
record_embed_type = getattr(record_embed_data, '$type', '')
|
record_embed_type = getattr(record_embed_data, '$type', '')
|
||||||
if not record_embed_type and isinstance(record_embed_data, dict): record_embed_type = record_embed_data.get('$type', '')
|
if not record_embed_type and isinstance(record_embed_data, dict): record_embed_type = record_embed_data.get('$type', '')
|
||||||
|
|
||||||
@@ -243,3 +255,275 @@ class BlueskiCompose:
|
|||||||
display_parts.append(f"\"{body_snippet}\"")
|
display_parts.append(f"\"{body_snippet}\"")
|
||||||
|
|
||||||
return " ".join(display_parts).strip()
|
return " ".join(display_parts).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def compose_post(post, db, settings, relative_times, show_screen_names=False, safe=True):
|
||||||
|
"""
|
||||||
|
Compose a Bluesky post into a list of strings [User, Text, Date, Source].
|
||||||
|
post: dict or ATProto model object.
|
||||||
|
"""
|
||||||
|
# Extract data using getattr for models or .get for dicts
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
# Resolve Post View or Feed View structure
|
||||||
|
# Feed items often have .post field. Direct post objects don't.
|
||||||
|
actual_post = g(post, "post", post)
|
||||||
|
|
||||||
|
record = g(actual_post, "record", {})
|
||||||
|
author = g(actual_post, "author", {})
|
||||||
|
|
||||||
|
# Author
|
||||||
|
handle = g(author, "handle", "")
|
||||||
|
display_name = g(author, "displayName") or g(author, "display_name") or handle or "Unknown"
|
||||||
|
|
||||||
|
if show_screen_names:
|
||||||
|
user_str = f"@{handle}"
|
||||||
|
else:
|
||||||
|
# "Display Name (@handle)"
|
||||||
|
if handle and display_name != handle:
|
||||||
|
user_str = f"{display_name} (@{handle})"
|
||||||
|
else:
|
||||||
|
user_str = f"@{handle}"
|
||||||
|
|
||||||
|
# Text
|
||||||
|
text = g(record, "text", "")
|
||||||
|
|
||||||
|
# Repost reason (so users know why they see an unfamiliar post)
|
||||||
|
reason = g(post, "reason", None)
|
||||||
|
if reason:
|
||||||
|
rtype = g(reason, "$type") or g(reason, "py_type")
|
||||||
|
if rtype and "reasonRepost" in rtype:
|
||||||
|
by = g(reason, "by", {})
|
||||||
|
by_handle = g(by, "handle", "")
|
||||||
|
reason_line = _("Reposted by @{handle}").format(handle=by_handle) if by_handle else _("Reposted")
|
||||||
|
text = f"{reason_line}\n{text}" if text else reason_line
|
||||||
|
|
||||||
|
# Labels / Content Warning
|
||||||
|
labels = g(actual_post, "labels", [])
|
||||||
|
cw_text = ""
|
||||||
|
is_sensitive = False
|
||||||
|
|
||||||
|
for label in labels:
|
||||||
|
val = g(label, "val", "")
|
||||||
|
if val in ["!warn", "porn", "sexual", "nudity", "gore", "graphic-media", "corpse", "self-harm", "hate", "spam", "impersonation"]:
|
||||||
|
is_sensitive = True
|
||||||
|
if not cw_text: cw_text = _("Sensitive Content")
|
||||||
|
elif val.startswith("warn:"):
|
||||||
|
is_sensitive = True
|
||||||
|
cw_text = val.split("warn:", 1)[-1].strip()
|
||||||
|
|
||||||
|
if cw_text:
|
||||||
|
text = f"CW: {cw_text}\n\n{text}"
|
||||||
|
|
||||||
|
# Embeds (Images, Quotes)
|
||||||
|
embed = g(actual_post, "embed", None)
|
||||||
|
if embed:
|
||||||
|
etype = g(embed, "$type") or g(embed, "py_type")
|
||||||
|
if etype and ("images" in etype):
|
||||||
|
images = g(embed, "images", [])
|
||||||
|
if images:
|
||||||
|
text += f"\n[{len(images)} {_('Images')}]"
|
||||||
|
|
||||||
|
# Handle Record (Quote) or RecordWithMedia (Quote + Media)
|
||||||
|
quote_rec = None
|
||||||
|
if etype and ("recordWithMedia" in etype):
|
||||||
|
# Extract the nested record
|
||||||
|
rec_embed = g(embed, "record", {})
|
||||||
|
if rec_embed:
|
||||||
|
quote_rec = g(rec_embed, "record", None) or rec_embed
|
||||||
|
# Also check for media in the wrapper
|
||||||
|
media = g(embed, "media", {})
|
||||||
|
mtype = g(media, "$type") or g(media, "py_type")
|
||||||
|
if mtype and "images" in mtype:
|
||||||
|
images = g(media, "images", [])
|
||||||
|
if images: text += f"\n[{len(images)} {_('Images')}]"
|
||||||
|
|
||||||
|
elif etype and ("record" in etype):
|
||||||
|
# Direct quote
|
||||||
|
quote_rec = g(embed, "record", {})
|
||||||
|
if isinstance(quote_rec, dict):
|
||||||
|
quote_rec = quote_rec.get("record") or quote_rec
|
||||||
|
|
||||||
|
if quote_rec:
|
||||||
|
# It is likely a ViewRecord
|
||||||
|
# Check type (ViewRecord, ViewNotFound, ViewBlocked, etc)
|
||||||
|
qtype = g(quote_rec, "$type") or g(quote_rec, "py_type")
|
||||||
|
|
||||||
|
if qtype and "viewNotFound" in qtype:
|
||||||
|
text += f"\n[{_('Quoted post not found')}]"
|
||||||
|
elif qtype and "viewBlocked" in qtype:
|
||||||
|
text += f"\n[{_('Quoted post blocked')}]"
|
||||||
|
elif qtype and "generatorView" in qtype:
|
||||||
|
# Feed generator
|
||||||
|
gen = g(quote_rec, "displayName", "Feed")
|
||||||
|
text += f"\n[{_('Quoting Feed')}: {gen}]"
|
||||||
|
else:
|
||||||
|
# Assume ViewRecord
|
||||||
|
q_author = g(quote_rec, "author", {})
|
||||||
|
q_handle = g(q_author, "handle", "unknown")
|
||||||
|
|
||||||
|
q_val = g(quote_rec, "value", {})
|
||||||
|
q_text = g(q_val, "text", "")
|
||||||
|
|
||||||
|
if q_text:
|
||||||
|
text += f"\n[{_('Quoting')} @{q_handle}: {q_text}]"
|
||||||
|
else:
|
||||||
|
text += f"\n[{_('Quoting')} @{q_handle}]"
|
||||||
|
|
||||||
|
elif etype and ("external" in etype):
|
||||||
|
ext = g(embed, "external", {})
|
||||||
|
uri = g(ext, "uri", "")
|
||||||
|
title = g(ext, "title", "")
|
||||||
|
text += f"\n[{_('Link')}: {title}]"
|
||||||
|
|
||||||
|
# Date
|
||||||
|
indexed_at = g(actual_post, "indexed_at", "")
|
||||||
|
ts_str = ""
|
||||||
|
if indexed_at:
|
||||||
|
try:
|
||||||
|
# Try arrow parsing
|
||||||
|
import arrow
|
||||||
|
ts = arrow.get(indexed_at)
|
||||||
|
if relative_times:
|
||||||
|
ts_str = ts.humanize(locale=languageHandler.curLang[:2])
|
||||||
|
else:
|
||||||
|
ts_str = ts.format(_("dddd, MMMM D, YYYY H:m"), locale=languageHandler.curLang[:2])
|
||||||
|
except Exception:
|
||||||
|
ts_str = str(indexed_at)[:16].replace("T", " ")
|
||||||
|
|
||||||
|
# Source (not always available in Bsky view, often just client)
|
||||||
|
# We'll leave it empty or mock it if needed
|
||||||
|
source = "Bluesky"
|
||||||
|
|
||||||
|
|
||||||
|
return [user_str, text, ts_str, source]
|
||||||
|
|
||||||
|
def compose_user(user, db, settings, relative_times, show_screen_names=False, safe=True):
|
||||||
|
"""
|
||||||
|
Compose a Bluesky user for list display.
|
||||||
|
Returns: [User summary string]
|
||||||
|
"""
|
||||||
|
# Extract data using getattr for models or .get for dicts
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
handle = g(user, "handle", "unknown")
|
||||||
|
display_name = g(user, "displayName") or g(user, "display_name") or handle
|
||||||
|
followers = g(user, "followersCount", None)
|
||||||
|
following = g(user, "followsCount", None)
|
||||||
|
posts = g(user, "postsCount", None)
|
||||||
|
created_at = g(user, "createdAt", None)
|
||||||
|
|
||||||
|
ts = ""
|
||||||
|
if created_at:
|
||||||
|
try:
|
||||||
|
import arrow
|
||||||
|
original_date = arrow.get(created_at)
|
||||||
|
if relative_times:
|
||||||
|
ts = original_date.humanize(locale=languageHandler.curLang[:2])
|
||||||
|
else:
|
||||||
|
offset = db.get("utc_offset", 0) if isinstance(db, dict) else 0
|
||||||
|
ts = original_date.shift(hours=offset).format(_("dddd, MMMM D, YYYY H:m"), locale=languageHandler.curLang[:2])
|
||||||
|
except Exception:
|
||||||
|
ts = str(created_at)
|
||||||
|
|
||||||
|
parts = [f"{display_name} (@{handle})."]
|
||||||
|
if followers is not None and following is not None and posts is not None:
|
||||||
|
parts.append(_("{followers} followers, {following} following, {posts} posts.").format(
|
||||||
|
followers=followers, following=following, posts=posts
|
||||||
|
))
|
||||||
|
if ts:
|
||||||
|
parts.append(_("Joined {date}").format(date=ts))
|
||||||
|
|
||||||
|
return [" ".join(parts).strip()]
|
||||||
|
|
||||||
|
def compose_convo(convo, db, settings, relative_times, show_screen_names=False, safe=True):
|
||||||
|
"""
|
||||||
|
Compose a Bluesky chat conversation for list display.
|
||||||
|
Returns: [Participants, Last Message, Date]
|
||||||
|
"""
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
members = g(convo, "members", [])
|
||||||
|
self_did = db.get("user_id") if isinstance(db, dict) else None
|
||||||
|
others = []
|
||||||
|
for m in members:
|
||||||
|
did = g(m, "did", None)
|
||||||
|
if self_did and did == self_did:
|
||||||
|
continue
|
||||||
|
label = g(m, "displayName") or g(m, "display_name") or g(m, "handle", "unknown")
|
||||||
|
others.append(label)
|
||||||
|
if not others:
|
||||||
|
others = [g(m, "displayName") or g(m, "display_name") or g(m, "handle", "unknown") for m in members]
|
||||||
|
participants = ", ".join(others)
|
||||||
|
|
||||||
|
last_msg_obj = g(convo, "lastMessage") or g(convo, "last_message")
|
||||||
|
last_text = ""
|
||||||
|
last_sender = ""
|
||||||
|
if last_msg_obj:
|
||||||
|
last_text = g(last_msg_obj, "text", "")
|
||||||
|
sender = g(last_msg_obj, "sender", None)
|
||||||
|
if sender:
|
||||||
|
last_sender = g(sender, "displayName") or g(sender, "display_name") or g(sender, "handle", "")
|
||||||
|
|
||||||
|
# Date (using lastMessage.sentAt)
|
||||||
|
date_str = ""
|
||||||
|
sent_at = None
|
||||||
|
if last_msg_obj:
|
||||||
|
sent_at = g(last_msg_obj, "sentAt") or g(last_msg_obj, "sent_at")
|
||||||
|
|
||||||
|
if sent_at:
|
||||||
|
try:
|
||||||
|
import arrow
|
||||||
|
ts = arrow.get(sent_at)
|
||||||
|
if relative_times:
|
||||||
|
date_str = ts.humanize(locale=languageHandler.curLang[:2])
|
||||||
|
else:
|
||||||
|
date_str = ts.format(_("dddd, MMMM D, YYYY H:m"), locale=languageHandler.curLang[:2])
|
||||||
|
except:
|
||||||
|
date_str = str(sent_at)[:16]
|
||||||
|
|
||||||
|
if last_sender and last_text:
|
||||||
|
last_text = _("Last message from {user}: {text}").format(user=last_sender, text=last_text)
|
||||||
|
elif last_text:
|
||||||
|
last_text = _("Last message: {text}").format(text=last_text)
|
||||||
|
|
||||||
|
return [participants, last_text, date_str]
|
||||||
|
|
||||||
|
def compose_chat_message(msg, db, settings, relative_times, show_screen_names=False, safe=True):
|
||||||
|
"""
|
||||||
|
Compose an individual chat message for display in a thread.
|
||||||
|
Returns: [Sender, Text, Date]
|
||||||
|
"""
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
sender = g(msg, "sender", {})
|
||||||
|
handle = g(sender, "displayName") or g(sender, "display_name") or g(sender, "handle", "unknown")
|
||||||
|
|
||||||
|
text = g(msg, "text", "")
|
||||||
|
|
||||||
|
sent_at = g(msg, "sentAt") or g(msg, "sent_at")
|
||||||
|
date_str = ""
|
||||||
|
if sent_at:
|
||||||
|
try:
|
||||||
|
import arrow
|
||||||
|
ts = arrow.get(sent_at)
|
||||||
|
if relative_times:
|
||||||
|
date_str = ts.humanize(locale=languageHandler.curLang[:2])
|
||||||
|
else:
|
||||||
|
date_str = ts.format(_("dddd, MMMM D, YYYY H:m"), locale=languageHandler.curLang[:2])
|
||||||
|
except:
|
||||||
|
date_str = str(sent_at)[:16]
|
||||||
|
|
||||||
|
return [handle, text, date_str]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import wx
|
import wx
|
||||||
@@ -66,6 +67,7 @@ class Session(base.baseSession):
|
|||||||
handle = (
|
handle = (
|
||||||
self.db.get("user_name")
|
self.db.get("user_name")
|
||||||
or (self.settings and self.settings.get("blueski", {}).get("handle"))
|
or (self.settings and self.settings.get("blueski", {}).get("handle"))
|
||||||
|
or (self.settings and self.settings.get("atprotosocial", {}).get("handle"))
|
||||||
or (getattr(getattr(self, "api", None), "me", None) and self.api.me.handle)
|
or (getattr(getattr(self, "api", None), "me", None) and self.api.me.handle)
|
||||||
)
|
)
|
||||||
if handle:
|
if handle:
|
||||||
@@ -129,9 +131,10 @@ class Session(base.baseSession):
|
|||||||
self.settings.write()
|
self.settings.write()
|
||||||
self.logged = True
|
self.logged = True
|
||||||
log.debug("Logged in to Bluesky as %s", api.me.handle)
|
log.debug("Logged in to Bluesky as %s", api.me.handle)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
log.exception("Bluesky login failed")
|
log.exception("Bluesky login failed")
|
||||||
self.logged = False
|
self.logged = False
|
||||||
|
raise e
|
||||||
|
|
||||||
def authorise(self):
|
def authorise(self):
|
||||||
self._ensure_settings_namespace()
|
self._ensure_settings_namespace()
|
||||||
@@ -175,7 +178,7 @@ class Session(base.baseSession):
|
|||||||
_("We could not log in to Bluesky. Please verify your handle and app password."),
|
_("We could not log in to Bluesky. Please verify your handle and app password."),
|
||||||
_("Login error"), wx.ICON_ERROR
|
_("Login error"), wx.ICON_ERROR
|
||||||
)
|
)
|
||||||
return
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_message_url(self, message_id, context=None):
|
def get_message_url(self, message_id, context=None):
|
||||||
@@ -207,6 +210,22 @@ class Session(base.baseSession):
|
|||||||
"$type": "app.bsky.feed.post",
|
"$type": "app.bsky.feed.post",
|
||||||
"text": text,
|
"text": text,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Facets (Links and Mentions)
|
||||||
|
try:
|
||||||
|
facets = self._get_facets(text, api)
|
||||||
|
if facets:
|
||||||
|
record["facets"] = facets
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Labels (CW)
|
||||||
|
if cw_text:
|
||||||
|
record["labels"] = {
|
||||||
|
"$type": "com.atproto.label.defs#selfLabels",
|
||||||
|
"values": [{"val": "warn"}]
|
||||||
|
}
|
||||||
|
|
||||||
# createdAt
|
# createdAt
|
||||||
try:
|
try:
|
||||||
record["createdAt"] = api.get_current_time_iso()
|
record["createdAt"] = api.get_current_time_iso()
|
||||||
@@ -360,16 +379,164 @@ class Session(base.baseSession):
|
|||||||
uri = None
|
uri = None
|
||||||
if not uri:
|
if not uri:
|
||||||
raise RuntimeError("Post did not return a URI")
|
raise RuntimeError("Post did not return a URI")
|
||||||
# Store last post id if useful
|
|
||||||
self.db.setdefault("sent", [])
|
|
||||||
self.db["sent"].append(dict(id=uri, text=message))
|
|
||||||
self.save_persistent_data()
|
|
||||||
return uri
|
return uri
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("Error sending Bluesky post")
|
log.exception("Error sending Bluesky post")
|
||||||
output.speak(_("An error occurred while posting to Bluesky."), True)
|
output.speak(_("An error occurred while posting to Bluesky."), True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _get_facets(self, text, api):
|
||||||
|
facets = []
|
||||||
|
# Mentions
|
||||||
|
for m in re.finditer(r'@([a-zA-Z0-9.-]+)', text):
|
||||||
|
handle = m.group(1)
|
||||||
|
try:
|
||||||
|
# We should probably cache this identity lookup
|
||||||
|
res = api.com.atproto.identity.resolve_handle({'handle': handle})
|
||||||
|
did = res.did
|
||||||
|
facets.append({
|
||||||
|
'index': {
|
||||||
|
'byteStart': len(text[:m.start()].encode('utf-8')),
|
||||||
|
'byteEnd': len(text[:m.end()].encode('utf-8'))
|
||||||
|
},
|
||||||
|
'features': [{'$type': 'app.bsky.richtext.facet#mention', 'did': did}]
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
# Links
|
||||||
|
for m in re.finditer(r'(https?://[^\s]+)', text):
|
||||||
|
url = m.group(1)
|
||||||
|
facets.append({
|
||||||
|
'index': {
|
||||||
|
'byteStart': len(text[:m.start()].encode('utf-8')),
|
||||||
|
'byteEnd': len(text[:m.end()].encode('utf-8'))
|
||||||
|
},
|
||||||
|
'features': [{'$type': 'app.bsky.richtext.facet#link', 'uri': url}]
|
||||||
|
})
|
||||||
|
return facets
|
||||||
|
|
||||||
|
def delete_post(self, uri: str) -> bool:
|
||||||
|
"""Delete a post by its AT URI."""
|
||||||
|
api = self._ensure_client()
|
||||||
|
try:
|
||||||
|
# at://did:plc:xxx/app.bsky.feed.post/rkey
|
||||||
|
parts = uri.split("/")
|
||||||
|
rkey = parts[-1]
|
||||||
|
api.com.atproto.repo.delete_record({
|
||||||
|
"repo": api.me.did,
|
||||||
|
"collection": "app.bsky.feed.post",
|
||||||
|
"rkey": rkey
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
log.exception("Error deleting Bluesky post")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def block_user(self, did: str) -> bool:
|
||||||
|
"""Block a user by their DID."""
|
||||||
|
api = self._ensure_client()
|
||||||
|
try:
|
||||||
|
api.com.atproto.repo.create_record({
|
||||||
|
"repo": api.me.did,
|
||||||
|
"collection": "app.bsky.graph.block",
|
||||||
|
"record": {
|
||||||
|
"$type": "app.bsky.graph.block",
|
||||||
|
"subject": did,
|
||||||
|
"createdAt": api.get_current_time_iso()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
log.exception("Error blocking Bluesky user")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def unblock_user(self, block_uri: str) -> bool:
|
||||||
|
"""Unblock a user by the URI of the block record."""
|
||||||
|
api = self._ensure_client()
|
||||||
|
try:
|
||||||
|
parts = block_uri.split("/")
|
||||||
|
rkey = parts[-1]
|
||||||
|
api.com.atproto.repo.delete_record({
|
||||||
|
"repo": api.me.did,
|
||||||
|
"collection": "app.bsky.graph.block",
|
||||||
|
"rkey": rkey
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
log.exception("Error unblocking Bluesky user")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_profile(self, actor: str) -> Any:
|
||||||
|
api = self._ensure_client()
|
||||||
|
try:
|
||||||
|
return api.app.bsky.actor.get_profile({"actor": actor})
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error fetching Bluesky profile for %s", actor)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def follow_user(self, did: str) -> bool:
|
||||||
|
api = self._ensure_client()
|
||||||
|
try:
|
||||||
|
api.com.atproto.repo.create_record({
|
||||||
|
"repo": api.me.did,
|
||||||
|
"collection": "app.bsky.graph.follow",
|
||||||
|
"record": {
|
||||||
|
"$type": "app.bsky.graph.follow",
|
||||||
|
"subject": did,
|
||||||
|
"createdAt": api.get_current_time_iso()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error following Bluesky user")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def unfollow_user(self, follow_uri: str) -> bool:
|
||||||
|
api = self._ensure_client()
|
||||||
|
try:
|
||||||
|
parts = follow_uri.split("/")
|
||||||
|
rkey = parts[-1]
|
||||||
|
api.com.atproto.repo.delete_record({
|
||||||
|
"repo": api.me.did,
|
||||||
|
"collection": "app.bsky.graph.follow",
|
||||||
|
"rkey": rkey
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error unfollowing Bluesky user")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def mute_user(self, did: str) -> bool:
|
||||||
|
api = self._ensure_client()
|
||||||
|
try:
|
||||||
|
graph = api.app.bsky.graph
|
||||||
|
if hasattr(graph, "mute_actor"):
|
||||||
|
graph.mute_actor({"actor": did})
|
||||||
|
elif hasattr(graph, "muteActor"):
|
||||||
|
graph.muteActor({"actor": did})
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error muting Bluesky user")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def unmute_user(self, did: str) -> bool:
|
||||||
|
api = self._ensure_client()
|
||||||
|
try:
|
||||||
|
graph = api.app.bsky.graph
|
||||||
|
if hasattr(graph, "unmute_actor"):
|
||||||
|
graph.unmute_actor({"actor": did})
|
||||||
|
elif hasattr(graph, "unmuteActor"):
|
||||||
|
graph.unmuteActor({"actor": did})
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error unmuting Bluesky user")
|
||||||
|
return False
|
||||||
|
|
||||||
def repost(self, post_uri: str, post_cid: str | None = None) -> str | None:
|
def repost(self, post_uri: str, post_cid: str | None = None) -> str | None:
|
||||||
"""Create a simple repost of a given post. Returns URI of the repost record or None."""
|
"""Create a simple repost of a given post. Returns URI of the repost record or None."""
|
||||||
if not self.logged:
|
if not self.logged:
|
||||||
@@ -415,3 +582,80 @@ class Session(base.baseSession):
|
|||||||
except Exception:
|
except Exception:
|
||||||
log.exception("Error creating Bluesky repost record")
|
log.exception("Error creating Bluesky repost record")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def like(self, post_uri: str, post_cid: str | None = None) -> str | None:
|
||||||
|
"""Create a like for a given post."""
|
||||||
|
if not self.logged:
|
||||||
|
raise Exceptions.NotLoggedSessionError("You are not logged in yet.")
|
||||||
|
try:
|
||||||
|
api = self._ensure_client()
|
||||||
|
|
||||||
|
# Resolve strong ref if needed
|
||||||
|
def _get_strong_ref(uri: str):
|
||||||
|
try:
|
||||||
|
posts_res = api.app.bsky.feed.get_posts({"uris": [uri]})
|
||||||
|
posts = getattr(posts_res, "posts", None) or []
|
||||||
|
except Exception:
|
||||||
|
try: posts_res = api.app.bsky.feed.get_posts(uris=[uri])
|
||||||
|
except: posts_res = None
|
||||||
|
posts = getattr(posts_res, "posts", None) or []
|
||||||
|
if posts:
|
||||||
|
p = posts[0]
|
||||||
|
return {"uri": getattr(p, "uri", uri), "cid": getattr(p, "cid", None)}
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not post_cid:
|
||||||
|
strong = _get_strong_ref(post_uri)
|
||||||
|
if not strong: return None
|
||||||
|
post_uri = strong["uri"]
|
||||||
|
post_cid = strong["cid"]
|
||||||
|
|
||||||
|
out = api.com.atproto.repo.create_record({
|
||||||
|
"repo": api.me.did,
|
||||||
|
"collection": "app.bsky.feed.like",
|
||||||
|
"record": {
|
||||||
|
"$type": "app.bsky.feed.like",
|
||||||
|
"subject": {"uri": post_uri, "cid": post_cid},
|
||||||
|
"createdAt": getattr(api, "get_current_time_iso", lambda: None)() or None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return getattr(out, "uri", None)
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error creating Bluesky like")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_followers(self, actor: str | None = None, limit: int = 50, cursor: str | None = None) -> dict[str, Any]:
|
||||||
|
api = self._ensure_client()
|
||||||
|
actor = actor or api.me.did
|
||||||
|
res = api.app.bsky.graph.get_followers({"actor": actor, "limit": limit, "cursor": cursor})
|
||||||
|
return {"items": res.followers, "cursor": res.cursor}
|
||||||
|
|
||||||
|
def get_follows(self, actor: str | None = None, limit: int = 50, cursor: str | None = None) -> dict[str, Any]:
|
||||||
|
api = self._ensure_client()
|
||||||
|
actor = actor or api.me.did
|
||||||
|
res = api.app.bsky.graph.get_follows({"actor": actor, "limit": limit, "cursor": cursor})
|
||||||
|
return {"items": res.follows, "cursor": res.cursor}
|
||||||
|
|
||||||
|
def get_blocks(self, limit: int = 50, cursor: str | None = None) -> dict[str, Any]:
|
||||||
|
api = self._ensure_client()
|
||||||
|
res = api.app.bsky.graph.get_blocks({"limit": limit, "cursor": cursor})
|
||||||
|
return {"items": res.blocks, "cursor": res.cursor}
|
||||||
|
|
||||||
|
def list_convos(self, limit: int = 50, cursor: str | None = None) -> dict[str, Any]:
|
||||||
|
api = self._ensure_client()
|
||||||
|
res = api.chat.bsky.convo.list_convos({"limit": limit, "cursor": cursor})
|
||||||
|
return {"items": res.convos, "cursor": res.cursor}
|
||||||
|
|
||||||
|
def get_convo_messages(self, convo_id: str, limit: int = 50, cursor: str | None = None) -> dict[str, Any]:
|
||||||
|
api = self._ensure_client()
|
||||||
|
res = api.chat.bsky.convo.get_messages({"convoId": convo_id, "limit": limit, "cursor": cursor})
|
||||||
|
return {"items": res.messages, "cursor": res.cursor}
|
||||||
|
|
||||||
|
def send_chat_message(self, convo_id: str, text: str) -> Any:
|
||||||
|
api = self._ensure_client()
|
||||||
|
return api.chat.bsky.convo.send_message({
|
||||||
|
"convoId": convo_id,
|
||||||
|
"message": {
|
||||||
|
"text": text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,393 +1,149 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import wx
|
import wx
|
||||||
import languageHandler # Ensure _() is available
|
import languageHandler
|
||||||
import logging
|
|
||||||
import wx
|
|
||||||
import config
|
|
||||||
from mysc.repeating_timer import RepeatingTimer
|
|
||||||
import arrow
|
|
||||||
import arrow
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from multiplatform_widgets import widgets
|
from multiplatform_widgets import widgets
|
||||||
|
|
||||||
log = logging.getLogger("wxUI.buffers.blueski.panels")
|
class HomePanel(wx.Panel):
|
||||||
|
def __init__(self, parent, name, account="Unknown"):
|
||||||
|
|
||||||
class BlueskiHomeTimelinePanel(object):
|
|
||||||
"""Minimal Home timeline buffer for Bluesky.
|
|
||||||
|
|
||||||
Exposes a .buffer wx.Panel with a List control and provides
|
|
||||||
start_stream()/get_more_items() to fetch items from atproto.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parent, name: str, session):
|
|
||||||
super().__init__()
|
|
||||||
self.session = session
|
|
||||||
self.account = session.get_name()
|
|
||||||
self.name = name
|
|
||||||
self.type = "home_timeline"
|
|
||||||
self.timeline_algorithm = None
|
|
||||||
self.invisible = True
|
|
||||||
self.needs_init = True
|
|
||||||
self.buffer = _HomePanel(parent, name)
|
|
||||||
self.buffer.session = session
|
|
||||||
self.buffer.name = name
|
|
||||||
# Ensure controller can resolve current account from the GUI panel
|
|
||||||
self.buffer.account = self.account
|
|
||||||
self.items = [] # list of dicts: {uri, author, handle, display_name, text, indexed_at}
|
|
||||||
self.cursor = None
|
|
||||||
self._auto_timer = None
|
|
||||||
|
|
||||||
def start_stream(self, mandatory=False, play_sound=True):
|
|
||||||
"""Fetch newest items and render them."""
|
|
||||||
try:
|
|
||||||
count = self.session.settings["general"]["max_posts_per_call"] or 40
|
|
||||||
except Exception:
|
|
||||||
count = 40
|
|
||||||
try:
|
|
||||||
api = self.session._ensure_client()
|
|
||||||
# The atproto SDK expects params, not raw kwargs
|
|
||||||
try:
|
|
||||||
from atproto import models as at_models # type: ignore
|
|
||||||
params = at_models.AppBskyFeedGetTimeline.Params(
|
|
||||||
limit=count,
|
|
||||||
algorithm=self.timeline_algorithm
|
|
||||||
)
|
|
||||||
res = api.app.bsky.feed.get_timeline(params)
|
|
||||||
except Exception:
|
|
||||||
payload = {"limit": count}
|
|
||||||
if self.timeline_algorithm:
|
|
||||||
payload["algorithm"] = self.timeline_algorithm
|
|
||||||
res = api.app.bsky.feed.get_timeline(payload)
|
|
||||||
feed = getattr(res, "feed", [])
|
|
||||||
self.cursor = getattr(res, "cursor", None)
|
|
||||||
self.items = []
|
|
||||||
for it in feed:
|
|
||||||
post = getattr(it, "post", None)
|
|
||||||
if not post:
|
|
||||||
continue
|
|
||||||
# No additional client-side filtering; server distinguishes timelines correctly
|
|
||||||
record = getattr(post, "record", None)
|
|
||||||
author = getattr(post, "author", None)
|
|
||||||
text = getattr(record, "text", "") if record else ""
|
|
||||||
handle = getattr(author, "handle", "") if author else ""
|
|
||||||
display_name = (
|
|
||||||
getattr(author, "display_name", None)
|
|
||||||
or getattr(author, "displayName", None)
|
|
||||||
or ""
|
|
||||||
) if author else ""
|
|
||||||
indexed_at = getattr(post, "indexed_at", None)
|
|
||||||
item = {
|
|
||||||
"uri": getattr(post, "uri", ""),
|
|
||||||
"author": display_name or handle,
|
|
||||||
"handle": handle,
|
|
||||||
"display_name": display_name,
|
|
||||||
"text": text,
|
|
||||||
"indexed_at": indexed_at,
|
|
||||||
}
|
|
||||||
self._append_item(item, to_top=self._reverse())
|
|
||||||
# Full rerender to ensure column widths and selection
|
|
||||||
self._render_list(replace=True)
|
|
||||||
return len(self.items)
|
|
||||||
except Exception:
|
|
||||||
log.exception("Failed to load Bluesky home timeline")
|
|
||||||
self.buffer.list.clear()
|
|
||||||
self.buffer.list.insert_item(False, _("Error"), _("Could not load timeline."), "")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_more_items(self):
|
|
||||||
if not self.cursor:
|
|
||||||
return 0
|
|
||||||
try:
|
|
||||||
api = self.session._ensure_client()
|
|
||||||
try:
|
|
||||||
from atproto import models as at_models # type: ignore
|
|
||||||
params = at_models.AppBskyFeedGetTimeline.Params(
|
|
||||||
limit=40,
|
|
||||||
cursor=self.cursor,
|
|
||||||
algorithm=self.timeline_algorithm
|
|
||||||
)
|
|
||||||
res = api.app.bsky.feed.get_timeline(params)
|
|
||||||
except Exception:
|
|
||||||
payload = {"limit": 40, "cursor": self.cursor}
|
|
||||||
if self.timeline_algorithm:
|
|
||||||
payload["algorithm"] = self.timeline_algorithm
|
|
||||||
res = api.app.bsky.feed.get_timeline(payload)
|
|
||||||
feed = getattr(res, "feed", [])
|
|
||||||
self.cursor = getattr(res, "cursor", None)
|
|
||||||
new_items = []
|
|
||||||
for it in feed:
|
|
||||||
post = getattr(it, "post", None)
|
|
||||||
if not post:
|
|
||||||
continue
|
|
||||||
# No additional client-side filtering
|
|
||||||
record = getattr(post, "record", None)
|
|
||||||
author = getattr(post, "author", None)
|
|
||||||
text = getattr(record, "text", "") if record else ""
|
|
||||||
handle = getattr(author, "handle", "") if author else ""
|
|
||||||
display_name = (
|
|
||||||
getattr(author, "display_name", None)
|
|
||||||
or getattr(author, "displayName", None)
|
|
||||||
or ""
|
|
||||||
) if author else ""
|
|
||||||
indexed_at = getattr(post, "indexed_at", None)
|
|
||||||
new_items.append({
|
|
||||||
"uri": getattr(post, "uri", ""),
|
|
||||||
"author": display_name or handle,
|
|
||||||
"handle": handle,
|
|
||||||
"display_name": display_name,
|
|
||||||
"text": text,
|
|
||||||
"indexed_at": indexed_at,
|
|
||||||
})
|
|
||||||
if not new_items:
|
|
||||||
return 0
|
|
||||||
for it in new_items:
|
|
||||||
self._append_item(it, to_top=self._reverse())
|
|
||||||
# Render only the newly added slice
|
|
||||||
self._render_list(replace=False, start=len(self.items) - len(new_items))
|
|
||||||
return len(new_items)
|
|
||||||
except Exception:
|
|
||||||
log.exception("Failed to load more Bluesky timeline items")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Alias to integrate with mainController expectations for Blueski
|
|
||||||
def load_more_posts(self, *args, **kwargs):
|
|
||||||
return self.get_more_items()
|
|
||||||
|
|
||||||
def _reverse(self) -> bool:
|
|
||||||
try:
|
|
||||||
return bool(self.session.settings["general"].get("reverse_timelines", False))
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _append_item(self, item: dict, to_top: bool = False):
|
|
||||||
if to_top:
|
|
||||||
self.items.insert(0, item)
|
|
||||||
else:
|
|
||||||
self.items.append(item)
|
|
||||||
|
|
||||||
def _render_list(self, replace: bool, start: int = 0):
|
|
||||||
if replace:
|
|
||||||
self.buffer.list.clear()
|
|
||||||
for i in range(start, len(self.items)):
|
|
||||||
it = self.items[i]
|
|
||||||
dt = ""
|
|
||||||
if it.get("indexed_at"):
|
|
||||||
try:
|
|
||||||
# Mastodon-like date formatting: relative or full date
|
|
||||||
rel = False
|
|
||||||
try:
|
|
||||||
rel = bool(self.session.settings["general"].get("relative_times", False))
|
|
||||||
except Exception:
|
|
||||||
rel = False
|
|
||||||
ts = arrow.get(str(it["indexed_at"]))
|
|
||||||
if rel:
|
|
||||||
dt = ts.humanize(locale=languageHandler.curLang[:2])
|
|
||||||
else:
|
|
||||||
dt = ts.format(_("dddd, MMMM D, YYYY H:m:s"), locale=languageHandler.curLang[:2])
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
dt = str(it["indexed_at"])[:16].replace("T", " ")
|
|
||||||
except Exception:
|
|
||||||
dt = ""
|
|
||||||
text = it.get("text", "").replace("\n", " ")
|
|
||||||
if len(text) > 200:
|
|
||||||
text = text[:197] + "..."
|
|
||||||
# Display name and handle like Mastodon: "Display (@handle)"
|
|
||||||
author_col = it.get("author", "")
|
|
||||||
handle = it.get("handle", "")
|
|
||||||
if handle and it.get("display_name"):
|
|
||||||
author_col = f"{it.get('display_name')} (@{handle})"
|
|
||||||
elif handle and not author_col:
|
|
||||||
author_col = f"@{handle}"
|
|
||||||
self.buffer.list.insert_item(False, author_col, text, dt)
|
|
||||||
|
|
||||||
# For compatibility with controller expectations
|
|
||||||
def save_positions(self):
|
|
||||||
try:
|
|
||||||
pos = self.buffer.list.get_selected()
|
|
||||||
self.session.db[self.name + "_pos"] = pos
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Support actions that need a selected item identifier (e.g., reply)
|
|
||||||
def get_selected_item_id(self):
|
|
||||||
try:
|
|
||||||
idx = self.buffer.list.get_selected()
|
|
||||||
if idx is None or idx < 0:
|
|
||||||
return None
|
|
||||||
return self.items[idx].get("uri")
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_message(self):
|
|
||||||
try:
|
|
||||||
idx = self.buffer.list.get_selected()
|
|
||||||
if idx is None or idx < 0:
|
|
||||||
return ""
|
|
||||||
it = self.items[idx]
|
|
||||||
author = it.get("display_name") or it.get("author") or ""
|
|
||||||
handle = it.get("handle")
|
|
||||||
if handle:
|
|
||||||
author = f"{author} (@{handle})" if author else f"@{handle}"
|
|
||||||
text = it.get("text", "").replace("\n", " ")
|
|
||||||
dt = ""
|
|
||||||
if it.get("indexed_at"):
|
|
||||||
try:
|
|
||||||
dt = str(it["indexed_at"])[:16].replace("T", " ")
|
|
||||||
except Exception:
|
|
||||||
dt = ""
|
|
||||||
parts = [p for p in [author, text, dt] if p]
|
|
||||||
return ", ".join(parts)
|
|
||||||
except Exception:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# Auto-refresh support (polling) to simulate near real-time updates
|
|
||||||
def _periodic_refresh(self):
|
|
||||||
try:
|
|
||||||
# Ensure UI updates happen on the main thread
|
|
||||||
wx.CallAfter(self.start_stream, False, False)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def enable_auto_refresh(self, seconds: int | None = None):
|
|
||||||
try:
|
|
||||||
if self._auto_timer:
|
|
||||||
return
|
|
||||||
if seconds is None:
|
|
||||||
# Use global update_period (minutes) → seconds; minimum 15s
|
|
||||||
minutes = config.app["app-settings"].get("update_period", 2)
|
|
||||||
seconds = max(15, int(minutes * 60))
|
|
||||||
self._auto_timer = RepeatingTimer(seconds, self._periodic_refresh)
|
|
||||||
self._auto_timer.start()
|
|
||||||
except Exception:
|
|
||||||
log.exception("Failed to enable auto refresh for Bluesky panel %s", self.name)
|
|
||||||
|
|
||||||
def disable_auto_refresh(self):
|
|
||||||
try:
|
|
||||||
if self._auto_timer:
|
|
||||||
self._auto_timer.stop()
|
|
||||||
self._auto_timer = None
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class _HomePanel(wx.Panel):
|
|
||||||
def __init__(self, parent, name):
|
|
||||||
super().__init__(parent, name=name)
|
super().__init__(parent, name=name)
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.account = account
|
||||||
self.type = "home_timeline"
|
self.type = "home_timeline"
|
||||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
|
||||||
self.list = widgets.list(self, _("Author"), _("Post"), _("Date"), style=wx.LC_REPORT | wx.LC_SINGLE_SEL)
|
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
|
# List
|
||||||
|
self.list = widgets.list(self, _("Author"), _("Post"), _("Date"), style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES)
|
||||||
self.list.set_windows_size(0, 120)
|
self.list.set_windows_size(0, 120)
|
||||||
self.list.set_windows_size(1, 360)
|
self.list.set_windows_size(1, 400)
|
||||||
self.list.set_windows_size(2, 150)
|
self.list.set_windows_size(2, 120)
|
||||||
self.list.set_size()
|
self.list.set_size()
|
||||||
sizer.Add(self.list.list, 1, wx.EXPAND | wx.ALL, 0)
|
|
||||||
self.SetSizer(sizer)
|
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
self.post = wx.Button(self, -1, _("Post"))
|
||||||
|
self.repost = wx.Button(self, -1, _("Repost"))
|
||||||
|
self.reply = wx.Button(self, -1, _("Reply"))
|
||||||
|
self.like = wx.Button(self, wx.ID_ANY, _("Like"))
|
||||||
|
# self.bookmark = wx.Button(self, wx.ID_ANY, _("Bookmark")) # Not yet common in Bsky API usage here
|
||||||
|
self.dm = wx.Button(self, -1, _("Chat"))
|
||||||
|
|
||||||
|
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
||||||
|
btnSizer.Add(self.repost, 0, wx.ALL, 5)
|
||||||
|
btnSizer.Add(self.reply, 0, wx.ALL, 5)
|
||||||
|
btnSizer.Add(self.like, 0, wx.ALL, 5)
|
||||||
|
# btnSizer.Add(self.bookmark, 0, wx.ALL, 5)
|
||||||
|
btnSizer.Add(self.dm, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
self.sizer.Add(self.list.list, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
self.SetSizer(self.sizer)
|
||||||
|
|
||||||
class BlueskiFollowingTimelinePanel(BlueskiHomeTimelinePanel):
|
# Some helper methods expected by controller might be needed?
|
||||||
"""Following-only timeline (reverse-chronological)."""
|
# Controller accesses self.buffer.list directly.
|
||||||
|
# Some older code expected .set_position, .post, .message, .actions attributes or buttons on the panel?
|
||||||
|
# Mastodon panels usually have bottom buttons (Post, Reply, etc).
|
||||||
|
# I should add them if I want to "reuse Mastodon".
|
||||||
|
|
||||||
|
# But for now, simple list is what the previous code had.
|
||||||
|
|
||||||
|
def set_focus_function(self, func):
|
||||||
|
self.list.list.Bind(wx.EVT_SET_FOCUS, func)
|
||||||
|
|
||||||
|
def set_position(self, reverse):
|
||||||
|
if reverse:
|
||||||
|
self.list.select_item(0)
|
||||||
|
else:
|
||||||
|
self.list.select_item(self.list.get_count() - 1)
|
||||||
|
|
||||||
def __init__(self, parent, name: str, session):
|
def set_focus_in_list(self):
|
||||||
super().__init__(parent, name, session)
|
self.list.list.SetFocus()
|
||||||
self.type = "following_timeline"
|
|
||||||
self.timeline_algorithm = "reverse-chronological"
|
|
||||||
# Make sure the underlying wx panel also reflects this type
|
|
||||||
try:
|
|
||||||
self.buffer.type = "following_timeline"
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def start_stream(self, mandatory=False, play_sound=True):
|
class NotificationPanel(HomePanel):
|
||||||
try:
|
pass
|
||||||
count = self.session.settings["general"]["max_posts_per_call"] or 40
|
|
||||||
except Exception:
|
|
||||||
count = 40
|
|
||||||
try:
|
|
||||||
api = self.session._ensure_client()
|
|
||||||
# Following timeline via reverse-chronological algorithm on get_timeline
|
|
||||||
# Use plain dict to avoid typed-model mismatches across SDK versions
|
|
||||||
res = api.app.bsky.feed.get_timeline({"limit": count, "algorithm": self.timeline_algorithm})
|
|
||||||
feed = getattr(res, "feed", [])
|
|
||||||
self.cursor = getattr(res, "cursor", None)
|
|
||||||
self.items = []
|
|
||||||
for it in feed:
|
|
||||||
post = getattr(it, "post", None)
|
|
||||||
if not post:
|
|
||||||
continue
|
|
||||||
record = getattr(post, "record", None)
|
|
||||||
author = getattr(post, "author", None)
|
|
||||||
text = getattr(record, "text", "") if record else ""
|
|
||||||
handle = getattr(author, "handle", "") if author else ""
|
|
||||||
display_name = (
|
|
||||||
getattr(author, "display_name", None)
|
|
||||||
or getattr(author, "displayName", None)
|
|
||||||
or ""
|
|
||||||
) if author else ""
|
|
||||||
indexed_at = getattr(post, "indexed_at", None)
|
|
||||||
item = {
|
|
||||||
"uri": getattr(post, "uri", ""),
|
|
||||||
"author": display_name or handle,
|
|
||||||
"handle": handle,
|
|
||||||
"display_name": display_name,
|
|
||||||
"text": text,
|
|
||||||
"indexed_at": indexed_at,
|
|
||||||
}
|
|
||||||
self._append_item(item, to_top=self._reverse())
|
|
||||||
self._render_list(replace=True)
|
|
||||||
return len(self.items)
|
|
||||||
except Exception:
|
|
||||||
log.exception("Failed to load Bluesky following timeline")
|
|
||||||
self.buffer.list.clear()
|
|
||||||
self.buffer.list.insert_item(False, _("Error"), _("Could not load timeline."), "")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_more_items(self):
|
class UserPanel(wx.Panel):
|
||||||
if not self.cursor:
|
def __init__(self, parent, name, account="Unknown"):
|
||||||
return 0
|
super().__init__(parent, name=name)
|
||||||
try:
|
self.name = name
|
||||||
api = self.session._ensure_client()
|
self.account = account
|
||||||
# Pagination via reverse-chronological algorithm on get_timeline
|
self.type = "user"
|
||||||
res = api.app.bsky.feed.get_timeline({
|
|
||||||
"limit": 40,
|
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
"cursor": self.cursor,
|
|
||||||
"algorithm": self.timeline_algorithm
|
# List: User
|
||||||
})
|
self.list = widgets.list(self, _("User"), style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES)
|
||||||
feed = getattr(res, "feed", [])
|
self.list.set_windows_size(0, 600)
|
||||||
self.cursor = getattr(res, "cursor", None)
|
self.list.set_size()
|
||||||
new_items = []
|
|
||||||
for it in feed:
|
|
||||||
post = getattr(it, "post", None)
|
|
||||||
if not post:
|
|
||||||
continue
|
|
||||||
record = getattr(post, "record", None)
|
|
||||||
author = getattr(post, "author", None)
|
|
||||||
text = getattr(record, "text", "") if record else ""
|
|
||||||
handle = getattr(author, "handle", "") if author else ""
|
|
||||||
display_name = (
|
|
||||||
getattr(author, "display_name", None)
|
|
||||||
or getattr(author, "displayName", None)
|
|
||||||
or ""
|
|
||||||
) if author else ""
|
|
||||||
indexed_at = getattr(post, "indexed_at", None)
|
|
||||||
new_items.append({
|
|
||||||
"uri": getattr(post, "uri", ""),
|
|
||||||
"author": display_name or handle,
|
|
||||||
"handle": handle,
|
|
||||||
"display_name": display_name,
|
|
||||||
"text": text,
|
|
||||||
"indexed_at": indexed_at,
|
|
||||||
})
|
|
||||||
if not new_items:
|
|
||||||
return 0
|
|
||||||
for it in new_items:
|
|
||||||
self._append_item(it, to_top=self._reverse())
|
|
||||||
self._render_list(replace=False, start=len(self.items) - len(new_items))
|
|
||||||
return len(new_items)
|
|
||||||
except Exception:
|
|
||||||
log.exception("Failed to load more items for following timeline")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
self.post = wx.Button(self, -1, _("Post"))
|
||||||
|
self.actions = wx.Button(self, -1, _("Actions"))
|
||||||
|
self.message = wx.Button(self, -1, _("Message"))
|
||||||
|
|
||||||
|
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
||||||
|
btnSizer.Add(self.actions, 0, wx.ALL, 5)
|
||||||
|
btnSizer.Add(self.message, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
self.sizer.Add(self.list.list, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
self.SetSizer(self.sizer)
|
||||||
|
|
||||||
|
def set_focus_function(self, func):
|
||||||
|
self.list.list.Bind(wx.EVT_SET_FOCUS, func)
|
||||||
|
|
||||||
|
def set_position(self, reverse):
|
||||||
|
if reverse:
|
||||||
|
self.list.select_item(0)
|
||||||
|
else:
|
||||||
|
self.list.select_item(self.list.get_count() - 1)
|
||||||
|
|
||||||
|
def set_focus_in_list(self):
|
||||||
|
self.list.list.SetFocus()
|
||||||
|
|
||||||
|
class ChatPanel(wx.Panel):
|
||||||
|
def __init__(self, parent, name, account="Unknown"):
|
||||||
|
super().__init__(parent, name=name)
|
||||||
|
self.name = name
|
||||||
|
self.account = account
|
||||||
|
self.type = "chat"
|
||||||
|
|
||||||
|
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
|
# List: Participants, Last Message, Date
|
||||||
|
self.list = widgets.list(self, _("Participants"), _("Last Message"), _("Date"), style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES)
|
||||||
|
self.list.set_windows_size(0, 200)
|
||||||
|
self.list.set_windows_size(1, 600)
|
||||||
|
self.list.set_windows_size(2, 200)
|
||||||
|
self.list.set_size()
|
||||||
|
|
||||||
|
self.sizer.Add(self.list.list, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
self.SetSizer(self.sizer)
|
||||||
|
|
||||||
|
def set_focus_function(self, func):
|
||||||
|
self.list.list.Bind(wx.EVT_SET_FOCUS, func)
|
||||||
|
|
||||||
|
def set_focus_in_list(self):
|
||||||
|
self.list.list.SetFocus()
|
||||||
|
|
||||||
|
class ChatMessagePanel(HomePanel):
|
||||||
|
def __init__(self, parent, name, account="Unknown"):
|
||||||
|
super().__init__(parent, name, account)
|
||||||
|
self.type = "chat_messages"
|
||||||
|
# Adjust buttons for chat
|
||||||
|
self.repost.Hide()
|
||||||
|
self.like.Hide()
|
||||||
|
self.reply.SetLabel(_("Send Message"))
|
||||||
|
|
||||||
|
# Refresh columns
|
||||||
|
self.list.list.ClearAll()
|
||||||
|
self.list.list.InsertColumn(0, _("Sender"))
|
||||||
|
self.list.list.InsertColumn(1, _("Message"))
|
||||||
|
self.list.list.InsertColumn(2, _("Date"))
|
||||||
|
self.list.set_windows_size(0, 100)
|
||||||
|
self.list.set_windows_size(1, 400)
|
||||||
|
self.list.set_windows_size(2, 100)
|
||||||
|
self.list.set_size()
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ class basePanel(wx.Panel):
|
|||||||
|
|
||||||
def create_list(self):
|
def create_list(self):
|
||||||
self.list = widgets.list(self, _(u"User"), _(u"Text"), _(u"Date"), _(u"Client"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
self.list = widgets.list(self, _(u"User"), _(u"Text"), _(u"Date"), _(u"Client"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
||||||
self.list.set_windows_size(0, 60)
|
self.list.set_windows_size(0, 200)
|
||||||
self.list.set_windows_size(1, 320)
|
self.list.set_windows_size(1, 600)
|
||||||
self.list.set_windows_size(2, 110)
|
self.list.set_windows_size(2, 200)
|
||||||
self.list.set_windows_size(3, 84)
|
self.list.set_windows_size(3, 200)
|
||||||
self.list.set_size()
|
self.list.set_size()
|
||||||
|
|
||||||
def __init__(self, parent, name):
|
def __init__(self, parent, name):
|
||||||
@@ -35,7 +35,7 @@ class basePanel(wx.Panel):
|
|||||||
btnSizer.Add(self.bookmark, 0, wx.ALL, 5)
|
btnSizer.Add(self.bookmark, 0, wx.ALL, 5)
|
||||||
btnSizer.Add(self.dm, 0, wx.ALL, 5)
|
btnSizer.Add(self.dm, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5)
|
self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5)
|
||||||
self.SetSizer(self.sizer)
|
self.SetSizer(self.sizer)
|
||||||
self.SetClientSize(self.sizer.CalcMin())
|
self.SetClientSize(self.sizer.CalcMin())
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ class conversationListPanel(wx.Panel):
|
|||||||
|
|
||||||
def create_list(self):
|
def create_list(self):
|
||||||
self.list = widgets.list(self, _(u"User"), _(u"Text"), _(u"Date"), _(u"Client"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
self.list = widgets.list(self, _(u"User"), _(u"Text"), _(u"Date"), _(u"Client"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
||||||
self.list.set_windows_size(0, 60)
|
self.list.set_windows_size(0, 200)
|
||||||
self.list.set_windows_size(1, 320)
|
self.list.set_windows_size(1, 600)
|
||||||
self.list.set_windows_size(2, 110)
|
self.list.set_windows_size(2, 200)
|
||||||
self.list.set_windows_size(3, 84)
|
self.list.set_windows_size(3, 200)
|
||||||
self.list.set_size()
|
self.list.set_size()
|
||||||
|
|
||||||
def __init__(self, parent, name):
|
def __init__(self, parent, name):
|
||||||
@@ -27,7 +27,7 @@ class conversationListPanel(wx.Panel):
|
|||||||
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
||||||
btnSizer.Add(self.reply, 0, wx.ALL, 5)
|
btnSizer.Add(self.reply, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5)
|
self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5)
|
||||||
self.SetSizer(self.sizer)
|
self.SetSizer(self.sizer)
|
||||||
self.SetClientSize(self.sizer.CalcMin())
|
self.SetClientSize(self.sizer.CalcMin())
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ class notificationsPanel(wx.Panel):
|
|||||||
|
|
||||||
def create_list(self):
|
def create_list(self):
|
||||||
self.list = widgets.list(self, _("Text"), _("Date"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
self.list = widgets.list(self, _("Text"), _("Date"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
||||||
self.list.set_windows_size(0, 320)
|
self.list.set_windows_size(0, 600)
|
||||||
self.list.set_windows_size(2, 110)
|
self.list.set_windows_size(1, 200)
|
||||||
self.list.set_size()
|
self.list.set_size()
|
||||||
|
|
||||||
def __init__(self, parent, name):
|
def __init__(self, parent, name):
|
||||||
@@ -25,7 +25,7 @@ class notificationsPanel(wx.Panel):
|
|||||||
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
||||||
btnSizer.Add(self.dismiss, 0, wx.ALL, 5)
|
btnSizer.Add(self.dismiss, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5)
|
self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5)
|
||||||
self.SetSizer(self.sizer)
|
self.SetSizer(self.sizer)
|
||||||
self.SetClientSize(self.sizer.CalcMin())
|
self.SetClientSize(self.sizer.CalcMin())
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class userPanel(wx.Panel):
|
|||||||
|
|
||||||
def create_list(self):
|
def create_list(self):
|
||||||
self.list = widgets.list(self, _("User"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
self.list = widgets.list(self, _("User"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
||||||
self.list.set_windows_size(0, 320)
|
self.list.set_windows_size(0, 600)
|
||||||
self.list.set_size()
|
self.list.set_size()
|
||||||
|
|
||||||
def __init__(self, parent, name):
|
def __init__(self, parent, name):
|
||||||
@@ -23,7 +23,7 @@ class userPanel(wx.Panel):
|
|||||||
btnSizer.Add(self.actions, 0, wx.ALL, 5)
|
btnSizer.Add(self.actions, 0, wx.ALL, 5)
|
||||||
btnSizer.Add(self.message, 0, wx.ALL, 5)
|
btnSizer.Add(self.message, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5)
|
self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5)
|
||||||
self.SetSizer(self.sizer)
|
self.SetSizer(self.sizer)
|
||||||
self.SetClientSize(self.sizer.CalcMin())
|
self.SetClientSize(self.sizer.CalcMin())
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ class Post(wx.Dialog):
|
|||||||
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
# Text
|
# Text
|
||||||
|
post_label = wx.StaticText(self, wx.ID_ANY, caption)
|
||||||
|
main_sizer.Add(post_label, 0, wx.ALL, 6)
|
||||||
self.text = wx.TextCtrl(self, wx.ID_ANY, text, style=wx.TE_MULTILINE)
|
self.text = wx.TextCtrl(self, wx.ID_ANY, text, style=wx.TE_MULTILINE)
|
||||||
|
self.Bind(wx.EVT_CHAR_HOOK, self.handle_keys, self.text)
|
||||||
self.text.SetMinSize((400, 160))
|
self.text.SetMinSize((400, 160))
|
||||||
main_sizer.Add(self.text, 1, wx.EXPAND | wx.ALL, 6)
|
main_sizer.Add(self.text, 1, wx.EXPAND | wx.ALL, 6)
|
||||||
|
|
||||||
@@ -58,6 +61,7 @@ class Post(wx.Dialog):
|
|||||||
|
|
||||||
self.SetSizer(main_sizer)
|
self.SetSizer(main_sizer)
|
||||||
main_sizer.Fit(self)
|
main_sizer.Fit(self)
|
||||||
|
self.SetEscapeId(cancel.GetId())
|
||||||
self.Layout()
|
self.Layout()
|
||||||
|
|
||||||
# Bindings
|
# Bindings
|
||||||
@@ -66,6 +70,13 @@ class Post(wx.Dialog):
|
|||||||
self.attach_list.Bind(wx.EVT_LIST_ITEM_SELECTED, lambda evt: self.btn_remove.Enable(True))
|
self.attach_list.Bind(wx.EVT_LIST_ITEM_SELECTED, lambda evt: self.btn_remove.Enable(True))
|
||||||
self.attach_list.Bind(wx.EVT_LIST_ITEM_DESELECTED, lambda evt: self.btn_remove.Enable(False))
|
self.attach_list.Bind(wx.EVT_LIST_ITEM_DESELECTED, lambda evt: self.btn_remove.Enable(False))
|
||||||
|
|
||||||
|
def handle_keys(self, event):
|
||||||
|
shift = event.ShiftDown()
|
||||||
|
if event.GetKeyCode() == wx.WXK_RETURN and not shift and hasattr(self, "send"):
|
||||||
|
self.EndModal(wx.ID_OK)
|
||||||
|
else:
|
||||||
|
event.Skip()
|
||||||
|
|
||||||
def on_add(self, evt):
|
def on_add(self, evt):
|
||||||
if self.attach_list.GetItemCount() >= 4:
|
if self.attach_list.GetItemCount() >= 4:
|
||||||
wx.MessageBox(_("You can attach up to 4 images."), _("Attachment limit"), wx.ICON_INFORMATION)
|
wx.MessageBox(_("You can attach up to 4 images."), _("Attachment limit"), wx.ICON_INFORMATION)
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import wx
|
import wx
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
from pubsub import pub
|
import languageHandler
|
||||||
|
import builtins
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from approve.translation import translate as _
|
_ = getattr(builtins, "_", lambda s: s)
|
||||||
from approve.notifications import NotificationError
|
|
||||||
# Assuming controller.blueski.userList.get_user_profile_details and session.util._format_profile_data exist
|
|
||||||
# For direct call to util:
|
|
||||||
# from sessions.blueski import utils as BlueskiUtils
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -25,7 +22,7 @@ class ShowUserProfileDialog(wx.Dialog):
|
|||||||
self.SetMinSize((400, 300))
|
self.SetMinSize((400, 300))
|
||||||
self.CentreOnParent()
|
self.CentreOnParent()
|
||||||
|
|
||||||
wx.CallAfter(asyncio.create_task, self.load_profile_data())
|
Thread(target=self.load_profile_data, daemon=True).start()
|
||||||
|
|
||||||
def _init_ui(self):
|
def _init_ui(self):
|
||||||
panel = wx.Panel(self)
|
panel = wx.Panel(self)
|
||||||
@@ -36,17 +33,23 @@ class ShowUserProfileDialog(wx.Dialog):
|
|||||||
self.info_grid_sizer.AddGrowableCol(1, 1)
|
self.info_grid_sizer.AddGrowableCol(1, 1)
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
(_("Display Name:"), "displayName"), (_("Handle:"), "handle"), (_("DID:"), "did"),
|
(_("&Name:"), "displayName"), (_("&Handle:"), "handle"), (_("&DID:"), "did"),
|
||||||
(_("Followers:"), "followersCount"), (_("Following:"), "followsCount"), (_("Posts:"), "postsCount"),
|
(_("&Followers:"), "followersCount"), (_("&Following:"), "followsCount"), (_("&Posts:"), "postsCount"),
|
||||||
(_("Bio:"), "description")
|
(_("&Bio:"), "description")
|
||||||
]
|
]
|
||||||
self.profile_field_ctrls = {}
|
self.profile_field_ctrls = {}
|
||||||
|
|
||||||
for label_text, data_key in fields:
|
for label_text, data_key in fields:
|
||||||
lbl = wx.StaticText(panel, label=label_text)
|
lbl = wx.StaticText(panel, label=label_text)
|
||||||
val_ctrl = wx.TextCtrl(panel, style=wx.TE_READONLY | wx.TE_MULTILINE if data_key == "description" else wx.TE_READONLY | wx.BORDER_NONE)
|
style = wx.TE_READONLY | wx.TE_PROCESS_TAB
|
||||||
|
if data_key == "description":
|
||||||
|
style |= wx.TE_MULTILINE
|
||||||
|
else:
|
||||||
|
style |= wx.BORDER_NONE
|
||||||
|
val_ctrl = wx.TextCtrl(panel, style=style)
|
||||||
if data_key != "description": # Make it look like a label
|
if data_key != "description": # Make it look like a label
|
||||||
val_ctrl.SetBackgroundColour(panel.GetBackgroundColour())
|
val_ctrl.SetBackgroundColour(panel.GetBackgroundColour())
|
||||||
|
val_ctrl.AcceptsFocusFromKeyboard = lambda: True
|
||||||
|
|
||||||
self.info_grid_sizer.Add(lbl, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
|
self.info_grid_sizer.Add(lbl, 0, wx.ALIGN_RIGHT | wx.ALIGN_TOP | wx.ALL, 2)
|
||||||
self.info_grid_sizer.Add(val_ctrl, 1, wx.EXPAND | wx.ALL, 2)
|
self.info_grid_sizer.Add(val_ctrl, 1, wx.EXPAND | wx.ALL, 2)
|
||||||
@@ -89,51 +92,62 @@ class ShowUserProfileDialog(wx.Dialog):
|
|||||||
main_sizer.Add(actions_sizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10)
|
main_sizer.Add(actions_sizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10)
|
||||||
|
|
||||||
# Close Button
|
# Close Button
|
||||||
close_btn = wx.Button(panel, wx.ID_CANCEL, _("Close"))
|
close_btn = wx.Button(panel, wx.ID_CANCEL, _("&Close"))
|
||||||
close_btn.SetDefault() # Allow Esc to close
|
close_btn.SetDefault() # Allow Esc to close
|
||||||
main_sizer.Add(close_btn, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
|
main_sizer.Add(close_btn, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
|
||||||
|
self.SetEscapeId(close_btn.GetId())
|
||||||
|
|
||||||
panel.SetSizer(main_sizer)
|
panel.SetSizer(main_sizer)
|
||||||
self.Fit() # Fit dialog to content
|
self.Fit() # Fit dialog to content
|
||||||
|
|
||||||
async def load_profile_data(self):
|
def load_profile_data(self):
|
||||||
self.SetStatusText(_("Loading profile..."))
|
wx.CallAfter(self.SetStatusText, _("Loading profile..."))
|
||||||
for ctrl in self.profile_field_ctrls.values():
|
for ctrl in self.profile_field_ctrls.values():
|
||||||
ctrl.SetValue(_("Loading..."))
|
wx.CallAfter(ctrl.SetValue, _("Loading..."))
|
||||||
|
|
||||||
# Initially hide all action buttons until state is known
|
# Initially hide all action buttons until state is known
|
||||||
self.follow_btn.Hide()
|
wx.CallAfter(self.follow_btn.Hide)
|
||||||
self.unfollow_btn.Hide()
|
wx.CallAfter(self.unfollow_btn.Hide)
|
||||||
self.mute_btn.Hide()
|
wx.CallAfter(self.mute_btn.Hide)
|
||||||
self.unmute_btn.Hide()
|
wx.CallAfter(self.unmute_btn.Hide)
|
||||||
self.block_btn.Hide()
|
wx.CallAfter(self.block_btn.Hide)
|
||||||
self.unblock_btn.Hide()
|
wx.CallAfter(self.unblock_btn.Hide)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw_profile = await self.session.util.get_user_profile(self.user_identifier)
|
api = self.session._ensure_client()
|
||||||
if raw_profile:
|
try:
|
||||||
self.profile_data = self.session.util._format_profile_data(raw_profile) # This should return a dict
|
raw_profile = api.app.bsky.actor.get_profile({"actor": self.user_identifier})
|
||||||
self.target_user_did = self.profile_data.get("did") # Store the canonical DID
|
except Exception:
|
||||||
self.user_identifier = self.target_user_did # Update identifier to resolved DID for consistency
|
raw_profile = None
|
||||||
|
wx.CallAfter(self._apply_profile_data, raw_profile)
|
||||||
self.update_ui_fields()
|
|
||||||
self.update_action_buttons_state()
|
|
||||||
self.SetTitle(_("Profile: {handle}").format(handle=self.profile_data.get("handle", "")))
|
|
||||||
self.SetStatusText(_("Profile loaded."))
|
|
||||||
else:
|
|
||||||
for ctrl in self.profile_field_ctrls.values():
|
|
||||||
ctrl.SetValue(_("Not found."))
|
|
||||||
self.SetStatusText(_("Profile not found for '{ident}'.").format(ident=self.user_identifier))
|
|
||||||
wx.MessageBox(_("User profile for '{ident}' not found.").format(ident=self.user_identifier), _("Error"), wx.OK | wx.ICON_ERROR, self)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading profile for {self.user_identifier}: {e}", exc_info=True)
|
logger.error(f"Error loading profile for {self.user_identifier}: {e}", exc_info=True)
|
||||||
|
wx.CallAfter(self._apply_profile_error, e)
|
||||||
|
|
||||||
|
def _apply_profile_data(self, raw_profile):
|
||||||
|
if raw_profile:
|
||||||
|
self.profile_data = self._format_profile_data(raw_profile)
|
||||||
|
self.target_user_did = self.profile_data.get("did")
|
||||||
|
self.user_identifier = self.target_user_did or self.user_identifier
|
||||||
|
|
||||||
|
self.update_ui_fields()
|
||||||
|
self.update_action_buttons_state()
|
||||||
|
self.SetTitle(_("Profile: {handle}").format(handle=self.profile_data.get("handle", "")))
|
||||||
|
self.SetStatusText(_("Profile loaded."))
|
||||||
|
else:
|
||||||
for ctrl in self.profile_field_ctrls.values():
|
for ctrl in self.profile_field_ctrls.values():
|
||||||
ctrl.SetValue(_("Error loading."))
|
ctrl.SetValue(_("Not found."))
|
||||||
self.SetStatusText(_("Error loading profile."))
|
self.SetStatusText(_("Profile not found for '{ident}'.").format(ident=self.user_identifier))
|
||||||
wx.MessageBox(_("Error loading profile: {error}").format(error=str(e)), _("Error"), wx.OK | wx.ICON_ERROR, self)
|
wx.MessageBox(_("User profile for '{ident}' not found.").format(ident=self.user_identifier), _("Error"), wx.OK | wx.ICON_ERROR, self)
|
||||||
finally:
|
self.Layout()
|
||||||
self.Layout() # Refresh layout after hiding/showing buttons
|
|
||||||
|
def _apply_profile_error(self, err):
|
||||||
|
for ctrl in self.profile_field_ctrls.values():
|
||||||
|
ctrl.SetValue(_("Error loading."))
|
||||||
|
self.SetStatusText(_("Error loading profile."))
|
||||||
|
wx.MessageBox(_("Error loading profile: {error}").format(error=str(err)), _("Error"), wx.OK | wx.ICON_ERROR, self)
|
||||||
|
self.Layout()
|
||||||
|
|
||||||
def update_ui_fields(self):
|
def update_ui_fields(self):
|
||||||
if not self.profile_data:
|
if not self.profile_data:
|
||||||
@@ -159,7 +173,7 @@ class ShowUserProfileDialog(wx.Dialog):
|
|||||||
self.Layout()
|
self.Layout()
|
||||||
|
|
||||||
def update_action_buttons_state(self):
|
def update_action_buttons_state(self):
|
||||||
if not self.profile_data or not self.target_user_did or self.target_user_did == self.session.util.get_own_did():
|
if not self.profile_data or not self.target_user_did or self.target_user_did == self._get_own_did():
|
||||||
self.follow_btn.Hide()
|
self.follow_btn.Hide()
|
||||||
self.unfollow_btn.Hide()
|
self.unfollow_btn.Hide()
|
||||||
self.mute_btn.Hide()
|
self.mute_btn.Hide()
|
||||||
@@ -218,80 +232,70 @@ class ShowUserProfileDialog(wx.Dialog):
|
|||||||
return
|
return
|
||||||
dlg.Destroy()
|
dlg.Destroy()
|
||||||
|
|
||||||
async def do_action():
|
wx.BeginBusyCursor()
|
||||||
wx.BeginBusyCursor()
|
self.SetStatusText(_("Performing action: {action}...").format(action=command))
|
||||||
self.SetStatusText(_("Performing action: {action}...").format(action=command))
|
action_button = event.GetEventObject()
|
||||||
action_button = event.GetEventObject()
|
if action_button:
|
||||||
if action_button: action_button.Disable() # Disable the clicked button
|
action_button.Disable()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Ensure controller_handler is available on the session
|
if command == "block_user" and hasattr(self.session, "block_user"):
|
||||||
if not hasattr(self.session, 'controller_handler') or not self.session.controller_handler:
|
ok = self.session.block_user(self.target_user_did)
|
||||||
app = wx.GetApp()
|
if not ok:
|
||||||
if hasattr(app, 'mainController'):
|
raise RuntimeError(_("Failed to block user."))
|
||||||
self.session.controller_handler = app.mainController.get_handler(self.session.KIND)
|
elif command == "unblock_user" and hasattr(self.session, "unblock_user"):
|
||||||
if not self.session.controller_handler: # Still not found
|
viewer_state = self.profile_data.get("viewer", {}) if self.profile_data else {}
|
||||||
raise RuntimeError("Controller handler not found for session.")
|
block_uri = viewer_state.get("blocking")
|
||||||
|
if not block_uri:
|
||||||
|
raise RuntimeError(_("Block information not available."))
|
||||||
|
ok = self.session.unblock_user(block_uri)
|
||||||
|
if not ok:
|
||||||
|
raise RuntimeError(_("Failed to unblock user."))
|
||||||
|
else:
|
||||||
|
raise RuntimeError(_("This action is not supported yet."))
|
||||||
|
|
||||||
result = await self.session.controller_handler.handle_user_command(
|
wx.EndBusyCursor()
|
||||||
command=command,
|
wx.MessageBox(_("Action completed."), _("Success"), wx.OK | wx.ICON_INFORMATION, self)
|
||||||
user_id=self.session.uid,
|
wx.CallAfter(asyncio.create_task, self.load_profile_data())
|
||||||
target_user_id=self.target_user_did,
|
except Exception as e:
|
||||||
payload={}
|
wx.EndBusyCursor()
|
||||||
)
|
if action_button:
|
||||||
wx.EndBusyCursor()
|
action_button.Enable()
|
||||||
# Use CallAfter for UI updates from async task
|
self.SetStatusText(_("Action failed."))
|
||||||
wx.CallAfter(wx.MessageBox, result.get("message", _("Action completed.")),
|
wx.MessageBox(str(e), _("Error"), wx.OK | wx.ICON_ERROR, self)
|
||||||
_("Success") if result.get("status") == "success" else _("Error"),
|
|
||||||
wx.OK | (wx.ICON_INFORMATION if result.get("status") == "success" else wx.ICON_ERROR),
|
|
||||||
self)
|
|
||||||
|
|
||||||
if result.get("status") == "success":
|
def _get_own_did(self):
|
||||||
# Re-fetch profile data to update UI (especially button states)
|
if isinstance(self.session.db, dict):
|
||||||
wx.CallAfter(asyncio.create_task, self.load_profile_data())
|
did = self.session.db.get("user_id")
|
||||||
else: # Re-enable button if action failed
|
if did:
|
||||||
if action_button: wx.CallAfter(action_button.Enable, True)
|
return did
|
||||||
self.SetStatusText(_("Action failed."))
|
try:
|
||||||
|
api = self.session._ensure_client()
|
||||||
|
if getattr(api, "me", None):
|
||||||
|
return api.me.did
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _format_profile_data(self, profile_model):
|
||||||
|
def g(obj, key, default=None):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
except NotificationError as e:
|
return {
|
||||||
wx.EndBusyCursor()
|
"did": g(profile_model, "did"),
|
||||||
if action_button: wx.CallAfter(action_button.Enable, True)
|
"handle": g(profile_model, "handle"),
|
||||||
self.SetStatusText(_("Action failed."))
|
"displayName": g(profile_model, "displayName") or g(profile_model, "display_name") or g(profile_model, "handle"),
|
||||||
wx.CallAfter(wx.MessageBox, str(e), _("Action Error"), wx.OK | wx.ICON_ERROR, self)
|
"description": g(profile_model, "description"),
|
||||||
except Exception as e:
|
"avatar": g(profile_model, "avatar"),
|
||||||
wx.EndBusyCursor()
|
"banner": g(profile_model, "banner"),
|
||||||
if action_button: wx.CallAfter(action_button.Enable, True)
|
"followersCount": g(profile_model, "followersCount"),
|
||||||
self.SetStatusText(_("Action failed."))
|
"followsCount": g(profile_model, "followsCount"),
|
||||||
logger.error(f"Error performing user action '{command}' on {self.target_user_did}: {e}", exc_info=True)
|
"postsCount": g(profile_model, "postsCount"),
|
||||||
wx.CallAfter(wx.MessageBox, _("An unexpected error occurred: {error}").format(error=str(e)), _("Error"), wx.OK | wx.ICON_ERROR, self)
|
"viewer": g(profile_model, "viewer") or {},
|
||||||
|
}
|
||||||
asyncio.create_task(do_action()) # No wx.CallAfter needed for starting the task itself
|
|
||||||
|
|
||||||
def SetStatusText(self, text): # Simple status text for dialog title
|
def SetStatusText(self, text): # Simple status text for dialog title
|
||||||
self.SetTitle(f"{_('User Profile')} - {text}")
|
self.SetTitle(f"{_('User Profile')} - {text}")
|
||||||
|
|
||||||
```python
|
|
||||||
# Example of how this dialog might be called from blueski.Handler.user_details:
|
|
||||||
# (This is conceptual, actual integration in handler.py will use the dialog)
|
|
||||||
#
|
|
||||||
# async def user_details(self, buffer_panel_or_user_ident):
|
|
||||||
# session = self._get_session(self.current_user_id_from_context) # Get current session
|
|
||||||
# user_identifier_to_show = None
|
|
||||||
# if isinstance(buffer_panel_or_user_ident, str): # It's a DID or handle
|
|
||||||
# user_identifier_to_show = buffer_panel_or_user_ident
|
|
||||||
# elif hasattr(buffer_panel_or_user_ident, 'get_selected_item_author_details'): # It's a panel
|
|
||||||
# author_details = buffer_panel_or_user_ident.get_selected_item_author_details()
|
|
||||||
# if author_details:
|
|
||||||
# user_identifier_to_show = author_details.get("did") or author_details.get("handle")
|
|
||||||
#
|
|
||||||
# if not user_identifier_to_show:
|
|
||||||
# # Optionally prompt for user_identifier if not found
|
|
||||||
# output.speak(_("No user selected or identified to view details."), True)
|
|
||||||
# return
|
|
||||||
#
|
|
||||||
# dialog = ShowUserProfileDialog(self.main_controller.view, session, user_identifier_to_show)
|
|
||||||
# dialog.ShowModal()
|
|
||||||
# dialog.Destroy()
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|||||||
85
src/wxUI/dialogs/blueski/userActions.py
Normal file
85
src/wxUI/dialogs/blueski/userActions.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import wx
|
||||||
|
|
||||||
|
|
||||||
|
class UserActionsDialog(wx.Dialog):
|
||||||
|
def __init__(self, users=None, default="follow", *args, **kwargs):
|
||||||
|
super(UserActionsDialog, self).__init__(parent=None, *args, **kwargs)
|
||||||
|
users = users or []
|
||||||
|
panel = wx.Panel(self)
|
||||||
|
self.SetTitle(_(u"Action"))
|
||||||
|
|
||||||
|
userSizer = wx.BoxSizer()
|
||||||
|
userLabel = wx.StaticText(panel, -1, _(u"&User"))
|
||||||
|
default_user = users[0] if users else ""
|
||||||
|
self.cb = wx.ComboBox(panel, -1, choices=users, value=default_user)
|
||||||
|
self.cb.SetFocus()
|
||||||
|
userSizer.Add(userLabel, 0, wx.ALL, 5)
|
||||||
|
userSizer.Add(self.cb, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
actionSizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
label2 = wx.StaticText(panel, -1, _(u"Action"))
|
||||||
|
self.follow = wx.RadioButton(panel, -1, _(u"&Follow"), name=_(u"Action"), style=wx.RB_GROUP)
|
||||||
|
self.unfollow = wx.RadioButton(panel, -1, _(u"U&nfollow"))
|
||||||
|
self.mute = wx.RadioButton(panel, -1, _(u"&Mute"))
|
||||||
|
self.unmute = wx.RadioButton(panel, -1, _(u"Unmu&te"))
|
||||||
|
self.block = wx.RadioButton(panel, -1, _(u"&Block"))
|
||||||
|
self.unblock = wx.RadioButton(panel, -1, _(u"Unbl&ock"))
|
||||||
|
self.setup_default(default)
|
||||||
|
|
||||||
|
hSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
hSizer.Add(label2, 0, wx.ALL, 5)
|
||||||
|
actionSizer.Add(self.follow, 0, wx.ALL, 5)
|
||||||
|
actionSizer.Add(self.unfollow, 0, wx.ALL, 5)
|
||||||
|
actionSizer.Add(self.mute, 0, wx.ALL, 5)
|
||||||
|
actionSizer.Add(self.unmute, 0, wx.ALL, 5)
|
||||||
|
actionSizer.Add(self.block, 0, wx.ALL, 5)
|
||||||
|
actionSizer.Add(self.unblock, 0, wx.ALL, 5)
|
||||||
|
hSizer.Add(actionSizer, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
ok = wx.Button(panel, wx.ID_OK, _(u"&OK"))
|
||||||
|
ok.SetDefault()
|
||||||
|
cancel = wx.Button(panel, wx.ID_CANCEL, _(u"&Close"))
|
||||||
|
btnsizer = wx.BoxSizer()
|
||||||
|
btnsizer.Add(ok)
|
||||||
|
btnsizer.Add(cancel)
|
||||||
|
sizer.Add(userSizer)
|
||||||
|
sizer.Add(hSizer, 0, wx.ALL, 5)
|
||||||
|
sizer.Add(btnsizer)
|
||||||
|
panel.SetSizer(sizer)
|
||||||
|
|
||||||
|
def get_action(self):
|
||||||
|
if self.follow.GetValue() == True:
|
||||||
|
return "follow"
|
||||||
|
elif self.unfollow.GetValue() == True:
|
||||||
|
return "unfollow"
|
||||||
|
elif self.mute.GetValue() == True:
|
||||||
|
return "mute"
|
||||||
|
elif self.unmute.GetValue() == True:
|
||||||
|
return "unmute"
|
||||||
|
elif self.block.GetValue() == True:
|
||||||
|
return "block"
|
||||||
|
elif self.unblock.GetValue() == True:
|
||||||
|
return "unblock"
|
||||||
|
|
||||||
|
def setup_default(self, default):
|
||||||
|
if default == "follow":
|
||||||
|
self.follow.SetValue(True)
|
||||||
|
elif default == "unfollow":
|
||||||
|
self.unfollow.SetValue(True)
|
||||||
|
elif default == "mute":
|
||||||
|
self.mute.SetValue(True)
|
||||||
|
elif default == "unmute":
|
||||||
|
self.unmute.SetValue(True)
|
||||||
|
elif default == "block":
|
||||||
|
self.block.SetValue(True)
|
||||||
|
elif default == "unblock":
|
||||||
|
self.unblock.SetValue(True)
|
||||||
|
|
||||||
|
def get_response(self):
|
||||||
|
return self.ShowModal()
|
||||||
|
|
||||||
|
def get_user(self):
|
||||||
|
return self.cb.GetValue()
|
||||||
|
|
||||||
@@ -134,9 +134,9 @@ class mainFrame(wx.Frame):
|
|||||||
self.buffers[name] = buffer.GetId()
|
self.buffers[name] = buffer.GetId()
|
||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
self.sizer.Add(self.nb, 0, wx.ALL, 5)
|
self.sizer.Add(self.nb, 1, wx.ALL | wx.EXPAND, 5)
|
||||||
self.panel.SetSizer(self.sizer)
|
self.panel.SetSizer(self.sizer)
|
||||||
# self.Maximize()
|
self.Maximize()
|
||||||
self.sizer.Layout()
|
self.sizer.Layout()
|
||||||
self.SetClientSize(self.sizer.CalcMin())
|
self.SetClientSize(self.sizer.CalcMin())
|
||||||
# print self.GetSize()
|
# print self.GetSize()
|
||||||
|
|||||||
33
srcantiguo/app-configuration.defaults
Normal file
33
srcantiguo/app-configuration.defaults
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[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="")
|
||||||
13
srcantiguo/application.py
Normal file
13
srcantiguo/application.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# -*- 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"
|
||||||
23
srcantiguo/audio_services/__init__.py
Normal file
23
srcantiguo/audio_services/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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
|
||||||
41
srcantiguo/audio_services/services.py
Normal file
41
srcantiguo/audio_services/services.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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
|
||||||
13
srcantiguo/audio_services/youtube_utils.py
Normal file
13
srcantiguo/audio_services/youtube_utils.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# -*- 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"]
|
||||||
12
srcantiguo/commandline.py
Normal file
12
srcantiguo/commandline.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# -*- 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
|
||||||
32
srcantiguo/config.py
Normal file
32
srcantiguo/config.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# -*- 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)
|
||||||
79
srcantiguo/config_utils.py
Normal file
79
srcantiguo/config_utils.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# -*- 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
|
||||||
1
srcantiguo/controller/__init__.py
Normal file
1
srcantiguo/controller/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
3
srcantiguo/controller/buffers/__init__.py
Normal file
3
srcantiguo/controller/buffers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import base as base
|
||||||
|
from . import mastodon as mastodon
|
||||||
4
srcantiguo/controller/buffers/base/__init__.py
Normal file
4
srcantiguo/controller/buffers/base/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from .account import AccountBuffer
|
||||||
|
from .base import Buffer
|
||||||
|
from .empty import EmptyBuffer
|
||||||
56
srcantiguo/controller/buffers/base/account.py
Normal file
56
srcantiguo/controller/buffers/base/account.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# -*- 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()
|
||||||
144
srcantiguo/controller/buffers/base/base.py
Normal file
144
srcantiguo/controller/buffers/base/base.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# -*- 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
|
||||||
19
srcantiguo/controller/buffers/base/empty.py
Normal file
19
srcantiguo/controller/buffers/base/empty.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- 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
|
||||||
8
srcantiguo/controller/buffers/mastodon/__init__.py
Normal file
8
srcantiguo/controller/buffers/mastodon/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# -*- 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
|
||||||
742
srcantiguo/controller/buffers/mastodon/base.py
Normal file
742
srcantiguo/controller/buffers/mastodon/base.py
Normal file
@@ -0,0 +1,742 @@
|
|||||||
|
# -*- 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:
|
||||||
|
if self.session.settings["general"]["reverse_timelines"]:
|
||||||
|
min_id = self.session.db[self.name][0].id
|
||||||
|
else:
|
||||||
|
min_id = self.session.db[self.name][-1].id
|
||||||
|
# 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)
|
||||||
|
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 self.session.settings["general"]["reverse_timelines"] == False:
|
||||||
|
max_id = self.session.db[self.name][0].id
|
||||||
|
else:
|
||||||
|
max_id = self.session.db[self.name][-1].id
|
||||||
|
try:
|
||||||
|
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], *self.args, **self.kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Error %s" % (str(e)))
|
||||||
|
return
|
||||||
|
items_db = self.session.db[self.name]
|
||||||
|
for i in items:
|
||||||
|
if utils.find_item(i, self.session.db[self.name]) == None:
|
||||||
|
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)
|
||||||
|
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.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 == True:
|
||||||
|
self._direct_boost(id)
|
||||||
|
else:
|
||||||
|
self._direct_boost(id)
|
||||||
|
|
||||||
|
def _direct_boost(self, id):
|
||||||
|
item = self.session.api_call(call_name="status_reblog", _sound="retweet_send.ogg", id=id)
|
||||||
|
|
||||||
|
def onFocus(self, *args, **kwargs):
|
||||||
|
post = self.get_item()
|
||||||
|
if self.session.settings["general"]["relative_times"] == True:
|
||||||
|
original_date = arrow.get(self.session.db[self.name][self.buffer.list.get_selected()].created_at)
|
||||||
|
ts = original_date.humanize(locale=languageHandler.getLanguage())
|
||||||
|
self.buffer.list.list.SetItem(self.buffer.list.get_selected(), 2, ts)
|
||||||
|
if 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()
|
||||||
160
srcantiguo/controller/buffers/mastodon/community.py
Normal file
160
srcantiguo/controller/buffers/mastodon/community.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# -*- 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:
|
||||||
|
if self.session.settings["general"]["reverse_timelines"]:
|
||||||
|
min_id = self.session.db[self.name][0].id
|
||||||
|
else:
|
||||||
|
min_id = self.session.db[self.name][-1].id
|
||||||
|
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 self.session.settings["general"]["reverse_timelines"] == False:
|
||||||
|
max_id = self.session.db[self.name][0].id
|
||||||
|
else:
|
||||||
|
max_id = self.session.db[self.name][-1].id
|
||||||
|
try:
|
||||||
|
items = 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)
|
||||||
249
srcantiguo/controller/buffers/mastodon/conversations.py
Normal file
249
srcantiguo/controller/buffers/mastodon/conversations.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# -*- 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
|
||||||
121
srcantiguo/controller/buffers/mastodon/mentions.py
Normal file
121
srcantiguo/controller/buffers/mastodon/mentions.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# -*- 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 self.session.settings["general"]["reverse_timelines"] == False:
|
||||||
|
max_id = self.session.db[self.name][0].id
|
||||||
|
else:
|
||||||
|
max_id = self.session.db[self.name][-1].id
|
||||||
|
try:
|
||||||
|
items = getattr(self.session.api, self.function)(max_id=max_id, limit=self.session.settings["general"]["max_posts_per_call"], 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"])
|
||||||
188
srcantiguo/controller/buffers/mastodon/notifications.py
Normal file
188
srcantiguo/controller/buffers/mastodon/notifications.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# -*- 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())
|
||||||
81
srcantiguo/controller/buffers/mastodon/search.py
Normal file
81
srcantiguo/controller/buffers/mastodon/search.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# -*- 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:
|
||||||
|
if self.session.settings["general"]["reverse_timelines"]:
|
||||||
|
min_id = self.session.db[self.name][0].id
|
||||||
|
else:
|
||||||
|
min_id = self.session.db[self.name][-1].id
|
||||||
|
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)
|
||||||
|
|
||||||
207
srcantiguo/controller/buffers/mastodon/users.py
Normal file
207
srcantiguo/controller/buffers/mastodon/users.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# -*- 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
|
||||||
1180
srcantiguo/controller/mainController.py
Normal file
1180
srcantiguo/controller/mainController.py
Normal file
File diff suppressed because it is too large
Load Diff
1
srcantiguo/controller/mastodon/__init__.py
Normal file
1
srcantiguo/controller/mastodon/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
1
srcantiguo/controller/mastodon/filters/__init__.py
Normal file
1
srcantiguo/controller/mastodon/filters/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
112
srcantiguo/controller/mastodon/filters/create_filter.py
Normal file
112
srcantiguo/controller/mastodon/filters/create_filter.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# -*- 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
|
||||||
99
srcantiguo/controller/mastodon/filters/manage_filters.py
Normal file
99
srcantiguo/controller/mastodon/filters/manage_filters.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# -*- 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
|
||||||
422
srcantiguo/controller/mastodon/handler.py
Normal file
422
srcantiguo/controller/mastodon/handler.py
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
# -*- 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"),
|
||||||
|
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))
|
||||||
|
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()
|
||||||
462
srcantiguo/controller/mastodon/messages.py
Normal file
462
srcantiguo/controller/mastodon/messages.py
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
# -*- 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
|
||||||
|
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 = item.content
|
||||||
|
# Remove HTML tags from content
|
||||||
|
import re
|
||||||
|
text = re.sub('<[^<]+?>', '', text)
|
||||||
|
# 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)
|
||||||
224
srcantiguo/controller/mastodon/settings.py
Normal file
224
srcantiguo/controller/mastodon/settings.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
# -*- 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"]
|
||||||
|
self.dialog.create_templates(post_template=post_template, conversation_template=conversation_template, person_template=person_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)
|
||||||
|
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 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")
|
||||||
|
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()
|
||||||
40
srcantiguo/controller/mastodon/templateEditor.py
Normal file
40
srcantiguo/controller/mastodon/templateEditor.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import wx
|
||||||
|
from typing import List
|
||||||
|
from sessions.mastodon.templates import post_variables, conversation_variables, person_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
|
||||||
|
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 ""
|
||||||
101
srcantiguo/controller/mastodon/userActions.py
Normal file
101
srcantiguo/controller/mastodon/userActions.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
import widgetUtils
|
||||||
|
import output
|
||||||
|
from wxUI.dialogs.mastodon import userActions as userActionsDialog
|
||||||
|
from wxUI.dialogs.mastodon import userTimeline as userTimelineDialog
|
||||||
|
from pubsub import pub
|
||||||
|
from mastodon import MastodonError, MastodonNotFoundError
|
||||||
|
#from extra.autocompletionUsers import completion
|
||||||
|
|
||||||
|
log = logging.getLogger("controller.mastodon.userActions")
|
||||||
|
|
||||||
|
class BasicUserSelector(object):
|
||||||
|
def __init__(self, session, users=[]):
|
||||||
|
super(BasicUserSelector, self).__init__()
|
||||||
|
self.session = session
|
||||||
|
self.create_dialog(users=users)
|
||||||
|
|
||||||
|
def create_dialog(self, users):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def autocomplete_users(self, *args, **kwargs):
|
||||||
|
c = completion.autocompletionUsers(self.dialog, self.session.session_id)
|
||||||
|
c.show_menu("dm")
|
||||||
|
|
||||||
|
def search_user(self, user):
|
||||||
|
try:
|
||||||
|
user = self.session.api.account_lookup(user)
|
||||||
|
return user
|
||||||
|
except MastodonError:
|
||||||
|
log.exception("Error searching for user %s.".format(user))
|
||||||
|
|
||||||
|
class userActions(BasicUserSelector):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(userActions, self).__init__(*args, **kwargs)
|
||||||
|
if self.dialog.get_response() == widgetUtils.OK:
|
||||||
|
self.process_action()
|
||||||
|
|
||||||
|
def create_dialog(self, users):
|
||||||
|
self.dialog = userActionsDialog.UserActionsDialog(users)
|
||||||
|
widgetUtils.connect_event(self.dialog.autocompletion, widgetUtils.BUTTON_PRESSED, self.autocomplete_users)
|
||||||
|
|
||||||
|
def process_action(self):
|
||||||
|
action = self.dialog.get_action()
|
||||||
|
user = self.dialog.get_user()
|
||||||
|
user = self.search_user(user)
|
||||||
|
if user == None:
|
||||||
|
return
|
||||||
|
getattr(self, action)(user)
|
||||||
|
|
||||||
|
def follow(self, user):
|
||||||
|
try:
|
||||||
|
self.session.api.account_follow(user.id)
|
||||||
|
except MastodonError as err:
|
||||||
|
output.speak("Error %s" % (str(err)), True)
|
||||||
|
|
||||||
|
def unfollow(self, user):
|
||||||
|
try:
|
||||||
|
result = self.session.api.account_unfollow(user.id)
|
||||||
|
except MastodonError as err:
|
||||||
|
output.speak("Error %s" % (str(err)), True)
|
||||||
|
|
||||||
|
def mute(self, user):
|
||||||
|
try:
|
||||||
|
id = self.session.api.account_mute(user.id)
|
||||||
|
except MastodonError as err:
|
||||||
|
output.speak("Error %s" % (str(err)), True)
|
||||||
|
|
||||||
|
def unmute(self, user):
|
||||||
|
try:
|
||||||
|
id = self.session.api.account_unmute(user.id)
|
||||||
|
except MastodonError as err:
|
||||||
|
output.speak("Error %s" % (str(err)), True)
|
||||||
|
|
||||||
|
def block(self, user):
|
||||||
|
try:
|
||||||
|
id = self.session.api.account_block(user.id)
|
||||||
|
except MastodonError as err:
|
||||||
|
output.speak("Error %s" % (str(err)), True)
|
||||||
|
|
||||||
|
def unblock(self, user):
|
||||||
|
try:
|
||||||
|
id = self.session.api.account_unblock(user.id)
|
||||||
|
except MastodonError as err:
|
||||||
|
output.speak("Error %s" % (str(err)), True)
|
||||||
|
|
||||||
|
class UserTimeline(BasicUserSelector):
|
||||||
|
|
||||||
|
def create_dialog(self, users):
|
||||||
|
self.dialog = userTimelineDialog.UserTimeline(users)
|
||||||
|
widgetUtils.connect_event(self.dialog.autocompletion, widgetUtils.BUTTON_PRESSED, self.autocomplete_users)
|
||||||
|
|
||||||
|
def process_action(self):
|
||||||
|
action = self.dialog.get_action()
|
||||||
|
user = self.dialog.get_user()
|
||||||
|
user = self.search_user(user)
|
||||||
|
if user == None:
|
||||||
|
return
|
||||||
|
self.user = user
|
||||||
|
return action
|
||||||
25
srcantiguo/controller/mastodon/userList.py
Normal file
25
srcantiguo/controller/mastodon/userList.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- 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()
|
||||||
37
srcantiguo/controller/messages.py
Normal file
37
srcantiguo/controller/messages.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# -*- 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()
|
||||||
119
srcantiguo/controller/settings.py
Normal file
119
srcantiguo/controller/settings.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# -*- 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()
|
||||||
52
srcantiguo/controller/userAlias.py
Normal file
52
srcantiguo/controller/userAlias.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# -*- 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
|
||||||
23
srcantiguo/controller/userList.py
Normal file
23
srcantiguo/controller/userList.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- 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
|
||||||
1
srcantiguo/extra/SoundsTutorial/__init__.py
Normal file
1
srcantiguo/extra/SoundsTutorial/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .soundsTutorial import soundsTutorial
|
||||||
11
srcantiguo/extra/SoundsTutorial/reverse_sort.py
Normal file
11
srcantiguo/extra/SoundsTutorial/reverse_sort.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#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)))
|
||||||
31
srcantiguo/extra/SoundsTutorial/soundsTutorial.py
Normal file
31
srcantiguo/extra/SoundsTutorial/soundsTutorial.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# -*- 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()],))
|
||||||
28
srcantiguo/extra/SoundsTutorial/soundsTutorial_constants.py
Normal file
28
srcantiguo/extra/SoundsTutorial/soundsTutorial_constants.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#-*- 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."))])
|
||||||
29
srcantiguo/extra/SoundsTutorial/wx_ui.py
Normal file
29
srcantiguo/extra/SoundsTutorial/wx_ui.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# -*- 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()
|
||||||
6
srcantiguo/extra/SpellChecker/__init__.py
Normal file
6
srcantiguo/extra/SpellChecker/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from . import spellchecker
|
||||||
|
import platform
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
from .wx_ui import *
|
||||||
83
srcantiguo/extra/SpellChecker/spellchecker.py
Normal file
83
srcantiguo/extra/SpellChecker/spellchecker.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# -*- 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()
|
||||||
15
srcantiguo/extra/SpellChecker/twitterFilter.py
Normal file
15
srcantiguo/extra/SpellChecker/twitterFilter.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# -*- 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
|
||||||
82
srcantiguo/extra/SpellChecker/wx_ui.py
Normal file
82
srcantiguo/extra/SpellChecker/wx_ui.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# -*- 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()
|
||||||
0
srcantiguo/extra/__init__.py
Normal file
0
srcantiguo/extra/__init__.py
Normal file
2
srcantiguo/extra/autocompletionUsers/__init__.py
Normal file
2
srcantiguo/extra/autocompletionUsers/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# -*- 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. """
|
||||||
66
srcantiguo/extra/autocompletionUsers/completion.py
Normal file
66
srcantiguo/extra/autocompletionUsers/completion.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# -*- 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"))
|
||||||
57
srcantiguo/extra/autocompletionUsers/manage.py
Normal file
57
srcantiguo/extra/autocompletionUsers/manage.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# -*- 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()
|
||||||
103
srcantiguo/extra/autocompletionUsers/mastodon/scan.py
Normal file
103
srcantiguo/extra/autocompletionUsers/mastodon/scan.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# -*- 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)
|
||||||
45
srcantiguo/extra/autocompletionUsers/mastodon/wx_scan.py
Normal file
45
srcantiguo/extra/autocompletionUsers/mastodon/wx_scan.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# -*- 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()
|
||||||
52
srcantiguo/extra/autocompletionUsers/storage.py
Normal file
52
srcantiguo/extra/autocompletionUsers/storage.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# -*- 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()
|
||||||
44
srcantiguo/extra/autocompletionUsers/wx_manage.py
Normal file
44
srcantiguo/extra/autocompletionUsers/wx_manage.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- 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()
|
||||||
25
srcantiguo/extra/autocompletionUsers/wx_menu.py
Normal file
25
srcantiguo/extra/autocompletionUsers/wx_menu.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- 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()
|
||||||
45
srcantiguo/extra/ocr/OCRSpace.py
Normal file
45
srcantiguo/extra/ocr/OCRSpace.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# -*- 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
|
||||||
|
|
||||||
5
srcantiguo/extra/ocr/__init__.py
Normal file
5
srcantiguo/extra/ocr/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import OCRSpace
|
||||||
2
srcantiguo/extra/translator/__init__.py
Normal file
2
srcantiguo/extra/translator/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from .translator import TranslatorController
|
||||||
1
srcantiguo/extra/translator/engines/__init__.py
Normal file
1
srcantiguo/extra/translator/engines/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
14
srcantiguo/extra/translator/engines/deep_l.py
Normal file
14
srcantiguo/extra/translator/engines/deep_l.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# -*- 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
|
||||||
45
srcantiguo/extra/translator/engines/libre_translate.py
Normal file
45
srcantiguo/extra/translator/engines/libre_translate.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# -*- 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)
|
||||||
58
srcantiguo/extra/translator/translator.py
Normal file
58
srcantiguo/extra/translator/translator.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# -*- 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]))
|
||||||
56
srcantiguo/extra/translator/wx_ui.py
Normal file
56
srcantiguo/extra/translator/wx_ui.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# -*- 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()
|
||||||
19
srcantiguo/fixes/__init__.py
Normal file
19
srcantiguo/fixes/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- 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()
|
||||||
49
srcantiguo/fixes/fix_arrow.py
Normal file
49
srcantiguo/fixes/fix_arrow.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# -*- 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](?=[ºª]))[ºª])"
|
||||||
|
|
||||||
42
srcantiguo/fixes/fix_libloader.py
Normal file
42
srcantiguo/fixes/fix_libloader.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# -*- 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.")
|
||||||
12
srcantiguo/fixes/fix_requests.py
Normal file
12
srcantiguo/fixes/fix_requests.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# -*- 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)))
|
||||||
27
srcantiguo/fixes/fix_urllib3_warnings.py
Normal file
27
srcantiguo/fixes/fix_urllib3_warnings.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# -*- 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
|
||||||
6
srcantiguo/fixes/fix_win32com.py
Normal file
6
srcantiguo/fixes/fix_win32com.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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()
|
||||||
BIN
srcantiguo/icon.ico
Normal file
BIN
srcantiguo/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 316 KiB |
3
srcantiguo/keyboard_handler/__init__.py
Normal file
3
srcantiguo/keyboard_handler/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from __future__ import absolute_import
|
||||||
|
from .main import KeyboardHandler, KeyboardHandlerError
|
||||||
|
__all__ = ["KeyboardHandler", "KeyboardHandlerError", ]
|
||||||
8
srcantiguo/keyboard_handler/global_handler.py
Normal file
8
srcantiguo/keyboard_handler/global_handler.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
128
srcantiguo/keyboard_handler/key_constants.py
Normal file
128
srcantiguo/keyboard_handler/key_constants.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
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}
|
||||||
|
|
||||||
58
srcantiguo/keyboard_handler/linux.py
Normal file
58
srcantiguo/keyboard_handler/linux.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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)]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user