slack-remote 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slack_remote/__init__.py +0 -0
- slack_remote/src/__init__.py +4 -0
- slack_remote/src/constants_slack_remote.py +7 -0
- slack_remote/src/slack_remote.py +93 -0
- slack_remote/src/slack_remote_exception.py +6 -0
- slack_remote-0.0.1.dist-info/METADATA +22 -0
- slack_remote-0.0.1.dist-info/RECORD +9 -0
- slack_remote-0.0.1.dist-info/WHEEL +5 -0
- slack_remote-0.0.1.dist-info/top_level.txt +1 -0
slack_remote/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
SLACK_API_BASE_URL = "https://slack.com/api"
|
|
2
|
+
SLACK_BOT_TOKEN_ENV = "SLACK_BOT_TOKEN"
|
|
3
|
+
SLACK_UNAVAILABLE_ERROR_CODE = "slack_unavailable"
|
|
4
|
+
HTTP_REQUEST_TIMEOUT_SECONDS = 15
|
|
5
|
+
# A bot usually replies in the thread within ~10 seconds
|
|
6
|
+
WAIT_FOR_THREAD_REPLY_TIMEOUT_SECONDS = 60
|
|
7
|
+
WAIT_FOR_THREAD_REPLY_POLL_SECONDS = 3
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from http import HTTPMethod
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from python_sdk_remote.utilities import our_get_env
|
|
10
|
+
|
|
11
|
+
from .constants_slack_remote import (
|
|
12
|
+
HTTP_REQUEST_TIMEOUT_SECONDS,
|
|
13
|
+
SLACK_API_BASE_URL,
|
|
14
|
+
SLACK_BOT_TOKEN_ENV,
|
|
15
|
+
SLACK_UNAVAILABLE_ERROR_CODE,
|
|
16
|
+
WAIT_FOR_THREAD_REPLY_POLL_SECONDS,
|
|
17
|
+
WAIT_FOR_THREAD_REPLY_TIMEOUT_SECONDS,
|
|
18
|
+
)
|
|
19
|
+
from .slack_remote_exception import SlackRemoteException
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SlackRemote:
|
|
23
|
+
"""Slack Web API with a bot token, and Events API requests signed as Slack signs them (for smoke tests of a
|
|
24
|
+
Request URL)."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, bot_token: Optional[str] = None) -> None:
|
|
27
|
+
self.bot_token = bot_token or our_get_env(SLACK_BOT_TOKEN_ENV, raise_if_empty=True)
|
|
28
|
+
|
|
29
|
+
def post_message(self, channel: str, text: str, thread_ts: Optional[str] = None) -> dict[str, Any]:
|
|
30
|
+
"""Needs the chat:write bot scope. In Slack's response "channel" is the channel id and "ts" the timestamp of
|
|
31
|
+
the message (the thread_ts of its replies)."""
|
|
32
|
+
message: dict[str, Any] = {"channel": channel, "text": text}
|
|
33
|
+
if thread_ts:
|
|
34
|
+
message["thread_ts"] = thread_ts
|
|
35
|
+
return self._call(HTTPMethod.POST, "chat.postMessage", json=message)
|
|
36
|
+
|
|
37
|
+
def get_thread_replies(self, channel: str, thread_ts: str) -> list[dict[str, Any]]:
|
|
38
|
+
"""Needs the channels:history (groups:history in a private channel) bot scope. Only the replies, without the
|
|
39
|
+
message that started the thread."""
|
|
40
|
+
response_json = self._call(HTTPMethod.GET, "conversations.replies",
|
|
41
|
+
params={"channel": channel, "ts": thread_ts})
|
|
42
|
+
thread_replies = [message for message in response_json.get("messages", []) if message.get("ts") != thread_ts]
|
|
43
|
+
return thread_replies
|
|
44
|
+
|
|
45
|
+
def wait_for_thread_reply(self, channel: str, thread_ts: str,
|
|
46
|
+
timeout_seconds: float = WAIT_FOR_THREAD_REPLY_TIMEOUT_SECONDS,
|
|
47
|
+
poll_seconds: float = WAIT_FOR_THREAD_REPLY_POLL_SECONDS,
|
|
48
|
+
sleep: Callable[[float], None] = time.sleep) -> Optional[dict[str, Any]]:
|
|
49
|
+
"""The first reply in the thread, or None when no one replied within timeout_seconds."""
|
|
50
|
+
thread_reply = None
|
|
51
|
+
waited_seconds = 0.0
|
|
52
|
+
while thread_reply is None and waited_seconds <= timeout_seconds:
|
|
53
|
+
thread_replies = self.get_thread_replies(channel, thread_ts)
|
|
54
|
+
if thread_replies:
|
|
55
|
+
thread_reply = thread_replies[0]
|
|
56
|
+
else:
|
|
57
|
+
sleep(poll_seconds)
|
|
58
|
+
waited_seconds += poll_seconds
|
|
59
|
+
return thread_reply
|
|
60
|
+
|
|
61
|
+
def get_user_email(self, user_id: str) -> Optional[str]:
|
|
62
|
+
"""Needs the users:read and users:read.email bot scopes."""
|
|
63
|
+
response_json = self._call(HTTPMethod.GET, "users.info", params={"user": user_id})
|
|
64
|
+
user_email: Optional[str] = (response_json.get("user") or {}).get("profile", {}).get("email")
|
|
65
|
+
return user_email
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def send_signed_event(request_url: str, signing_secret: str, event: dict[str, Any],
|
|
69
|
+
timestamp: Optional[int] = None) -> requests.Response:
|
|
70
|
+
"""POSTs an Events API event_callback to a Request URL with the signature Slack sends
|
|
71
|
+
(https://api.slack.com/authentication/verifying-requests-from-slack)."""
|
|
72
|
+
body = json.dumps({"type": "event_callback", "event": event})
|
|
73
|
+
request_timestamp = str(timestamp if timestamp is not None else int(time.time()))
|
|
74
|
+
signature = "v0=" + hmac.new(signing_secret.encode(), f"v0:{request_timestamp}:{body}".encode(),
|
|
75
|
+
hashlib.sha256).hexdigest()
|
|
76
|
+
response = requests.post(request_url, data=body.encode(), timeout=HTTP_REQUEST_TIMEOUT_SECONDS,
|
|
77
|
+
headers={"Content-Type": "application/json",
|
|
78
|
+
"X-Slack-Request-Timestamp": request_timestamp,
|
|
79
|
+
"X-Slack-Signature": signature})
|
|
80
|
+
return response
|
|
81
|
+
|
|
82
|
+
def _call(self, method: HTTPMethod, api_method: str, **kwargs: Any) -> dict[str, Any]:
|
|
83
|
+
url = f"{SLACK_API_BASE_URL}/{api_method}"
|
|
84
|
+
try:
|
|
85
|
+
response_json: dict[str, Any] = requests.request(
|
|
86
|
+
method.value, url, headers={"Authorization": f"Bearer {self.bot_token}"},
|
|
87
|
+
timeout=HTTP_REQUEST_TIMEOUT_SECONDS, **kwargs).json()
|
|
88
|
+
except (requests.RequestException, ValueError) as exception:
|
|
89
|
+
raise SlackRemoteException(SLACK_UNAVAILABLE_ERROR_CODE, f"Slack {api_method} failed: {exception}")
|
|
90
|
+
if not response_json.get("ok"):
|
|
91
|
+
error_code = response_json.get("error") or "unknown_error"
|
|
92
|
+
raise SlackRemoteException(error_code, f"Slack {api_method} failed: {error_code}")
|
|
93
|
+
return response_json
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slack-remote
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: PyPI slack-remote Python Package owned by Circlez.ai
|
|
5
|
+
Home-page: https://github.com/circles-zone/slack-remote-python-package
|
|
6
|
+
Author: Circles
|
|
7
|
+
Author-email: info@circlez.ai
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: python-sdk-remote
|
|
12
|
+
Requires-Dist: requests
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: classifier
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: requires-dist
|
|
20
|
+
Dynamic: summary
|
|
21
|
+
|
|
22
|
+
Slack Web API and Events API: post a message, read the replies in its thread, send a signed event
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
slack_remote/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
slack_remote/src/__init__.py,sha256=OYa_3TuQAyyjuGMNMV8gZls3lu7FTcMlI4RW3XtO7nw,146
|
|
3
|
+
slack_remote/src/constants_slack_remote.py,sha256=a_23VF5tthFdZRSdNY9QL42vkbxcagtl55wsB7ljp1k,309
|
|
4
|
+
slack_remote/src/slack_remote.py,sha256=S_KOKohPe8oMUSepPdXPgdmg17cPw0MEAYBYMtJRCOA,4917
|
|
5
|
+
slack_remote/src/slack_remote_exception.py,sha256=fVPOBIrCJ8mQsO-bkvlJQv0EA9ryosf562uv15JMbwA,279
|
|
6
|
+
slack_remote-0.0.1.dist-info/METADATA,sha256=lNOAoV4RoMaA6L_I0K3dfDjkAtO7rmIzXa_zbdV8MYI,698
|
|
7
|
+
slack_remote-0.0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
slack_remote-0.0.1.dist-info/top_level.txt,sha256=PX9ZX-ucaB89Yrm-wDL-g2zonoUwZfW41sRTM6JN9Bk,13
|
|
9
|
+
slack_remote-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
slack_remote
|