aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTrygve Aaberge <trygveaa@gmail.com>2023-09-17 11:30:53 +0200
committerTrygve Aaberge <trygveaa@gmail.com>2024-02-18 11:32:54 +0100
commit994e2d246fc09ed942a871696ccae1bdc3002217 (patch)
tree2c1ac01192e50c6d1c717f8c78b70fd47585fcf9
parent2a9cad66efec738e68230d335540e6c9e7d9c7e8 (diff)
downloadwee-slack-994e2d246fc09ed942a871696ccae1bdc3002217.tar.gz
Support thread buffers
-rw-r--r--slack/commands.py113
-rw-r--r--slack/register.py29
-rw-r--r--slack/shared.py2
-rw-r--r--slack/slack_buffer.py275
-rw-r--r--slack/slack_conversation.py283
-rw-r--r--slack/slack_message.py21
-rw-r--r--slack/slack_thread.py87
-rw-r--r--slack/slack_workspace.py19
8 files changed, 525 insertions, 304 deletions
diff --git a/slack/commands.py b/slack/commands.py
index 57a257f..8e807e1 100644
--- a/slack/commands.py
+++ b/slack/commands.py
@@ -13,10 +13,9 @@ from slack.error import SlackError, SlackRtmError, UncaughtError
from slack.log import print_error
from slack.python_compatibility import format_exception, removeprefix, removesuffix
from slack.shared import shared
-from slack.slack_conversation import (
- SlackConversation,
- get_conversation_from_buffer_pointer,
-)
+from slack.slack_buffer import SlackBuffer
+from slack.slack_conversation import SlackConversation
+from slack.slack_thread import SlackThread
from slack.slack_user import name_from_user_info_without_spaces
from slack.slack_workspace import SlackWorkspace
from slack.task import run_async
@@ -143,9 +142,9 @@ def command_slack_connect(
else:
workspace_connect(workspace)
else:
- conversation = get_conversation_from_buffer_pointer(buffer)
- if conversation:
- workspace_connect(conversation.workspace)
+ slack_buffer = shared.buffers.get(buffer)
+ if slack_buffer:
+ workspace_connect(slack_buffer.workspace)
def workspace_disconnect(workspace: SlackWorkspace):
@@ -170,18 +169,18 @@ def command_slack_disconnect(
else:
workspace_disconnect(workspace)
else:
- conversation = get_conversation_from_buffer_pointer(buffer)
- if conversation:
- workspace_disconnect(conversation.workspace)
+ slack_buffer = shared.buffers.get(buffer)
+ if slack_buffer:
+ workspace_disconnect(slack_buffer.workspace)
@weechat_command()
def command_slack_rehistory(
buffer: str, args: List[str], options: Dict[str, Optional[str]]
):
- conversation = get_conversation_from_buffer_pointer(buffer)
- if conversation:
- run_async(conversation.rerender_history())
+ slack_buffer = shared.buffers.get(buffer)
+ if slack_buffer:
+ run_async(slack_buffer.rerender_history())
@weechat_command()
@@ -272,6 +271,15 @@ def command_slack_workspace_del(
)
+@weechat_command(min_args=1)
+def command_slack_thread(
+ buffer: str, args: List[str], options: Dict[str, Optional[str]]
+):
+ slack_buffer = shared.buffers.get(buffer)
+ if isinstance(slack_buffer, SlackConversation):
+ run_async(slack_buffer.open_thread(args[0], switch=True))
+
+
def print_uncaught_error(
error: UncaughtError, detailed: bool, options: Dict[str, Optional[str]]
):
@@ -300,9 +308,14 @@ def command_slack_debug(
weechat.prnt("", "Active futures:")
weechat.prnt("", pprint.pformat(shared.active_futures))
elif args[0] == "buffer":
- conversation = get_conversation_from_buffer_pointer(buffer)
- if conversation:
- weechat.prnt("", f"Conversation id: {conversation.id}")
+ slack_buffer = shared.buffers.get(buffer)
+ if isinstance(slack_buffer, SlackConversation):
+ weechat.prnt("", f"Conversation id: {slack_buffer.id}")
+ elif isinstance(slack_buffer, SlackThread):
+ weechat.prnt(
+ "",
+ f"Conversation id: {slack_buffer.parent.conversation.id}, Thread ts: {slack_buffer.parent.thread_ts}, Thread hash: {slack_buffer.parent.hash}",
+ )
elif args[0] == "errors":
num_arg = int(args[1]) if len(args) > 1 and args[1].isdecimal() else 5
num = min(num_arg, len(shared.uncaught_errors))
@@ -427,22 +440,22 @@ def completion_irc_channels_cb(
return weechat.WEECHAT_RC_OK
-def complete_input(conversation: SlackConversation, query: str):
+def complete_input(slack_buffer: SlackBuffer, query: str):
if (
- conversation.completion_context == "ACTIVE_COMPLETION"
- and conversation.completion_values
+ slack_buffer.completion_context == "ACTIVE_COMPLETION"
+ and slack_buffer.completion_values
):
- input_value = weechat.buffer_get_string(conversation.buffer_pointer, "input")
- input_pos = weechat.buffer_get_integer(conversation.buffer_pointer, "input_pos")
- result = conversation.completion_values[conversation.completion_index]
+ input_value = weechat.buffer_get_string(slack_buffer.buffer_pointer, "input")
+ input_pos = weechat.buffer_get_integer(slack_buffer.buffer_pointer, "input_pos")
+ result = slack_buffer.completion_values[slack_buffer.completion_index]
input_before = removesuffix(input_value[:input_pos], query)
input_after = input_value[input_pos:]
new_input = input_before + result + input_after
new_pos = input_pos - len(query) + len(result)
- with conversation.completing():
- weechat.buffer_set(conversation.buffer_pointer, "input", new_input)
- weechat.buffer_set(conversation.buffer_pointer, "input_pos", str(new_pos))
+ with slack_buffer.completing():
+ weechat.buffer_set(slack_buffer.buffer_pointer, "input", new_input)
+ weechat.buffer_set(slack_buffer.buffer_pointer, "input_pos", str(new_pos))
def nick_suffix():
@@ -452,47 +465,47 @@ def nick_suffix():
async def complete_user_next(
- conversation: SlackConversation, query: str, is_first_word: bool
+ slack_buffer: SlackBuffer, query: str, is_first_word: bool
):
- if conversation.completion_context == "NO_COMPLETION":
- conversation.completion_context = "PENDING_COMPLETION"
- search = await conversation.workspace.api.edgeapi.fetch_users_search(query)
- if conversation.completion_context != "PENDING_COMPLETION":
+ if slack_buffer.completion_context == "NO_COMPLETION":
+ slack_buffer.completion_context = "PENDING_COMPLETION"
+ search = await slack_buffer.workspace.api.edgeapi.fetch_users_search(query)
+ if slack_buffer.completion_context != "PENDING_COMPLETION":
return
- conversation.completion_context = "ACTIVE_COMPLETION"
+ slack_buffer.completion_context = "ACTIVE_COMPLETION"
suffix = nick_suffix() if is_first_word else " "
- conversation.completion_values = [
- name_from_user_info_without_spaces(conversation.workspace, user) + suffix
+ slack_buffer.completion_values = [
+ name_from_user_info_without_spaces(slack_buffer.workspace, user) + suffix
for user in search["results"]
]
- conversation.completion_index = 0
- elif conversation.completion_context == "ACTIVE_COMPLETION":
- conversation.completion_index += 1
- if conversation.completion_index >= len(conversation.completion_values):
- conversation.completion_index = 0
+ slack_buffer.completion_index = 0
+ elif slack_buffer.completion_context == "ACTIVE_COMPLETION":
+ slack_buffer.completion_index += 1
+ if slack_buffer.completion_index >= len(slack_buffer.completion_values):
+ slack_buffer.completion_index = 0
- complete_input(conversation, query)
+ complete_input(slack_buffer, query)
-def complete_previous(conversation: SlackConversation, query: str) -> int:
- if conversation.completion_context == "ACTIVE_COMPLETION":
- conversation.completion_index -= 1
- if conversation.completion_index < 0:
- conversation.completion_index = len(conversation.completion_values) - 1
- complete_input(conversation, query)
+def complete_previous(slack_buffer: SlackBuffer, query: str) -> int:
+ if slack_buffer.completion_context == "ACTIVE_COMPLETION":
+ slack_buffer.completion_index -= 1
+ if slack_buffer.completion_index < 0:
+ slack_buffer.completion_index = len(slack_buffer.completion_values) - 1
+ complete_input(slack_buffer, query)
return weechat.WEECHAT_RC_OK_EAT
return weechat.WEECHAT_RC_OK
def input_complete_cb(data: str, buffer: str, command: str) -> int:
- conversation = get_conversation_from_buffer_pointer(buffer)
- if conversation:
+ slack_buffer = shared.buffers.get(buffer)
+ if slack_buffer:
input_value = weechat.buffer_get_string(buffer, "input")
input_pos = weechat.buffer_get_integer(buffer, "input_pos")
input_before_cursor = input_value[:input_pos]
word_index = (
- -2 if conversation.completion_context == "ACTIVE_COMPLETION" else -1
+ -2 if slack_buffer.completion_context == "ACTIVE_COMPLETION" else -1
)
word_until_cursor = " ".join(input_before_cursor.split(" ")[word_index:])
@@ -501,10 +514,10 @@ def input_complete_cb(data: str, buffer: str, command: str) -> int:
is_first_word = word_until_cursor == input_before_cursor
if command == "/input complete_next":
- run_async(complete_user_next(conversation, query, is_first_word))
+ run_async(complete_user_next(slack_buffer, query, is_first_word))
return weechat.WEECHAT_RC_OK_EAT
else:
- return complete_previous(conversation, query)
+ return complete_previous(slack_buffer, query)
return weechat.WEECHAT_RC_OK
diff --git a/slack/register.py b/slack/register.py
index d45fc41..5e81792 100644
--- a/slack/register.py
+++ b/slack/register.py
@@ -5,7 +5,6 @@ import weechat
from slack.commands import register_commands
from slack.config import SlackConfig
from slack.shared import shared
-from slack.slack_conversation import get_conversation_from_buffer_pointer
from slack.slack_emoji import load_standard_emojis
from slack.task import run_async, sleep
from slack.util import get_callback_name, with_color
@@ -21,9 +20,9 @@ def shutdown_cb():
def signal_buffer_switch_cb(data: str, signal: str, buffer_pointer: str) -> int:
- conversation = get_conversation_from_buffer_pointer(buffer_pointer)
- if conversation:
- run_async(conversation.buffer_switched_to())
+ slack_buffer = shared.buffers.get(buffer_pointer)
+ if slack_buffer:
+ run_async(slack_buffer.buffer_switched_to())
return weechat.WEECHAT_RC_OK
@@ -38,29 +37,29 @@ def input_text_cursor_moved_cb(data: str, signal: str, buffer_pointer: str) -> i
def reset_completion_context_on_input(buffer_pointer: str):
- conversation = get_conversation_from_buffer_pointer(buffer_pointer)
- if conversation and conversation.completion_context != "IN_PROGRESS_COMPLETION":
- conversation.completion_context = "NO_COMPLETION"
+ slack_buffer = shared.buffers.get(buffer_pointer)
+ if slack_buffer and slack_buffer.completion_context != "IN_PROGRESS_COMPLETION":
+ slack_buffer.completion_context = "NO_COMPLETION"
def modifier_input_text_display_with_cursor_cb(
data: str, modifier: str, buffer_pointer: str, string: str
) -> str:
prefix = ""
- conversation = get_conversation_from_buffer_pointer(buffer_pointer)
- if conversation:
+ slack_buffer = shared.buffers.get(buffer_pointer)
+ if slack_buffer:
input_delim_color = weechat.config_string(
weechat.config_get("weechat.bar.input.color_delim")
)
input_delim_start = with_color(input_delim_color, "[")
input_delim_end = with_color(input_delim_color, "]")
- if not conversation.workspace.is_connected:
+ if not slack_buffer.workspace.is_connected:
prefix += (
f"{input_delim_start}"
f"{with_color(shared.config.color.disconnected.value, 'disconnected')}"
f"{input_delim_end} "
)
- if conversation.is_loading:
+ if slack_buffer.is_loading:
prefix += (
f"{input_delim_start}"
f"{with_color(shared.config.color.loading.value, 'loading')}"
@@ -70,12 +69,12 @@ def modifier_input_text_display_with_cursor_cb(
def typing_self_cb(data: str, signal: str, signal_data: str) -> int:
- if not shared.config.look.typing_status_self:
+ if not shared.config.look.typing_status_self or signal != "typing_self_typing":
return weechat.WEECHAT_RC_OK
- conversation = get_conversation_from_buffer_pointer(signal_data)
- if conversation:
- conversation.typing_update_self(signal)
+ slack_buffer = shared.buffers.get(signal_data)
+ if slack_buffer:
+ slack_buffer.set_typing_self()
return weechat.WEECHAT_RC_OK
diff --git a/slack/shared.py b/slack/shared.py
index 600723b..0cf25f4 100644
--- a/slack/shared.py
+++ b/slack/shared.py
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Union
if TYPE_CHECKING:
from slack.config import SlackConfig
from slack.error import UncaughtError
+ from slack.slack_buffer import SlackBuffer
from slack.slack_emoji import Emoji
from slack.slack_workspace import SlackWorkspace
from slack.task import Future, Task
@@ -22,6 +23,7 @@ class Shared:
self.weechat_callbacks: Dict[str, Callable[..., WeechatCallbackReturnType]]
self.active_tasks: Dict[str, List[Task[object]]] = defaultdict(list)
self.active_futures: Dict[str, Future[object]] = {}
+ self.buffers: Dict[str, SlackBuffer] = {}
self.workspaces: Dict[str, SlackWorkspace] = {}
self.config: SlackConfig
self.uncaught_errors: List[UncaughtError] = []
diff --git a/slack/slack_buffer.py b/slack/slack_buffer.py
new file mode 100644
index 0000000..e5adb19
--- /dev/null
+++ b/slack/slack_buffer.py
@@ -0,0 +1,275 @@
+from __future__ import annotations
+
+import time
+from abc import ABC, abstractmethod
+from contextlib import contextmanager
+from typing import TYPE_CHECKING, Dict, List, Mapping, Optional, Tuple
+
+import weechat
+
+from slack.shared import shared
+from slack.slack_message import SlackMessage, SlackTs
+from slack.util import get_callback_name
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from slack.slack_api import SlackApi
+ from slack.slack_workspace import SlackWorkspace
+
+
+def hdata_line_ts(line_pointer: str) -> Optional[SlackTs]:
+ data = weechat.hdata_pointer(weechat.hdata_get("line"), line_pointer, "data")
+ for i in range(
+ weechat.hdata_integer(weechat.hdata_get("line_data"), data, "tags_count")
+ ):
+ tag = weechat.hdata_string(
+ weechat.hdata_get("line_data"), data, f"{i}|tags_array"
+ )
+ if tag.startswith("slack_ts_"):
+ return SlackTs(tag[9:])
+ return None
+
+
+def tags_set_notify_none(tags: List[str]) -> List[str]:
+ notify_tags = {"notify_highlight", "notify_message", "notify_private"}
+ tags = [tag for tag in tags if tag not in notify_tags]
+ tags += ["no_highlight", "notify_none"]
+ return tags
+
+
+def modify_buffer_line(buffer_pointer: str, ts: SlackTs, new_text: str):
+ own_lines = weechat.hdata_pointer(
+ weechat.hdata_get("buffer"), buffer_pointer, "own_lines"
+ )
+ line_pointer = weechat.hdata_pointer(
+ weechat.hdata_get("lines"), own_lines, "last_line"
+ )
+
+ # Find the last line with this ts
+ is_last_line = True
+ while line_pointer and hdata_line_ts(line_pointer) != ts:
+ is_last_line = False
+ line_pointer = weechat.hdata_move(weechat.hdata_get("line"), line_pointer, -1)
+
+ if not line_pointer:
+ return
+
+ if shared.weechat_version >= 0x04000000:
+ data = weechat.hdata_pointer(weechat.hdata_get("line"), line_pointer, "data")
+ weechat.hdata_update(
+ weechat.hdata_get("line_data"), data, {"message": new_text}
+ )
+ return
+
+ # Find all lines for the message
+ pointers: List[str] = []
+ while line_pointer and hdata_line_ts(line_pointer) == ts:
+ pointers.append(line_pointer)
+ line_pointer = weechat.hdata_move(weechat.hdata_get("line"), line_pointer, -1)
+ pointers.reverse()
+
+ if not pointers:
+ return
+
+ if is_last_line:
+ lines = new_text.split("\n")
+ extra_lines_count = len(lines) - len(pointers)
+ if extra_lines_count > 0:
+ line_data = weechat.hdata_pointer(
+ weechat.hdata_get("line"), pointers[0], "data"
+ )
+ tags_count = weechat.hdata_integer(
+ weechat.hdata_get("line_data"), line_data, "tags_count"
+ )
+ tags = [
+ weechat.hdata_string(
+ weechat.hdata_get("line_data"), line_data, f"{i}|tags_array"
+ )
+ for i in range(tags_count)
+ ]
+ tags = tags_set_notify_none(tags)
+ tags_str = ",".join(tags)
+ last_read_line = weechat.hdata_pointer(
+ weechat.hdata_get("lines"), own_lines, "last_read_line"
+ )
+ should_set_unread = last_read_line == pointers[-1]
+
+ # Insert new lines to match the number of lines in the message
+ weechat.buffer_set(buffer_pointer, "print_hooks_enabled", "0")
+ for _ in range(extra_lines_count):
+ weechat.prnt_date_tags(buffer_pointer, ts.major, tags_str, " \t ")
+ pointers.append(
+ weechat.hdata_pointer(
+ weechat.hdata_get("lines"), own_lines, "last_line"
+ )
+ )
+ if should_set_unread:
+ weechat.buffer_set(buffer_pointer, "unread", "")
+ weechat.buffer_set(buffer_pointer, "print_hooks_enabled", "1")
+ else:
+ # Split the message into at most the number of existing lines as we can't insert new lines
+ lines = new_text.split("\n", len(pointers) - 1)
+ # Replace newlines to prevent garbled lines in bare display mode
+ lines = [line.replace("\n", " | ") for line in lines]
+
+ # Extend lines in case the new message is shorter than the old as we can't delete lines
+ lines += [""] * (len(pointers) - len(lines))
+
+ for pointer, line in zip(pointers, lines):
+ data = weechat.hdata_pointer(weechat.hdata_get("line"), pointer, "data")
+ weechat.hdata_update(weechat.hdata_get("line_data"), data, {"message": line})
+
+
+class SlackBuffer(ABC):
+ def __init__(self):
+ self._typing_self_last_sent = 0
+ # TODO: buffer_pointer may be accessed by buffer_switch before it's initialized
+ self.buffer_pointer: str = ""
+ self.is_loading = False
+ self.history_filled = False
+ self.history_pending = False
+
+ self.completion_context: Literal[
+ "NO_COMPLETION",
+ "PENDING_COMPLETION",
+ "ACTIVE_COMPLETION",
+ "IN_PROGRESS_COMPLETION",
+ ] = "NO_COMPLETION"
+ self.completion_values: List[str] = []
+ self.completion_index = 0
+
+ @property
+ def _api(self) -> SlackApi:
+ return self.workspace.api
+
+ @contextmanager
+ def loading(self):
+ self.is_loading = True
+ weechat.bar_item_update("input_text")
+ try:
+ yield
+ finally:
+ self.is_loading = False
+ weechat.bar_item_update("input_text")
+
+ @contextmanager
+ def completing(self):
+ self.completion_context = "IN_PROGRESS_COMPLETION"
+ try:
+ yield
+ finally:
+ self.completion_context = "ACTIVE_COMPLETION"
+
+ @property
+ @abstractmethod
+ def workspace(self) -> SlackWorkspace:
+ raise NotImplementedError()
+
+ @property
+ @abstractmethod
+ def context(self) -> Literal["conversation", "thread"]:
+ raise NotImplementedError()
+
+ @property
+ @abstractmethod
+ def messages(self) -> Mapping[SlackTs, SlackMessage]:
+ raise NotImplementedError()
+
+ @abstractmethod
+ async def get_name_and_buffer_props(self) -> Tuple[str, Dict[str, str]]:
+ raise NotImplementedError()
+
+ @abstractmethod
+ async def buffer_switched_to(self) -> None:
+ raise NotImplementedError()
+
+ def get_full_name(self, name: str) -> str:
+ return f"{shared.SCRIPT_NAME}.{self.workspace.name}.{name}"
+
+ async def open_buffer(self, switch: bool = False):
+ if self.buffer_pointer:
+ if switch:
+ weechat.buffer_set(self.buffer_pointer, "display", "1")
+ return
+
+ name, buffer_props = await self.get_name_and_buffer_props()
+ full_name = self.get_full_name(name)
+
+ if switch:
+ buffer_props["display"] = "1"
+
+ if shared.weechat_version >= 0x03050000:
+ self.buffer_pointer = weechat.buffer_new_props(
+ full_name,
+ buffer_props,
+ get_callback_name(self._buffer_input_cb),
+ "",
+ get_callback_name(self._buffer_close_cb),
+ "",
+ )
+ else:
+ self.buffer_pointer = weechat.buffer_new(
+ full_name,
+ get_callback_name(self._buffer_input_cb),
+ "",
+ get_callback_name(self._buffer_close_cb),
+ "",
+ )
+ for prop_name, value in buffer_props.items():
+ weechat.buffer_set(self.buffer_pointer, prop_name, value)
+
+ shared.buffers[self.buffer_pointer] = self
+ if switch:
+ await self.buffer_switched_to()
+
+ async def update_buffer_props(self) -> None:
+ name, buffer_props = await self.get_name_and_buffer_props()
+ buffer_props["name"] = self.get_full_name(name)
+ for key, value in buffer_props.items():
+ weechat.buffer_set(self.buffer_pointer, key, value)
+
+ async def rerender_message(self, message: SlackMessage):
+ modify_buffer_line(
+ self.buffer_pointer,
+ message.ts,
+ await message.render_message(context=self.context, rerender=True),
+ )
+
+ async def rerender_history(self):
+ for message in self.messages.values():
+ await self.rerender_message(message)
+
+ async def typing_add_user(self, user_id: str, thread_ts: Optional[str]):
+ if not shared.config.look.typing_status_nicks:
+ return
+
+ if not thread_ts:
+ user = await self.workspace.users[user_id]
+ weechat.hook_signal_send(
+ "typing_set_nick",
+ weechat.WEECHAT_HOOK_SIGNAL_STRING,
+ f"{self.buffer_pointer};typing;{user.nick()}",
+ )
+
+ def set_typing_self(self):
+ now = time.time()
+ if now - 4 > self._typing_self_last_sent:
+ self._typing_self_last_sent = now
+ self.workspace.send_typing(self)
+
+ async def print_message(self, message: SlackMessage, backlog: bool = False):
+ tags = await message.tags(backlog=backlog)
+ rendered = await message.render(self.context)
+ weechat.prnt_date_tags(self.buffer_pointer, message.ts.major, tags, rendered)
+
+ def _buffer_input_cb(self, data: str, buffer: str, input_data: str) -> int:
+ weechat.prnt(buffer, "Text: %s" % input_data)
+ return weechat.WEECHAT_RC_OK
+
+ def _buffer_close_cb(self, data: str, buffer: str) -> int:
+ if self.buffer_pointer in shared.buffers:
+ del shared.buffers[self.buffer_pointer]
+ self.buffer_pointer = ""
+ self.history_filled = False
+ return weechat.WEECHAT_RC_OK
diff --git a/slack/slack_conversation.py b/slack/slack_conversation.py
index 6a24616..4baa17c 100644
--- a/slack/slack_conversation.py
+++ b/slack/slack_conversation.py
@@ -1,17 +1,17 @@
from __future__ import annotations
import hashlib
-import time
from collections import OrderedDict
-from contextlib import contextmanager
-from typing import TYPE_CHECKING, Dict, List, Mapping, NoReturn, Optional, Union
+from typing import TYPE_CHECKING, Dict, List, Mapping, NoReturn, Optional, Tuple, Union
import weechat
+from slack.python_compatibility import removeprefix
from slack.shared import shared
+from slack.slack_buffer import SlackBuffer
from slack.slack_message import SlackMessage, SlackTs
+from slack.slack_thread import SlackThread
from slack.task import gather, run_async
-from slack.util import get_callback_name
if TYPE_CHECKING:
from slack_api.slack_conversations_info import SlackConversationsInfo
@@ -24,129 +24,15 @@ if TYPE_CHECKING:
)
from typing_extensions import Literal
- from slack.slack_api import SlackApi
from slack.slack_workspace import SlackWorkspace
-def get_conversation_from_buffer_pointer(
- buffer_pointer: str,
-) -> Optional[SlackConversation]:
- for workspace in shared.workspaces.values():
- for conversation in workspace.open_conversations.values():
- if conversation.buffer_pointer == buffer_pointer:
- return conversation
- return None
-
-
def invalidate_nicklists():
for workspace in shared.workspaces.values():
for conversation in workspace.open_conversations.values():
conversation.nicklist_needs_refresh = True
-def hdata_line_ts(line_pointer: str) -> Optional[SlackTs]:
- data = weechat.hdata_pointer(weechat.hdata_get("line"), line_pointer, "data")
- for i in range(
- weechat.hdata_integer(weechat.hdata_get("line_data"), data, "tags_count")
- ):
- tag = weechat.hdata_string(
- weechat.hdata_get("line_data"), data, f"{i}|tags_array"
- )
- if tag.startswith("slack_ts_"):
- return SlackTs(tag[9:])
- return None
-
-
-def tags_set_notify_none(tags: List[str]) -> List[str]:
- notify_tags = {"notify_highlight", "notify_message", "notify_private"}
- tags = [tag for tag in tags if tag not in notify_tags]
- tags += ["no_highlight", "notify_none"]
- return tags
-
-
-def modify_buffer_line(buffer_pointer: str, ts: SlackTs, new_text: str):
- own_lines = weechat.hdata_pointer(
- weechat.hdata_get("buffer"), buffer_pointer, "own_lines"
- )
- line_pointer = weechat.hdata_pointer(
- weechat.hdata_get("lines"), own_lines, "last_line"
- )
-
- # Find the last line with this ts
- is_last_line = True
- while line_pointer and hdata_line_ts(line_pointer) != ts:
- is_last_line = False
- line_pointer = weechat.hdata_move(weechat.hdata_get("line"), line_pointer, -1)
-
- if not line_pointer:
- return
-
- if shared.weechat_version >= 0x04000000:
- data = weechat.hdata_pointer(weechat.hdata_get("line"), line_pointer, "data")
- weechat.hdata_update(
- weechat.hdata_get("line_data"), data, {"message": new_text}
- )
- return
-
- # Find all lines for the message
- pointers: List[str] = []
- while line_pointer and hdata_line_ts(line_pointer) == ts:
- pointers.append(line_pointer)
- line_pointer = weechat.hdata_move(weechat.hdata_get("line"), line_pointer, -1)
- pointers.reverse()
-
- if not pointers:
- return
-
- if is_last_line:
- lines = new_text.split("\n")
- extra_lines_count = len(lines) - len(pointers)
- if extra_lines_count > 0:
- line_data = weechat.hdata_pointer(
- weechat.hdata_get("line"), pointers[0], "data"
- )
- tags_count = weechat.hdata_integer(
- weechat.hdata_get("line_data"), line_data, "tags_count"
- )
- tags = [
- weechat.hdata_string(
- weechat.hdata_get("line_data"), line_data, f"{i}|tags_array"
- )
- for i in range(tags_count)
- ]
- tags = tags_set_notify_none(tags)
- tags_str = ",".join(tags)
- last_read_line = weechat.hdata_pointer(
- weechat.hdata_get("lines"), own_lines, "last_read_line"
- )
- should_set_unread = last_read_line == pointers[-1]
-
- # Insert new lines to match the number of lines in the message
- weechat.buffer_set(buffer_pointer, "print_hooks_enabled", "0")
- for _ in range(extra_lines_count):
- weechat.prnt_date_tags(buffer_pointer, ts.major, tags_str, " \t ")
- pointers.append(
- weechat.hdata_pointer(
- weechat.hdata_get("lines"), own_lines, "last_line"
- )
- )
- if should_set_unread:
- weechat.buffer_set(buffer_pointer, "unread", "")
- weechat.buffer_set(buffer_pointer, "print_hooks_enabled", "1")
- else:
- # Split the message into at most the number of existing lines as we can't insert new lines
- lines = new_text.split("\n", len(pointers) - 1)
- # Replace newlines to prevent garbled lines in bare display mode
- lines = [line.replace("\n", " | ") for line in lines]
-
- # Extend lines in case the new message is shorter than the old as we can't delete lines
- lines += [""] * (len(pointers) - len(lines))
-
- for pointer, line in zip(pointers, lines):
- data = weechat.hdata_pointer(weechat.hdata_get("line"), pointer, "data")
- weechat.hdata_update(weechat.hdata_get("line_data"), data, {"message": line})
-
-
def sha1_hex(string: str) -> str:
return str(hashlib.sha1(string.encode()).hexdigest())
@@ -198,6 +84,8 @@ class SlackConversationMessageHashes(Dict[SlackTs, str]):
other_message = self._conversation.messages[ts_with_same_hash]
run_async(self._conversation.rerender_message(other_message))
+ if other_message.thread_buffer is not None:
+ run_async(other_message.thread_buffer.update_buffer_props())
for reply in other_message.replies.values():
run_async(self._conversation.rerender_message(reply))
@@ -205,49 +93,43 @@ class SlackConversationMessageHashes(Dict[SlackTs, str]):
self._inverse_map[short_hash] = key
return self[key]
+ def get_ts(self, ts_hash: str) -> Optional[SlackTs]:
+ hash_without_prefix = removeprefix(ts_hash, "$")
+ return self._inverse_map.get(hash_without_prefix)
+
-class SlackConversation:
+class SlackConversation(SlackBuffer):
def __init__(
self,
workspace: SlackWorkspace,
info: SlackConversationsInfo,
):
- self.workspace = workspace
+ super().__init__()
+ self._workspace = workspace
self._info = info
self._members: Optional[List[str]] = None
self._messages: OrderedDict[SlackTs, SlackMessage] = OrderedDict()
- self._typing_self_last_sent = time.time()
- # TODO: buffer_pointer may be accessed by buffer_switch before it's initialized
- self.buffer_pointer: str = ""
- self.is_loading = False
- self.history_filled = False
- self.history_pending = False
self.nicklist_needs_refresh = True
self.message_hashes = SlackConversationMessageHashes(self)
- self.completion_context: Literal[
- "NO_COMPLETION",
- "PENDING_COMPLETION",
- "ACTIVE_COMPLETION",
- "IN_PROGRESS_COMPLETION",
- ] = "NO_COMPLETION"
- self.completion_values: List[str] = []
- self.completion_index = 0
-
@classmethod
async def create(cls, workspace: SlackWorkspace, conversation_id: str):
info_response = await workspace.api.fetch_conversations_info(conversation_id)
return cls(workspace, info_response["channel"])
@property
- def _api(self) -> SlackApi:
- return self.workspace.api
-
- @property
def id(self) -> str:
return self._info["id"]
@property
+ def workspace(self) -> SlackWorkspace:
+ return self._workspace
+
+ @property
+ def context(self) -> Literal["conversation", "thread"]:
+ return "conversation"
+
+ @property
def messages(self) -> Mapping[SlackTs, SlackMessage]:
return self._messages
@@ -262,6 +144,10 @@ class SlackConversation:
else:
return "channel"
+ @property
+ def buffer_type(self) -> Literal["private", "channel"]:
+ return "private" if self.type in ("im", "mpim") else "channel"
+
async def name(self) -> str:
if self._info["is_im"] is True:
im_user = await self.workspace.users[self._info["user"]]
@@ -302,24 +188,6 @@ class SlackConversation:
) -> str:
return f"{self.name_prefix(name_type)}{await self.name()}"
- @contextmanager
- def loading(self):
- self.is_loading = True
- weechat.bar_item_update("input_text")
- try:
- yield
- finally:
- self.is_loading = False
- weechat.bar_item_update("input_text")
-
- @contextmanager
- def completing(self):
- self.completion_context = "IN_PROGRESS_COMPLETION"
- try:
- yield
- finally:
- self.completion_context = "ACTIVE_COMPLETION"
-
async def open_if_open(self):
if "is_open" in self._info:
if self._info["is_open"]:
@@ -327,54 +195,36 @@ class SlackConversation:
elif self._info.get("is_member"):
await self.open_buffer()
- async def open_buffer(self):
- if self.buffer_pointer:
- return
+ async def get_name_and_buffer_props(self) -> Tuple[str, Dict[str, str]]:
+ name_without_prefix = await self.name()
+ name = f"{self.name_prefix('full_name')}{name_without_prefix}"
+ short_name = self.name_prefix("short_name") + name_without_prefix
- name = await self.name()
- name_with_prefix_for_full_name = f"{self.name_prefix('full_name')}{name}"
- full_name = f"{shared.SCRIPT_NAME}.{self.workspace.name}.{name_with_prefix_for_full_name}"
- short_name = self.name_prefix("short_name") + name
-
- buffer_props = {
+ return name, {
"short_name": short_name,
"title": "topic",
"input_multiline": "1",
"nicklist": "0" if self.type == "im" else "1",
"nicklist_display_groups": "0",
- "localvar_set_type": (
- "private" if self.type in ("im", "mpim") else "channel"
- ),
+ "localvar_set_type": self.buffer_type,
"localvar_set_slack_type": self.type,
"localvar_set_nick": self.workspace.my_user.nick(),
- "localvar_set_channel": name_with_prefix_for_full_name,
+ "localvar_set_channel": name,
"localvar_set_server": self.workspace.name,
}
- if shared.weechat_version >= 0x03050000:
- self.buffer_pointer = weechat.buffer_new_props(
- full_name,
- buffer_props,
- get_callback_name(self._buffer_input_cb),
- "",
- get_callback_name(self._buffer_close_cb),
- "",
- )
- else:
- self.buffer_pointer = weechat.buffer_new(
- full_name,
- get_callback_name(self._buffer_input_cb),
- "",
- get_callback_name(self._buffer_close_cb),
- "",
- )
- for prop_name, value in buffer_props.items():
- weechat.buffer_set(self.buffer_pointer, prop_name, value)
+ async def buffer_switched_to(self):
+ await gather(self.nicklist_update(), self.fill_history())
+ async def open_buffer(self, switch: bool = False):
+ await super().open_buffer(switch)
self.workspace.open_conversations[self.id] = self
- async def buffer_switched_to(self):
- await gather(self.nicklist_update(), self.fill_history())
+ async def rerender_message(self, message: SlackMessage):
+ await super().rerender_message(message)
+ parent_message = message.parent_message
+ if parent_message and parent_message.thread_buffer:
+ await parent_message.thread_buffer.rerender_message(message)
async def load_members(self, load_all: bool = False):
if self._members is None:
@@ -439,7 +289,7 @@ class SlackConversation:
sender_bot_ids = [m.sender_bot_id for m in messages if m.sender_bot_id]
self.workspace.bots.initialize_items(sender_bot_ids)
- await gather(*(message.render() for message in messages))
+ await gather(*(message.render(self.context) for message in messages))
for message in messages:
await self.print_message(message, backlog=True)
@@ -492,6 +342,8 @@ class SlackConversation:
parent_message = message.parent_message
if parent_message:
parent_message.replies[message.ts] = message
+ if parent_message.thread_buffer:
+ await parent_message.thread_buffer.print_message(message)
if message.sender_user_id:
# TODO: thread buffers
@@ -502,15 +354,6 @@ class SlackConversation:
f"{self.buffer_pointer};off;{user.nick()}",
)
- async def rerender_message(self, message: SlackMessage):
- modify_buffer_line(
- self.buffer_pointer, message.ts, await message.render_message(rerender=True)
- )
-
- async def rerender_history(self):
- for message in self._messages.values():
- await self.rerender_message(message)
-
async def change_message(
self, data: Union[SlackMessageChanged, SlackMessageReplied]
):
@@ -550,34 +393,10 @@ class SlackConversation:
message.reaction_remove(reaction, user_id)
await self.rerender_message(message)
- async def typing_add_user(self, user_id: str, thread_ts: Optional[str]):
- if not shared.config.look.typing_status_nicks:
- return
-
- if not thread_ts:
- user = await self.workspace.users[user_id]
- weechat.hook_signal_send(
- "typing_set_nick",
- weechat.WEECHAT_HOOK_SIGNAL_STRING,
- f"{self.buffer_pointer};typing;{user.nick()}",
- )
-
- def typing_update_self(self, typing_state: str):
- now = time.time()
- if now - 4 > self._typing_self_last_sent:
- self._typing_self_last_sent = now
- self.workspace.send_typing(self.id)
-
- async def print_message(self, message: SlackMessage, backlog: bool = False):
- tags = await message.tags(backlog=backlog)
- rendered = await message.render()
- weechat.prnt_date_tags(self.buffer_pointer, message.ts.major, tags, rendered)
-
- def _buffer_input_cb(self, data: str, buffer: str, input_data: str) -> int:
- weechat.prnt(buffer, "Text: %s" % input_data)
- return weechat.WEECHAT_RC_OK
-
- def _buffer_close_cb(self, data: str, buffer: str) -> int:
- self.buffer_pointer = ""
- self.history_filled = False
- return weechat.WEECHAT_RC_OK
+ async def open_thread(self, thread_hash: str, switch: bool = False):
+ thread_ts = self.message_hashes.get_ts(thread_hash)
+ if thread_ts:
+ thread_message = self.messages[thread_ts]
+ if thread_message.thread_buffer is None:
+ thread_message.thread_buffer = SlackThread(thread_message)
+ await thread_message.thread_buffer.open_buffer(switch)
diff --git a/slack/slack_message.py b/slack/slack_message.py
index 0466559..6728038 100644
--- a/slack/slack_message.py
+++ b/slack/slack_message.py
@@ -35,9 +35,10 @@ if TYPE_CHECKING:
SlackMessageReaction,
SlackMessageSubtypeHuddleThreadRoom,
)
- from typing_extensions import assert_never
+ from typing_extensions import Literal, assert_never
from slack.slack_conversation import SlackConversation
+ from slack.slack_thread import SlackThread
from slack.slack_workspace import SlackWorkspace
@@ -110,6 +111,7 @@ class SlackMessage:
self.ts = SlackTs(message_json["ts"])
self.replies: OrderedDict[SlackTs, SlackMessage] = OrderedDict()
self.reply_history_filled = False
+ self.thread_buffer: Optional[SlackThread] = None
self._deleted = False
@property
@@ -261,9 +263,12 @@ class SlackMessage:
return ",".join(tags)
- async def render(self) -> str:
+ async def render(
+ self,
+ context: Literal["conversation", "thread"],
+ ) -> str:
prefix_coro = self.render_prefix()
- message_coro = self.render_message()
+ message_coro = self.render_message(context)
prefix, message = await gather(prefix_coro, message_coro)
self._rendered = f"{prefix}\t{message}"
return self._rendered
@@ -382,9 +387,15 @@ class SlackMessage:
self._rendered_message = text + text_edited + reactions
return self._rendered_message
- async def render_message(self, rerender: bool = False) -> str:
- thread_prefix = self._create_thread_prefix()
+ async def render_message(
+ self,
+ context: Literal["conversation", "thread"],
+ rerender: bool = False,
+ ) -> str:
text = await self._render_message(rerender=rerender)
+ if context == "thread":
+ return text
+ thread_prefix = self._create_thread_prefix()
thread = self._create_thread_string()
return thread_prefix + text + thread
diff --git a/slack/slack_thread.py b/slack/slack_thread.py
new file mode 100644
index 0000000..f92dfc4
--- /dev/null
+++ b/slack/slack_thread.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+
+from itertools import chain
+from typing import TYPE_CHECKING, Dict, Mapping, Tuple
+
+from slack.slack_buffer import SlackBuffer
+from slack.slack_message import SlackMessage, SlackTs
+from slack.task import gather
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
+ from slack.slack_workspace import SlackWorkspace
+
+
+class SlackThread(SlackBuffer):
+ def __init__(self, parent: SlackMessage) -> None:
+ super().__init__()
+ self.parent = parent
+
+ @property
+ def workspace(self) -> SlackWorkspace:
+ return self.parent.workspace
+
+ @property
+ def context(self) -> Literal["conversation", "thread"]:
+ return "thread"
+
+ @property
+ def messages(self) -> Mapping[SlackTs, SlackMessage]:
+ return self.parent.replies
+
+ async def get_name_and_buffer_props(self) -> Tuple[str, Dict[str, str]]:
+ conversation_name = await self.parent.conversation.name_with_prefix("full_name")
+ name = f"{conversation_name}.${self.parent.hash}"
+ short_name = f" ${self.parent.hash}"
+
+ return name, {
+ "short_name": short_name,
+ "title": "topic",
+ "input_multiline": "1",
+ "localvar_set_type": self.parent.conversation.buffer_type,
+ "localvar_set_slack_type": "thread",
+ "localvar_set_nick": self.workspace.my_user.nick(),
+ "localvar_set_channel": name,
+ "localvar_set_server": self.workspace.name,
+ }
+
+ async def buffer_switched_to(self):
+ await self.fill_history()
+
+ async def print_history(self):
+ if self.history_filled:
+ return
+
+ self.history_filled = True
+
+ with self.loading():
+ messages = chain([self.parent], self.parent.replies.values())
+ for message in messages:
+ await self.print_message(message, backlog=True)
+
+ async def fill_history(self):
+ if self.history_pending:
+ return
+
+ if self.parent.reply_history_filled:
+ await self.print_history()
+ return
+
+ with self.loading():
+ self.history_pending = True
+
+ messages = await self.parent.conversation.fetch_replies(self.parent)
+ if messages is None:
+ return
+
+ sender_user_ids = [m.sender_user_id for m in messages if m.sender_user_id]
+ self.workspace.users.initialize_items(sender_user_ids)
+
+ sender_bot_ids = [m.sender_bot_id for m in messages if m.sender_bot_id]
+ self.workspace.bots.initialize_items(sender_bot_ids)
+
+ await gather(*(message.render(self.context) for message in messages))
+ await self.print_history()
+
+ self.history_pending = False
diff --git a/slack/slack_workspace.py b/slack/slack_workspace.py
index a570658..08895d1 100644
--- a/slack/slack_workspace.py
+++ b/slack/slack_workspace.py
@@ -25,8 +25,10 @@ from slack.log import print_error
from slack.proxy import Proxy
from slack.shared import shared
from slack.slack_api import SlackApi
+from slack.slack_buffer import SlackBuffer
from slack.slack_conversation import SlackConversation
from slack.slack_message import SlackMessage, SlackTs
+from slack.slack_thread import SlackThread
from slack.slack_user import SlackBot, SlackUser, SlackUsergroup
from slack.task import Future, Task, create_task, gather, run_async
from slack.util import get_callback_name
@@ -344,12 +346,25 @@ class SlackWorkspace:
print("lost connection on ping, reconnecting")
run_async(self.reconnect())
- def send_typing(self, conversation_id: str):
+ def send_typing(self, buffer: SlackBuffer):
if not self.is_connected:
raise SlackError(self, "Can't send typing when not connected")
if self._ws is None:
raise SlackError(self, "is_connected is True while _ws is None")
- msg = {"type": "typing", "channel": conversation_id}
+
+ if isinstance(buffer, SlackConversation):
+ conversation_id = buffer.id
+ elif isinstance(buffer, SlackThread):
+ conversation_id = buffer.parent.conversation.id
+ else:
+ raise NotImplementedError(f"Unknown buffer type: {type(buffer)}")
+
+ msg = {
+ "type": "user_typing",
+ "channel": conversation_id,
+ }
+ if isinstance(buffer, SlackThread):
+ msg["thread_ts"] = buffer.parent.ts
self._ws.send(json.dumps(msg))
async def reconnect(self):