hivemind-http-protocol 0.0.2a2__tar.gz → 0.0.3a1__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.
Files changed (22) hide show
  1. hivemind_http_protocol-0.0.3a1/PKG-INFO +24 -0
  2. hivemind_http_protocol-0.0.3a1/README.md +121 -0
  3. {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/hivemind_http_protocol/__init__.py +1 -3
  4. hivemind_http_protocol-0.0.3a1/hivemind_http_protocol/version.py +8 -0
  5. hivemind_http_protocol-0.0.3a1/hivemind_http_protocol.egg-info/PKG-INFO +24 -0
  6. {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/hivemind_http_protocol.egg-info/SOURCES.txt +5 -2
  7. hivemind_http_protocol-0.0.3a1/hivemind_http_protocol.egg-info/requires.txt +15 -0
  8. hivemind_http_protocol-0.0.3a1/pyproject.toml +45 -0
  9. hivemind_http_protocol-0.0.3a1/tests/test_decode_auth.py +61 -0
  10. hivemind_http_protocol-0.0.3a1/tests/test_handlers.py +547 -0
  11. hivemind_http_protocol-0.0.3a1/tests/test_protocol_unit.py +138 -0
  12. hivemind_http_protocol-0.0.2a2/PKG-INFO +0 -20
  13. hivemind_http_protocol-0.0.2a2/README.md +0 -158
  14. hivemind_http_protocol-0.0.2a2/hivemind_http_protocol/version.py +0 -6
  15. hivemind_http_protocol-0.0.2a2/hivemind_http_protocol.egg-info/PKG-INFO +0 -20
  16. hivemind_http_protocol-0.0.2a2/hivemind_http_protocol.egg-info/requires.txt +0 -4
  17. hivemind_http_protocol-0.0.2a2/setup.py +0 -55
  18. {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/LICENSE.md +0 -0
  19. {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/hivemind_http_protocol.egg-info/dependency_links.txt +0 -0
  20. {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/hivemind_http_protocol.egg-info/entry_points.txt +0 -0
  21. {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/hivemind_http_protocol.egg-info/top_level.txt +0 -0
  22. {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/setup.cfg +0 -0
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: hivemind-http-protocol
3
+ Version: 0.0.3a1
4
+ Summary: http network protocol for hivemind-core
5
+ Author-email: jarbasAi <jarbasai@mailfence.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/JarbasHiveMind/hivemind-http-protocol
8
+ Requires-Python: >=3.10
9
+ License-File: LICENSE.md
10
+ Requires-Dist: tornado
11
+ Requires-Dist: hivemind-plugin-manager
12
+ Requires-Dist: poorman_handshake>=0.1.0
13
+ Requires-Dist: pyOpenSSL
14
+ Requires-Dist: pybase64
15
+ Requires-Dist: hivemind-bus-client
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest; extra == "test"
18
+ Requires-Dist: pytest-cov; extra == "test"
19
+ Requires-Dist: hivemind-core; extra == "test"
20
+ Requires-Dist: hivemind-bus-client; extra == "test"
21
+ Requires-Dist: ovos-bus-client; extra == "test"
22
+ Requires-Dist: ovos-utils; extra == "test"
23
+ Requires-Dist: hivescope; extra == "test"
24
+ Dynamic: license-file
@@ -0,0 +1,121 @@
1
+ # hivemind-http-protocol
2
+
3
+ REST/HTTP transport plugin for [hivemind-core](https://github.com/JarbasHiveMind/HiveMind-core).
4
+
5
+ An alternative to the default WebSocket transport. Clients use HTTP polling (POST to send,
6
+ GET to receive) instead of a persistent WebSocket connection. Suitable for environments where
7
+ long-lived TCP connections are not possible (firewalls, IoT gateways, HTTP-only proxies).
8
+
9
+ ## Where it fits
10
+
11
+ ```
12
+ hivemind-core
13
+ └── hivemind-plugin-manager (NetworkProtocolFactory loads plugins by entry-point)
14
+ └── hivemind-http-protocol ← this repo
15
+ └── Tornado HTTP server (REST endpoints)
16
+ ```
17
+
18
+ The plugin registers under the `hivemind.network.protocol` entry-point group as
19
+ `hivemind-http-plugin`. It can run alongside the WebSocket transport if both are listed
20
+ in the `network_protocol` config.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install hivemind-http-protocol
26
+ ```
27
+
28
+ ## Quickstart
29
+
30
+ Add to `~/.config/hivemind-core/server.json`:
31
+
32
+ ```json
33
+ {
34
+ "network_protocol": {
35
+ "module": "hivemind-http-plugin",
36
+ "hivemind-http-plugin": {
37
+ "host": "0.0.0.0",
38
+ "port": 5679
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ Start hivemind-core:
45
+
46
+ ```bash
47
+ hivemind-core listen
48
+ ```
49
+
50
+ ### Running alongside WebSocket
51
+
52
+ Both transports can run at the same time by configuring them both:
53
+
54
+ ```json
55
+ {
56
+ "network_protocol": {
57
+ "hivemind-websocket-plugin": {
58
+ "host": "0.0.0.0",
59
+ "port": 5678
60
+ },
61
+ "hivemind-http-plugin": {
62
+ "host": "0.0.0.0",
63
+ "port": 5679
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ ### Python client example
70
+
71
+ ```python
72
+ from hivemind_bus_client.http_client import HiveMindHTTPClient, BinaryDataCallbacks
73
+ from hivemind_bus_client.message import HiveMessage, HiveMessageType
74
+ from ovos_bus_client.message import Message
75
+
76
+
77
+ class MyBinaryCallbacks(BinaryDataCallbacks):
78
+ def handle_receive_tts(self, bin_data: bytes, utterance: str,
79
+ lang: str, file_name: str):
80
+ print(f"received {len(bin_data)} bytes of TTS for: {utterance}")
81
+
82
+
83
+ client = HiveMindHTTPClient(
84
+ host="http://localhost",
85
+ port=5679,
86
+ bin_callbacks=MyBinaryCallbacks(),
87
+ )
88
+ client.emit(HiveMessage(HiveMessageType.BUS,
89
+ Message("speak:synth", {"utterance": "hello world"})))
90
+ ```
91
+
92
+ ## Configuration reference
93
+
94
+ | Key | Default | Description |
95
+ |---|---|---|
96
+ | `host` | `0.0.0.0` | Bind address. |
97
+ | `port` | `5679` | Listen port. |
98
+ | `ssl` | `false` | Enable TLS. |
99
+ | `cert_dir` | `$XDG_DATA_HOME/hivemind` | Directory for TLS cert/key files. |
100
+ | `cert_name` | `hivemind` | Base filename for cert and key. |
101
+
102
+ ## REST API
103
+
104
+ Authentication uses an HTTP `authorization` parameter (not a header) containing
105
+ a Base64-encoded `useragent:access_key` string.
106
+
107
+ | Endpoint | Method | Description |
108
+ |---|---|---|
109
+ | `/connect` | POST | Register a client session. Parameters: `authorization`. |
110
+ | `/disconnect` | POST | Remove a client session. Parameters: `authorization`. |
111
+ | `/send_message` | POST | Send a HiveMessage. Parameters: `authorization`, `message`. |
112
+ | `/get_messages` | GET | Poll for pending text messages. Parameters: `authorization`. |
113
+ | `/get_binary_messages` | GET | Poll for pending binary messages (Base64-encoded). Parameters: `authorization`. |
114
+
115
+ See [docs/api.md](docs/api.md) for full endpoint documentation.
116
+
117
+ ## Docs
118
+
119
+ - [docs/api.md](docs/api.md) — REST endpoint reference
120
+ - [docs/architecture.md](docs/architecture.md) — handler lifecycle, polling model, TLS
121
+ - [docs/operations.md](docs/operations.md) — authoring a transport plugin
@@ -117,8 +117,7 @@ class HiveMindHttpProtocol(NetworkProtocol):
117
117
  cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
118
118
  cert.set_issuer(cert.get_subject())
119
119
  cert.set_pubkey(k)
120
- # TODO: Don't use SHA1
121
- cert.sign(k, "sha1")
120
+ cert.sign(k, "sha256")
122
121
 
123
122
  open(cert_path, "wb").write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
124
123
  open(key_path, "wb").write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
@@ -178,7 +177,6 @@ class HiveMindHttpHandler(web.RequestHandler):
178
177
 
179
178
  client.name = f"{useragent}::{user.client_id}::{user.name}"
180
179
  client.crypto_key = user.crypto_key
181
- client.msg_blacklist = user.message_blacklist or []
182
180
  client.skill_blacklist = user.skill_blacklist or []
183
181
  client.intent_blacklist = user.intent_blacklist or []
184
182
  client.allowed_types = user.allowed_types
@@ -0,0 +1,8 @@
1
+ # START_VERSION_BLOCK
2
+ VERSION_MAJOR = 0
3
+ VERSION_MINOR = 0
4
+ VERSION_BUILD = 3
5
+ VERSION_ALPHA = 1
6
+ # END_VERSION_BLOCK
7
+
8
+ __version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}" + (f"a{VERSION_ALPHA}" if VERSION_ALPHA else "")
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: hivemind-http-protocol
3
+ Version: 0.0.3a1
4
+ Summary: http network protocol for hivemind-core
5
+ Author-email: jarbasAi <jarbasai@mailfence.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/JarbasHiveMind/hivemind-http-protocol
8
+ Requires-Python: >=3.10
9
+ License-File: LICENSE.md
10
+ Requires-Dist: tornado
11
+ Requires-Dist: hivemind-plugin-manager
12
+ Requires-Dist: poorman_handshake>=0.1.0
13
+ Requires-Dist: pyOpenSSL
14
+ Requires-Dist: pybase64
15
+ Requires-Dist: hivemind-bus-client
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest; extra == "test"
18
+ Requires-Dist: pytest-cov; extra == "test"
19
+ Requires-Dist: hivemind-core; extra == "test"
20
+ Requires-Dist: hivemind-bus-client; extra == "test"
21
+ Requires-Dist: ovos-bus-client; extra == "test"
22
+ Requires-Dist: ovos-utils; extra == "test"
23
+ Requires-Dist: hivescope; extra == "test"
24
+ Dynamic: license-file
@@ -1,6 +1,6 @@
1
1
  LICENSE.md
2
2
  README.md
3
- setup.py
3
+ pyproject.toml
4
4
  hivemind_http_protocol/__init__.py
5
5
  hivemind_http_protocol/version.py
6
6
  hivemind_http_protocol.egg-info/PKG-INFO
@@ -8,4 +8,7 @@ hivemind_http_protocol.egg-info/SOURCES.txt
8
8
  hivemind_http_protocol.egg-info/dependency_links.txt
9
9
  hivemind_http_protocol.egg-info/entry_points.txt
10
10
  hivemind_http_protocol.egg-info/requires.txt
11
- hivemind_http_protocol.egg-info/top_level.txt
11
+ hivemind_http_protocol.egg-info/top_level.txt
12
+ tests/test_decode_auth.py
13
+ tests/test_handlers.py
14
+ tests/test_protocol_unit.py
@@ -0,0 +1,15 @@
1
+ tornado
2
+ hivemind-plugin-manager
3
+ poorman_handshake>=0.1.0
4
+ pyOpenSSL
5
+ pybase64
6
+ hivemind-bus-client
7
+
8
+ [test]
9
+ pytest
10
+ pytest-cov
11
+ hivemind-core
12
+ hivemind-bus-client
13
+ ovos-bus-client
14
+ ovos-utils
15
+ hivescope
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hivemind-http-protocol"
7
+ dynamic = ["version"]
8
+ description = "http network protocol for hivemind-core"
9
+ license = "Apache-2.0"
10
+ authors = [
11
+ { name = "jarbasAi", email = "jarbasai@mailfence.com" }
12
+ ]
13
+ requires-python = ">=3.10"
14
+ dependencies = [
15
+ "tornado",
16
+ "hivemind-plugin-manager",
17
+ "poorman_handshake>=0.1.0",
18
+ "pyOpenSSL",
19
+ "pybase64",
20
+ "hivemind-bus-client",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ test = [
25
+ "pytest",
26
+ "pytest-cov",
27
+ "hivemind-core",
28
+ "hivemind-bus-client",
29
+ "ovos-bus-client",
30
+ "ovos-utils",
31
+ "hivescope",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/JarbasHiveMind/hivemind-http-protocol"
36
+
37
+ [project.entry-points."hivemind.network.protocol"]
38
+ "hivemind-http-plugin" = "hivemind_http_protocol:HiveMindHttpProtocol"
39
+
40
+ [tool.setuptools.dynamic]
41
+ version = { attr = "hivemind_http_protocol.version.__version__" }
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["."]
45
+ include = ["hivemind_http_protocol*"]
@@ -0,0 +1,61 @@
1
+ """Unit tests for HiveMindHttpHandler.decode_auth.
2
+
3
+ decode_auth parses a base64-encoded "useragent:key" string passed as the
4
+ `authorization` query parameter. Valid input splits on the first colon;
5
+ everything to the left is the useragent, everything to the right is the
6
+ API key. When the parameter is absent or malformed the method returns
7
+ (None, None) with a 400 status set.
8
+ """
9
+ import pybase64
10
+ import pytest
11
+ from unittest.mock import patch, MagicMock
12
+
13
+ from hivemind_http_protocol import HiveMindHttpHandler
14
+
15
+
16
+ def _encode(s: str) -> str:
17
+ return pybase64.b64encode(s.encode("utf-8")).decode("ascii")
18
+
19
+
20
+ def _make_handler(auth_value: str):
21
+ """Return a HiveMindHttpHandler instance whose get_argument() returns auth_value."""
22
+ handler = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
23
+ handler.get_argument = MagicMock(return_value=auth_value)
24
+ handler.set_status = MagicMock()
25
+ return handler
26
+
27
+
28
+ class TestDecodeAuthValid:
29
+ def test_simple_useragent_and_key(self):
30
+ h = _make_handler(_encode("myagent:secretkey"))
31
+ useragent, key = h.decode_auth()
32
+ assert useragent == "myagent"
33
+ assert key == "secretkey"
34
+
35
+ def test_key_with_colon_splits_on_first(self):
36
+ # The current implementation splits on all colons and returns
37
+ # a list — the caller is expected to consume index 0 and 1.
38
+ h = _make_handler(_encode("agent:secretkey"))
39
+ result = h.decode_auth()
40
+ assert result[0] == "agent"
41
+ assert result[1] == "secretkey"
42
+
43
+ def test_unicode_values(self):
44
+ h = _make_handler(_encode("ünïcødé:kéy"))
45
+ useragent, key = h.decode_auth()
46
+ assert useragent == "ünïcødé"
47
+ assert key == "kéy"
48
+
49
+
50
+ class TestDecodeAuthMissing:
51
+ def test_empty_auth_returns_none_pair(self):
52
+ h = _make_handler("")
53
+ result = h.decode_auth()
54
+ assert result == (None, None)
55
+ h.set_status.assert_called_once_with(400)
56
+
57
+ def test_none_value_treated_as_missing(self):
58
+ h = _make_handler(None)
59
+ result = h.decode_auth()
60
+ assert result == (None, None)
61
+ h.set_status.assert_called_once_with(400)
@@ -0,0 +1,547 @@
1
+ """Unit tests for HiveMindHttpHandler and all derived handler classes.
2
+
3
+ Covers lines 147-347 (get_client, ConnectHandler, DisconnectHandler,
4
+ SendMessageHandler, GetMessagesHandler, GetBinMessagesHandler).
5
+
6
+ Uses a real MasterNode from hivescope (so HiveMindClientConnection gets a
7
+ valid identity/private-key path) but patches the DB and protocol callbacks
8
+ to keep tests deterministic and side-effect-free.
9
+ """
10
+ import asyncio
11
+ from collections import defaultdict
12
+ from queue import Queue
13
+ from unittest.mock import MagicMock, patch, call
14
+
15
+ import pybase64
16
+ import pytest
17
+
18
+ from hivemind_http_protocol import (
19
+ HiveMindHttpHandler,
20
+ ConnectHandler,
21
+ DisconnectHandler,
22
+ SendMessageHandler,
23
+ GetMessagesHandler,
24
+ GetBinMessagesHandler,
25
+ )
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Module-level fixtures
30
+ # ---------------------------------------------------------------------------
31
+
32
+ @pytest.fixture(scope="module")
33
+ def master():
34
+ """One MasterNode reused across all tests in this module."""
35
+ hivescope = pytest.importorskip("hivescope")
36
+ from hivescope.node import MasterNode
37
+ return MasterNode.create("HH", require_crypto=False, handshake_enabled=True)
38
+
39
+
40
+ def _encode(s: str) -> str:
41
+ return pybase64.b64encode(s.encode("utf-8")).decode("ascii")
42
+
43
+
44
+ def _make_user(
45
+ *,
46
+ client_id=42,
47
+ name="testclient",
48
+ crypto_key="cryptokey",
49
+ skill_blacklist=None,
50
+ intent_blacklist=None,
51
+ allowed_types=None,
52
+ can_propagate=True,
53
+ can_escalate=True,
54
+ is_admin=False,
55
+ password=None,
56
+ ):
57
+ u = MagicMock()
58
+ u.client_id = client_id
59
+ u.name = name
60
+ u.crypto_key = crypto_key
61
+ u.skill_blacklist = skill_blacklist
62
+ u.intent_blacklist = intent_blacklist
63
+ u.allowed_types = allowed_types or []
64
+ u.can_propagate = can_propagate
65
+ u.can_escalate = can_escalate
66
+ u.is_admin = is_admin
67
+ u.password = password
68
+ return u
69
+
70
+
71
+ def _clean_class_state():
72
+ """Reset class-level dicts to prevent test bleed."""
73
+ HiveMindHttpHandler.clients = {}
74
+ HiveMindHttpHandler.undelivered = defaultdict(Queue)
75
+ HiveMindHttpHandler.undelivered_bin = defaultdict(Queue)
76
+
77
+
78
+ def _make_handler(cls, auth_value, proto, *, extra_get_arg=None):
79
+ """Return a handler instance bypassing Tornado's __init__."""
80
+ _clean_class_state()
81
+ cls.hm_protocol = proto
82
+
83
+ h = cls.__new__(cls)
84
+ h.set_status = MagicMock()
85
+ h.write = MagicMock()
86
+
87
+ def _get_arg(name, default=""):
88
+ if name == "authorization":
89
+ return auth_value
90
+ if extra_get_arg and name in extra_get_arg:
91
+ return extra_get_arg[name]
92
+ return default
93
+
94
+ h.get_argument = MagicMock(side_effect=_get_arg)
95
+ return h
96
+
97
+
98
+ def _run(coro):
99
+ return asyncio.get_event_loop().run_until_complete(coro)
100
+
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # get_client
104
+ # ---------------------------------------------------------------------------
105
+
106
+ class TestGetClient:
107
+ def test_returns_cached_client(self, master):
108
+ proto = master.hm_protocol
109
+ _clean_class_state()
110
+ HiveMindHttpHandler.hm_protocol = proto
111
+
112
+ existing = MagicMock()
113
+ HiveMindHttpHandler.clients["mykey"] = existing
114
+
115
+ h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
116
+ result = h.get_client("agent", "mykey", cache=True)
117
+ assert result is existing
118
+
119
+ def test_invalid_key_returns_none(self, master):
120
+ proto = master.hm_protocol
121
+ _clean_class_state()
122
+ HiveMindHttpHandler.hm_protocol = proto
123
+
124
+ with patch.object(proto.db, "get_client_by_api_key", return_value=None):
125
+ with patch.object(proto, "handle_invalid_key_connected") as mock_invalid:
126
+ h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
127
+ result = h.get_client("agent", "badkey", cache=False)
128
+ assert result is None
129
+ mock_invalid.assert_called_once()
130
+
131
+ def test_valid_key_builds_client(self, master):
132
+ proto = master.hm_protocol
133
+ _clean_class_state()
134
+ HiveMindHttpHandler.hm_protocol = proto
135
+
136
+ user = _make_user()
137
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
138
+ h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
139
+ result = h.get_client("agent", "validkey", cache=False)
140
+ assert result is not None
141
+ assert result.crypto_key == user.crypto_key
142
+ assert result.is_admin == user.is_admin
143
+ assert result.can_propagate == user.can_propagate
144
+
145
+ def test_valid_key_caches_client(self, master):
146
+ proto = master.hm_protocol
147
+ _clean_class_state()
148
+ HiveMindHttpHandler.hm_protocol = proto
149
+
150
+ user = _make_user()
151
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
152
+ h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
153
+ result = h.get_client("agent", "cachekey", cache=True)
154
+ assert HiveMindHttpHandler.clients.get("cachekey") is result
155
+
156
+ def test_no_cache_does_not_store(self, master):
157
+ proto = master.hm_protocol
158
+ _clean_class_state()
159
+ HiveMindHttpHandler.hm_protocol = proto
160
+
161
+ user = _make_user()
162
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
163
+ h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
164
+ h.get_client("agent", "nocachekey", cache=False)
165
+ assert "nocachekey" not in HiveMindHttpHandler.clients
166
+
167
+ def test_user_with_password_sets_handshake(self, master):
168
+ proto = master.hm_protocol
169
+ _clean_class_state()
170
+ HiveMindHttpHandler.hm_protocol = proto
171
+
172
+ user = _make_user(password="hunter2")
173
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
174
+ h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
175
+ result = h.get_client("agent", "pwdkey", cache=False)
176
+ assert result is not None
177
+ assert result.pswd_handshake is not None
178
+
179
+ def test_user_without_password_no_handshake(self, master):
180
+ proto = master.hm_protocol
181
+ _clean_class_state()
182
+ HiveMindHttpHandler.hm_protocol = proto
183
+
184
+ user = _make_user(password=None)
185
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
186
+ h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
187
+ result = h.get_client("agent", "nopwdkey", cache=False)
188
+ assert result is not None
189
+ assert not getattr(result, "pswd_handshake", None)
190
+
191
+ def test_do_send_text_goes_to_undelivered(self, master):
192
+ proto = master.hm_protocol
193
+ _clean_class_state()
194
+ HiveMindHttpHandler.hm_protocol = proto
195
+
196
+ user = _make_user()
197
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
198
+ h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
199
+ client = h.get_client("agent", "sendkey", cache=False)
200
+ client.send_msg("hello", is_bin=False)
201
+ assert not HiveMindHttpHandler.undelivered["sendkey"].empty()
202
+ assert HiveMindHttpHandler.undelivered["sendkey"].get() == "hello"
203
+
204
+ def test_do_send_binary_goes_to_undelivered_bin(self, master):
205
+ proto = master.hm_protocol
206
+ _clean_class_state()
207
+ HiveMindHttpHandler.hm_protocol = proto
208
+
209
+ user = _make_user()
210
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
211
+ h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
212
+ client = h.get_client("agent", "binkey", cache=False)
213
+ client.send_msg(b"\x00\x01", is_bin=True)
214
+ assert not HiveMindHttpHandler.undelivered_bin["binkey"].empty()
215
+
216
+ def test_do_disconnect_removes_client_and_queue(self, master):
217
+ proto = master.hm_protocol
218
+ _clean_class_state()
219
+ HiveMindHttpHandler.hm_protocol = proto
220
+
221
+ user = _make_user()
222
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
223
+ h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
224
+ client = h.get_client("agent", "disckey", cache=True)
225
+ HiveMindHttpHandler.undelivered["disckey"].put("msg")
226
+ client.disconnect()
227
+ assert "disckey" not in HiveMindHttpHandler.clients
228
+ assert "disckey" not in HiveMindHttpHandler.undelivered
229
+
230
+ def test_skill_blacklist_none_defaults_to_empty(self, master):
231
+ proto = master.hm_protocol
232
+ _clean_class_state()
233
+ HiveMindHttpHandler.hm_protocol = proto
234
+
235
+ user = _make_user(skill_blacklist=None)
236
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
237
+ h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
238
+ client = h.get_client("agent", "blkey", cache=False)
239
+ assert client.skill_blacklist == []
240
+
241
+ def test_intent_blacklist_none_defaults_to_empty(self, master):
242
+ proto = master.hm_protocol
243
+ _clean_class_state()
244
+ HiveMindHttpHandler.hm_protocol = proto
245
+
246
+ user = _make_user(intent_blacklist=None)
247
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
248
+ h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
249
+ client = h.get_client("agent", "iblkey", cache=False)
250
+ assert client.intent_blacklist == []
251
+
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # ConnectHandler
255
+ # ---------------------------------------------------------------------------
256
+
257
+ class TestConnectHandler:
258
+ def test_missing_auth_returns_error(self, master):
259
+ proto = master.hm_protocol
260
+ h = _make_handler(ConnectHandler, "", proto)
261
+ _run(h.post())
262
+ h.write.assert_called_with({"error": "Missing authorization"})
263
+
264
+ def test_invalid_key_does_not_call_handle_new_client(self, master):
265
+ proto = master.hm_protocol
266
+ with patch.object(proto.db, "get_client_by_api_key", return_value=None):
267
+ with patch.object(proto, "handle_invalid_key_connected"):
268
+ with patch.object(proto, "handle_new_client") as mock_new:
269
+ h = _make_handler(ConnectHandler, _encode("agent:badkey"), proto)
270
+ _run(h.post())
271
+ mock_new.assert_not_called()
272
+
273
+ def test_valid_connect_calls_handle_new_client(self, master):
274
+ proto = master.hm_protocol
275
+ user = _make_user()
276
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
277
+ with patch.object(proto, "handle_new_client") as mock_new:
278
+ h = _make_handler(ConnectHandler, _encode("agent:validkey"), proto)
279
+ _run(h.post())
280
+ mock_new.assert_called_once()
281
+ h.write.assert_called_with({"status": "Connected"})
282
+
283
+ def test_no_crypto_key_handshake_disabled_require_crypto_triggers_invalid_protocol(self, master):
284
+ proto = master.hm_protocol
285
+ user = _make_user(crypto_key=None)
286
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
287
+ with patch.object(proto, "handle_invalid_protocol_version") as mock_ipv:
288
+ with patch.object(proto, "handle_new_client") as mock_new:
289
+ with patch.object(type(proto), "handshake_enabled",
290
+ new_callable=lambda: property(lambda s: False)):
291
+ with patch.object(type(proto), "require_crypto",
292
+ new_callable=lambda: property(lambda s: True)):
293
+ h = _make_handler(ConnectHandler, _encode("agent:key"), proto)
294
+ _run(h.post())
295
+ mock_ipv.assert_called_once()
296
+ mock_new.assert_not_called()
297
+
298
+ def test_connect_exception_returns_500(self, master):
299
+ proto = master.hm_protocol
300
+ with patch.object(proto.db, "sync", side_effect=RuntimeError("boom")):
301
+ h = _make_handler(ConnectHandler, _encode("agent:key"), proto)
302
+ _run(h.post())
303
+ h.set_status.assert_called_with(500)
304
+ h.write.assert_called_with({"error": "Connection failed"})
305
+
306
+
307
+ # ---------------------------------------------------------------------------
308
+ # DisconnectHandler
309
+ # ---------------------------------------------------------------------------
310
+
311
+ class TestDisconnectHandler:
312
+ def test_missing_auth_returns_error(self, master):
313
+ proto = master.hm_protocol
314
+ h = _make_handler(DisconnectHandler, "", proto)
315
+ _run(h.post())
316
+ h.write.assert_called_with({"error": "Missing authorization"})
317
+
318
+ def test_not_connected_returns_already_disconnected(self, master):
319
+ proto = master.hm_protocol
320
+ h = _make_handler(DisconnectHandler, _encode("agent:key"), proto)
321
+ # clients is empty after _clean_class_state
322
+ _run(h.post())
323
+ h.write.assert_called_with({"error": "Already Disconnected"})
324
+
325
+ def test_connected_client_is_disconnected(self, master):
326
+ proto = master.hm_protocol
327
+ user = _make_user()
328
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
329
+ with patch.object(proto, "handle_client_disconnected") as mock_disc:
330
+ h = _make_handler(DisconnectHandler, _encode("agent:disckey"), proto)
331
+ mock_client = MagicMock()
332
+ DisconnectHandler.clients["disckey"] = mock_client
333
+ _run(h.post())
334
+ mock_disc.assert_called_once()
335
+ h.write.assert_called_with({"status": "Disconnected"})
336
+
337
+ def test_disconnect_exception_returns_500(self, master):
338
+ proto = master.hm_protocol
339
+ user = _make_user()
340
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
341
+ with patch.object(proto, "handle_client_disconnected",
342
+ side_effect=RuntimeError("fail")):
343
+ h = _make_handler(DisconnectHandler, _encode("agent:errkey"), proto)
344
+ DisconnectHandler.clients["errkey"] = MagicMock()
345
+ _run(h.post())
346
+ h.set_status.assert_called_with(500)
347
+ h.write.assert_called_with({"error": "Disconnection failed"})
348
+
349
+
350
+ # ---------------------------------------------------------------------------
351
+ # SendMessageHandler
352
+ # ---------------------------------------------------------------------------
353
+
354
+ class TestSendMessageHandler:
355
+ def test_missing_auth_returns_error(self, master):
356
+ proto = master.hm_protocol
357
+ h = _make_handler(SendMessageHandler, "", proto)
358
+ _run(h.post())
359
+ h.write.assert_called_with({"error": "Missing authorization"})
360
+
361
+ def test_not_connected_returns_error(self, master):
362
+ proto = master.hm_protocol
363
+ h = _make_handler(SendMessageHandler, _encode("agent:key"), proto)
364
+ # clients empty
365
+ _run(h.post())
366
+ h.write.assert_called_with({"error": "Client is not connected"})
367
+
368
+ def test_missing_message_returns_400(self, master):
369
+ proto = master.hm_protocol
370
+ user = _make_user()
371
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
372
+ h = _make_handler(SendMessageHandler, _encode("agent:msgkey"), proto,
373
+ extra_get_arg={"message": ""})
374
+ SendMessageHandler.clients["msgkey"] = MagicMock()
375
+ _run(h.post())
376
+ h.set_status.assert_called_with(400)
377
+ h.write.assert_called_with({"error": "Missing message"})
378
+
379
+ def test_valid_message_dispatched(self, master):
380
+ proto = master.hm_protocol
381
+ user = _make_user()
382
+
383
+ from hivemind_bus_client.message import HiveMessage, HiveMessageType
384
+ from ovos_bus_client.message import Message
385
+ bus_msg = Message("test_msg", {})
386
+ hive_msg = HiveMessage(HiveMessageType.BUS, payload=bus_msg)
387
+
388
+ mock_client = MagicMock()
389
+ mock_client.decode.return_value = hive_msg
390
+
391
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
392
+ with patch.object(proto, "handle_message") as mock_handle:
393
+ h = _make_handler(SendMessageHandler, _encode("agent:sendmsg"), proto,
394
+ extra_get_arg={"message": "encoded_payload"})
395
+ # inject mock client to avoid re-building from DB
396
+ SendMessageHandler.clients["sendmsg"] = mock_client
397
+ _run(h.post())
398
+ mock_handle.assert_called_once()
399
+ h.write.assert_called_with({"status": "message sent"})
400
+
401
+ def test_b64_audio_message_dispatched(self, master):
402
+ proto = master.hm_protocol
403
+ user = _make_user()
404
+
405
+ from hivemind_bus_client.message import HiveMessage, HiveMessageType
406
+ from ovos_bus_client.message import Message
407
+ bus_msg = Message("recognizer_loop:b64_audio", {})
408
+ hive_msg = HiveMessage(HiveMessageType.BUS, payload=bus_msg)
409
+
410
+ mock_client = MagicMock()
411
+ mock_client.decode.return_value = hive_msg
412
+
413
+ with patch.object(proto.db, "get_client_by_api_key", return_value=user):
414
+ with patch.object(proto, "handle_message") as mock_handle:
415
+ h = _make_handler(SendMessageHandler, _encode("agent:audiokey"), proto,
416
+ extra_get_arg={"message": "audio_encoded"})
417
+ SendMessageHandler.clients["audiokey"] = mock_client
418
+ _run(h.post())
419
+ mock_handle.assert_called_once()
420
+ h.write.assert_called_with({"status": "message sent"})
421
+
422
+ def test_send_exception_returns_500(self, master):
423
+ proto = master.hm_protocol
424
+ with patch.object(proto.db, "sync", side_effect=RuntimeError("boom")):
425
+ h = _make_handler(SendMessageHandler, _encode("agent:errkey"), proto,
426
+ extra_get_arg={"message": "msg"})
427
+ SendMessageHandler.clients["errkey"] = MagicMock()
428
+ _run(h.post())
429
+ h.set_status.assert_called_with(500)
430
+ h.write.assert_called_with({"error": "Message sending failed"})
431
+
432
+
433
+ # ---------------------------------------------------------------------------
434
+ # GetMessagesHandler
435
+ # ---------------------------------------------------------------------------
436
+
437
+ class TestGetMessagesHandler:
438
+ def test_missing_auth_returns_error(self, master):
439
+ proto = master.hm_protocol
440
+ h = _make_handler(GetMessagesHandler, "", proto)
441
+ _run(h.get())
442
+ h.write.assert_called_with({"error": "Missing authorization"})
443
+
444
+ def test_not_connected_returns_error(self, master):
445
+ proto = master.hm_protocol
446
+ h = _make_handler(GetMessagesHandler, _encode("agent:key"), proto)
447
+ _run(h.get())
448
+ h.write.assert_called_with({"error": "Client is not connected"})
449
+
450
+ def test_empty_queue_returns_empty_messages(self, master):
451
+ proto = master.hm_protocol
452
+ h = _make_handler(GetMessagesHandler, _encode("agent:emptykey"), proto)
453
+ GetMessagesHandler.clients["emptykey"] = MagicMock()
454
+ _run(h.get())
455
+ h.write.assert_called_with({"status": "messages retrieved", "messages": []})
456
+
457
+ def test_queued_messages_are_returned(self, master):
458
+ proto = master.hm_protocol
459
+ h = _make_handler(GetMessagesHandler, _encode("agent:qkey"), proto)
460
+ GetMessagesHandler.clients["qkey"] = MagicMock()
461
+ HiveMindHttpHandler.undelivered["qkey"].put("msg1")
462
+ HiveMindHttpHandler.undelivered["qkey"].put("msg2")
463
+ _run(h.get())
464
+ h.write.assert_called_with({"status": "messages retrieved", "messages": ["msg1", "msg2"]})
465
+
466
+ def test_get_messages_exception_returns_500(self, master):
467
+ proto = master.hm_protocol
468
+ h = _make_handler(GetMessagesHandler, _encode("agent:exckey"), proto)
469
+ GetMessagesHandler.clients["exckey"] = MagicMock()
470
+ with patch.object(HiveMindHttpHandler.undelivered["exckey"], "empty",
471
+ side_effect=RuntimeError("fail")):
472
+ _run(h.get())
473
+ h.set_status.assert_called_with(500)
474
+ h.write.assert_called_with({"error": "Retrieving messages failed"})
475
+
476
+ def test_get_nowait_exception_breaks_loop(self, master):
477
+ """Inner except: get_nowait raises while queue reports non-empty."""
478
+ proto = master.hm_protocol
479
+ h = _make_handler(GetMessagesHandler, _encode("agent:getnowaitkey"), proto)
480
+ GetMessagesHandler.clients["getnowaitkey"] = MagicMock()
481
+
482
+ mock_queue = MagicMock()
483
+ mock_queue.empty.return_value = False
484
+ mock_queue.get_nowait.side_effect = RuntimeError("get_nowait fail")
485
+ HiveMindHttpHandler.undelivered["getnowaitkey"] = mock_queue
486
+
487
+ _run(h.get())
488
+ # The inner except breaks; we still write the (empty) messages list
489
+ h.write.assert_called_with({"status": "messages retrieved", "messages": []})
490
+
491
+
492
+ # ---------------------------------------------------------------------------
493
+ # GetBinMessagesHandler
494
+ # ---------------------------------------------------------------------------
495
+
496
+ class TestGetBinMessagesHandler:
497
+ def test_missing_auth_returns_error(self, master):
498
+ proto = master.hm_protocol
499
+ h = _make_handler(GetBinMessagesHandler, "", proto)
500
+ _run(h.get())
501
+ h.write.assert_called_with({"error": "Missing authorization"})
502
+
503
+ def test_not_connected_returns_error(self, master):
504
+ proto = master.hm_protocol
505
+ h = _make_handler(GetBinMessagesHandler, _encode("agent:key"), proto)
506
+ _run(h.get())
507
+ h.write.assert_called_with({"error": "Client is not connected"})
508
+
509
+ def test_empty_queue_returns_empty_messages(self, master):
510
+ proto = master.hm_protocol
511
+ h = _make_handler(GetBinMessagesHandler, _encode("agent:binempty"), proto)
512
+ GetBinMessagesHandler.clients["binempty"] = MagicMock()
513
+ _run(h.get())
514
+ h.write.assert_called_with({"status": "messages retrieved", "b64_messages": []})
515
+
516
+ def test_queued_bin_messages_are_returned(self, master):
517
+ proto = master.hm_protocol
518
+ h = _make_handler(GetBinMessagesHandler, _encode("agent:binq"), proto)
519
+ GetBinMessagesHandler.clients["binq"] = MagicMock()
520
+ HiveMindHttpHandler.undelivered_bin["binq"].put("b64data1")
521
+ HiveMindHttpHandler.undelivered_bin["binq"].put("b64data2")
522
+ _run(h.get())
523
+ h.write.assert_called_with({"status": "messages retrieved", "b64_messages": ["b64data1", "b64data2"]})
524
+
525
+ def test_get_bin_messages_exception_returns_500(self, master):
526
+ proto = master.hm_protocol
527
+ h = _make_handler(GetBinMessagesHandler, _encode("agent:binexc"), proto)
528
+ GetBinMessagesHandler.clients["binexc"] = MagicMock()
529
+ with patch.object(HiveMindHttpHandler.undelivered_bin["binexc"], "empty",
530
+ side_effect=RuntimeError("fail")):
531
+ _run(h.get())
532
+ h.set_status.assert_called_with(500)
533
+ h.write.assert_called_with({"error": "Retrieving messages failed"})
534
+
535
+ def test_get_nowait_exception_breaks_loop(self, master):
536
+ """Inner except: get_nowait raises while queue reports non-empty."""
537
+ proto = master.hm_protocol
538
+ h = _make_handler(GetBinMessagesHandler, _encode("agent:binnowait"), proto)
539
+ GetBinMessagesHandler.clients["binnowait"] = MagicMock()
540
+
541
+ mock_queue = MagicMock()
542
+ mock_queue.empty.return_value = False
543
+ mock_queue.get_nowait.side_effect = RuntimeError("get_nowait fail")
544
+ HiveMindHttpHandler.undelivered_bin["binnowait"] = mock_queue
545
+
546
+ _run(h.get())
547
+ h.write.assert_called_with({"status": "messages retrieved", "b64_messages": []})
@@ -0,0 +1,138 @@
1
+ """Unit-level coverage for HiveMindHttpProtocol.
2
+
3
+ Targets:
4
+ - version.py module loading.
5
+ - create_self_signed_cert() certificate / key generation.
6
+ - run() lifecycle (plain and SSL paths).
7
+ """
8
+ import os
9
+ import socket
10
+ import threading
11
+ import time
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ from hivemind_http_protocol import HiveMindHttpProtocol
17
+
18
+
19
+ # --- version.py -----------------------------------------------------------
20
+
21
+ def test_version_module_exposes_constants_and_string():
22
+ from hivemind_http_protocol import version as v
23
+ assert isinstance(v.VERSION_MAJOR, int)
24
+ assert isinstance(v.VERSION_MINOR, int)
25
+ assert isinstance(v.VERSION_BUILD, int)
26
+ assert isinstance(v.VERSION_ALPHA, int)
27
+ assert isinstance(v.__version__, str)
28
+ assert v.__version__.startswith(
29
+ f"{v.VERSION_MAJOR}.{v.VERSION_MINOR}.{v.VERSION_BUILD}"
30
+ )
31
+
32
+
33
+ # --- self-signed cert generation ------------------------------------------
34
+
35
+ def test_create_self_signed_cert_writes_files(tmp_path):
36
+ cert, key = HiveMindHttpProtocol.create_self_signed_cert(
37
+ cert_dir=str(tmp_path), name="http-test"
38
+ )
39
+ assert Path(cert).exists()
40
+ assert Path(key).exists()
41
+ assert Path(cert).read_bytes().startswith(b"-----BEGIN CERTIFICATE-----")
42
+ assert b"PRIVATE KEY" in Path(key).read_bytes()
43
+
44
+
45
+ def test_create_self_signed_cert_idempotent(tmp_path):
46
+ c1, k1 = HiveMindHttpProtocol.create_self_signed_cert(
47
+ cert_dir=str(tmp_path), name="http-test"
48
+ )
49
+ mtime = os.path.getmtime(c1)
50
+ time.sleep(0.05)
51
+ c2, k2 = HiveMindHttpProtocol.create_self_signed_cert(
52
+ cert_dir=str(tmp_path), name="http-test"
53
+ )
54
+ assert c1 == c2 and k1 == k2
55
+ assert os.path.getmtime(c1) == mtime, "cert should not be rewritten"
56
+
57
+
58
+ # --- run() lifecycle -------------------------------------------------------
59
+
60
+ def _free_port() -> int:
61
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
62
+ s.bind(("127.0.0.1", 0))
63
+ return s.getsockname()[1]
64
+
65
+
66
+ def _make_mock_hm_protocol():
67
+ from unittest.mock import MagicMock
68
+ m = MagicMock()
69
+ m.handshake_enabled = True
70
+ m.require_crypto = False
71
+ m.clients = {}
72
+ return m
73
+
74
+
75
+ def _spawn_proto(proto, *, timeout=5.0):
76
+ """Start proto.run() in a daemon thread; return when the port is reachable.
77
+
78
+ The thread is intentionally not stopped here — tests that only need to
79
+ check that the server binds can call this and let the daemon thread die
80
+ when the process exits. Returns True when the port accepted a connection
81
+ within *timeout* seconds, False otherwise.
82
+ """
83
+ port = int(proto.config["port"])
84
+ host = proto.config.get("host", "127.0.0.1")
85
+ ready = threading.Event()
86
+
87
+ def _run():
88
+ import asyncio
89
+ from tornado.platform.asyncio import AnyThreadEventLoopPolicy
90
+ asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
91
+ proto.run()
92
+
93
+ t = threading.Thread(target=_run, daemon=True)
94
+ t.start()
95
+
96
+ deadline = time.monotonic() + timeout
97
+ while time.monotonic() < deadline:
98
+ try:
99
+ s = socket.socket()
100
+ s.settimeout(0.3)
101
+ s.connect((host, port))
102
+ s.close()
103
+ return True
104
+ except OSError:
105
+ time.sleep(0.05)
106
+ return False
107
+
108
+
109
+ def test_run_starts_and_serves_on_plain_http():
110
+ """run() actually binds a port and the HTTP server accepts connections."""
111
+ pytest.importorskip("hivescope")
112
+ from hivescope.node import MasterNode
113
+ master = MasterNode.create("MH", require_crypto=False, handshake_enabled=True)
114
+ port = _free_port()
115
+ proto = HiveMindHttpProtocol(
116
+ config={"host": "127.0.0.1", "port": port, "ssl": False},
117
+ hm_protocol=master.hm_protocol,
118
+ )
119
+ assert _spawn_proto(proto), "HTTP server never became reachable"
120
+
121
+
122
+ def test_run_ssl_generates_cert(tmp_path):
123
+ """SSL branch: missing cert/key are generated then the server starts."""
124
+ pytest.importorskip("hivescope")
125
+ from hivescope.node import MasterNode
126
+ master = MasterNode.create("MH2", require_crypto=False, handshake_enabled=True)
127
+ port = _free_port()
128
+ proto = HiveMindHttpProtocol(
129
+ config={
130
+ "host": "127.0.0.1", "port": port, "ssl": True,
131
+ "cert_dir": str(tmp_path), "cert_name": "gen-http",
132
+ },
133
+ hm_protocol=master.hm_protocol,
134
+ )
135
+ _spawn_proto(proto, timeout=5.0)
136
+ # Cert files must exist regardless of whether the SSL port was reachable.
137
+ assert (tmp_path / "gen-http.crt").exists()
138
+ assert (tmp_path / "gen-http.key").exists()
@@ -1,20 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: hivemind-http-protocol
3
- Version: 0.0.2a2
4
- Summary: http network protocol for hivemind-core
5
- Home-page: https://github.com/JarbasHiveMind/hivemind-http-protocol
6
- Author: jarbasAi
7
- Author-email: jarbasai@mailfence.com
8
- License: Apache-2.0
9
- License-File: LICENSE.md
10
- Requires-Dist: tornado
11
- Requires-Dist: hivemind-plugin-manager
12
- Requires-Dist: poorman_handshake>=0.1.0
13
- Requires-Dist: pyOpenSSL
14
- Dynamic: author
15
- Dynamic: author-email
16
- Dynamic: home-page
17
- Dynamic: license
18
- Dynamic: license-file
19
- Dynamic: requires-dist
20
- Dynamic: summary
@@ -1,158 +0,0 @@
1
- # HiveMind HTTP Protocol
2
-
3
- The HiveMind HTTP Protocol provides an alternative REST-based implementation for message exchange in the HiveMind ecosystem.
4
-
5
-
6
- ---
7
-
8
- ## Configuration
9
-
10
- This plugin integrates with the `hivemind-core` framework. It is not a standalone project, and its behavior is controlled by the `hivemind-core` configuration.
11
-
12
- To enable and configure the HiveMind HTTP Protocol, update the `network_protocol` entry in the `hivemind-core` configuration file. Below is an example configuration:
13
-
14
- ```json
15
- "network_protocol": {
16
- "hivemind-websocket-plugin": {
17
- "host": "0.0.0.0",
18
- "port": 5678
19
- },
20
- "hivemind-http-plugin": {
21
- "host": "0.0.0.0",
22
- "port": 5679
23
- }
24
- }
25
- ```
26
-
27
- ---
28
- ## Client Library
29
-
30
-
31
- ```python
32
- from hivemind_bus_client.http_client import HiveMindHTTPClient, BinaryDataCallbacks
33
- from hivemind_bus_client.message import HiveMessage, HiveMessageType
34
- from ovos_bus_client.message import Message
35
-
36
-
37
- class BinaryDataHandler(BinaryDataCallbacks):
38
- def handle_receive_tts(self, bin_data: bytes,
39
- utterance: str,
40
- lang: str,
41
- file_name: str):
42
- # we can play it or save to file or whatever
43
- print(f"got {len(bin_data)} bytes of TTS audio")
44
- print(f"utterance: {utterance}", f"lang: {lang}", f"file_name: {file_name}")
45
- # got 33836 bytes of TTS audio
46
- # utterance: hello world lang: en-US file_name: 5eb63bbbe01eeed093cb22bb8f5acdc3.wav
47
-
48
- # not passing key etc so it uses hivemind identity file for details
49
- client = HiveMindHTTPClient(host="http://localhost", port=5679,
50
- bin_callbacks=BinaryDataHandler())
51
-
52
- client.emit(HiveMessage(HiveMessageType.BUS,
53
- Message("speak:synth", {"utterance": "hello world"})))
54
- ```
55
-
56
- ---
57
-
58
- ## REST API Documentation
59
-
60
- ### Authentication
61
-
62
- Authentication is handled via an HTTP `authorization` parameter in the request. The value should be a Base64-encoded string in the format `useragent:access_key`.
63
-
64
- ### Endpoints
65
-
66
- #### 1. Connect to the Server
67
-
68
- **Endpoint:** `/connect`
69
- **Method:** `POST`
70
-
71
- **Request Parameters:**
72
- - `authorization` (string, mandatory): Base64-encoded `useragent:access_key`.
73
-
74
- **Response:**
75
- - `200 OK`: `{ "status": "Connected" }`
76
- - `400 Bad Request`: `{ "error": "Missing authorization" }`
77
- - `500 Internal Server Error`: `{ "error": "Connection failed" }`
78
-
79
- ---
80
-
81
- #### 2. Disconnect from the Server
82
-
83
- **Endpoint:** `/disconnect`
84
- **Method:** `POST`
85
-
86
- **Request Parameters:**
87
- - `authorization` (string, mandatory): Base64-encoded `useragent:access_key`.
88
-
89
- **Response:**
90
- - `200 OK`: `{ "status": "Disconnected" }`
91
- - `400 Bad Request`: `{ "error": "Missing authorization" }`
92
- - `500 Internal Server Error`: `{ "error": "Disconnection failed" }`
93
-
94
- ---
95
-
96
- #### 3. Send a Message
97
-
98
- **Endpoint:** `/send_message`
99
- **Method:** `POST`
100
-
101
- **Request Parameters:**
102
- - `authorization` (string, mandatory): Base64-encoded `useragent:access_key`.
103
- - `message` (string, mandatory): Encoded message payload.
104
-
105
- **Response:**
106
- - `200 OK`: `{ "status": "message sent" }`
107
- - `400 Bad Request`: `{ "error": "Missing message" }`
108
- - `500 Internal Server Error`: `{ "error": "Message sending failed" }`
109
-
110
- ---
111
-
112
- #### 4. Retrieve Messages
113
-
114
- **Endpoint:** `/get_messages`
115
- **Method:** `GET`
116
-
117
- **Request Parameters:**
118
- - `authorization` (string, mandatory): Base64-encoded `useragent:access_key`.
119
-
120
- **Response:**
121
- - `200 OK`: `{ "messages": ["message1", "message2"] }`
122
- - `400 Bad Request`: `{ "error": "Missing authorization" }`
123
- - `500 Internal Server Error`: `{ "error": "Failed to retrieve messages" }`
124
-
125
- ---
126
-
127
- #### 5. Retrieve Binary Messages
128
-
129
- **Endpoint:** `/get_binary_messages`
130
- **Method:** `GET`
131
-
132
- **Request Parameters:**
133
- - `authorization` (string, mandatory): Base64-encoded `useragent:access_key`.
134
-
135
- **Response:**
136
- - `200 OK`: `{ "messages": ["Base64Message1", "Base64Message2"] }`
137
- - `400 Bad Request`: `{ "error": "Missing authorization" }`
138
- - `500 Internal Server Error`: `{ "error": "Failed to retrieve messages" }`
139
-
140
- ---
141
-
142
- ## Notes
143
-
144
- - The `connect` and `disconnect` endpoints enable state management in scenarios where persistent connections are not feasible
145
- - Binary messages are Base64-encoded to ensure compatibility with REST APIs, which are text-based protocols.
146
-
147
-
148
- ---
149
-
150
- ## Contributing
151
-
152
- Contributions are welcome! Please submit a pull request or open an issue for bug reports or feature requests.
153
-
154
- ---
155
-
156
- ## License
157
-
158
- This project is licensed under the Apache 2.0 License. See the `LICENSE` file for more details.
@@ -1,6 +0,0 @@
1
- # START_VERSION_BLOCK
2
- VERSION_MAJOR = 0
3
- VERSION_MINOR = 0
4
- VERSION_BUILD = 2
5
- VERSION_ALPHA = 2
6
- # END_VERSION_BLOCK
@@ -1,20 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: hivemind-http-protocol
3
- Version: 0.0.2a2
4
- Summary: http network protocol for hivemind-core
5
- Home-page: https://github.com/JarbasHiveMind/hivemind-http-protocol
6
- Author: jarbasAi
7
- Author-email: jarbasai@mailfence.com
8
- License: Apache-2.0
9
- License-File: LICENSE.md
10
- Requires-Dist: tornado
11
- Requires-Dist: hivemind-plugin-manager
12
- Requires-Dist: poorman_handshake>=0.1.0
13
- Requires-Dist: pyOpenSSL
14
- Dynamic: author
15
- Dynamic: author-email
16
- Dynamic: home-page
17
- Dynamic: license
18
- Dynamic: license-file
19
- Dynamic: requires-dist
20
- Dynamic: summary
@@ -1,4 +0,0 @@
1
- tornado
2
- hivemind-plugin-manager
3
- poorman_handshake>=0.1.0
4
- pyOpenSSL
@@ -1,55 +0,0 @@
1
- import os
2
- from setuptools import setup
3
-
4
- BASEDIR = os.path.abspath(os.path.dirname(__file__))
5
-
6
-
7
- def get_version():
8
- """ Find the version of the package"""
9
- version_file = os.path.join(BASEDIR, 'hivemind_http_protocol', 'version.py')
10
- major, minor, build, alpha = (None, None, None, None)
11
- with open(version_file) as f:
12
- for line in f:
13
- if 'VERSION_MAJOR' in line:
14
- major = line.split('=')[1].strip()
15
- elif 'VERSION_MINOR' in line:
16
- minor = line.split('=')[1].strip()
17
- elif 'VERSION_BUILD' in line:
18
- build = line.split('=')[1].strip()
19
- elif 'VERSION_ALPHA' in line:
20
- alpha = line.split('=')[1].strip()
21
-
22
- if ((major and minor and build and alpha) or
23
- '# END_VERSION_BLOCK' in line):
24
- break
25
- version = f"{major}.{minor}.{build}"
26
- if int(alpha) > 0:
27
- version += f"a{alpha}"
28
- return version
29
-
30
-
31
- def required(requirements_file):
32
- """ Read requirements file and remove comments and empty lines. """
33
- with open(os.path.join(BASEDIR, requirements_file), 'r') as f:
34
- requirements = f.read().splitlines()
35
- if 'MYCROFT_LOOSE_REQUIREMENTS' in os.environ:
36
- print('USING LOOSE REQUIREMENTS!')
37
- requirements = [r.replace('==', '>=').replace('~=', '>=') for r in requirements]
38
- return [pkg for pkg in requirements
39
- if pkg.strip() and not pkg.startswith("#")]
40
-
41
-
42
- PLUGIN_ENTRY_POINT = 'hivemind-http-plugin=hivemind_http_protocol:HiveMindHttpProtocol'
43
-
44
- setup(
45
- name='hivemind-http-protocol',
46
- version=get_version(),
47
- packages=['hivemind_http_protocol'],
48
- url='https://github.com/JarbasHiveMind/hivemind-http-protocol',
49
- license='Apache-2.0',
50
- author='jarbasAi',
51
- install_requires=required("requirements.txt"),
52
- entry_points={'hivemind.network.protocol': PLUGIN_ENTRY_POINT},
53
- author_email='jarbasai@mailfence.com',
54
- description='http network protocol for hivemind-core'
55
- )