Skip to content

Commit ebefef9

Browse files
committed
chore: Split out protocols from Trust Ping
Signed-off-by: Colton Wolkins (Laptop) <colton@indicio.tech>
1 parent be0dfee commit ebefef9

22 files changed

Lines changed: 890 additions & 165 deletions

acapy_agent/protocols_v2/basicmessage/__init__.py

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Version definitions for this protocol."""
2+
3+
versions = [
4+
{
5+
"major_version": 1,
6+
"minimum_minor_version": 0,
7+
"current_minor_version": 0,
8+
"path": "v1_0",
9+
}
10+
]

acapy_agent/protocols_v2/basicmessage/v1_0/__init__.py

Whitespace-only changes.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Message type identifiers for Trust Pings."""
2+
3+
#from ...didcomm_prefix import DIDCommPrefix
4+
import logging
5+
from ....messaging.v2_agent_message import V2AgentMessage
6+
from ....connections.models.connection_target import ConnectionTarget
7+
from didcomm_messaging import DIDCommMessaging, RoutingService
8+
9+
SPEC_URI = (
10+
"https://didcomm.org/basicmessage/2.0/message"
11+
)
12+
13+
# Message types
14+
BASIC_MESSAGE = "https://didcomm.org/basicmessage/2.0/message"
15+
16+
PROTOCOL_PACKAGE = "acapy_agent.protocols_v2.basicmessage.v1_0"
17+
18+
class basic_message:
19+
async def __call__(self, *args, **kwargs):
20+
await self.handle(*args, **kwargs)
21+
@staticmethod
22+
async def handle(context, responder, payload):
23+
logger = logging.getLogger(__name__)
24+
their_did = context.message_receipt.sender_verkey.split('#')[0]
25+
our_did = context.message_receipt.recipient_verkey.split('#')[0]
26+
error_result = V2AgentMessage(
27+
message={
28+
"type": BASIC_MESSAGE,
29+
"body": {
30+
"content": "Hello from acapy",
31+
},
32+
"to": [their_did],
33+
"from": our_did,
34+
"lang": "en",
35+
}
36+
)
37+
await responder.send_reply(error_result)
38+
39+
40+
HANDLERS = {
41+
BASIC_MESSAGE: f"{PROTOCOL_PACKAGE}.message_types.basic_message",
42+
}.items()
43+
44+
MESSAGE_TYPES = {
45+
BASIC_MESSAGE: f"{PROTOCOL_PACKAGE}.message_types.basic_message",
46+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
"""Trust ping admin routes."""
2+
3+
from aiohttp import web
4+
from aiohttp_apispec import docs, match_info_schema, request_schema, response_schema
5+
from marshmallow import fields
6+
from didcomm_messaging import DIDCommMessaging, RoutingService
7+
from didcomm_messaging.resolver import DIDResolver as DMPResolver
8+
9+
from ....admin.decorators.auth import tenant_authentication
10+
from ....admin.request_context import AdminRequestContext
11+
from ....connections.models.conn_record import ConnRecord
12+
from ....messaging.models.openapi import OpenAPISchema
13+
from ....messaging.valid import UUID4_EXAMPLE
14+
from ....storage.error import StorageNotFoundError
15+
from .message_types import SPEC_URI
16+
17+
18+
class BaseDIDCommV2Schema(OpenAPISchema):
19+
"""Request schema for performing a ping."""
20+
21+
to_did = fields.Str(
22+
required=True,
23+
allow_none=False,
24+
metadata={"description": "Comment for the ping message"},
25+
)
26+
27+
28+
class PingRequestSchema(BaseDIDCommV2Schema):
29+
"""Request schema for performing a ping."""
30+
31+
response_requested = fields.Bool(
32+
required=False,
33+
allow_none=True,
34+
metadata={"description": "Comment for the ping message"},
35+
)
36+
37+
38+
class PingRequestResponseSchema(OpenAPISchema):
39+
"""Request schema for performing a ping."""
40+
41+
thread_id = fields.Str(
42+
required=False, metadata={"description": "Thread ID of the ping message"}
43+
)
44+
45+
46+
class PingConnIdMatchInfoSchema(OpenAPISchema):
47+
"""Path parameters and validators for request taking connection id."""
48+
49+
conn_id = fields.Str(
50+
required=True,
51+
metadata={"description": "Connection identifier", "example": UUID4_EXAMPLE},
52+
)
53+
54+
from ....wallet.base import BaseWallet
55+
from ....wallet.did_info import DIDInfo
56+
from ....wallet.did_method import KEY, PEER2, PEER4, SOV, DIDMethod, DIDMethods, HolderDefinedDid
57+
from ....wallet.did_posture import DIDPosture
58+
from ....wallet.error import WalletError, WalletNotFoundError
59+
from ....messaging.v2_agent_message import V2AgentMessage
60+
from ....connections.models.connection_target import ConnectionTarget
61+
from didcomm_messaging import DIDCommMessaging, RoutingService
62+
def format_did_info(info: DIDInfo):
63+
"""Serialize a DIDInfo object."""
64+
if info:
65+
return {
66+
"did": info.did,
67+
"verkey": info.verkey,
68+
"posture": DIDPosture.get(info.metadata).moniker,
69+
"key_type": info.key_type.key_type,
70+
"method": info.method.method_name,
71+
"metadata": info.metadata,
72+
}
73+
74+
async def get_mydid(request: web.BaseRequest):
75+
context: AdminRequestContext = request["context"]
76+
#filter_did = request.query.get("did")
77+
#filter_verkey = request.query.get("verkey")
78+
filter_posture = DIDPosture.get(request.query.get("posture"))
79+
results = []
80+
async with context.session() as session:
81+
did_methods: DIDMethods = session.inject(DIDMethods)
82+
filter_method: DIDMethod | None = did_methods.from_method(
83+
request.query.get("method") or "did:peer:2"
84+
)
85+
#key_types = session.inject(KeyTypes)
86+
#filter_key_type = key_types.from_key_type(request.query.get("key_type", ""))
87+
wallet: BaseWallet | None = session.inject_or(BaseWallet)
88+
if not wallet:
89+
raise web.HTTPForbidden(reason="No wallet available")
90+
else:
91+
dids = await wallet.get_local_dids()
92+
results = [
93+
format_did_info(info)
94+
for info in dids
95+
if (
96+
filter_posture is None
97+
or DIDPosture.get(info.metadata) is DIDPosture.WALLET_ONLY
98+
)
99+
and (not filter_method or info.method == filter_method)
100+
#and (not filter_key_type or info.key_type == filter_key_type)
101+
]
102+
103+
results.sort(key=lambda info: (DIDPosture.get(info["posture"]).ordinal, info["did"]))
104+
our_did = results[0]["did"]
105+
return our_did
106+
107+
async def get_target(request: web.BaseRequest, to_did: str, from_did: str):
108+
context: AdminRequestContext = request["context"]
109+
110+
try:
111+
async with context.profile.session() as session:
112+
resolver = session.inject(DMPResolver)
113+
did_doc = await resolver.resolve(to_did)
114+
except Exception as err:
115+
raise web.HTTPNotFound(reason=str(err)) from err
116+
117+
async with context.session() as session:
118+
ctx = session
119+
messaging = ctx.inject(DIDCommMessaging)
120+
routing_service = ctx.inject(RoutingService)
121+
frm = to_did
122+
services = await routing_service._resolve_services(messaging.resolver, frm)
123+
chain = [
124+
{
125+
"did": frm,
126+
"service": services,
127+
}
128+
]
129+
130+
# Loop through service DIDs until we run out of DIDs to forward to
131+
to_target = services[0].service_endpoint.uri
132+
found_forwardable_service = await routing_service.is_forwardable_service(
133+
messaging.resolver, services[0]
134+
)
135+
while found_forwardable_service:
136+
services = await routing_service._resolve_services(messaging.resolver, to_target)
137+
if services:
138+
chain.append(
139+
{
140+
"did": to_target,
141+
"service": services,
142+
}
143+
)
144+
to_target = services[0].service_endpoint.uri
145+
found_forwardable_service = (
146+
await routing_service.is_forwardable_service(messaging.resolver, services[0])
147+
if services
148+
else False
149+
)
150+
reply_destination = [
151+
ConnectionTarget(
152+
did=f"{to_did}#key-1",
153+
endpoint=service.service_endpoint.uri,
154+
recipient_keys=[f"{to_did}#key-1"],
155+
sender_key=from_did + "#key-1",
156+
)
157+
for service in chain[-1]["service"]
158+
]
159+
return reply_destination
160+
161+
162+
class BasicMessageSchema(BaseDIDCommV2Schema):
163+
"""Request schema for performing a ping."""
164+
165+
content = fields.Str(
166+
required=True,
167+
allow_none=False,
168+
metadata={"description": "Basic Message message content"},
169+
)
170+
171+
172+
@docs(tags=["basicmessagev2", "didcommv2"], summary="Send a Basic Message")
173+
@request_schema(BasicMessageSchema())
174+
@response_schema(PingRequestResponseSchema(), 200, description="")
175+
@tenant_authentication
176+
async def basic_message_send(request: web.BaseRequest):
177+
"""Request handler for sending a trust ping to a connection.
178+
179+
Args:
180+
request: aiohttp request object
181+
182+
"""
183+
context: AdminRequestContext = request["context"]
184+
outbound_handler = request["outbound_message_router"]
185+
body = await request.json()
186+
to_did = body.get("to_did")
187+
message = body.get("content")
188+
189+
our_did = await get_mydid(request)
190+
their_did = to_did
191+
reply_destination = await get_target(request, to_did, our_did)
192+
msg = V2AgentMessage(
193+
message={
194+
"type": "https://didcomm.org/basicmessage/2.0/message",
195+
"body": {
196+
"content": message
197+
},
198+
"lang": "en",
199+
"to": [their_did],
200+
"from": our_did,
201+
}
202+
)
203+
await outbound_handler(msg, target_list=reply_destination)
204+
return web.json_response(msg.message)
205+
206+
207+
async def register(app: web.Application):
208+
"""Register routes."""
209+
210+
app.add_routes([web.post("/basic-message/send-message", basic_message_send)])
211+
212+
213+
def post_process_routes(app: web.Application):
214+
"""Amend swagger API."""
215+
216+
# Add top-level tags description
217+
if "tags" not in app._state["swagger_dict"]:
218+
app._state["swagger_dict"]["tags"] = []
219+
app._state["swagger_dict"]["tags"].append(
220+
{
221+
"name": "basicmessagev2",
222+
"description": "Basic Message to contact",
223+
"externalDocs": {"description": "Specification", "url": SPEC_URI},
224+
}
225+
)
226+
app._state["swagger_dict"]["tags"].append(
227+
{
228+
"name": "didcommv2",
229+
"description": "DIDComm V2 based protocols for Interop-a-thon",
230+
"externalDocs": {"description": "Specification", "url": "https://didcomm.org"},
231+
}
232+
)

acapy_agent/protocols_v2/basicmessage/v1_0/tests/__init__.py

Whitespace-only changes.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from unittest import IsolatedAsyncioTestCase
2+
3+
from acapy_agent.tests import mock
4+
5+
from .....admin.request_context import AdminRequestContext
6+
from .....core.in_memory import InMemoryProfile
7+
from .. import routes as test_module
8+
9+
10+
class TestTrustpingRoutes(IsolatedAsyncioTestCase):
11+
def setUp(self):
12+
self.session_inject = {}
13+
profile = InMemoryProfile.test_profile(
14+
settings={
15+
"admin.admin_api_key": "secret-key",
16+
}
17+
)
18+
self.context = AdminRequestContext.test_context(self.session_inject, profile)
19+
self.request_dict = {
20+
"context": self.context,
21+
"outbound_message_router": mock.CoroutineMock(),
22+
}
23+
self.request = mock.MagicMock(
24+
app={},
25+
match_info={},
26+
query={},
27+
__getitem__=lambda _, k: self.request_dict[k],
28+
headers={"x-api-key": "secret-key"},
29+
)
30+
31+
async def test_connections_send_ping(self):
32+
self.request.json = mock.CoroutineMock(return_value={"comment": "some comment"})
33+
self.request.match_info = {"conn_id": "dummy"}
34+
35+
with mock.patch.object(
36+
test_module.ConnRecord, "retrieve_by_id", mock.CoroutineMock()
37+
) as mock_retrieve, mock.patch.object(
38+
test_module, "Ping", mock.MagicMock()
39+
) as mock_ping, mock.patch.object(
40+
test_module.web, "json_response", mock.MagicMock()
41+
) as json_response:
42+
mock_ping.return_value = mock.MagicMock(_thread_id="dummy")
43+
mock_retrieve.return_value = mock.MagicMock(is_ready=True)
44+
result = await test_module.connections_send_ping(self.request)
45+
json_response.assert_called_once_with({"thread_id": "dummy"})
46+
assert result is json_response.return_value
47+
48+
async def test_connections_send_ping_no_conn(self):
49+
self.request.json = mock.CoroutineMock(return_value={"comment": "some comment"})
50+
self.request.match_info = {"conn_id": "dummy"}
51+
52+
with mock.patch.object(
53+
test_module.ConnRecord, "retrieve_by_id", mock.CoroutineMock()
54+
) as mock_retrieve, mock.patch.object(
55+
test_module.web, "json_response", mock.MagicMock()
56+
) as json_response:
57+
mock_retrieve.side_effect = test_module.StorageNotFoundError()
58+
with self.assertRaises(test_module.web.HTTPNotFound):
59+
await test_module.connections_send_ping(self.request)
60+
61+
async def test_connections_send_ping_not_ready(self):
62+
self.request.json = mock.CoroutineMock(return_value={"comment": "some comment"})
63+
self.request.match_info = {"conn_id": "dummy"}
64+
65+
with mock.patch.object(
66+
test_module.ConnRecord, "retrieve_by_id", mock.CoroutineMock()
67+
) as mock_retrieve, mock.patch.object(
68+
test_module.web, "json_response", mock.MagicMock()
69+
) as json_response:
70+
mock_retrieve.return_value = mock.MagicMock(is_ready=False)
71+
with self.assertRaises(test_module.web.HTTPBadRequest):
72+
await test_module.connections_send_ping(self.request)
73+
74+
async def test_register(self):
75+
mock_app = mock.MagicMock()
76+
mock_app.add_routes = mock.MagicMock()
77+
78+
await test_module.register(mock_app)
79+
mock_app.add_routes.assert_called_once()
80+
81+
async def test_post_process_routes(self):
82+
mock_app = mock.MagicMock(_state={"swagger_dict": {}})
83+
test_module.post_process_routes(mock_app)
84+
assert "tags" in mock_app._state["swagger_dict"]

acapy_agent/protocols_v2/discovery/__init__.py

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Version definitions for this protocol."""
2+
3+
versions = [
4+
{
5+
"major_version": 1,
6+
"minimum_minor_version": 0,
7+
"current_minor_version": 0,
8+
"path": "v1_0",
9+
}
10+
]

acapy_agent/protocols_v2/discovery/v1_0/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)