webex-bot 1.0.8__tar.gz → 1.1.12__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.
- {webex_bot-1.0.8 → webex_bot-1.1.12}/CONTRIBUTING.rst +8 -7
- {webex_bot-1.0.8 → webex_bot-1.1.12}/MANIFEST.in +2 -0
- {webex_bot-1.0.8/webex_bot.egg-info → webex_bot-1.1.12}/PKG-INFO +17 -22
- {webex_bot-1.0.8 → webex_bot-1.1.12}/README.md +12 -4
- {webex_bot-1.0.8 → webex_bot-1.1.12}/setup.cfg +2 -1
- {webex_bot-1.0.8 → webex_bot-1.1.12}/setup.py +3 -3
- webex_bot-1.1.12/tests/conftest.py +116 -0
- webex_bot-1.1.12/tests/test_command_model.py +49 -0
- webex_bot-1.1.12/tests/test_commands.py +49 -0
- webex_bot-1.1.12/tests/test_exceptions.py +8 -0
- webex_bot-1.1.12/tests/test_formatting.py +12 -0
- webex_bot-1.1.12/tests/test_response.py +32 -0
- webex_bot-1.1.12/tests/test_webex_bot.py +149 -0
- webex_bot-1.1.12/tests/test_webex_websocket_client.py +37 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/__init__.py +1 -1
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/commands/echo.py +1 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/webex_bot.py +52 -37
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/websockets/webex_websocket_client.py +41 -8
- {webex_bot-1.0.8 → webex_bot-1.1.12/webex_bot.egg-info}/PKG-INFO +17 -22
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot.egg-info/SOURCES.txt +7 -0
- webex_bot-1.1.12/webex_bot.egg-info/requires.txt +7 -0
- webex_bot-1.0.8/tests/test_webex_bot.py +0 -21
- webex_bot-1.0.8/webex_bot.egg-info/requires.txt +0 -7
- {webex_bot-1.0.8 → webex_bot-1.1.12}/LICENSE +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/Makefile +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/conf.py +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/contributing.rst +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/index.rst +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/installation.rst +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/make.bat +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/usage.rst +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/tests/__init__.py +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/cards/__init__.py +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/commands/__init__.py +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/commands/help.py +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/exceptions.py +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/formatting.py +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/models/__init__.py +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/models/command.py +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/models/response.py +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/websockets/__init__.py +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot.egg-info/dependency_links.txt +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot.egg-info/not-zip-safe +0 -0
- {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot.egg-info/top_level.txt +0 -0
|
@@ -80,10 +80,12 @@ Ready to contribute? Here's how to set up `webex_bot` for local development.
|
|
|
80
80
|
tests, including testing other Python versions with tox::
|
|
81
81
|
|
|
82
82
|
$ flake8 webex_bot tests
|
|
83
|
-
$
|
|
83
|
+
$ pytest
|
|
84
84
|
$ tox
|
|
85
85
|
|
|
86
|
-
To get
|
|
86
|
+
To get the dev dependencies, install them into your virtualenv::
|
|
87
|
+
|
|
88
|
+
$ python -m pip install -r requirements_dev.txt
|
|
87
89
|
|
|
88
90
|
6. Commit your changes and push your branch to GitHub::
|
|
89
91
|
|
|
@@ -102,9 +104,8 @@ Before you submit a pull request, check that it meets these guidelines:
|
|
|
102
104
|
2. If the pull request adds functionality, the docs should be updated. Put
|
|
103
105
|
your new functionality into a function with a docstring, and add the
|
|
104
106
|
feature to the list in README.rst.
|
|
105
|
-
3. The pull request should work for Python 3.10, 3.11,
|
|
106
|
-
|
|
107
|
-
and make sure that the tests pass for all supported Python versions.
|
|
107
|
+
3. The pull request should work for Python 3.10, 3.11, and 3.12. Check GitHub
|
|
108
|
+
Actions and make sure that the tests pass for all supported Python versions.
|
|
108
109
|
|
|
109
110
|
Tips
|
|
110
111
|
----
|
|
@@ -121,8 +122,8 @@ A reminder for the maintainers on how to deploy.
|
|
|
121
122
|
Make sure all your changes are committed
|
|
122
123
|
Then run::
|
|
123
124
|
|
|
124
|
-
$ bump2version patch # possible: major / minor / patch
|
|
125
|
+
$ bump2version patch # possible: major / minor / patch (updates webex_bot/__init__.py)
|
|
125
126
|
$ git push
|
|
126
127
|
$ git push --tags
|
|
127
128
|
|
|
128
|
-
|
|
129
|
+
GitHub Actions will then deploy to PyPI if tests pass.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: webex_bot
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.12
|
|
4
4
|
Summary: Python package for a Webex Bot based on websockets.
|
|
5
5
|
Home-page: https://github.com/fbradyirl/webex_bot
|
|
6
6
|
Author: Finbarr Brady
|
|
@@ -18,29 +18,16 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Requires-Python: >=3.10
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: webexpythonsdk==2.0.
|
|
21
|
+
Requires-Dist: webexpythonsdk==2.0.5
|
|
22
22
|
Requires-Dist: coloredlogs
|
|
23
|
-
Requires-Dist: websockets==
|
|
23
|
+
Requires-Dist: websockets==16
|
|
24
24
|
Requires-Dist: backoff
|
|
25
25
|
Provides-Extra: proxy
|
|
26
|
-
Requires-Dist: websockets_proxy>=0.1.
|
|
27
|
-
Dynamic: author
|
|
28
|
-
Dynamic: author-email
|
|
29
|
-
Dynamic: classifier
|
|
30
|
-
Dynamic: description
|
|
31
|
-
Dynamic: description-content-type
|
|
32
|
-
Dynamic: home-page
|
|
33
|
-
Dynamic: keywords
|
|
34
|
-
Dynamic: license
|
|
35
|
-
Dynamic: license-file
|
|
36
|
-
Dynamic: provides-extra
|
|
37
|
-
Dynamic: requires-dist
|
|
38
|
-
Dynamic: requires-python
|
|
39
|
-
Dynamic: summary
|
|
26
|
+
Requires-Dist: websockets_proxy>=0.1.3; extra == "proxy"
|
|
40
27
|
|
|
41
28
|
# Introduction
|
|
42
29
|
|
|
43
|
-
[](https://pypi.python.org/pypi/webex_bot) [](https://github.com/fbradyirl/webex_bot/actions)
|
|
30
|
+
[](https://pypi.python.org/pypi/webex_bot) [](https://github.com/fbradyirl/webex_bot/actions) [](https://github.com/fbradyirl/webex_bot/actions/workflows/codeql.yml) [](https://codecov.io/gh/fbradyirl/webex_bot) [](https://github.com/fbradyirl/webex_bot/releases) [](https://pypi.python.org/pypi/webex_bot)
|
|
44
31
|
|
|
45
32
|
> [!IMPORTANT]
|
|
46
33
|
> This repository is only sporadically maintained. Breaking API changes will be maintained on a best efforts basis.
|
|
@@ -50,7 +37,7 @@ Dynamic: summary
|
|
|
50
37
|
> Bug reports unrelated to API changes may not get the attention you want.
|
|
51
38
|
|
|
52
39
|
|
|
53
|
-
By using this module, you can create a [Webex
|
|
40
|
+
By using this module, you can create a [Webex][5] messaging bot quickly in just a couple of lines of code.
|
|
54
41
|
|
|
55
42
|
This module does not require you to set up an ngrok tunnel to receive incoming messages when behind a firewall or
|
|
56
43
|
inside a LAN. This package instead uses a websocket to receive messages from the Webex cloud.
|
|
@@ -75,7 +62,7 @@ You can find a sample project, using OpenAI/ChatGPT with this library here: http
|
|
|
75
62
|
|
|
76
63
|
----
|
|
77
64
|
|
|
78
|
-
**
|
|
65
|
+
**Python 3.10, 3.11, and 3.12 are tested at this time.**
|
|
79
66
|
|
|
80
67
|
1. Install this module from pypi:
|
|
81
68
|
|
|
@@ -113,7 +100,7 @@ proxies = {
|
|
|
113
100
|
# Create a Bot Object
|
|
114
101
|
bot = WebexBot(teams_bot_token=os.getenv("WEBEX_ACCESS_TOKEN"),
|
|
115
102
|
approved_rooms=['06586d8d-6aad-4201-9a69-0bf9eeb5766e'],
|
|
116
|
-
bot_name="My
|
|
103
|
+
bot_name="My Webex Ops Bot",
|
|
117
104
|
include_demo_commands=True,
|
|
118
105
|
proxies=proxies)
|
|
119
106
|
|
|
@@ -464,6 +451,14 @@ bot = WebexBot(teams_bot_token=os.getenv("WEBEX_ACCESS_TOKEN")
|
|
|
464
451
|
|
|
465
452
|
* Allow flag to disable bot to bot check
|
|
466
453
|
|
|
454
|
+
### 1.1.6 (2026-Feb-02)
|
|
455
|
+
|
|
456
|
+
* Update dependancies.
|
|
457
|
+
* Expand test coverage and add CI matrix for Python 3.10–3.12
|
|
458
|
+
* Add coverage reporting and update contributor docs
|
|
459
|
+
* Refresh websocket status handling to avoid deprecations
|
|
460
|
+
|
|
461
|
+
|
|
467
462
|
[1]: https://github.com/aaugustin/websockets
|
|
468
463
|
|
|
469
464
|
[2]: https://github.com/WebexCommunity/WebexPythonSDK
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Introduction
|
|
2
2
|
|
|
3
|
-
[](https://pypi.python.org/pypi/webex_bot) [](https://github.com/fbradyirl/webex_bot/actions)
|
|
3
|
+
[](https://pypi.python.org/pypi/webex_bot) [](https://github.com/fbradyirl/webex_bot/actions) [](https://github.com/fbradyirl/webex_bot/actions/workflows/codeql.yml) [](https://codecov.io/gh/fbradyirl/webex_bot) [](https://github.com/fbradyirl/webex_bot/releases) [](https://pypi.python.org/pypi/webex_bot)
|
|
4
4
|
|
|
5
5
|
> [!IMPORTANT]
|
|
6
6
|
> This repository is only sporadically maintained. Breaking API changes will be maintained on a best efforts basis.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
> Bug reports unrelated to API changes may not get the attention you want.
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
By using this module, you can create a [Webex
|
|
13
|
+
By using this module, you can create a [Webex][5] messaging bot quickly in just a couple of lines of code.
|
|
14
14
|
|
|
15
15
|
This module does not require you to set up an ngrok tunnel to receive incoming messages when behind a firewall or
|
|
16
16
|
inside a LAN. This package instead uses a websocket to receive messages from the Webex cloud.
|
|
@@ -35,7 +35,7 @@ You can find a sample project, using OpenAI/ChatGPT with this library here: http
|
|
|
35
35
|
|
|
36
36
|
----
|
|
37
37
|
|
|
38
|
-
**
|
|
38
|
+
**Python 3.10, 3.11, and 3.12 are tested at this time.**
|
|
39
39
|
|
|
40
40
|
1. Install this module from pypi:
|
|
41
41
|
|
|
@@ -73,7 +73,7 @@ proxies = {
|
|
|
73
73
|
# Create a Bot Object
|
|
74
74
|
bot = WebexBot(teams_bot_token=os.getenv("WEBEX_ACCESS_TOKEN"),
|
|
75
75
|
approved_rooms=['06586d8d-6aad-4201-9a69-0bf9eeb5766e'],
|
|
76
|
-
bot_name="My
|
|
76
|
+
bot_name="My Webex Ops Bot",
|
|
77
77
|
include_demo_commands=True,
|
|
78
78
|
proxies=proxies)
|
|
79
79
|
|
|
@@ -424,6 +424,14 @@ bot = WebexBot(teams_bot_token=os.getenv("WEBEX_ACCESS_TOKEN")
|
|
|
424
424
|
|
|
425
425
|
* Allow flag to disable bot to bot check
|
|
426
426
|
|
|
427
|
+
### 1.1.6 (2026-Feb-02)
|
|
428
|
+
|
|
429
|
+
* Update dependancies.
|
|
430
|
+
* Expand test coverage and add CI matrix for Python 3.10–3.12
|
|
431
|
+
* Add coverage reporting and update contributor docs
|
|
432
|
+
* Refresh websocket status handling to avoid deprecations
|
|
433
|
+
|
|
434
|
+
|
|
427
435
|
[1]: https://github.com/aaugustin/websockets
|
|
428
436
|
|
|
429
437
|
[2]: https://github.com/WebexCommunity/WebexPythonSDK
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[bumpversion]
|
|
2
|
-
current_version = 1.
|
|
2
|
+
current_version = 1.1.12
|
|
3
3
|
commit = True
|
|
4
4
|
tag = True
|
|
5
5
|
|
|
@@ -22,6 +22,7 @@ test = pytest
|
|
|
22
22
|
|
|
23
23
|
[tool:pytest]
|
|
24
24
|
collect_ignore = ['setup.py']
|
|
25
|
+
addopts = --cov=webex_bot --cov-report=term-missing --cov-report=xml --cov-report=html --cov-fail-under=80
|
|
25
26
|
|
|
26
27
|
[egg_info]
|
|
27
28
|
tag_build =
|
|
@@ -7,14 +7,14 @@ from setuptools import setup, find_packages
|
|
|
7
7
|
with open('README.md') as readme_file:
|
|
8
8
|
readme = readme_file.read()
|
|
9
9
|
|
|
10
|
-
requirements = ['webexpythonsdk==2.0.
|
|
10
|
+
requirements = ['webexpythonsdk==2.0.5', 'coloredlogs', 'websockets==16', 'backoff']
|
|
11
11
|
|
|
12
12
|
setup_requirements = ['pytest-runner', ]
|
|
13
13
|
|
|
14
14
|
test_requirements = ['pytest>=3', ]
|
|
15
15
|
|
|
16
16
|
extras_requirements = {
|
|
17
|
-
"proxy": ["websockets_proxy>=0.1.
|
|
17
|
+
"proxy": ["websockets_proxy>=0.1.3"]
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
setup(
|
|
@@ -45,6 +45,6 @@ setup(
|
|
|
45
45
|
test_suite='tests',
|
|
46
46
|
tests_require=test_requirements,
|
|
47
47
|
url='https://github.com/fbradyirl/webex_bot',
|
|
48
|
-
version='1.
|
|
48
|
+
version='1.1.12',
|
|
49
49
|
zip_safe=False,
|
|
50
50
|
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import types
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
warnings.filterwarnings(
|
|
7
|
+
"ignore",
|
|
8
|
+
category=PendingDeprecationWarning,
|
|
9
|
+
module=r"webexpythonsdk\.environment",
|
|
10
|
+
)
|
|
11
|
+
warnings.filterwarnings(
|
|
12
|
+
"ignore",
|
|
13
|
+
category=DeprecationWarning,
|
|
14
|
+
module=r"websockets\..*",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from webex_bot.webex_bot import WebexBot
|
|
18
|
+
from webex_bot.websockets.webex_websocket_client import WebexWebsocketClient
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DummyPerson:
|
|
22
|
+
def __init__(self, display_name="Webex Bot", email="bot@example.com", avatar="https://example.com/avatar.png"):
|
|
23
|
+
self.displayName = display_name
|
|
24
|
+
self.emails = [email]
|
|
25
|
+
self.avatar = avatar
|
|
26
|
+
self.type = "bot"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DummyPeople:
|
|
30
|
+
def __init__(self, me_person):
|
|
31
|
+
self._me_person = me_person
|
|
32
|
+
|
|
33
|
+
def me(self):
|
|
34
|
+
return self._me_person
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DummyMessages:
|
|
38
|
+
def __init__(self):
|
|
39
|
+
self.created = []
|
|
40
|
+
self.deleted = []
|
|
41
|
+
|
|
42
|
+
def create(self, **kwargs):
|
|
43
|
+
self.created.append(kwargs)
|
|
44
|
+
return types.SimpleNamespace(id="message-1")
|
|
45
|
+
|
|
46
|
+
def delete(self, message_id):
|
|
47
|
+
self.deleted.append(message_id)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DummyMemberships:
|
|
51
|
+
def __init__(self, member_emails=None):
|
|
52
|
+
self.member_emails = set(member_emails or [])
|
|
53
|
+
|
|
54
|
+
def list(self, roomId, personEmail):
|
|
55
|
+
if personEmail in self.member_emails:
|
|
56
|
+
return [types.SimpleNamespace(personEmail=personEmail)]
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DummyTeams:
|
|
61
|
+
def __init__(self, person_email="bot@example.com", member_emails=None):
|
|
62
|
+
self.people = DummyPeople(DummyPerson(email=person_email))
|
|
63
|
+
self.messages = DummyMessages()
|
|
64
|
+
self.memberships = DummyMemberships(member_emails=member_emails)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.fixture
|
|
68
|
+
def teams_api():
|
|
69
|
+
return DummyTeams(member_emails={"member@example.com"})
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.fixture
|
|
73
|
+
def bot(monkeypatch, teams_api):
|
|
74
|
+
def fake_init(self, access_token, bot_name, on_message=None, on_card_action=None, proxies=None):
|
|
75
|
+
self.access_token = access_token
|
|
76
|
+
self.teams = teams_api
|
|
77
|
+
self.on_message = on_message
|
|
78
|
+
self.on_card_action = on_card_action
|
|
79
|
+
self.proxies = proxies
|
|
80
|
+
self.websocket = None
|
|
81
|
+
|
|
82
|
+
monkeypatch.setattr(WebexWebsocketClient, "__init__", fake_init)
|
|
83
|
+
return WebexBot(teams_bot_token="test-token", include_demo_commands=False)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def make_activity(actor_type="PERSON", actor_email="user@example.com", tags=None, parent=None, activity_id="act-1"):
|
|
87
|
+
if tags is None:
|
|
88
|
+
tags = ["ONE_ON_ONE"]
|
|
89
|
+
activity = {
|
|
90
|
+
"id": activity_id,
|
|
91
|
+
"actor": {"type": actor_type, "emailAddress": actor_email},
|
|
92
|
+
"target": {"tags": tags},
|
|
93
|
+
}
|
|
94
|
+
if parent is not None:
|
|
95
|
+
activity["parent"] = parent
|
|
96
|
+
return activity
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.fixture
|
|
100
|
+
def one_on_one_activity():
|
|
101
|
+
return make_activity()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.fixture
|
|
105
|
+
def group_activity():
|
|
106
|
+
return make_activity(tags=["GROUP"])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.fixture
|
|
110
|
+
def teams_message():
|
|
111
|
+
return types.SimpleNamespace(
|
|
112
|
+
roomId="room-1",
|
|
113
|
+
text="help",
|
|
114
|
+
personEmail="user@example.com",
|
|
115
|
+
messageId="msg-1",
|
|
116
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from webex_bot.models.command import Command, CALLBACK_KEYWORD_KEY, COMMAND_KEYWORD_KEY
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SimpleCommand(Command):
|
|
5
|
+
def __init__(self, command_keyword=None, card=None):
|
|
6
|
+
super().__init__(command_keyword=command_keyword, card=card)
|
|
7
|
+
|
|
8
|
+
def execute(self, message, attachment_actions, activity):
|
|
9
|
+
return "ok"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_command_uses_callback_keyword_from_card():
|
|
13
|
+
card = {
|
|
14
|
+
"actions": [
|
|
15
|
+
{
|
|
16
|
+
"type": "Action.Submit",
|
|
17
|
+
"data": {CALLBACK_KEYWORD_KEY: "cb"},
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
command = SimpleCommand(command_keyword="ping", card=card)
|
|
22
|
+
assert command.card_callback_keyword == "cb"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_command_uses_command_keyword_from_card():
|
|
26
|
+
card = {
|
|
27
|
+
"actions": [
|
|
28
|
+
{
|
|
29
|
+
"type": "Action.Submit",
|
|
30
|
+
"data": {COMMAND_KEYWORD_KEY: "cmd"},
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
command = SimpleCommand(command_keyword=None, card=card)
|
|
35
|
+
assert command.command_keyword == "cmd"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_command_sets_default_callback_keyword_when_missing():
|
|
39
|
+
card = {
|
|
40
|
+
"actions": [
|
|
41
|
+
{
|
|
42
|
+
"type": "Action.Submit",
|
|
43
|
+
"data": {},
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
command = SimpleCommand(command_keyword="ping", card=card)
|
|
48
|
+
assert command.card_callback_keyword == "callback___ping"
|
|
49
|
+
assert card["actions"][0]["data"][CALLBACK_KEYWORD_KEY] == "callback___ping"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import types
|
|
2
|
+
|
|
3
|
+
from webex_bot.commands.echo import EchoCommand, EchoCallback
|
|
4
|
+
from webex_bot.commands.help import HelpCommand, HELP_COMMAND_KEYWORD
|
|
5
|
+
from webex_bot.models.command import Command, COMMAND_KEYWORD_KEY
|
|
6
|
+
from webex_bot.models.response import Response
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SimpleCommand(Command):
|
|
10
|
+
def __init__(self, keyword, help_message="Help me"):
|
|
11
|
+
super().__init__(command_keyword=keyword, help_message=help_message)
|
|
12
|
+
|
|
13
|
+
def execute(self, message, attachment_actions, activity):
|
|
14
|
+
return "ok"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_echo_command_returns_response():
|
|
18
|
+
command = EchoCommand()
|
|
19
|
+
response = command.execute("ignored", None, {})
|
|
20
|
+
assert isinstance(response, Response)
|
|
21
|
+
assert response.attachments[0]["contentType"] == "application/vnd.microsoft.card.adaptive"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_echo_callback_uses_input_text():
|
|
25
|
+
command = EchoCallback()
|
|
26
|
+
attachment_actions = types.SimpleNamespace(inputs={"message_typed": "hello"})
|
|
27
|
+
result = command.execute("", attachment_actions, {})
|
|
28
|
+
assert "hello" in result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_echo_command_pre_execute_returns_response():
|
|
32
|
+
command = EchoCommand()
|
|
33
|
+
response = command.pre_execute("", types.SimpleNamespace(inputs={}), {})
|
|
34
|
+
assert isinstance(response, Response)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_help_command_builds_actions_excluding_help():
|
|
38
|
+
help_command = HelpCommand(bot_name="Bot", bot_help_subtitle="Help", bot_help_image="https://example.com/image.png")
|
|
39
|
+
other_command = SimpleCommand(keyword="ping", help_message="Ping")
|
|
40
|
+
help_command.commands = {help_command, other_command}
|
|
41
|
+
actions, hints = help_command.build_actions_and_hints(thread_parent_id="thread-1")
|
|
42
|
+
assert len(actions) == 1
|
|
43
|
+
assert actions[0].data[COMMAND_KEYWORD_KEY] == "ping"
|
|
44
|
+
assert actions[0].data["thread_parent_id"] == "thread-1"
|
|
45
|
+
assert len(hints) == 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_help_command_keyword_constant():
|
|
49
|
+
assert HELP_COMMAND_KEYWORD == "help"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from webex_bot.formatting import quote_info, quote_warning, quote_danger, code, html_link
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_quote_helpers():
|
|
5
|
+
assert quote_info("hi") == "<blockquote class=info>hi</blockquote>"
|
|
6
|
+
assert quote_warning("hi") == "<blockquote class=warning>hi</blockquote>"
|
|
7
|
+
assert quote_danger("hi") == "<blockquote class=danger>hi</blockquote>"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_code_and_html_link():
|
|
11
|
+
assert code("x") == "<code>x</code>"
|
|
12
|
+
assert html_link("Label", "https://example.com") == "<a href='https://example.com'>Label</a>"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from webexpythonsdk.models.cards import AdaptiveCard
|
|
2
|
+
|
|
3
|
+
from webex_bot.models.response import Response, response_from_adaptive_card
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_response_as_dict_filters_empty():
|
|
7
|
+
response = Response()
|
|
8
|
+
response.markdown = "hello"
|
|
9
|
+
response.files = "file-1"
|
|
10
|
+
response.attachments = {"contentType": "application/vnd.microsoft.card.adaptive", "content": {}}
|
|
11
|
+
result = response.as_dict()
|
|
12
|
+
assert result["markdown"] == "hello"
|
|
13
|
+
assert result["files"] == ["file-1"]
|
|
14
|
+
assert result["attachments"] == [
|
|
15
|
+
{"contentType": "application/vnd.microsoft.card.adaptive", "content": {}}
|
|
16
|
+
]
|
|
17
|
+
assert "roomId" not in result
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_response_from_adaptive_card_populates_attachment():
|
|
21
|
+
card = AdaptiveCard(body=[], actions=[])
|
|
22
|
+
response = response_from_adaptive_card(card)
|
|
23
|
+
assert response.text == "This bot requires a client which can render cards."
|
|
24
|
+
assert response.markdown == "This bot requires a client which can render cards."
|
|
25
|
+
assert response.attachments[0]["contentType"] == "application/vnd.microsoft.card.adaptive"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_response_json_and_html_property():
|
|
29
|
+
response = Response(attributes={"text": "hello"})
|
|
30
|
+
response.html = "<b>hi</b>"
|
|
31
|
+
assert response.html == "<b>hi</b>"
|
|
32
|
+
assert '"text": "hello"' in response.json()
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import types
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from webex_bot.models.command import Command
|
|
6
|
+
from webex_bot.models.response import Response
|
|
7
|
+
from webex_bot.exceptions import BotException
|
|
8
|
+
from webex_bot.webex_bot import WebexBot
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DummyCommand(Command):
|
|
12
|
+
def __init__(self, command_keyword="ping", exact_match=False, card_callback_keyword=None):
|
|
13
|
+
super().__init__(
|
|
14
|
+
command_keyword=command_keyword,
|
|
15
|
+
exact_command_keyword_match=exact_match,
|
|
16
|
+
help_message="Ping command",
|
|
17
|
+
card_callback_keyword=card_callback_keyword,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def execute(self, message, attachment_actions, activity):
|
|
21
|
+
return "pong"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PreExecuteCommand(Command):
|
|
25
|
+
def __init__(self, command_keyword="work", delete_previous_message=False):
|
|
26
|
+
super().__init__(
|
|
27
|
+
command_keyword=command_keyword,
|
|
28
|
+
help_message="Pre execute",
|
|
29
|
+
delete_previous_message=delete_previous_message,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def pre_execute(self, message, attachment_actions, activity):
|
|
33
|
+
return "Working"
|
|
34
|
+
|
|
35
|
+
def execute(self, message, attachment_actions, activity):
|
|
36
|
+
return "Done"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ExceptionCommand(Command):
|
|
40
|
+
def __init__(self, command_keyword="fail"):
|
|
41
|
+
super().__init__(
|
|
42
|
+
command_keyword=command_keyword,
|
|
43
|
+
help_message="Failing command",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def pre_execute(self, message, attachment_actions, activity):
|
|
47
|
+
raise BotException("debug", "pre-reply", reply_one_to_one=True)
|
|
48
|
+
|
|
49
|
+
def execute(self, message, attachment_actions, activity):
|
|
50
|
+
raise BotException("debug", "reply", reply_one_to_one=False)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_get_message_passed_to_command():
|
|
54
|
+
assert WebexBot.get_message_passed_to_command("help", "help me") == " me"
|
|
55
|
+
assert WebexBot.get_message_passed_to_command("help", "hello") == "hello"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_check_user_approved_unrestricted(bot):
|
|
59
|
+
assert bot.check_user_approved("user@example.com", approved_rooms=[]) is True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_check_user_approved_domain(bot):
|
|
63
|
+
bot.approved_domains = ["example.com"]
|
|
64
|
+
assert bot.check_user_approved("user@example.com", approved_rooms=[]) is True
|
|
65
|
+
assert bot.check_user_approved("user@other.com", approved_rooms=[]) is False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_check_user_approved_room_membership(bot):
|
|
69
|
+
assert bot.check_user_approved("member@example.com", approved_rooms=["room-1"]) is True
|
|
70
|
+
assert bot.check_user_approved("outsider@example.com", approved_rooms=["room-1"]) is False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_add_command_duplicate_callback_keyword_raises(bot):
|
|
74
|
+
first = DummyCommand(command_keyword="first", card_callback_keyword="dup")
|
|
75
|
+
second = DummyCommand(command_keyword="second", card_callback_keyword="dup")
|
|
76
|
+
bot.add_command(first)
|
|
77
|
+
with pytest.raises(Exception):
|
|
78
|
+
bot.add_command(second)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_process_incoming_message_ignores_other_bot(bot, teams_message, one_on_one_activity):
|
|
82
|
+
bot.allow_bot_to_bot = False
|
|
83
|
+
teams_message.personEmail = "otherbot@example.com"
|
|
84
|
+
activity = dict(one_on_one_activity)
|
|
85
|
+
activity["actor"]["type"] = "BOT"
|
|
86
|
+
bot.process_incoming_message(teams_message, activity)
|
|
87
|
+
assert bot.teams.messages.created == []
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_process_incoming_message_runs_help(bot, teams_message, one_on_one_activity):
|
|
91
|
+
teams_message.text = "unknown"
|
|
92
|
+
bot.process_incoming_message(teams_message, one_on_one_activity)
|
|
93
|
+
assert len(bot.teams.messages.created) >= 1
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_do_reply_with_response_sets_room_id(bot):
|
|
97
|
+
response = Response()
|
|
98
|
+
response.markdown = "hi"
|
|
99
|
+
bot.do_reply(response, "room-1", "user@example.com", False, True, "thread-1")
|
|
100
|
+
assert bot.teams.messages.created[-1]["roomId"] == "room-1"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_do_reply_with_response_list(bot):
|
|
104
|
+
response = Response()
|
|
105
|
+
response.markdown = "hello"
|
|
106
|
+
bot.do_reply([response], "room-1", "user@example.com", False, True, "thread-1")
|
|
107
|
+
assert bot.teams.messages.created[-1]["roomId"] == "room-1"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_process_raw_command_exact_match(bot, teams_message, one_on_one_activity):
|
|
111
|
+
command = DummyCommand(command_keyword="ping", exact_match=True)
|
|
112
|
+
bot.add_command(command)
|
|
113
|
+
bot.process_raw_command("ping", teams_message, "user@example.com", one_on_one_activity)
|
|
114
|
+
assert bot.teams.messages.created[-1]["markdown"] == "pong"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_process_raw_command_callback(bot, one_on_one_activity):
|
|
118
|
+
command = DummyCommand(command_keyword="ping", card_callback_keyword="ping_cb")
|
|
119
|
+
bot.add_command(command)
|
|
120
|
+
attachment_actions = types.SimpleNamespace(
|
|
121
|
+
inputs={"callback_keyword": "ping_cb"},
|
|
122
|
+
roomId="room-1",
|
|
123
|
+
)
|
|
124
|
+
activity = dict(one_on_one_activity)
|
|
125
|
+
activity["actor"]["emailAddress"] = "user@example.com"
|
|
126
|
+
bot.process_incoming_card_action(attachment_actions, activity)
|
|
127
|
+
assert bot.teams.messages.created[-1]["markdown"] == "pong"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_process_raw_command_delete_previous_message(bot, teams_message, one_on_one_activity):
|
|
131
|
+
command = PreExecuteCommand(delete_previous_message=True)
|
|
132
|
+
bot.add_command(command)
|
|
133
|
+
bot.process_raw_command("work", teams_message, "user@example.com", one_on_one_activity)
|
|
134
|
+
assert teams_message.messageId in bot.teams.messages.deleted
|
|
135
|
+
assert "message-1" in bot.teams.messages.deleted
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_run_pre_execute_handles_bot_exception(bot, teams_message, one_on_one_activity):
|
|
139
|
+
command = ExceptionCommand()
|
|
140
|
+
reply, one_to_one = bot.run_pre_execute(command, "msg", teams_message, one_on_one_activity)
|
|
141
|
+
assert reply == "pre-reply"
|
|
142
|
+
assert one_to_one is True
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_run_command_and_handle_bot_exceptions(bot, teams_message, one_on_one_activity):
|
|
146
|
+
command = ExceptionCommand()
|
|
147
|
+
reply, one_to_one = bot.run_command_and_handle_bot_exceptions(command, "msg", teams_message, one_on_one_activity)
|
|
148
|
+
assert reply == "reply"
|
|
149
|
+
assert one_to_one is False
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from webex_bot.websockets.webex_websocket_client import WebexWebsocketClient
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _make_client():
|
|
5
|
+
client = WebexWebsocketClient.__new__(WebexWebsocketClient)
|
|
6
|
+
client._get_headers = lambda: {"Authorization": "Bearer test"}
|
|
7
|
+
return client
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_get_websocket_connect_kwargs_prefers_extra_headers():
|
|
11
|
+
def connect(*, extra_headers=None):
|
|
12
|
+
return extra_headers
|
|
13
|
+
|
|
14
|
+
client = _make_client()
|
|
15
|
+
assert client._get_websocket_connect_kwargs(connect) == {
|
|
16
|
+
"extra_headers": {"Authorization": "Bearer test"}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_get_websocket_connect_kwargs_uses_additional_headers():
|
|
21
|
+
def connect(*, additional_headers=None):
|
|
22
|
+
return additional_headers
|
|
23
|
+
|
|
24
|
+
client = _make_client()
|
|
25
|
+
assert client._get_websocket_connect_kwargs(connect) == {
|
|
26
|
+
"additional_headers": {"Authorization": "Bearer test"}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_get_websocket_connect_kwargs_fallback_to_extra_headers():
|
|
31
|
+
def connect(*, headers=None):
|
|
32
|
+
return headers
|
|
33
|
+
|
|
34
|
+
client = _make_client()
|
|
35
|
+
assert client._get_websocket_connect_kwargs(connect) == {
|
|
36
|
+
"extra_headers": {"Authorization": "Bearer test"}
|
|
37
|
+
}
|
|
@@ -174,7 +174,7 @@ class WebexBot(WebexWebsocketClient):
|
|
|
174
174
|
if member.personEmail == user_email:
|
|
175
175
|
is_user_member = True
|
|
176
176
|
except webexpythonsdk.exceptions.ApiError as apie:
|
|
177
|
-
log.
|
|
177
|
+
log.warning(f"API error: {apie}")
|
|
178
178
|
return is_user_member
|
|
179
179
|
|
|
180
180
|
def process_incoming_card_action(self, attachment_actions, activity):
|
|
@@ -308,6 +308,8 @@ class WebexBot(WebexWebsocketClient):
|
|
|
308
308
|
log.info(f"delete_previous_message is True. Deleting message with ID: {previous_message_id}")
|
|
309
309
|
self.teams.messages.delete(previous_message_id)
|
|
310
310
|
|
|
311
|
+
pre_reply_message_id = None
|
|
312
|
+
|
|
311
313
|
if not is_card_callback_command and command.card is not None:
|
|
312
314
|
response = Response()
|
|
313
315
|
response.text = "This bot requires a client which can render cards."
|
|
@@ -320,7 +322,7 @@ class WebexBot(WebexWebsocketClient):
|
|
|
320
322
|
message=message_without_command,
|
|
321
323
|
teams_message=teams_message,
|
|
322
324
|
activity=activity)
|
|
323
|
-
self.do_reply(pre_card_load_reply, room_id, user_email, pre_card_load_reply_one_to_one, is_one_on_one_space, thread_parent_id)
|
|
325
|
+
pre_reply_message_id = self.do_reply(pre_card_load_reply, room_id, user_email, pre_card_load_reply_one_to_one, is_one_on_one_space, thread_parent_id)
|
|
324
326
|
reply = response
|
|
325
327
|
else:
|
|
326
328
|
log.debug(f"Going to run command: '{command}' with input: '{message_without_command}'")
|
|
@@ -328,25 +330,36 @@ class WebexBot(WebexWebsocketClient):
|
|
|
328
330
|
message=message_without_command,
|
|
329
331
|
teams_message=teams_message,
|
|
330
332
|
activity=activity)
|
|
331
|
-
self.do_reply(pre_execute_reply, room_id, user_email, pre_execute_reply_one_to_one, is_one_on_one_space, thread_parent_id)
|
|
333
|
+
pre_reply_message_id = self.do_reply(pre_execute_reply, room_id, user_email, pre_execute_reply_one_to_one, is_one_on_one_space, thread_parent_id)
|
|
332
334
|
reply, reply_one_to_one = self.run_command_and_handle_bot_exceptions(command=command,
|
|
333
335
|
message=message_without_command,
|
|
334
336
|
teams_message=teams_message,
|
|
335
337
|
activity=activity)
|
|
336
338
|
log.info(f"Using thread id={thread_parent_id}")
|
|
337
|
-
|
|
339
|
+
final_message_id = self.do_reply(reply, room_id, user_email, reply_one_to_one, is_one_on_one_space, thread_parent_id)
|
|
340
|
+
|
|
341
|
+
# If requested, delete the pre-execute (or pre-card-load) message once the final reply has been sent
|
|
342
|
+
if command.delete_previous_message and pre_reply_message_id:
|
|
343
|
+
try:
|
|
344
|
+
log.info(f"Deleting pre-execute message with ID: {pre_reply_message_id}")
|
|
345
|
+
self.teams.messages.delete(pre_reply_message_id)
|
|
346
|
+
except Exception as e:
|
|
347
|
+
log.warning(f"Failed to delete pre-execute message {pre_reply_message_id}: {e}")
|
|
348
|
+
|
|
349
|
+
return final_message_id
|
|
338
350
|
|
|
339
351
|
def do_reply(self, reply, room_id, user_email, reply_one_to_one, is_one_on_one_space, conv_target_id):
|
|
340
352
|
# allow command handlers to craft their own Teams message
|
|
353
|
+
created_message_id = None
|
|
341
354
|
if reply and isinstance(reply, Response):
|
|
342
355
|
# If the Response lacks a roomId, set it to the incoming room
|
|
343
356
|
if not reply.roomId:
|
|
344
357
|
reply.roomId = room_id
|
|
345
358
|
if not reply.parentId and conv_target_id and self.threads:
|
|
346
359
|
reply.parentId = conv_target_id
|
|
347
|
-
|
|
348
|
-
self.teams.messages.create(**
|
|
349
|
-
|
|
360
|
+
reply_dict = reply.as_dict()
|
|
361
|
+
created = self.teams.messages.create(**reply_dict)
|
|
362
|
+
created_message_id = getattr(created, 'id', None)
|
|
350
363
|
# Support returning a list of Responses
|
|
351
364
|
elif reply and (isinstance(reply, list) or isinstance(reply, types.GeneratorType)):
|
|
352
365
|
for response in reply:
|
|
@@ -356,24 +369,24 @@ class WebexBot(WebexWebsocketClient):
|
|
|
356
369
|
response.roomId = room_id
|
|
357
370
|
if not response.parentId and conv_target_id:
|
|
358
371
|
response.parentId = conv_target_id
|
|
359
|
-
self.teams.messages.create(**response.as_dict())
|
|
372
|
+
created = self.teams.messages.create(**response.as_dict())
|
|
373
|
+
created_message_id = getattr(created, 'id', None)
|
|
360
374
|
else:
|
|
361
375
|
# Just a plain message
|
|
362
|
-
self.send_message_to_room_or_person(user_email,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
reply = "ok"
|
|
376
|
+
created_message_id = self.send_message_to_room_or_person(user_email,
|
|
377
|
+
room_id,
|
|
378
|
+
reply_one_to_one,
|
|
379
|
+
is_one_on_one_space,
|
|
380
|
+
response,
|
|
381
|
+
conv_target_id)
|
|
369
382
|
elif reply:
|
|
370
|
-
self.send_message_to_room_or_person(user_email,
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
return
|
|
383
|
+
created_message_id = self.send_message_to_room_or_person(user_email,
|
|
384
|
+
room_id,
|
|
385
|
+
reply_one_to_one,
|
|
386
|
+
is_one_on_one_space,
|
|
387
|
+
reply,
|
|
388
|
+
conv_target_id)
|
|
389
|
+
return created_message_id
|
|
377
390
|
|
|
378
391
|
def send_message_to_room_or_person(self,
|
|
379
392
|
user_email,
|
|
@@ -384,27 +397,29 @@ class WebexBot(WebexWebsocketClient):
|
|
|
384
397
|
conv_target_id):
|
|
385
398
|
default_move_to_one_to_one_heads_up = \
|
|
386
399
|
quote_info(f"{user_email} I've messaged you 1-1. Please reply to me there.")
|
|
400
|
+
last_created = None
|
|
387
401
|
if reply_one_to_one:
|
|
388
402
|
if not is_one_on_one_space:
|
|
389
403
|
if self.threads:
|
|
390
|
-
self.teams.messages.create(roomId=room_id,
|
|
391
|
-
|
|
392
|
-
|
|
404
|
+
last_created = self.teams.messages.create(roomId=room_id,
|
|
405
|
+
markdown=default_move_to_one_to_one_heads_up,
|
|
406
|
+
parentId=conv_target_id)
|
|
393
407
|
else:
|
|
394
|
-
self.teams.messages.create(roomId=room_id,
|
|
395
|
-
|
|
408
|
+
last_created = self.teams.messages.create(roomId=room_id,
|
|
409
|
+
markdown=default_move_to_one_to_one_heads_up)
|
|
396
410
|
if self.threads:
|
|
397
|
-
self.teams.messages.create(toPersonEmail=user_email,
|
|
398
|
-
|
|
399
|
-
|
|
411
|
+
last_created = self.teams.messages.create(toPersonEmail=user_email,
|
|
412
|
+
markdown=reply,
|
|
413
|
+
parentId=conv_target_id)
|
|
400
414
|
else:
|
|
401
|
-
self.teams.messages.create(toPersonEmail=user_email,
|
|
402
|
-
|
|
415
|
+
last_created = self.teams.messages.create(toPersonEmail=user_email,
|
|
416
|
+
markdown=reply)
|
|
403
417
|
else:
|
|
404
418
|
if self.threads:
|
|
405
|
-
self.teams.messages.create(roomId=room_id, markdown=reply, parentId=conv_target_id)
|
|
419
|
+
last_created = self.teams.messages.create(roomId=room_id, markdown=reply, parentId=conv_target_id)
|
|
406
420
|
else:
|
|
407
|
-
self.teams.messages.create(roomId=room_id, markdown=reply)
|
|
421
|
+
last_created = self.teams.messages.create(roomId=room_id, markdown=reply)
|
|
422
|
+
return getattr(last_created, 'id', None)
|
|
408
423
|
|
|
409
424
|
def run_pre_card_load_reply(self, command, message, teams_message, activity):
|
|
410
425
|
"""
|
|
@@ -413,7 +428,7 @@ class WebexBot(WebexWebsocketClient):
|
|
|
413
428
|
try:
|
|
414
429
|
return command.pre_card_load_reply(message, teams_message, activity), False
|
|
415
430
|
except BotException as e:
|
|
416
|
-
log.
|
|
431
|
+
log.warning(f"BotException: {e.debug_message}")
|
|
417
432
|
return e.reply_message, e.reply_one_to_one
|
|
418
433
|
|
|
419
434
|
def run_pre_execute(self, command, message, teams_message, activity):
|
|
@@ -423,14 +438,14 @@ class WebexBot(WebexWebsocketClient):
|
|
|
423
438
|
try:
|
|
424
439
|
return command.pre_execute(message, teams_message, activity), False
|
|
425
440
|
except BotException as e:
|
|
426
|
-
log.
|
|
441
|
+
log.warning(f"BotException: {e.debug_message}")
|
|
427
442
|
return e.reply_message, e.reply_one_to_one
|
|
428
443
|
|
|
429
444
|
def run_command_and_handle_bot_exceptions(self, command, message, teams_message, activity):
|
|
430
445
|
try:
|
|
431
446
|
return command.card_callback(message, teams_message, activity), False
|
|
432
447
|
except BotException as e:
|
|
433
|
-
log.
|
|
448
|
+
log.warning(f"BotException: {e.debug_message}")
|
|
434
449
|
return e.reply_message, e.reply_one_to_one
|
|
435
450
|
|
|
436
451
|
@staticmethod
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
+
import inspect
|
|
3
4
|
import logging
|
|
4
5
|
import socket
|
|
5
6
|
import ssl
|
|
@@ -10,7 +11,10 @@ import certifi
|
|
|
10
11
|
import requests
|
|
11
12
|
import websockets
|
|
12
13
|
from webexpythonsdk import WebexAPI
|
|
13
|
-
|
|
14
|
+
try:
|
|
15
|
+
from websockets import InvalidStatus
|
|
16
|
+
except ImportError: # pragma: no cover - fallback for older websockets versions
|
|
17
|
+
from websockets.exceptions import InvalidStatus
|
|
14
18
|
|
|
15
19
|
from webex_bot import __version__
|
|
16
20
|
|
|
@@ -79,6 +83,20 @@ class WebexWebsocketClient(object):
|
|
|
79
83
|
"trackingid": self.tracking_id
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
def _get_websocket_connect_kwargs(self, connect_func):
|
|
87
|
+
headers = self._get_headers()
|
|
88
|
+
try:
|
|
89
|
+
params = inspect.signature(connect_func).parameters
|
|
90
|
+
except (TypeError, ValueError):
|
|
91
|
+
return {"extra_headers": headers}
|
|
92
|
+
|
|
93
|
+
if "extra_headers" in params:
|
|
94
|
+
return {"extra_headers": headers}
|
|
95
|
+
if "additional_headers" in params:
|
|
96
|
+
return {"additional_headers": headers}
|
|
97
|
+
|
|
98
|
+
return {"extra_headers": headers}
|
|
99
|
+
|
|
82
100
|
def _process_incoming_websocket_message(self, msg):
|
|
83
101
|
"""
|
|
84
102
|
Handle websocket data.
|
|
@@ -247,7 +265,7 @@ class WebexWebsocketClient(object):
|
|
|
247
265
|
websockets.ConnectionClosedOK,
|
|
248
266
|
websockets.ConnectionClosed,
|
|
249
267
|
socket.gaierror,
|
|
250
|
-
|
|
268
|
+
InvalidStatus,
|
|
251
269
|
),
|
|
252
270
|
max_time=MAX_BACKOFF_TIME
|
|
253
271
|
)
|
|
@@ -258,14 +276,28 @@ class WebexWebsocketClient(object):
|
|
|
258
276
|
if self.proxies and "wss" in self.proxies:
|
|
259
277
|
logger.info(f"Using proxy for websocket connection: {self.proxies['wss']}")
|
|
260
278
|
proxy = Proxy.from_url(self.proxies["wss"])
|
|
261
|
-
connect = proxy_connect(
|
|
279
|
+
connect = proxy_connect(
|
|
280
|
+
ws_url,
|
|
281
|
+
ssl=ssl_context,
|
|
282
|
+
proxy=proxy,
|
|
283
|
+
**self._get_websocket_connect_kwargs(proxy_connect),
|
|
284
|
+
)
|
|
262
285
|
elif self.proxies and "https" in self.proxies:
|
|
263
286
|
logger.info(f"Using proxy for websocket connection: {self.proxies['https']}")
|
|
264
287
|
proxy = Proxy.from_url(self.proxies["https"])
|
|
265
|
-
connect = proxy_connect(
|
|
288
|
+
connect = proxy_connect(
|
|
289
|
+
ws_url,
|
|
290
|
+
ssl=ssl_context,
|
|
291
|
+
proxy=proxy,
|
|
292
|
+
**self._get_websocket_connect_kwargs(proxy_connect),
|
|
293
|
+
)
|
|
266
294
|
else:
|
|
267
295
|
logger.debug(f"Not using proxy for websocket connection.")
|
|
268
|
-
connect = websockets.connect(
|
|
296
|
+
connect = websockets.connect(
|
|
297
|
+
ws_url,
|
|
298
|
+
ssl=ssl_context,
|
|
299
|
+
**self._get_websocket_connect_kwargs(websockets.connect),
|
|
300
|
+
)
|
|
269
301
|
|
|
270
302
|
async with connect as _websocket:
|
|
271
303
|
self.websocket = _websocket
|
|
@@ -287,10 +319,11 @@ class WebexWebsocketClient(object):
|
|
|
287
319
|
asyncio.get_event_loop().run_until_complete(_connect_and_listen())
|
|
288
320
|
# If we get here, the connection was successful, so break out of the loop
|
|
289
321
|
break
|
|
290
|
-
except
|
|
291
|
-
|
|
322
|
+
except InvalidStatus as e:
|
|
323
|
+
status_code = getattr(e.response, "status_code", None)
|
|
324
|
+
logger.error(f"WebSocket handshake to {ws_url} failed with status {status_code}")
|
|
292
325
|
|
|
293
|
-
if
|
|
326
|
+
if status_code == 404:
|
|
294
327
|
current_404_retries += 1
|
|
295
328
|
if current_404_retries >= max_404_retries:
|
|
296
329
|
logger.error(f"Reached maximum retries ({max_404_retries}) for 404 errors. Giving up.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: webex_bot
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.12
|
|
4
4
|
Summary: Python package for a Webex Bot based on websockets.
|
|
5
5
|
Home-page: https://github.com/fbradyirl/webex_bot
|
|
6
6
|
Author: Finbarr Brady
|
|
@@ -18,29 +18,16 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Requires-Python: >=3.10
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: webexpythonsdk==2.0.
|
|
21
|
+
Requires-Dist: webexpythonsdk==2.0.5
|
|
22
22
|
Requires-Dist: coloredlogs
|
|
23
|
-
Requires-Dist: websockets==
|
|
23
|
+
Requires-Dist: websockets==16
|
|
24
24
|
Requires-Dist: backoff
|
|
25
25
|
Provides-Extra: proxy
|
|
26
|
-
Requires-Dist: websockets_proxy>=0.1.
|
|
27
|
-
Dynamic: author
|
|
28
|
-
Dynamic: author-email
|
|
29
|
-
Dynamic: classifier
|
|
30
|
-
Dynamic: description
|
|
31
|
-
Dynamic: description-content-type
|
|
32
|
-
Dynamic: home-page
|
|
33
|
-
Dynamic: keywords
|
|
34
|
-
Dynamic: license
|
|
35
|
-
Dynamic: license-file
|
|
36
|
-
Dynamic: provides-extra
|
|
37
|
-
Dynamic: requires-dist
|
|
38
|
-
Dynamic: requires-python
|
|
39
|
-
Dynamic: summary
|
|
26
|
+
Requires-Dist: websockets_proxy>=0.1.3; extra == "proxy"
|
|
40
27
|
|
|
41
28
|
# Introduction
|
|
42
29
|
|
|
43
|
-
[](https://pypi.python.org/pypi/webex_bot) [](https://github.com/fbradyirl/webex_bot/actions)
|
|
30
|
+
[](https://pypi.python.org/pypi/webex_bot) [](https://github.com/fbradyirl/webex_bot/actions) [](https://github.com/fbradyirl/webex_bot/actions/workflows/codeql.yml) [](https://codecov.io/gh/fbradyirl/webex_bot) [](https://github.com/fbradyirl/webex_bot/releases) [](https://pypi.python.org/pypi/webex_bot)
|
|
44
31
|
|
|
45
32
|
> [!IMPORTANT]
|
|
46
33
|
> This repository is only sporadically maintained. Breaking API changes will be maintained on a best efforts basis.
|
|
@@ -50,7 +37,7 @@ Dynamic: summary
|
|
|
50
37
|
> Bug reports unrelated to API changes may not get the attention you want.
|
|
51
38
|
|
|
52
39
|
|
|
53
|
-
By using this module, you can create a [Webex
|
|
40
|
+
By using this module, you can create a [Webex][5] messaging bot quickly in just a couple of lines of code.
|
|
54
41
|
|
|
55
42
|
This module does not require you to set up an ngrok tunnel to receive incoming messages when behind a firewall or
|
|
56
43
|
inside a LAN. This package instead uses a websocket to receive messages from the Webex cloud.
|
|
@@ -75,7 +62,7 @@ You can find a sample project, using OpenAI/ChatGPT with this library here: http
|
|
|
75
62
|
|
|
76
63
|
----
|
|
77
64
|
|
|
78
|
-
**
|
|
65
|
+
**Python 3.10, 3.11, and 3.12 are tested at this time.**
|
|
79
66
|
|
|
80
67
|
1. Install this module from pypi:
|
|
81
68
|
|
|
@@ -113,7 +100,7 @@ proxies = {
|
|
|
113
100
|
# Create a Bot Object
|
|
114
101
|
bot = WebexBot(teams_bot_token=os.getenv("WEBEX_ACCESS_TOKEN"),
|
|
115
102
|
approved_rooms=['06586d8d-6aad-4201-9a69-0bf9eeb5766e'],
|
|
116
|
-
bot_name="My
|
|
103
|
+
bot_name="My Webex Ops Bot",
|
|
117
104
|
include_demo_commands=True,
|
|
118
105
|
proxies=proxies)
|
|
119
106
|
|
|
@@ -464,6 +451,14 @@ bot = WebexBot(teams_bot_token=os.getenv("WEBEX_ACCESS_TOKEN")
|
|
|
464
451
|
|
|
465
452
|
* Allow flag to disable bot to bot check
|
|
466
453
|
|
|
454
|
+
### 1.1.6 (2026-Feb-02)
|
|
455
|
+
|
|
456
|
+
* Update dependancies.
|
|
457
|
+
* Expand test coverage and add CI matrix for Python 3.10–3.12
|
|
458
|
+
* Add coverage reporting and update contributor docs
|
|
459
|
+
* Refresh websocket status handling to avoid deprecations
|
|
460
|
+
|
|
461
|
+
|
|
467
462
|
[1]: https://github.com/aaugustin/websockets
|
|
468
463
|
|
|
469
464
|
[2]: https://github.com/WebexCommunity/WebexPythonSDK
|
|
@@ -12,7 +12,14 @@ docs/installation.rst
|
|
|
12
12
|
docs/make.bat
|
|
13
13
|
docs/usage.rst
|
|
14
14
|
tests/__init__.py
|
|
15
|
+
tests/conftest.py
|
|
16
|
+
tests/test_command_model.py
|
|
17
|
+
tests/test_commands.py
|
|
18
|
+
tests/test_exceptions.py
|
|
19
|
+
tests/test_formatting.py
|
|
20
|
+
tests/test_response.py
|
|
15
21
|
tests/test_webex_bot.py
|
|
22
|
+
tests/test_webex_websocket_client.py
|
|
16
23
|
webex_bot/__init__.py
|
|
17
24
|
webex_bot/exceptions.py
|
|
18
25
|
webex_bot/formatting.py
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
|
|
3
|
-
"""Tests for `webex_bot` package."""
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@pytest.fixture
|
|
9
|
-
def response():
|
|
10
|
-
"""Sample pytest fixture.
|
|
11
|
-
|
|
12
|
-
See more at: http://doc.pytest.org/en/latest/fixture.html
|
|
13
|
-
"""
|
|
14
|
-
# import requests
|
|
15
|
-
# return requests.get('https://github.com/audreyr/cookiecutter-pypackage')
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_content(response):
|
|
19
|
-
"""Sample pytest test function with the pytest fixture as an argument."""
|
|
20
|
-
# from bs4 import BeautifulSoup
|
|
21
|
-
# assert 'GitHub' in BeautifulSoup(response.content).title.string
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|