iembot 0.3.0__tar.gz → 0.3.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.
- {iembot-0.3.0 → iembot-0.3.1}/.github/workflows/build.yml +1 -1
- {iembot-0.3.0 → iembot-0.3.1}/CHANGELOG.md +5 -1
- {iembot-0.3.0 → iembot-0.3.1}/PKG-INFO +3 -3
- {iembot-0.3.0 → iembot-0.3.1}/README.md +1 -1
- {iembot-0.3.0 → iembot-0.3.1}/environment.yml +2 -0
- iembot-0.3.1/pip_requirements.txt +4 -0
- {iembot-0.3.0 → iembot-0.3.1}/pyproject.toml +2 -2
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/main.py +3 -5
- iembot-0.3.1/src/iembot/memcache.py +45 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/slack.py +42 -10
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot.egg-info/PKG-INFO +3 -3
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot.egg-info/SOURCES.txt +1 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot.egg-info/requires.txt +1 -1
- {iembot-0.3.0 → iembot-0.3.1}/tests/test_main.py +2 -6
- {iembot-0.3.0 → iembot-0.3.1}/tests/test_slack.py +41 -0
- iembot-0.3.0/pip_requirements.txt +0 -4
- {iembot-0.3.0 → iembot-0.3.1}/.deepsource.toml +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/.github/dependabot.yml +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/.github/workflows/codeql.yml +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/.github/workflows/etchosts.txt +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/.gitignore +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/.pre-commit-config.yaml +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/LICENSE +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/MANIFEST.in +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/conda_requirements.txt +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/conftest.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/run.sh +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/scripts/remove_twuser_oauth.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/settings_example.json +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/setup.cfg +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/__init__.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/atmosphere.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/bot.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/data/startrek +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/mastodon.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/msghandlers.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/twitter.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/types.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/util.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/webhooks.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/webservices.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot/xmpp.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot.egg-info/dependency_links.txt +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot.egg-info/entry_points.txt +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/src/iembot.egg-info/top_level.txt +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/tests/test_atmosphere.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/tests/test_bot.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/tests/test_init.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/tests/test_mastodon.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/tests/test_twitter.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/tests/test_util.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/tests/test_util_channels.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/tests/test_webservices.py +0 -0
- {iembot-0.3.0 → iembot-0.3.1}/tests/test_xmpp.py +0 -0
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
|
|
4
4
|
All notable changes to this library are documented in this file.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## **0.3.1** (4 Feb 2026)
|
|
7
7
|
|
|
8
8
|
### API Changes
|
|
9
9
|
|
|
10
|
+
- Replace `txyam2` dependency with `pymemcache` called from a thread (#140).
|
|
11
|
+
|
|
10
12
|
### New Features
|
|
11
13
|
|
|
12
14
|
### Bug Fixes
|
|
13
15
|
|
|
16
|
+
- Correct slack subscription logic when a new channel is joined (#136).
|
|
17
|
+
|
|
14
18
|
## **0.3.0** (4 Feb 2026)
|
|
15
19
|
|
|
16
20
|
First sane release of this code base onto pypi and hopefully conda-forge.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iembot
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A poorly written XMPP bot that does other things
|
|
5
5
|
Author-email: daryl herzmann <akrherz@gmail.com>
|
|
6
6
|
License: Apache
|
|
@@ -20,11 +20,11 @@ Requires-Dist: httpx
|
|
|
20
20
|
Requires-Dist: mastodon-py
|
|
21
21
|
Requires-Dist: psycopg
|
|
22
22
|
Requires-Dist: pyiem>=1.26
|
|
23
|
+
Requires-Dist: pymemcache
|
|
23
24
|
Requires-Dist: python-twitter
|
|
24
25
|
Requires-Dist: requests
|
|
25
26
|
Requires-Dist: service-identity
|
|
26
27
|
Requires-Dist: twisted>=18.4
|
|
27
|
-
Requires-Dist: txyam2
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: cartopy; extra == "dev"
|
|
30
30
|
Requires-Dist: codecov; extra == "dev"
|
|
@@ -41,7 +41,7 @@ Dynamic: license-file
|
|
|
41
41
|
[](https://landscape.io/github/akrherz/iembot/master)
|
|
42
42
|
|
|
43
43
|
I am a XMPP client with limited bot capabilities. In general, I am a message
|
|
44
|
-
router more than anything.
|
|
44
|
+
router more than anything. Currently requires python 3.11+.
|
|
45
45
|
|
|
46
46
|
## Run iembot in development
|
|
47
47
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://landscape.io/github/akrherz/iembot/master)
|
|
6
6
|
|
|
7
7
|
I am a XMPP client with limited bot capabilities. In general, I am a message
|
|
8
|
-
router more than anything.
|
|
8
|
+
router more than anything. Currently requires python 3.11+.
|
|
9
9
|
|
|
10
10
|
## Run iembot in development
|
|
11
11
|
|
|
@@ -28,11 +28,11 @@ dependencies = [
|
|
|
28
28
|
"mastodon-py",
|
|
29
29
|
"psycopg",
|
|
30
30
|
"pyiem>=1.26",
|
|
31
|
+
"pymemcache",
|
|
31
32
|
"python-twitter",
|
|
32
33
|
"requests",
|
|
33
34
|
"service-identity",
|
|
34
35
|
"twisted>=18.4",
|
|
35
|
-
"txyam2",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
optional-dependencies.dev = [
|
|
@@ -57,7 +57,7 @@ include-package-data = true
|
|
|
57
57
|
version_scheme = "post-release"
|
|
58
58
|
|
|
59
59
|
[tool.ruff]
|
|
60
|
-
target-version = "
|
|
60
|
+
target-version = "py311"
|
|
61
61
|
|
|
62
62
|
line-length = 79
|
|
63
63
|
lint.select = [
|
|
@@ -18,10 +18,10 @@ from twisted.internet import reactor
|
|
|
18
18
|
from twisted.python import log
|
|
19
19
|
from twisted.python.logfile import DailyLogFile
|
|
20
20
|
from twisted.web import server
|
|
21
|
-
from txyam.client import YamClient
|
|
22
21
|
|
|
23
22
|
from iembot import webservices
|
|
24
23
|
from iembot.bot import JabberClient
|
|
24
|
+
from iembot.memcache import build_memcache_client
|
|
25
25
|
from iembot.msghandlers import register_handler
|
|
26
26
|
|
|
27
27
|
|
|
@@ -43,10 +43,8 @@ def _build_dbpool(config: dict) -> adbapi.ConnectionPool:
|
|
|
43
43
|
)
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def _build_memcache_client(memcache: str)
|
|
47
|
-
|
|
48
|
-
client.connect()
|
|
49
|
-
return client
|
|
46
|
+
def _build_memcache_client(memcache: str):
|
|
47
|
+
return build_memcache_client(memcache)
|
|
50
48
|
|
|
51
49
|
|
|
52
50
|
def _start_logging(logfile: str | None) -> None:
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Memcache client helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pymemcache.client.base import Client as MemcacheClient
|
|
6
|
+
from twisted.internet import threads
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_memcache_addr(memcache: str) -> tuple[str, int]:
|
|
10
|
+
"""."""
|
|
11
|
+
memcache = memcache.removeprefix("tcp:")
|
|
12
|
+
host = memcache
|
|
13
|
+
port = 11211
|
|
14
|
+
if ":" in memcache:
|
|
15
|
+
host, port_str = memcache.rsplit(":", 1)
|
|
16
|
+
try:
|
|
17
|
+
port = int(port_str)
|
|
18
|
+
except ValueError:
|
|
19
|
+
host = memcache
|
|
20
|
+
if not host:
|
|
21
|
+
host = "localhost"
|
|
22
|
+
return host, port
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ThreadedMemcacheClient:
|
|
26
|
+
def __init__(self, host: str, port: int) -> None:
|
|
27
|
+
self._client = MemcacheClient((host, port))
|
|
28
|
+
|
|
29
|
+
def get(self, key: bytes):
|
|
30
|
+
def _get():
|
|
31
|
+
data = self._client.get(key)
|
|
32
|
+
return (0, data)
|
|
33
|
+
|
|
34
|
+
return threads.deferToThread(_get)
|
|
35
|
+
|
|
36
|
+
def disconnect(self) -> None:
|
|
37
|
+
try:
|
|
38
|
+
self._client.close()
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_memcache_client(memcache: str) -> ThreadedMemcacheClient:
|
|
44
|
+
host, port = parse_memcache_addr(memcache)
|
|
45
|
+
return ThreadedMemcacheClient(host, port)
|
|
@@ -91,22 +91,51 @@ class SlackSubscribeChannel(resource.Resource):
|
|
|
91
91
|
def store_slack_subscription(
|
|
92
92
|
self,
|
|
93
93
|
txn,
|
|
94
|
-
team_id,
|
|
95
|
-
channel_id,
|
|
96
|
-
subkey,
|
|
94
|
+
team_id: str,
|
|
95
|
+
channel_id: str,
|
|
96
|
+
subkey: str,
|
|
97
97
|
):
|
|
98
98
|
"""write to database."""
|
|
99
|
+
log.msg(
|
|
100
|
+
f"Handing slack subscription for T:`{team_id}` TC:`{channel_id}` "
|
|
101
|
+
f"C:`{subkey}`"
|
|
102
|
+
)
|
|
103
|
+
# Step 1, ensure that the slack channel exists
|
|
104
|
+
txn.execute(
|
|
105
|
+
"""
|
|
106
|
+
select iembot_account_id from iembot_slack_team_channels
|
|
107
|
+
where team_id = %s and channel_id = %s
|
|
108
|
+
""",
|
|
109
|
+
(team_id, channel_id),
|
|
110
|
+
)
|
|
111
|
+
res = txn.fetchall()
|
|
112
|
+
if not res:
|
|
113
|
+
log.msg(
|
|
114
|
+
f"Slack Team: {team_id} Channel: {channel_id} does not exist "
|
|
115
|
+
"creating..."
|
|
116
|
+
)
|
|
117
|
+
txn.execute(
|
|
118
|
+
"""
|
|
119
|
+
insert into iembot_slack_team_channels
|
|
120
|
+
(iembot_account_id, team_id, channel_id)
|
|
121
|
+
values (
|
|
122
|
+
(select create_iembot_account('slack')), %s, %s)
|
|
123
|
+
returning iembot_account_id
|
|
124
|
+
""",
|
|
125
|
+
(team_id, channel_id),
|
|
126
|
+
)
|
|
127
|
+
res = txn.fetchall()
|
|
128
|
+
iembot_account_id = res[0]["iembot_account_id"]
|
|
99
129
|
txn.execute(
|
|
100
130
|
"""
|
|
101
131
|
insert into iembot_subscriptions(iembot_account_id, channel_id)
|
|
102
132
|
values (
|
|
103
|
-
|
|
104
|
-
where team_id = %s and channel_id = %s),
|
|
133
|
+
%s,
|
|
105
134
|
(select get_or_create_iembot_channel_id(%s))
|
|
106
135
|
)
|
|
107
136
|
on conflict do nothing
|
|
108
137
|
""",
|
|
109
|
-
(
|
|
138
|
+
(iembot_account_id, subkey),
|
|
110
139
|
)
|
|
111
140
|
|
|
112
141
|
def render(self, request):
|
|
@@ -120,11 +149,14 @@ class SlackSubscribeChannel(resource.Resource):
|
|
|
120
149
|
channel_id,
|
|
121
150
|
subkey,
|
|
122
151
|
)
|
|
152
|
+
defer.addErrback(
|
|
153
|
+
lambda _: request.write(b"Error processing subscription")
|
|
154
|
+
)
|
|
123
155
|
defer.addCallback(
|
|
124
156
|
lambda _: request.write(f"Subscribed to {subkey}".encode("ascii"))
|
|
125
157
|
)
|
|
126
|
-
defer.
|
|
127
|
-
defer.addCallback(self.iembot.load_slack)
|
|
158
|
+
defer.addBoth(lambda _: request.finish())
|
|
159
|
+
defer.addCallback(lambda _: self.iembot.load_slack())
|
|
128
160
|
|
|
129
161
|
return NOT_DONE_YET
|
|
130
162
|
|
|
@@ -173,7 +205,7 @@ class SlackUnsubscribeChannel(resource.Resource):
|
|
|
173
205
|
)
|
|
174
206
|
)
|
|
175
207
|
defer.addCallback(lambda _: request.finish())
|
|
176
|
-
defer.addCallback(self.iembot.load_slack)
|
|
208
|
+
defer.addCallback(lambda _: self.iembot.load_slack())
|
|
177
209
|
|
|
178
210
|
return NOT_DONE_YET
|
|
179
211
|
|
|
@@ -249,7 +281,7 @@ class SlackOauthChannel(resource.Resource):
|
|
|
249
281
|
self.do_request_in_thread, data
|
|
250
282
|
)
|
|
251
283
|
defer.addCallback(self._cb_oauth, request)
|
|
252
|
-
defer.addCallback(self.iembot.load_slack)
|
|
284
|
+
defer.addCallback(lambda _: self.iembot.load_slack())
|
|
253
285
|
defer.addErrback(self._eb_oauth, request)
|
|
254
286
|
return NOT_DONE_YET
|
|
255
287
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iembot
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A poorly written XMPP bot that does other things
|
|
5
5
|
Author-email: daryl herzmann <akrherz@gmail.com>
|
|
6
6
|
License: Apache
|
|
@@ -20,11 +20,11 @@ Requires-Dist: httpx
|
|
|
20
20
|
Requires-Dist: mastodon-py
|
|
21
21
|
Requires-Dist: psycopg
|
|
22
22
|
Requires-Dist: pyiem>=1.26
|
|
23
|
+
Requires-Dist: pymemcache
|
|
23
24
|
Requires-Dist: python-twitter
|
|
24
25
|
Requires-Dist: requests
|
|
25
26
|
Requires-Dist: service-identity
|
|
26
27
|
Requires-Dist: twisted>=18.4
|
|
27
|
-
Requires-Dist: txyam2
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: cartopy; extra == "dev"
|
|
30
30
|
Requires-Dist: codecov; extra == "dev"
|
|
@@ -41,7 +41,7 @@ Dynamic: license-file
|
|
|
41
41
|
[](https://landscape.io/github/akrherz/iembot/master)
|
|
42
42
|
|
|
43
43
|
I am a XMPP client with limited bot capabilities. In general, I am a message
|
|
44
|
-
router more than anything.
|
|
44
|
+
router more than anything. Currently requires python 3.11+.
|
|
45
45
|
|
|
46
46
|
## Run iembot in development
|
|
47
47
|
|
|
@@ -80,13 +80,9 @@ def test_build_dbpool(monkeypatch):
|
|
|
80
80
|
assert main_mod._build_dbpool(settings) is fakepool
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
def test_build_memcache_client(
|
|
84
|
-
fake = mock.Mock()
|
|
85
|
-
monkeypatch.setattr(main_mod, "YamClient", lambda _reactor, _addrs: fake)
|
|
86
|
-
fake.connect = mock.Mock()
|
|
83
|
+
def test_build_memcache_client():
|
|
87
84
|
result = main_mod._build_memcache_client("tcp:foo:1234")
|
|
88
|
-
assert result
|
|
89
|
-
fake.connect.assert_called_once()
|
|
85
|
+
assert result
|
|
90
86
|
|
|
91
87
|
|
|
92
88
|
def test_fatal_stops_reactor(monkeypatch):
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
from unittest import mock
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
+
from twisted.internet import defer
|
|
7
|
+
from twisted.web.server import NOT_DONE_YET
|
|
8
|
+
from twisted.web.test.requesthelper import DummyRequest
|
|
6
9
|
|
|
7
10
|
from iembot.slack import (
|
|
8
11
|
SlackInstallChannel,
|
|
@@ -14,6 +17,32 @@ from iembot.slack import (
|
|
|
14
17
|
from iembot.types import JabberClient
|
|
15
18
|
|
|
16
19
|
|
|
20
|
+
@defer.inlineCallbacks
|
|
21
|
+
def test_subscribe_channel_render(bot: JabberClient):
|
|
22
|
+
"""Test that we can run the render workflow."""
|
|
23
|
+
ss = SlackSubscribeChannel(bot)
|
|
24
|
+
request = DummyRequest([])
|
|
25
|
+
request.args = {
|
|
26
|
+
b"team_id": [b"T12345"],
|
|
27
|
+
b"channel_id": [b"C67890"],
|
|
28
|
+
b"text": [b"AFDDMX"],
|
|
29
|
+
}
|
|
30
|
+
bot.dbpool.runInteraction = mock.Mock(return_value=defer.succeed(None))
|
|
31
|
+
result = ss.render(request)
|
|
32
|
+
assert result == NOT_DONE_YET
|
|
33
|
+
yield defer.succeed(None)
|
|
34
|
+
assert b"Subscribed" in b"".join(request.written)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_install_channel_render(bot: JabberClient):
|
|
38
|
+
"""See if we can render the install channel."""
|
|
39
|
+
ss = SlackInstallChannel(bot)
|
|
40
|
+
request = DummyRequest([b""])
|
|
41
|
+
result = ss.render(request)
|
|
42
|
+
assert result is not None
|
|
43
|
+
assert request.responseCode == 302
|
|
44
|
+
|
|
45
|
+
|
|
17
46
|
@pytest.mark.parametrize("database", ["iembot"])
|
|
18
47
|
def test_subscribe_channel(dbcursor, bot):
|
|
19
48
|
"""See if we can subscribe?"""
|
|
@@ -26,6 +55,18 @@ def test_subscribe_channel(dbcursor, bot):
|
|
|
26
55
|
)
|
|
27
56
|
|
|
28
57
|
|
|
58
|
+
@pytest.mark.parametrize("database", ["iembot"])
|
|
59
|
+
def test_gh136_slack_first_channel_subscription(dbcursor, bot):
|
|
60
|
+
"""See if subscribe works when we need to create a slack channel."""
|
|
61
|
+
ss = SlackSubscribeChannel(bot)
|
|
62
|
+
ss.store_slack_subscription(
|
|
63
|
+
dbcursor,
|
|
64
|
+
"TSS", # needs to exist in iembot_slack_teams
|
|
65
|
+
"CSD2", # new channel on slack unknown to the bot so far
|
|
66
|
+
"AFDDMX",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
29
70
|
@pytest.mark.parametrize("database", ["iembot"])
|
|
30
71
|
def test_load_from_db(dbcursor, bot: JabberClient):
|
|
31
72
|
"""Exercise."""
|
|
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
|
|
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
|