diff options
-rw-r--r-- | slack/commands.py | 74 | ||||
-rw-r--r-- | slack/register.py | 11 | ||||
-rw-r--r-- | slack/slack_api.py | 31 | ||||
-rw-r--r-- | slack/slack_conversation.py | 16 | ||||
-rw-r--r-- | slack/slack_user.py | 28 | ||||
-rw-r--r-- | slack/slack_workspace.py | 1 | ||||
-rw-r--r-- | typings/slack_api/slack_rtm_connect.pyi | 3 | ||||
-rw-r--r-- | typings/slack_api/slack_users_info.pyi | 6 | ||||
-rw-r--r-- | typings/slack_edgeapi/slack_users_search.pyi | 11 |
9 files changed, 166 insertions, 15 deletions
diff --git a/slack/commands.py b/slack/commands.py index c33372a..2b27113 100644 --- a/slack/commands.py +++ b/slack/commands.py @@ -9,6 +9,11 @@ import weechat from slack.log import print_error from slack.shared import shared +from slack.slack_conversation import ( + SlackConversation, + get_conversation_from_buffer_pointer, +) +from slack.slack_user import name_from_user_info_without_spaces from slack.slack_workspace import SlackWorkspace from slack.task import create_task from slack.util import get_callback_name, with_color @@ -298,7 +303,76 @@ def completion_irc_channels_cb( return weechat.WEECHAT_RC_OK +def complete_input(conversation: SlackConversation): + if conversation.completion_context and conversation.completion_query: + 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_before = input_value[:input_pos].removesuffix( + conversation.completion_query + ) + input_after = input_value[input_pos:] + new_input = input_before + result + input_after + new_pos = input_pos - len(conversation.completion_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)) + + conversation.completion_query = result + + +async def complete_user(conversation: SlackConversation): + if not conversation.completion_context and conversation.completion_query: + conversation.completion_context = 1 + search = await conversation.workspace.api.fetch_users_search( + conversation.completion_query + ) + conversation.completion_values = [ + name_from_user_info_without_spaces(conversation.workspace, user) + for user in search["results"] + ] + conversation.completion_index = 0 + else: + conversation.completion_index += 1 + if conversation.completion_index >= len(conversation.completion_values): + conversation.completion_index = 0 + + complete_input(conversation) + + +def input_complete_next_cb(data: str, buffer: str, command: str) -> int: + conversation = get_conversation_from_buffer_pointer(buffer) + if conversation: + input_value = weechat.buffer_get_string(buffer, "input") + input_pos = weechat.buffer_get_integer(buffer, "input_pos") + word_until_cursor = input_value[:input_pos].split()[-1] + + if word_until_cursor.startswith("@") and len(word_until_cursor) > 1: + conversation.completion_query = word_until_cursor[1:] + create_task(complete_user(conversation)) + return weechat.WEECHAT_RC_OK_EAT + return weechat.WEECHAT_RC_OK + + +def input_complete_previous_cb(data: str, buffer: str, command: str) -> int: + conversation = get_conversation_from_buffer_pointer(buffer) + if conversation and conversation.completion_context: + conversation.completion_index -= 1 + if conversation.completion_index < 0: + conversation.completion_index = len(conversation.completion_values) - 1 + complete_input(conversation) + return weechat.WEECHAT_RC_OK_EAT + return weechat.WEECHAT_RC_OK + + def register_commands(): + weechat.hook_command_run( + "/input complete_next", get_callback_name(input_complete_next_cb), "" + ) + weechat.hook_command_run( + "/input complete_previous", get_callback_name(input_complete_previous_cb), "" + ) weechat.hook_completion( "slack_workspaces", "Slack workspaces (internal names)", diff --git a/slack/register.py b/slack/register.py index c390362..fd38956 100644 --- a/slack/register.py +++ b/slack/register.py @@ -30,6 +30,14 @@ def signal_buffer_switch_cb(data: str, signal: str, buffer_pointer: str) -> int: return weechat.WEECHAT_RC_OK +def input_text_changed_cb(data: str, signal: str, buffer_pointer: str) -> int: + conversation = get_conversation_from_buffer_pointer(buffer_pointer) + if conversation: + if not conversation.is_completing and conversation.completion_context: + conversation.completion_context = 0 + return weechat.WEECHAT_RC_OK + + def modifier_input_text_display_with_cursor_cb( data: str, modifier: str, buffer_pointer: str, string: str ) -> str: @@ -100,6 +108,9 @@ def register(): weechat.hook_signal( "window_switch", get_callback_name(signal_buffer_switch_cb), "" ) + weechat.hook_signal( + "input_text_changed", get_callback_name(input_text_changed_cb), "" + ) weechat.hook_modifier( "input_text_display_with_cursor", get_callback_name(modifier_input_text_display_with_cursor_cb), diff --git a/slack/slack_api.py b/slack/slack_api.py index f68abd1..44e1477 100644 --- a/slack/slack_api.py +++ b/slack/slack_api.py @@ -15,6 +15,7 @@ if TYPE_CHECKING: from slack_api.slack_rtm_connect import SlackRtmConnectResponse from slack_api.slack_users_conversations import SlackUsersConversationsResponse from slack_api.slack_users_info import SlackUserInfoResponse, SlackUsersInfoResponse + from slack_edgeapi.slack_users_search import SlackUsersSearchResponse from slack.slack_conversation import SlackConversation from slack.slack_workspace import SlackWorkspace @@ -60,6 +61,21 @@ class SlackApi: return response return response + async def _fetch_edgeapi(self, method: str, params: Params = {}): + enterprise_id_part = ( + f"{self.workspace.enterprise_id}/" if self.workspace.enterprise_id else "" + ) + url = f"https://edgeapi.slack.com/cache/{enterprise_id_part}{self.workspace.id}/{method}" + options = self._get_request_options() + options["postfields"] = json.dumps(params) + options["httpheader"] += "\nContent-Type: application/json" + response = await http_request( + url, + options, + self.workspace.config.network_timeout.value * 1000, + ) + return json.loads(response) + async def fetch_rtm_connect(self): method = "rtm.connect" response: SlackRtmConnectResponse = await self._fetch(method) @@ -137,3 +153,18 @@ class SlackApi: if response["ok"] is False: raise SlackApiError(self.workspace, method, response, params) return response + + async def fetch_users_search(self, query: str): + method = "users/search" + params = { + "include_profile_only_users": True, + "query": query, + "count": 25, + "fuzz": 1, + "uax29_tokenizer": False, + "filter": "NOT deactivated", + } + response: SlackUsersSearchResponse = await self._fetch_edgeapi(method, params) + if response["ok"] is False: + raise SlackApiError(self.workspace, method, response, params) + return response diff --git a/slack/slack_conversation.py b/slack/slack_conversation.py index 8fb1c0e..647d0fe 100644 --- a/slack/slack_conversation.py +++ b/slack/slack_conversation.py @@ -2,7 +2,7 @@ from __future__ import annotations import time from contextlib import contextmanager -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, List, Optional import weechat @@ -37,6 +37,12 @@ class SlackConversation: self.history_filled = False self.history_pending = False + self.is_completing = False + self.completion_context = 0 + self.completion_query: Optional[str] = None + self.completion_values: List[str] = [] + self.completion_index = 0 + @property def _api(self) -> SlackApi: return self.workspace.api @@ -51,6 +57,14 @@ class SlackConversation: self.is_loading = False weechat.bar_item_update("input_text") + @contextmanager + def completing(self): + self.is_completing = True + try: + yield + finally: + self.is_completing = False + async def init(self): with self.loading(): info = await self._api.fetch_conversations_info(self) diff --git a/slack/slack_user.py b/slack/slack_user.py index b895872..439bbdd 100644 --- a/slack/slack_user.py +++ b/slack/slack_user.py @@ -19,6 +19,21 @@ def nick_color(nick: str) -> str: return weechat.info_get("nick_color_name", nick) +# TODO: Probably need to do some mapping here based on the existing users, in case some has been changed to avoid duplicate names +def _name_from_user_info(workspace: SlackWorkspace, info: SlackUserInfo) -> str: + display_name = info["profile"].get("display_name") + if display_name and not workspace.config.use_real_names.value: + return display_name + + return info["profile"].get("display_name") or info.get("real_name") or info["name"] + + +def name_from_user_info_without_spaces( + workspace: SlackWorkspace, info: SlackUserInfo +) -> str: + return _name_from_user_info(workspace, info).replace(" ", "") + + def format_bot_nick(nick: str, colorize: bool = False) -> str: nick = nick.replace(" ", "") @@ -66,19 +81,8 @@ class SlackUser: return nick - def _name_from_profile(self) -> str: - display_name = self._info["profile"].get("display_name") - if display_name and not self.workspace.config.use_real_names.value: - return display_name - - return ( - self._info["profile"].get("display_name") - or self._info.get("real_name") - or self._info["name"] - ) - def _name_without_spaces(self) -> str: - return self._name_from_profile().replace(" ", "") + return name_from_user_info_without_spaces(self.workspace, self._info) def _nick_color(self) -> str: if self.id == self.workspace.my_user.id: diff --git a/slack/slack_workspace.py b/slack/slack_workspace.py index 96740ec..e900101 100644 --- a/slack/slack_workspace.py +++ b/slack/slack_workspace.py @@ -114,6 +114,7 @@ class SlackWorkspace: async def connect(self): rtm_connect = await self.api.fetch_rtm_connect() self.id = rtm_connect["team"]["id"] + self.enterprise_id = rtm_connect["team"].get("enterprise_id") self.my_user = await self.users[rtm_connect["self"]["id"]] await self.connect_ws(rtm_connect["url"]) diff --git a/typings/slack_api/slack_rtm_connect.pyi b/typings/slack_api/slack_rtm_connect.pyi index e5dcd8c..63e23b4 100644 --- a/typings/slack_api/slack_rtm_connect.pyi +++ b/typings/slack_api/slack_rtm_connect.pyi @@ -3,12 +3,15 @@ from __future__ import annotations from typing import Literal, TypedDict, final from slack_api.slack_error import SlackErrorResponse +from typing_extensions import NotRequired @final class SlackRtmConnectTeam(TypedDict): id: str name: str domain: str + enterprise_id: NotRequired[str] + enterprise_name: NotRequired[str] @final class SlackRtmConnectSelf(TypedDict): diff --git a/typings/slack_api/slack_users_info.pyi b/typings/slack_api/slack_users_info.pyi index 3d6a39b..20ba3d1 100644 --- a/typings/slack_api/slack_users_info.pyi +++ b/typings/slack_api/slack_users_info.pyi @@ -92,6 +92,9 @@ class SlackUserInfoCommon(TypedDict): updated: int is_email_confirmed: NotRequired[bool] who_can_share_contact_card: str + enterprise_user: NotRequired[SlackEnterpriseUser] + enterprise_id: NotRequired[str] + presence: NotRequired[Literal["active"]] @final class SlackUserInfoPerson(SlackUserInfoCommon): @@ -99,13 +102,12 @@ class SlackUserInfoPerson(SlackUserInfoCommon): is_bot: Literal[False] is_stranger: NotRequired[bool] has_2fa: bool - enterprise_user: NotRequired[SlackEnterpriseUser] - enterprise_id: NotRequired[str] @final class SlackUserInfoBot(SlackUserInfoCommon): profile: SlackProfileBot is_bot: Literal[True] + is_workflow_bot: NotRequired[bool] SlackUserInfo = SlackUserInfoPerson | SlackUserInfoBot diff --git a/typings/slack_edgeapi/slack_users_search.pyi b/typings/slack_edgeapi/slack_users_search.pyi new file mode 100644 index 0000000..47a9f38 --- /dev/null +++ b/typings/slack_edgeapi/slack_users_search.pyi @@ -0,0 +1,11 @@ +from typing import List, Literal, TypedDict + +from slack_api.slack_error import SlackErrorResponse +from slack_api.slack_users_info import SlackUserInfo + +class SlackUsersSearchSuccessResponse(TypedDict): + ok: Literal[True] + results: List[SlackUserInfo] + presence_active_ids: List[str] + +SlackUsersSearchResponse = SlackUsersSearchSuccessResponse | SlackErrorResponse |