aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--slack/commands.py74
-rw-r--r--slack/register.py11
-rw-r--r--slack/slack_api.py31
-rw-r--r--slack/slack_conversation.py16
-rw-r--r--slack/slack_user.py28
-rw-r--r--slack/slack_workspace.py1
-rw-r--r--typings/slack_api/slack_rtm_connect.pyi3
-rw-r--r--typings/slack_api/slack_users_info.pyi6
-rw-r--r--typings/slack_edgeapi/slack_users_search.pyi11
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