Skip to content

Commit 8e64ddb

Browse files
WilliamBergaminseratchfilmaj
authored
feat: implement basic hook logic (#4)
* Add logic and tests --------- Co-authored-by: Kazuhiro Sera <seratch@gmail.com> Co-authored-by: Fil Maj <maj.fil@gmail.com>
1 parent ab6dcec commit 8e64ddb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1100
-14
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: Install dependencies
2323
run: |
2424
pip install -U pip
25-
pip install .
25+
pip install -r requirements.txt
2626
pip install -r requirements/testing.txt
2727
- name: Run tests
2828
run: |

README.md

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,154 @@
1-
# python-slack-hooks
2-
Helper library implementing the contract between the Slack CLI and Bolt for Python
1+
<h1 align="center">Python Slack Hooks</h1>
2+
3+
A helper library implementing the contract between the
4+
[Slack CLI][slack-cli-docs] and
5+
[Bolt for Python](https://slack.dev/bolt-python/)
6+
7+
## Environment requirements
8+
9+
Before getting started, make sure you have a development workspace where you
10+
have permissions to install apps. **Please note that leveraging all features in
11+
this project require that the workspace be part of
12+
[a Slack paid plan](https://slack.com/pricing).**
13+
14+
### Install the Slack CLI
15+
16+
Install the Slack CLI. Step-by-step instructions can be found in this
17+
[Quickstart Guide][slack-cli-docs].
18+
19+
### Environment Setup
20+
21+
Create a project folder and a
22+
[virtual environment](https://docs.python.org/3/library/venv.html#module-venv)
23+
within it
24+
25+
```zsh
26+
# Python 3.6+ required
27+
mkdir myproject
28+
cd myproject
29+
python3 -m venv .venv
30+
```
31+
32+
Activate the environment
33+
34+
```zsh
35+
source .venv/bin/activate
36+
```
37+
38+
### Pypi
39+
40+
Install this package using pip.
41+
42+
```zsh
43+
pip install -U slack-cli-hooks
44+
```
45+
46+
### Clone
47+
48+
Clone this project using git.
49+
50+
```zsh
51+
git clone https://github.com/slackapi/python-slack-hooks.git
52+
```
53+
54+
Follow the
55+
[Develop Locally](https://github.com/slackapi/python-slack-hooks/blob/main/.github/maintainers_guide.md#develop-locally)
56+
steps in the maintainers guide to build and use this package.
57+
58+
## Simple project
59+
60+
In the same directory where we installed `slack-cli-hooks`
61+
62+
1. Define basic information and metadata about our app via an
63+
[App Manifest](https://api.slack.com/reference/manifests) (`manifest.json`).
64+
2. Create a `slack.json` file that defines the interface between the
65+
[Slack CLI][slack-cli-docs] and [Bolt for Python][bolt-python-docs].
66+
3. Use an `app.py` file to define the entrypoint for a
67+
[Bolt for Python][bolt-python-docs] project.
68+
69+
### Application Configuration
70+
71+
Define your [Application Manifest](https://api.slack.com/reference/manifests) in
72+
a `manifest.json` file.
73+
74+
```json
75+
{
76+
"display_information": {
77+
"name": "simple-app"
78+
},
79+
"outgoing_domains": [],
80+
"settings": {
81+
"org_deploy_enabled": true,
82+
"socket_mode_enabled": true,
83+
},
84+
"features": {
85+
"bot_user": {
86+
"display_name": "simple-app"
87+
}
88+
},
89+
"oauth_config": {
90+
"scopes": {
91+
"bot": ["chat:write"]
92+
}
93+
}
94+
}
95+
```
96+
97+
### CLI/Bolt Interface Configuration
98+
99+
Define the Slack CLI configuration in a file named `slack.json`.
100+
101+
```json
102+
{
103+
"hooks": {
104+
"get-hooks": "python3 -m slack_cli_hooks.hooks.get_hooks"
105+
}
106+
}
107+
```
108+
109+
### Source code
110+
111+
Create a [Bolt for Python][bolt-python-docs] app in a file named `app.py`.
112+
Alternatively you can use an existing app instead.
113+
114+
```python
115+
from slack_bolt import App
116+
from slack_bolt.adapter.socket_mode import SocketModeHandler
117+
118+
app = App()
119+
120+
# Add functionality here
121+
122+
if __name__ == "__main__":
123+
SocketModeHandler(app).start()
124+
```
125+
126+
## Running the app
127+
128+
You should now be able to harness the power of the Slack CLI and Bolt.
129+
130+
Run the app this way:
131+
132+
```zsh
133+
slack run
134+
```
135+
136+
## Getting Help
137+
138+
If you get stuck we're here to help. Ensure your issue is related to this
139+
project and not to [Bolt for Python][bolt-python-docs]. The following are the
140+
best ways to get assistance working through your issue:
141+
142+
- [Issue Tracker](https://github.com/slackapi/python-slack-hooks/issues) for
143+
questions, bug reports, feature requests, and general discussion. **Try
144+
searching for an existing issue before creating a new one.**
145+
- Email our developer support team: `support@slack.com`
146+
147+
## Contributing
148+
149+
Contributions are more then welcome. Please look at the
150+
[contributing guidelines](https://github.com/slackapi/python-slack-hooks/blob/main/.github/CONTRIBUTING.md)
151+
for more info!
152+
153+
[slack-cli-docs]: https://api.slack.com/automation/cli
154+
[bolt-python-docs]: https://slack.dev/bolt-python/concepts

requirements/format.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
black==22.8.0; python_version=="3.6"
22
black; python_version>"3.6" # Until we drop Python 3.6 support, we have to stay with this version
3-
flake8>=5.0.4; python_version=="3.6"
4-
flake8==6.0.0; python_version>"3.6"
3+
flake8>=5.0.4, <7;
54
pytype; (python_version<"3.11" or python_version>"3.11")
65
pytype==2023.11.29; python_version=="3.11"

requirements/testing.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
# pip install -r requirements/testing.txt
22
pytest>=6.2.5,<7
33
pytest-cov>=3,<4
4+
Flask>=2.0.3,<4
5+
gevent>=22.10.2,<24
6+
gevent-websocket>=0.10.1,<1

slack_cli_hooks/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""
2+
A Slack CLI hooks implementation in Python to build Bolt Slack apps leveraging the full power of the [Slack CLI](https://api.slack.com/automation/cli/install). Look at our [code examples](https://github.com/slackapi/python-slack-hooks/tree/main/examples) to learn how to build apps using the SLack CLI and Bolt.
3+
4+
* Slack CLI: https://api.slack.com/automation/cli/install
5+
* Bolt Website: https://slack.dev/bolt-python/
6+
* GitHub repository: https://github.com/slackapi/python-slack-hooks
7+
""" # noqa: E501

slack_cli_hooks/error/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class CliError(Exception):
2+
"""General class for cli error"""

slack_cli_hooks/hooks/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""Slack CLI contract implementation for Bolt.
2+
"""

slack_cli_hooks/hooks/get_hooks.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env python
2+
import json
3+
from slack_cli_hooks.protocol import Protocol, MessageBoundaryProtocol, DefaultProtocol, build_protocol
4+
5+
PROTOCOL: Protocol
6+
EXEC = "python3"
7+
8+
9+
hooks_payload = {
10+
"hooks": {
11+
"get-manifest": f"{EXEC} -m slack_cli_hooks.hooks.get_manifest",
12+
"start": f"{EXEC} -X dev -m slack_cli_hooks.hooks.start",
13+
},
14+
"config": {
15+
"watch": {"filter-regex": "(^manifest\\.json$)", "paths": ["."]},
16+
"protocol-version": [MessageBoundaryProtocol.name, DefaultProtocol.name],
17+
"sdk-managed-connection-enabled": True,
18+
},
19+
}
20+
21+
if __name__ == "__main__":
22+
PROTOCOL = build_protocol()
23+
PROTOCOL.respond(json.dumps(hooks_payload))

slack_cli_hooks/hooks/get_manifest.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env python
2+
import os
3+
import re
4+
from typing import List
5+
6+
from slack_cli_hooks.error import CliError
7+
from slack_cli_hooks.protocol import Protocol, build_protocol
8+
9+
PROTOCOL: Protocol
10+
11+
EXCLUDED_DIRECTORIES = [
12+
"lib",
13+
"bin",
14+
"include",
15+
"node_modules",
16+
"packages",
17+
"logs",
18+
"build",
19+
"coverage",
20+
"target",
21+
"tmp",
22+
"test",
23+
"tests",
24+
]
25+
26+
DIRECTORY_IGNORE_REGEX = re.compile(r"(^\.|^\_|^{}$)".format("$|^".join(EXCLUDED_DIRECTORIES)), re.IGNORECASE)
27+
28+
29+
def filter_directories(directories: List[str]) -> List[str]:
30+
return [directory for directory in directories if not DIRECTORY_IGNORE_REGEX.match(directory)]
31+
32+
33+
def find_file_path(path: str, file_name: str) -> str:
34+
for root, dirs, files in os.walk(path, topdown=True, followlinks=False):
35+
dirs[:] = filter_directories(dirs)
36+
if file_name in files:
37+
return os.path.join(root, file_name)
38+
raise CliError(f"Could not find a {file_name} file")
39+
40+
41+
def get_manifest(working_directory: str) -> str:
42+
file_path = find_file_path(working_directory, "manifest.json")
43+
44+
with open(file_path, "r") as manifest:
45+
return manifest.read()
46+
47+
48+
if __name__ == "__main__":
49+
PROTOCOL = build_protocol()
50+
PROTOCOL.respond(get_manifest(os.getcwd()))

slack_cli_hooks/hooks/start.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env python
2+
import os
3+
import runpy
4+
import sys
5+
6+
from slack_cli_hooks.error import CliError
7+
from slack_cli_hooks.hooks.utils import ManagedOSEnvVars
8+
from slack_cli_hooks.protocol import Protocol, build_protocol
9+
10+
PROTOCOL: Protocol
11+
12+
DEFAULT_MAIN_FILE = "app.py"
13+
14+
SLACK_CLI_XOXB = "SLACK_CLI_XOXB"
15+
SLACK_CLI_XAPP = "SLACK_CLI_XAPP"
16+
SLACK_BOT_TOKEN = "SLACK_BOT_TOKEN"
17+
SLACK_APP_TOKEN = "SLACK_APP_TOKEN"
18+
19+
20+
def validate_env() -> None:
21+
if not os.environ.get(SLACK_CLI_XOXB):
22+
raise CliError(f"Missing local run bot token ({SLACK_CLI_XOXB}).")
23+
if not os.environ.get(SLACK_CLI_XAPP):
24+
raise CliError(f"Missing local run app token ({SLACK_CLI_XAPP}).")
25+
26+
27+
def get_main_file() -> str:
28+
custom_file = os.environ.get("SLACK_APP_PATH")
29+
if custom_file:
30+
return custom_file
31+
return DEFAULT_MAIN_FILE
32+
33+
34+
def get_main_path(working_directory: str) -> str:
35+
main_file = get_main_file()
36+
main_raw_path = os.path.join(working_directory, main_file)
37+
return os.path.abspath(main_raw_path)
38+
39+
40+
def start(working_directory: str) -> None:
41+
validate_env()
42+
43+
entrypoint_path = get_main_path(working_directory)
44+
45+
if not os.path.exists(entrypoint_path):
46+
raise CliError(f"Could not find {get_main_file()} file")
47+
48+
parent_package = os.path.dirname(entrypoint_path)
49+
os_env_vars = ManagedOSEnvVars(PROTOCOL)
50+
51+
try:
52+
os_env_vars.set_if_absent(SLACK_BOT_TOKEN, os.environ[SLACK_CLI_XOXB])
53+
os_env_vars.set_if_absent(SLACK_APP_TOKEN, os.environ[SLACK_CLI_XAPP])
54+
sys.path.insert(0, parent_package) # Add parent package to sys path
55+
56+
runpy.run_path(entrypoint_path, run_name="__main__")
57+
finally:
58+
sys.path.remove(parent_package)
59+
os_env_vars.clear()
60+
61+
62+
if __name__ == "__main__":
63+
PROTOCOL = build_protocol()
64+
start(os.getcwd())
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .managed_os_env_vars import ManagedOSEnvVars
2+
3+
__all__ = [
4+
"ManagedOSEnvVars",
5+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import os
2+
from typing import List
3+
from slack_cli_hooks.protocol import Protocol
4+
5+
6+
class ManagedOSEnvVars:
7+
def __init__(self, protocol: Protocol) -> None:
8+
self._protocol = protocol
9+
self._os_env_vars: List[str] = []
10+
11+
def set_if_absent(self, os_env_var: str, value: str) -> None:
12+
if os_env_var in os.environ:
13+
self._protocol.info(
14+
f"{os_env_var} environment variable detected in session, using it over the provided one!"
15+
)
16+
return
17+
self._os_env_vars.append(os_env_var)
18+
os.environ[os_env_var] = value
19+
20+
def clear(self) -> None:
21+
for os_env_var in self._os_env_vars:
22+
os.environ.pop(os_env_var, None)

slack_cli_hooks/protocol/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import argparse
2+
import sys
3+
from typing import List
4+
from .default_protocol import DefaultProtocol
5+
from .message_boundary_protocol import MessageBoundaryProtocol
6+
from .protocol import Protocol
7+
8+
__all__ = [
9+
"DefaultProtocol",
10+
"MessageBoundaryProtocol",
11+
"Protocol",
12+
]
13+
14+
15+
def build_protocol(argv: List[str] = sys.argv[1:]) -> Protocol:
16+
parser = argparse.ArgumentParser()
17+
parser.add_argument("--protocol", type=str, required=False)
18+
parser.add_argument("--boundary", type=str, required=False)
19+
20+
args, unknown = parser.parse_known_args(args=argv)
21+
22+
if args.protocol == MessageBoundaryProtocol.name:
23+
return MessageBoundaryProtocol(boundary=args.boundary)
24+
return DefaultProtocol()

0 commit comments

Comments
 (0)