structural refactor and codebase improvements
This commit is contained in:
103
crates/domain/src/ports/federation.rs
Normal file
103
crates/domain/src/ports/federation.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
errors::DomainError,
|
||||
models::{BlockedDomainInfo, FollowedActorInfo},
|
||||
};
|
||||
|
||||
/// Serves ActivityPub documents for content negotiation. Presentation calls
|
||||
/// this when a peer sends `Accept: application/activity+json`.
|
||||
#[async_trait]
|
||||
pub trait ApDocumentPort: Send + Sync {
|
||||
async fn actor_json(&self, user_id: &str) -> Result<String, DomainError>;
|
||||
async fn followers_collection_json(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
page: Option<u32>,
|
||||
) -> Result<String, DomainError>;
|
||||
async fn following_collection_json(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
page: Option<u32>,
|
||||
) -> Result<String, DomainError>;
|
||||
}
|
||||
|
||||
/// Instance-wide domain blocklist administration. Presentation calls this from
|
||||
/// the admin API and the admin HTML pages; nothing outside presentation does.
|
||||
#[async_trait]
|
||||
pub trait InstanceBlocklistPort: Send + Sync {
|
||||
async fn get_blocked_domains(&self) -> Result<Vec<BlockedDomainInfo>, DomainError>;
|
||||
async fn add_blocked_domain(
|
||||
&self,
|
||||
domain: &str,
|
||||
reason: Option<&str>,
|
||||
) -> Result<(), DomainError>;
|
||||
async fn remove_blocked_domain(&self, domain: &str) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
/// Pulling remote content in, and pushing local content out, after a follow
|
||||
/// is established. Worker-side only — no HTTP handler calls this.
|
||||
#[async_trait]
|
||||
pub trait ApBackfillPort: Send + Sync {
|
||||
async fn get_following(
|
||||
&self,
|
||||
local_user_id: Uuid,
|
||||
) -> Result<Vec<FollowedActorInfo>, DomainError>;
|
||||
async fn import_remote_outbox(
|
||||
&self,
|
||||
outbox_url: &str,
|
||||
actor_url: &str,
|
||||
) -> Result<(), DomainError>;
|
||||
async fn run_backfill_for_follower(
|
||||
&self,
|
||||
owner_user_id: Uuid,
|
||||
follower_inbox_url: String,
|
||||
) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ports::noop::{NoopApBackfill, NoopApDocument, NoopInstanceBlocklist};
|
||||
|
||||
#[tokio::test]
|
||||
async fn noop_document_returns_empty_strings() {
|
||||
let p = NoopApDocument;
|
||||
assert_eq!(p.actor_json("anything").await.unwrap(), "");
|
||||
assert_eq!(
|
||||
p.followers_collection_json(uuid::Uuid::nil(), None)
|
||||
.await
|
||||
.unwrap(),
|
||||
""
|
||||
);
|
||||
assert_eq!(
|
||||
p.following_collection_json(uuid::Uuid::nil(), Some(2))
|
||||
.await
|
||||
.unwrap(),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn noop_blocklist_returns_empty_and_ok() {
|
||||
let p = NoopInstanceBlocklist;
|
||||
assert!(p.get_blocked_domains().await.unwrap().is_empty());
|
||||
p.add_blocked_domain("evil.example", Some("spam"))
|
||||
.await
|
||||
.unwrap();
|
||||
p.remove_blocked_domain("evil.example").await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn noop_backfill_returns_empty_and_ok() {
|
||||
let p = NoopApBackfill;
|
||||
assert!(p.get_following(uuid::Uuid::nil()).await.unwrap().is_empty());
|
||||
p.import_remote_outbox("https://a/outbox", "https://a")
|
||||
.await
|
||||
.unwrap();
|
||||
p.run_backfill_for_follower(uuid::Uuid::nil(), "https://a/inbox".into())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
errors::DomainError,
|
||||
value_objects::{FollowStatus, SocialActor},
|
||||
value_objects::{FollowRelation, FollowStatus, SocialActor},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
@@ -50,31 +50,29 @@ pub trait FollowCommand: Send + Sync {
|
||||
|
||||
#[async_trait]
|
||||
pub trait FollowQuery: Send + Sync {
|
||||
async fn get_following(
|
||||
&self,
|
||||
user_id: uuid::Uuid,
|
||||
base_url: &str,
|
||||
) -> Result<Vec<SocialActor>, DomainError>;
|
||||
async fn get_following(&self, user_id: uuid::Uuid) -> Result<Vec<SocialActor>, DomainError>;
|
||||
|
||||
async fn get_followers(
|
||||
&self,
|
||||
user_id: uuid::Uuid,
|
||||
base_url: &str,
|
||||
) -> Result<Vec<SocialActor>, DomainError>;
|
||||
async fn get_followers(&self, user_id: uuid::Uuid) -> Result<Vec<SocialActor>, DomainError>;
|
||||
|
||||
async fn get_pending_followers(
|
||||
&self,
|
||||
user_id: uuid::Uuid,
|
||||
base_url: &str,
|
||||
) -> Result<Vec<SocialActor>, DomainError>;
|
||||
|
||||
async fn get_pending_following(
|
||||
&self,
|
||||
user_id: uuid::Uuid,
|
||||
) -> Result<Vec<SocialActor>, DomainError>;
|
||||
|
||||
async fn count_following(&self, user_id: uuid::Uuid) -> Result<usize, DomainError>;
|
||||
|
||||
async fn count_followers(&self, user_id: uuid::Uuid) -> Result<usize, DomainError>;
|
||||
|
||||
async fn is_following(
|
||||
async fn count_pending_followers(&self, user_id: uuid::Uuid) -> Result<usize, DomainError>;
|
||||
|
||||
async fn get_relation(
|
||||
&self,
|
||||
follower_id: uuid::Uuid,
|
||||
viewer_id: uuid::Uuid,
|
||||
target_actor_url: &str,
|
||||
) -> Result<bool, DomainError>;
|
||||
) -> Result<FollowRelation, DomainError>;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod auth;
|
||||
pub mod diary;
|
||||
pub mod events;
|
||||
pub mod federated_profile;
|
||||
pub mod federation;
|
||||
pub mod follow;
|
||||
pub mod goals;
|
||||
pub mod image_fetcher;
|
||||
@@ -22,6 +23,7 @@ pub use auth::*;
|
||||
pub use diary::*;
|
||||
pub use events::*;
|
||||
pub use federated_profile::*;
|
||||
pub use federation::*;
|
||||
pub use follow::*;
|
||||
pub use goals::*;
|
||||
pub use image_fetcher::*;
|
||||
|
||||
@@ -2,7 +2,7 @@ use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
errors::DomainError,
|
||||
value_objects::{SocialActor, SocialIdentity, UserId},
|
||||
value_objects::{FollowRelation, SocialActor, SocialIdentity, UserId},
|
||||
};
|
||||
|
||||
// ── NoopRemoteWatchlistRepository ─────────────────────────────────────────────
|
||||
@@ -73,7 +73,7 @@ impl super::SocialCommand for NoopSocialCommand {
|
||||
pub struct NoopSocialQuery;
|
||||
|
||||
#[async_trait]
|
||||
impl super::SocialQuery for NoopSocialQuery {
|
||||
impl super::FollowGraphQuery for NoopSocialQuery {
|
||||
async fn get_following(&self, _: &UserId) -> Result<Vec<SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
@@ -83,18 +83,32 @@ impl super::SocialQuery for NoopSocialQuery {
|
||||
async fn get_pending_followers(&self, _: &UserId) -> Result<Vec<SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn get_pending_following(&self, _: &UserId) -> Result<Vec<SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn count_following(&self, _: &UserId) -> Result<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn count_followers(&self, _: &UserId) -> Result<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn count_pending_followers(&self, _: &UserId) -> Result<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn get_relation(
|
||||
&self,
|
||||
_: &UserId,
|
||||
_: &SocialIdentity,
|
||||
) -> Result<FollowRelation, DomainError> {
|
||||
Ok(FollowRelation::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl super::BlockQuery for NoopSocialQuery {
|
||||
async fn get_blocked(&self, _: &UserId) -> Result<Vec<SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn is_following(&self, _: &UserId, _: &SocialIdentity) -> Result<bool, DomainError> {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
// ── NoopFederationAdminQuery ─────────────────────────────────────────────────
|
||||
@@ -110,3 +124,70 @@ impl super::FederationAdminQuery for NoopFederationAdminQuery {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
// ── NoopApDocument ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Stub used when federation is disabled — every operation is a no-op.
|
||||
pub struct NoopApDocument;
|
||||
|
||||
#[async_trait]
|
||||
impl super::ApDocumentPort for NoopApDocument {
|
||||
async fn actor_json(&self, _: &str) -> Result<String, DomainError> {
|
||||
Ok(String::new())
|
||||
}
|
||||
async fn followers_collection_json(
|
||||
&self,
|
||||
_: uuid::Uuid,
|
||||
_: Option<u32>,
|
||||
) -> Result<String, DomainError> {
|
||||
Ok(String::new())
|
||||
}
|
||||
async fn following_collection_json(
|
||||
&self,
|
||||
_: uuid::Uuid,
|
||||
_: Option<u32>,
|
||||
) -> Result<String, DomainError> {
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
// ── NoopInstanceBlocklist ────────────────────────────────────────────────────
|
||||
|
||||
/// Stub used when federation is disabled — every operation is a no-op.
|
||||
pub struct NoopInstanceBlocklist;
|
||||
|
||||
#[async_trait]
|
||||
impl super::InstanceBlocklistPort for NoopInstanceBlocklist {
|
||||
async fn get_blocked_domains(
|
||||
&self,
|
||||
) -> Result<Vec<crate::models::BlockedDomainInfo>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn add_blocked_domain(&self, _: &str, _: Option<&str>) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn remove_blocked_domain(&self, _: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ── NoopApBackfill ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Stub used when federation is disabled — every operation is a no-op.
|
||||
pub struct NoopApBackfill;
|
||||
|
||||
#[async_trait]
|
||||
impl super::ApBackfillPort for NoopApBackfill {
|
||||
async fn get_following(
|
||||
&self,
|
||||
_: uuid::Uuid,
|
||||
) -> Result<Vec<crate::models::FollowedActorInfo>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn import_remote_outbox(&self, _: &str, _: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn run_backfill_for_follower(&self, _: uuid::Uuid, _: String) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
DiaryEntry, FederationFlags, RemoteActorInfo, RemoteGoalEntry, RemoteWatchlistEntry,
|
||||
WatchlistWithMovie,
|
||||
},
|
||||
value_objects::{FollowTarget, MovieId, SocialActor, SocialIdentity, UserId},
|
||||
value_objects::{FollowRelation, FollowTarget, MovieId, SocialActor, SocialIdentity, UserId},
|
||||
};
|
||||
|
||||
// ── Unified social ports (ADR-0002) ─────────────────────────────────────────
|
||||
@@ -43,24 +43,31 @@ pub trait SocialCommand: Send + Sync {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait SocialQuery: Send + Sync {
|
||||
pub trait FollowGraphQuery: Send + Sync {
|
||||
async fn get_following(&self, user: &UserId) -> Result<Vec<SocialActor>, DomainError>;
|
||||
|
||||
async fn get_followers(&self, user: &UserId) -> Result<Vec<SocialActor>, DomainError>;
|
||||
|
||||
async fn get_pending_followers(&self, user: &UserId) -> Result<Vec<SocialActor>, DomainError>;
|
||||
|
||||
async fn get_pending_following(&self, user: &UserId) -> Result<Vec<SocialActor>, DomainError>;
|
||||
|
||||
async fn count_following(&self, user: &UserId) -> Result<usize, DomainError>;
|
||||
|
||||
async fn count_followers(&self, user: &UserId) -> Result<usize, DomainError>;
|
||||
|
||||
async fn get_blocked(&self, user: &UserId) -> Result<Vec<SocialActor>, DomainError>;
|
||||
async fn count_pending_followers(&self, user: &UserId) -> Result<usize, DomainError>;
|
||||
|
||||
async fn is_following(
|
||||
async fn get_relation(
|
||||
&self,
|
||||
follower: &UserId,
|
||||
viewer: &UserId,
|
||||
target: &SocialIdentity,
|
||||
) -> Result<bool, DomainError>;
|
||||
) -> Result<FollowRelation, DomainError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait BlockQuery: Send + Sync {
|
||||
async fn get_blocked(&self, user: &UserId) -> Result<Vec<SocialActor>, DomainError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -119,3 +126,223 @@ pub trait LocalApContentQuery: Send + Sync {
|
||||
limit: usize,
|
||||
) -> Result<Vec<DiaryEntry>, DomainError>;
|
||||
}
|
||||
|
||||
/// Resolves a `FollowTarget` (a handle or an already-known identity) to the
|
||||
/// `SocialIdentity` that should be dispatched on — local vs. remote.
|
||||
///
|
||||
/// Split out from `SocialCommand::follow` because the decision must happen
|
||||
/// once, before dispatch, and both `LocalSocialService` and
|
||||
/// `CompositeSocialAdapter` need to observe its result rather than each
|
||||
/// re-deriving it (which would need `UserRepository` on the composite and
|
||||
/// duplicate the local/remote fallthrough logic).
|
||||
#[async_trait]
|
||||
pub trait FollowTargetResolver: Send + Sync {
|
||||
async fn resolve_target(&self, target: &FollowTarget) -> Result<SocialIdentity, DomainError>;
|
||||
}
|
||||
|
||||
/// Performs the local follow write for a target whose `SocialIdentity` a caller
|
||||
/// has *already* resolved via `FollowTargetResolver::resolve_target`.
|
||||
///
|
||||
/// Exists so `CompositeSocialAdapter` — which must call `resolve_target` first
|
||||
/// to decide local vs. remote dispatch — doesn't then hand the raw
|
||||
/// `FollowTarget` to `SocialCommand::follow` and pay for a second resolution of
|
||||
/// the same handle. That second resolution wasn't just wasteful: on a
|
||||
/// federation-ON deployment it could also change the answer (the target user
|
||||
/// deleted between the two calls, e.g.), turning a local follow into a bogus
|
||||
/// "federation is not enabled" error.
|
||||
///
|
||||
/// `LocalSocialService::follow` is `resolve_target_identity` then
|
||||
/// `follow_resolved` — the self-follow guard and the two-sided write live only
|
||||
/// in the latter, so a caller that already has the identity (the composite)
|
||||
/// and one that doesn't (federation-off `SocialCommand::follow`, wired
|
||||
/// directly) both end up running the exact same write path.
|
||||
#[async_trait]
|
||||
pub trait ResolvedFollow: Send + Sync {
|
||||
async fn follow_resolved(
|
||||
&self,
|
||||
follower: &UserId,
|
||||
target: &SocialIdentity,
|
||||
) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
/// The subset of social behavior that needs no ActivityPub — everything a
|
||||
/// single-instance deployment can do with only its own database.
|
||||
///
|
||||
/// A marker supertrait with a blanket impl, so any type implementing all five
|
||||
/// parts is usable as `Arc<dyn LocalSocial>` without a separate registration.
|
||||
/// Same shape as `k_ap::FollowRepository` over its five follow traits.
|
||||
///
|
||||
/// `CompositeSocialAdapter` takes one of these for its local branches, which is
|
||||
/// what lets the `activitypub` crate stay unaware of `application`.
|
||||
pub trait LocalSocial:
|
||||
SocialCommand + FollowGraphQuery + BlockQuery + FollowTargetResolver + ResolvedFollow
|
||||
{
|
||||
}
|
||||
|
||||
impl<T: SocialCommand + FollowGraphQuery + BlockQuery + FollowTargetResolver + ResolvedFollow>
|
||||
LocalSocial for T
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod local_social_tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct Stub;
|
||||
|
||||
#[async_trait]
|
||||
impl SocialCommand for Stub {
|
||||
async fn follow(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::FollowTarget,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn unfollow(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn accept_follow(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn reject_follow(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn remove_follower(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn block(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn unblock(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FollowGraphQuery for Stub {
|
||||
async fn get_following(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn get_followers(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn get_pending_followers(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn get_pending_following(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn count_following(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn count_followers(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn count_pending_followers(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<usize, DomainError> {
|
||||
Ok(0)
|
||||
}
|
||||
async fn get_relation(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &crate::value_objects::SocialIdentity,
|
||||
) -> Result<crate::value_objects::FollowRelation, DomainError> {
|
||||
Err(DomainError::NotFound("stub".into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BlockQuery for Stub {
|
||||
async fn get_blocked(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
) -> Result<Vec<crate::value_objects::SocialActor>, DomainError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FollowTargetResolver for Stub {
|
||||
async fn resolve_target(
|
||||
&self,
|
||||
target: &crate::value_objects::FollowTarget,
|
||||
) -> Result<SocialIdentity, DomainError> {
|
||||
match target {
|
||||
crate::value_objects::FollowTarget::Identity(id) => Ok(id.clone()),
|
||||
crate::value_objects::FollowTarget::Handle(handle) => Ok(SocialIdentity::Remote {
|
||||
actor_url: handle.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ResolvedFollow for Stub {
|
||||
async fn follow_resolved(
|
||||
&self,
|
||||
_: &crate::value_objects::UserId,
|
||||
_: &SocialIdentity,
|
||||
) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The blanket impl must make any type implementing all five usable as
|
||||
/// `Arc<dyn LocalSocial>`, and `LocalSocial` must stay object-safe.
|
||||
#[test]
|
||||
fn blanket_impl_yields_a_trait_object() {
|
||||
let local: Arc<dyn LocalSocial> = Arc::new(Stub);
|
||||
// Reachable through each supertrait without a separate Arc.
|
||||
let _: &dyn SocialCommand = local.as_ref();
|
||||
let _: &dyn FollowGraphQuery = local.as_ref();
|
||||
let _: &dyn BlockQuery = local.as_ref();
|
||||
let _: &dyn FollowTargetResolver = local.as_ref();
|
||||
let _: &dyn ResolvedFollow = local.as_ref();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user