slack-remote 0.0.1__tar.gz
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-0.0.1/PKG-INFO +22 -0
- slack_remote-0.0.1/README.md +34 -0
- slack_remote-0.0.1/pyproject.toml +12 -0
- slack_remote-0.0.1/python_packages/slack_remote/__init__.py +0 -0
- slack_remote-0.0.1/python_packages/slack_remote/src/__init__.py +4 -0
- slack_remote-0.0.1/python_packages/slack_remote/src/constants_slack_remote.py +7 -0
- slack_remote-0.0.1/python_packages/slack_remote/src/slack_remote.py +93 -0
- slack_remote-0.0.1/python_packages/slack_remote/src/slack_remote_exception.py +6 -0
- slack_remote-0.0.1/python_packages/slack_remote.egg-info/PKG-INFO +22 -0
- slack_remote-0.0.1/python_packages/slack_remote.egg-info/SOURCES.txt +13 -0
- slack_remote-0.0.1/python_packages/slack_remote.egg-info/dependency_links.txt +1 -0
- slack_remote-0.0.1/python_packages/slack_remote.egg-info/requires.txt +2 -0
- slack_remote-0.0.1/python_packages/slack_remote.egg-info/top_level.txt +1 -0
- slack_remote-0.0.1/setup.cfg +4 -0
- slack_remote-0.0.1/setup.py +24 -0
|
@@ -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,34 @@
|
|
|
1
|
+
# slack-remote-python-package
|
|
2
|
+
|
|
3
|
+
PyPI [slack-remote](https://pypi.org/project/slack-remote/): the Slack Web API with a bot token, and Events API
|
|
4
|
+
requests signed as Slack signs them - for packages that talk to Slack and for the Sanity/Smoke Tests of our Slack
|
|
5
|
+
apps (i.e. the play1 Slack webhook in `#request-to-open-a-jira-work-item-play1`, never in a channel of real users).
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from slack_remote.src import SlackRemote
|
|
9
|
+
|
|
10
|
+
slack_remote = SlackRemote() # SLACK_BOT_TOKEN
|
|
11
|
+
message = slack_remote.post_message(channel_id, "Summary: ...")
|
|
12
|
+
thread_reply = slack_remote.wait_for_thread_reply(channel_id, message["ts"])
|
|
13
|
+
|
|
14
|
+
# Smoke test of a Request URL: the event Slack would send for a message, signed with the app's Signing Secret
|
|
15
|
+
response = SlackRemote.send_signed_event(request_url, signing_secret, {"type": "message", "channel": channel_id,
|
|
16
|
+
"user": user_id, "text": "...", "ts": ts})
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| Method | Bot scopes |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `post_message` | chat:write |
|
|
22
|
+
| `get_thread_replies`, `wait_for_thread_reply` | channels:history (groups:history in a private channel) |
|
|
23
|
+
| `get_user_email` | users:read, users:read.email |
|
|
24
|
+
|
|
25
|
+
## Environment variables
|
|
26
|
+
- `SLACK_BOT_TOKEN` (or `SlackRemote(bot_token=...)`)
|
|
27
|
+
- System tests: also `SLACK_TEST_CHANNEL_ID` of a test channel the bot is a member of; without them they are skipped.
|
|
28
|
+
|
|
29
|
+
## Tests
|
|
30
|
+
```shell
|
|
31
|
+
cd slack-remote-python-package
|
|
32
|
+
pip install -r requirements.txt -r requirements-dev.txt
|
|
33
|
+
pytest
|
|
34
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "slack-remote"
|
|
7
|
+
version = "0.0.1" # https://pypi.org/project/slack-remote
|
|
8
|
+
description = "slack-remote Python Package"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
"Circlez.ai <info@circlez.ai>",
|
|
12
|
+
]
|
|
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,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
python_packages/slack_remote/__init__.py
|
|
5
|
+
python_packages/slack_remote.egg-info/PKG-INFO
|
|
6
|
+
python_packages/slack_remote.egg-info/SOURCES.txt
|
|
7
|
+
python_packages/slack_remote.egg-info/dependency_links.txt
|
|
8
|
+
python_packages/slack_remote.egg-info/requires.txt
|
|
9
|
+
python_packages/slack_remote.egg-info/top_level.txt
|
|
10
|
+
python_packages/slack_remote/src/__init__.py
|
|
11
|
+
python_packages/slack_remote/src/constants_slack_remote.py
|
|
12
|
+
python_packages/slack_remote/src/slack_remote.py
|
|
13
|
+
python_packages/slack_remote/src/slack_remote_exception.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
slack_remote
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import setuptools
|
|
2
|
+
|
|
3
|
+
PACKAGE_NAME = "slack-remote"
|
|
4
|
+
|
|
5
|
+
setuptools.setup(
|
|
6
|
+
name=PACKAGE_NAME,
|
|
7
|
+
version='0.0.1', # https://pypi.org/project/slack-remote#history
|
|
8
|
+
author="Circles",
|
|
9
|
+
author_email="info@circlez.ai",
|
|
10
|
+
description=f"PyPI {PACKAGE_NAME} Python Package owned by Circlez.ai",
|
|
11
|
+
long_description="Slack Web API and Events API: post a message, read the replies in its thread, send a signed event",
|
|
12
|
+
long_description_content_type='text/markdown',
|
|
13
|
+
url=f"https://github.com/circles-zone/{PACKAGE_NAME}-python-package",
|
|
14
|
+
packages=setuptools.find_packages(where="python_packages", exclude=["*.tests", "*.tests.*"]),
|
|
15
|
+
package_dir={"": "python_packages"},
|
|
16
|
+
classifiers=[
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
],
|
|
20
|
+
install_requires=[
|
|
21
|
+
'python-sdk-remote',
|
|
22
|
+
'requests',
|
|
23
|
+
]
|
|
24
|
+
)
|