Skip to content

Commit

Permalink
Migrate the Fediverse protocol API to use acct identifiers, not Actor…
Browse files Browse the repository at this point in the history
… URIs (#385)

* Use `acct` URIs instead of `https` Actor URIs to drive the protocols/fediverse API. This allows us to get around a limitation in the WordPress / Enable Mastodon Apps implementation of search (cannot search for Actor URI)
* This also allows us to simplify how many subtypes of Account we need.
* This also allows us the user to only specify an `acct` URI for existing accounts, and not both `acct` and Actor `https`, which was cumbersome.
* Cleaned up smart factory methods in `NodeWithMastodonAPI` to more easily get right, instead of more easily get wrong :-P
* Query the Mastodon API by `acct`, not `id` -- the WordPress implementation has same behavior with `acct`, but not with `id` as the WordPress federated users aren't first-class users in WordPress.
* Last smoke test (WordPress against Mastodon) is now working

---------

Co-authored-by: Johannes Ernst <git@j12t.org>
  • Loading branch information
jernst and Johannes Ernst authored Oct 12, 2024
1 parent 9556b18 commit a8e9b28
Show file tree
Hide file tree
Showing 14 changed files with 497 additions and 582 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ FEDITEST?=$(VENV)/bin/feditest -v
DOMAIN?=--domain 1234.lan


default : all
default : lint

all : build lint tests
all : lint tests

build : venv
$(VENV)/bin/pip install .
Expand Down Expand Up @@ -55,7 +55,7 @@ tests.smoke : venv
$(FEDITEST) run --testsdir tests.smoke/tests --session tests.smoke/mastodon_api.session.json --constellation tests.smoke/mastodon.ubos.constellation.json $(DOMAIN)
$(FEDITEST) run --testsdir tests.smoke/tests --session tests.smoke/mastodon_api.session.json --constellation tests.smoke/wordpress.ubos.constellation.json $(DOMAIN)
$(FEDITEST) run --testsdir tests.smoke/tests --session tests.smoke/mastodon_api_mastodon_api.session.json --constellation tests.smoke/mastodon_mastodon.ubos.constellation.json $(DOMAIN)
# Currently broken: $(FEDITEST) run --testsdir tests.smoke/tests --session tests.smoke/mastodon_api_mastodon_api.session.json --constellation tests.smoke/wordpress_mastodon.ubos.constellation.json $(DOMAIN)
$(FEDITEST) run --testsdir tests.smoke/tests --session tests.smoke/mastodon_api_mastodon_api.session.json --constellation tests.smoke/wordpress_mastodon.ubos.constellation.json $(DOMAIN)

release :
@which $(PYTHON) || ( echo 'No executable called "python". Append your python to the make command, like "make PYTHON=your-python"' && false )
Expand Down
2 changes: 1 addition & 1 deletion src/feditest/nodedrivers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ def start_delay(self) -> float:


def __str__(self) -> str:
return f'NodeConfiguration ({ type(self).__name__ }): node driver: "{ self.node_driver }", app: "{ self.app }", hostname: "{ self.hostname }"'
return f'NodeConfiguration: node driver: "{ self.node_driver }", app: "{ self.app }", hostname: "{ self.hostname }"'


class Node(ABC):
Expand Down
228 changes: 51 additions & 177 deletions src/feditest/nodedrivers/fallback/fediverse.py

Large diffs are not rendered by default.

365 changes: 175 additions & 190 deletions src/feditest/nodedrivers/mastodon/__init__.py

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/feditest/nodedrivers/mastodon/ubos.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from feditest.nodedrivers.mastodon import (
MastodonAccount,
MastodonNode,
MastodonNonExistingAccount,
MastodonUserPasswordAccount,
NodeWithMastodonApiConfiguration,
EMAIL_ACCOUNT_FIELD,
Expand Down Expand Up @@ -49,6 +48,7 @@
TLSCERT_PAR,
TLSKEY_PAR
)
from feditest.protocols.fediverse import FediverseNonExistingAccount
from feditest.registry import registry_singleton
from feditest.reporting import error, trace
from feditest.testplan import TestPlanConstellationNode, TestPlanNodeAccountField, TestPlanNodeNonExistingAccountField, TestPlanNodeParameterMalformedError
Expand Down Expand Up @@ -199,7 +199,7 @@ def provision_non_existing_account_for_role(self, role: str | None = None) -> No
# We just make it up
userid = self._generate_candidate_userid()

return MastodonNonExistingAccount(role, userid)
return FediverseNonExistingAccount(role, userid)


def add_cert_to_trust_store(self, root_cert: str) -> None:
Expand Down Expand Up @@ -266,7 +266,7 @@ def create_configuration_account_manager(self, rolename: str, test_plan_node: Te
non_existing_accounts : list[NonExistingAccount] = []
if test_plan_node.non_existing_accounts:
for index, non_existing_account_info in enumerate(test_plan_node.non_existing_accounts):
non_existing_accounts.append(MastodonNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_accounts.append(FediverseNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_account_info,
f'Constellation role "{ rolename }", NodeDriver "{ self }, Non-existing account { index }: '))

Expand Down
103 changes: 27 additions & 76 deletions src/feditest/nodedrivers/wordpress/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
"""

import re
import time
from typing import cast

from feditest.nodedrivers import (
Expand All @@ -18,11 +18,17 @@
from feditest.nodedrivers.mastodon import (
AccountOnNodeWithMastodonAPI,
Mastodon, # Re-import from there to avoid duplicating the package import hackery
MastodonOAuthApp,
NodeWithMastodonAPI,
NodeWithMastodonApiConfiguration
)
from feditest.protocols.fediverse import FediverseNode
from feditest.protocols.fediverse import (
ROLE_ACCOUNT_FIELD,
ROLE_NON_EXISTING_ACCOUNT_FIELD,
USERID_ACCOUNT_FIELD,
USERID_NON_EXISTING_ACCOUNT_FIELD,
FediverseNode,
FediverseNonExistingAccount
)
from feditest.reporting import is_trace_active, trace
from feditest.testplan import TestPlanConstellationNode, TestPlanNodeAccountField, TestPlanNodeNonExistingAccountField, TestPlanNodeParameter
from feditest.utils import boolean_parse_validate, hostname_validate, prompt_user
Expand All @@ -43,41 +49,11 @@ def _oauth_token_validate(candidate: str) -> str | None:
return candidate if len(candidate)>10 else None


def _userid_validate(candidate: str) -> str | None:
"""
Validate a WordPress user name. Avoids user input errors.
FIXME this is a wild guess and can be better.
"""
candidate = candidate.strip()
return candidate if re.match(r'[a-zA-Z0-9_]', candidate) else None


USERID_ACCOUNT_FIELD = TestPlanNodeAccountField(
'userid',
"""Mastodon userid for a user (e.g. "joe") (required).""",
_userid_validate
)
OAUTH_TOKEN_ACCOUNT_FIELD = TestPlanNodeAccountField(
'oauth_token',
"""OAuth token of a user so the "Enable Mastodon apps" API can be invoked.""",
_oauth_token_validate
)
ROLE_ACCOUNT_FIELD = TestPlanNodeAccountField(
'role',
"""A symbolic name for the Account as used by tests (optional).""",
lambda x: len(x)
)

USERID_NON_EXISTING_ACCOUNT_FIELD = TestPlanNodeNonExistingAccountField(
'userid',
"""Mastodon userid for a non-existing user (e.g. "joe") (required).""",
_userid_validate
)
ROLE_NON_EXISTING_ACCOUNT_FIELD = TestPlanNodeNonExistingAccountField(
'role',
"""A symbolic name for the non-existing Account as used by tests (optional).""",
lambda x: len(x)
)


class WordPressAccount(AccountOnNodeWithMastodonAPI):
Expand Down Expand Up @@ -105,14 +81,10 @@ def create_from_account_info_in_testplan(account_info_in_testplan: dict[str, str


@property
def actor_uri(self):
return f'https://{ self.node.hostname }/author/{ self.userid }/'


def mastodon_user_client(self) -> Mastodon:
if self._mastodon_user_client is None:
node = cast(NodeWithMastodonAPI, self._node)
oauth_app = cast(MastodonOAuthApp, node._mastodon_oauth_app)
oauth_app = node._obtain_mastodon_oauth_app()
self._ensure_oauth_token(oauth_app.client_id)
trace(f'Logging into WordPress at "{ oauth_app.api_base_url }" with userid "{ self.userid }" with OAuth token "{ self._oauth_token }".')
client = Mastodon(
Expand All @@ -138,54 +110,33 @@ def _ensure_oauth_token(self, oauth_client_id: str) -> None:
self._oauth_token = real_node._provision_oauth_token_for(self, oauth_client_id)


class WordPressNonExistingAccount(NonExistingAccount):
def __init__(self, role: str | None, userid: str):
super().__init__(role)
self.userid = userid


@staticmethod
def create_from_non_existing_account_info_in_testplan(non_existing_account_info_in_testplan: dict[str, str | None], context_msg: str = ''):
"""
Parses the information provided in an "non_existing_account" dict of TestPlanConstellationNode
"""
userid = USERID_NON_EXISTING_ACCOUNT_FIELD.get_validate_from_or_raise(non_existing_account_info_in_testplan, context_msg)
role = ROLE_ACCOUNT_FIELD.get_validate_from(non_existing_account_info_in_testplan, context_msg)
return WordPressNonExistingAccount(role, userid)


@property
def webfinger_uri(self):
return f'acct:{ self.userid }@{ self.node.hostname }'


@property
def actor_uri(self):
return f'https://{ self.node.hostname }/users/{ self.userid }'


class WordPressPlusPluginsNode(NodeWithMastodonAPI):
"""
A Node running WordPress with the ActivityPub plugin.
"""
# Python 3.12 @override -- implement WordPress scheme
def _actor_uri_to_userid(self, actor_uri: str) -> str:
if m:= re.match('^https://([^/]+)/author/([^/]+)/?$', actor_uri):
if m.group(1) == self._config.hostname:
return m.group(2)
raise ValueError( f'Cannot find actor at this node: { actor_uri }' )


def _provision_oauth_token_for(self, account: WordPressAccount, oauth_client_id: str) -> str:
ret = cast(str, prompt_user(f'Enter the OAuth token for the Mastodon API for user "{ account.userid }"'
+ f' on constellation role "{ self.rolename }", OAuth client id "{ oauth_client_id }" (user field "{ OAUTH_TOKEN_ACCOUNT_FIELD }"): ',
parse_validate=_oauth_token_validate))
return ret


# Python 3.12 @override
def _run_poor_mans_cron(self) -> None:
# Seems we need two HTTP GETs
url = f'https://{ self.hostname }/wp-cron.php?doing_wp_cron'
session = self._obtain_requests_session()

# There must be a better way. But this seems to do it. 15 might be enough. 10 might not.
for _ in range(20):
time.sleep(1)
trace('Triggering wp-cron at { url }')
session.get(url)


class WordPressPlusPluginsSaasNodeDriver(NodeDriver):
"""
Create a WordPress + ActivityPubPlugin Node that already runs as Saas
Create a WordPress+plugins Node that already runs as Saas
"""
# Python 3.12 @override
@staticmethod
Expand All @@ -207,13 +158,13 @@ def test_plan_node_non_existing_account_fields() -> list[TestPlanNodeNonExisting

# Python 3.12 @override
def create_configuration_account_manager(self, rolename: str, test_plan_node: TestPlanConstellationNode) -> tuple[NodeConfiguration, AccountManager | None]:
app = test_plan_node.parameter_or_raise(APP_PAR, { APP_PAR.name: 'WordPress + ActivityPub plugin' }) # Let user give a more descriptive name if they want to
app = test_plan_node.parameter_or_raise(APP_PAR, { APP_PAR.name: 'WordPress+plugins' }) # Let user give a more descriptive name if they want to
app_version = test_plan_node.parameter(APP_VERSION_PAR)
hostname = test_plan_node.parameter_or_raise(HOSTNAME_PAR)
verify_tls_certificate = test_plan_node.parameter_or_raise(VERIFY_API_TLS_CERTIFICATE_PAR, { VERIFY_API_TLS_CERTIFICATE_PAR.name: 'true' })

if not hostname:
hostname = prompt_user(f'Enter the hostname for the WordPress + ActivityPub plugin Node of constellation role "{ rolename }"'
hostname = prompt_user(f'Enter the hostname for the WordPress+plugins Node of constellation role "{ rolename }"'
+ f' (node parameter "{ HOSTNAME_PAR }"): ',
parse_validate=hostname_validate)

Expand All @@ -227,7 +178,7 @@ def create_configuration_account_manager(self, rolename: str, test_plan_node: Te
non_existing_accounts : list[NonExistingAccount] = []
if test_plan_node.non_existing_accounts:
for index, non_existing_account_info in enumerate(test_plan_node.non_existing_accounts):
non_existing_accounts.append(WordPressNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_accounts.append(FediverseNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_account_info,
f'Constellation role "{ rolename }", NodeDriver "{ self }, Non-existing account { index }: '))

Expand Down
8 changes: 4 additions & 4 deletions src/feditest/nodedrivers/wordpress/ubos.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
USERID_ACCOUNT_FIELD,
USERID_NON_EXISTING_ACCOUNT_FIELD,
WordPressAccount,
WordPressNonExistingAccount,
WordPressPlusPluginsNode
)
from feditest.protocols.fediverse import FediverseNonExistingAccount
from feditest.reporting import trace
from feditest.testplan import TestPlanConstellationNode, TestPlanNodeAccountField, TestPlanNodeNonExistingAccountField

Expand All @@ -49,7 +49,7 @@ def set_node(self, node: Node) -> None:

class WordPressPlusPluginsUbosNode(WordPressPlusPluginsNode):
"""
A WordPress + plugins Node running on UBOS. This means we know how to interact with it exactly.
A WordPress+plugins Node running on UBOS. This means we know how to interact with it exactly.
"""
# Python 3.12 @override
def provision_account_for_role(self, role: str | None = None) -> Account | None:
Expand Down Expand Up @@ -135,7 +135,7 @@ def create_configuration_account_manager(self, rolename: str, test_plan_node: Te
non_existing_accounts : list[NonExistingAccount] = []
if test_plan_node.non_existing_accounts:
for index, non_existing_account_info in enumerate(test_plan_node.non_existing_accounts):
non_existing_accounts.append(WordPressNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_accounts.append(FediverseNonExistingAccount.create_from_non_existing_account_info_in_testplan(
non_existing_account_info,
f'Constellation role "{ rolename }", NodeDriver "{ self }, Non-existing account { index }: '))

Expand Down Expand Up @@ -163,7 +163,7 @@ def create_configuration_account_manager(self, rolename: str, test_plan_node: Te
}
},
defaults = {
'app' : 'WordPress + plugins'
'app' : 'WordPress+plugins'
}),
WordPressUbosAccountManager(accounts, non_existing_accounts)
)
Expand Down
18 changes: 9 additions & 9 deletions src/feditest/protocols/activitypub/diag.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,37 +144,37 @@ class ActivityPubDiagNode(WebDiagClient, WebDiagServer,ActivityPubNode):

# Work in progress

# def fetch_remote_actor_document(remote_actor_uri: str) -> Actor:
# def fetch_remote_actor_document(remote_actor_acct_uri: str) -> Actor:
# pass


# def set_inbox_uri_to(actor_uri: str, inbox_uri: str | None):
# def set_inbox_uri_to(actor_acct_uri: str, inbox_uri: str | None):
# pass


# def set_outbox_uri_to(actor_uri: str, outbox_uri: str | None):
# def set_outbox_uri_to(actor_acct_uri: str, outbox_uri: str | None):
# pass


# def add_to_followers_collection(actor_uri: str, to_be_added_actor_uri: str):
# def add_to_followers_collection(actor_acct_uri: str, to_be_added_actor_acct_uri: str):
# pass


# def add_to_following_collection(actor_uri: str, to_be_added_actor_uri: str):
# def add_to_following_collection(actor_acct_uri: str, to_be_added_actor_acct_uri: str):
# pass


# def add_to_outbox(actor_uri: str, to_be_added_activity: Activity):
# def add_to_outbox(actor_acct_uri: str, to_be_added_activity: Activity):
# pass


# def add_to_inbox(actor_uri: str, to_be_added_activity: Activity):
# def add_to_inbox(actor_acct_uri: str, to_be_added_activity: Activity):
# pass


# def read_inbox_of(actor_uri: str, inbox_collection: Collection):
# def read_inbox_of(actor_acct_uri: str, inbox_collection: Collection):
# pass


# def read_outbox_of(actor_uri: str, outbox_collection: Collection):
# def read_outbox_of(actor_acct_uri: str, outbox_collection: Collection):
# pass
Loading

0 comments on commit a8e9b28

Please sign in to comment.