Compare commits
3 Commits
72a3c2f3cf
...
c5eeb147a5
Author | SHA1 | Date |
---|---|---|
projectmoon | c5eeb147a5 | |
projectmoon | 8f6e7a0d02 | |
projectmoon | 6a73ee7a43 |
|
@ -1,16 +1,31 @@
|
||||||
use crate::error::GementionError;
|
use crate::error::GementionError;
|
||||||
use crate::models::mentions::{Mention, MentionUpload};
|
use crate::models::mentions::{C2SMentionRequest, Mention};
|
||||||
use crate::models::web::MentionResponse;
|
use crate::models::web::MentionResponse;
|
||||||
use crate::{verification::verify};
|
use crate::validation::Validation;
|
||||||
|
use crate::verification::verify;
|
||||||
use fluffer::Client;
|
use fluffer::Client;
|
||||||
|
|
||||||
// Receive a mention over Titan.
|
/// Receive a client-to-server Gemention: i.e. a user posting a
|
||||||
// - Make Gemini request to see if target page supports gemention.
|
/// comment to a gemention-enabled page. The client must identify
|
||||||
// - If so, store mention in DB.
|
/// themselves with a cert that at least has a username.
|
||||||
pub(crate) async fn receive_mention(client: Client) -> Result<MentionResponse, GementionError> {
|
pub(crate) async fn receive_c2s_gemention(
|
||||||
// TODO change to return MentionResponse or something like that.
|
client: Client,
|
||||||
let titan = MentionUpload::try_from(&client)?;
|
) -> Result<MentionResponse, GementionError> {
|
||||||
let mut mention = Mention::try_from(titan)?;
|
let mention_upload = C2SMentionRequest::try_from(&client)?.validate()?;
|
||||||
|
let mut mention = Mention::try_from(mention_upload)?;
|
||||||
|
verify(&mut mention).await?;
|
||||||
|
|
||||||
|
Ok(MentionResponse::from(mention))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive a server-to-server Gemention: another capsule linking to a
|
||||||
|
/// gemention-enabled page on this capsule. There is an exchange of
|
||||||
|
/// titan keys so that the servers can talk to one another.
|
||||||
|
pub(crate) async fn receive_s2s_gemention(
|
||||||
|
client: Client,
|
||||||
|
) -> Result<MentionResponse, GementionError> {
|
||||||
|
let mention_upload = C2SMentionRequest::try_from(&client)?;
|
||||||
|
let mut mention = Mention::try_from(mention_upload)?;
|
||||||
verify(&mut mention).await?;
|
verify(&mut mention).await?;
|
||||||
|
|
||||||
Ok(MentionResponse::from(mention))
|
Ok(MentionResponse::from(mention))
|
||||||
|
|
|
@ -4,6 +4,8 @@ use async_trait::async_trait;
|
||||||
use fluffer::GemBytes;
|
use fluffer::GemBytes;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::validation::c2s::C2SValidationError;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub(crate) enum GementionError {
|
pub(crate) enum GementionError {
|
||||||
#[error("No content found for target")]
|
#[error("No content found for target")]
|
||||||
|
@ -30,6 +32,9 @@ pub(crate) enum GementionError {
|
||||||
#[error("value was not utf8: {0}")]
|
#[error("value was not utf8: {0}")]
|
||||||
Utf8Error(#[from] Utf8Error),
|
Utf8Error(#[from] Utf8Error),
|
||||||
|
|
||||||
|
#[error("c2s validation error: {0}")]
|
||||||
|
C2SValidationError(#[from] C2SValidationError),
|
||||||
|
|
||||||
#[error("generic error: {0}")]
|
#[error("generic error: {0}")]
|
||||||
UnclassifiedError(#[from] anyhow::Error),
|
UnclassifiedError(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ use fluffer::AppErr;
|
||||||
mod comments;
|
mod comments;
|
||||||
mod error;
|
mod error;
|
||||||
mod models;
|
mod models;
|
||||||
|
mod validation;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod verification;
|
mod verification;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use fluffer::{Client, GemBytes};
|
use fluffer::{Client, GemBytes};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::error::GementionError;
|
use crate::error::GementionError;
|
||||||
|
|
||||||
|
@ -28,15 +29,20 @@ fn parse_content_body(raw_body: &[u8]) -> Result<&str, GementionError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wraps an incoming Titan request and provides handy methods for
|
/// Wraps an incoming Titan request and provides handy methods for
|
||||||
/// extracting the expected information.
|
/// extracting the expected information for a client-to-server
|
||||||
pub(crate) struct MentionUpload<'a> {
|
/// gemention.
|
||||||
|
pub(crate) struct C2SMentionRequest<'a> {
|
||||||
client: &'a Client,
|
client: &'a Client,
|
||||||
|
accessed_url: &'a Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MentionUpload<'a> {
|
impl<'a> C2SMentionRequest<'a> {
|
||||||
pub fn from_client(client: &'a Client) -> Result<MentionUpload, GementionError> {
|
pub fn from_client(client: &'a Client) -> Result<C2SMentionRequest, GementionError> {
|
||||||
if let Some(_) = client.titan {
|
if let Some(_) = client.titan {
|
||||||
Ok(Self { client: &client })
|
Ok(Self {
|
||||||
|
client: &client,
|
||||||
|
accessed_url: &client.url,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(GementionError::NotTitanResource)
|
Err(GementionError::NotTitanResource)
|
||||||
}
|
}
|
||||||
|
@ -58,6 +64,10 @@ impl<'a> MentionUpload<'a> {
|
||||||
titan!(self).size
|
titan!(self).size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn certificate(&self) -> Option<String> {
|
||||||
|
self.client.certificate()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn fingerprint(&self) -> Option<String> {
|
pub fn fingerprint(&self) -> Option<String> {
|
||||||
self.client.fingerprint()
|
self.client.fingerprint()
|
||||||
}
|
}
|
||||||
|
@ -73,13 +83,18 @@ impl<'a> MentionUpload<'a> {
|
||||||
pub fn content(&self) -> Result<&str, GementionError> {
|
pub fn content(&self) -> Result<&str, GementionError> {
|
||||||
parse_content_body(self.raw_body())
|
parse_content_body(self.raw_body())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The full URL that was accessed for the incoming request.
|
||||||
|
pub fn accessed_url(&self) -> &Url {
|
||||||
|
self.accessed_url
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TryFrom<&'a Client> for MentionUpload<'a> {
|
impl<'a> TryFrom<&'a Client> for C2SMentionRequest<'a> {
|
||||||
type Error = GementionError;
|
type Error = GementionError;
|
||||||
|
|
||||||
fn try_from(client: &'a Client) -> Result<Self, Self::Error> {
|
fn try_from(client: &'a Client) -> Result<Self, Self::Error> {
|
||||||
MentionUpload::from_client(client)
|
C2SMentionRequest::from_client(client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +106,7 @@ pub(crate) enum MentionType {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct Mention {
|
pub(crate) struct Mention {
|
||||||
|
receiving_endpoint: Url,
|
||||||
mention_type: MentionType,
|
mention_type: MentionType,
|
||||||
target: String,
|
target: String,
|
||||||
user: String,
|
user: String,
|
||||||
|
@ -103,6 +119,14 @@ impl Mention {
|
||||||
&self.target
|
&self.target
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn receiving_endpoint(&self) -> &Url {
|
||||||
|
&self.receiving_endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn target_url(&self) -> Url {
|
||||||
|
Url::parse(&format!("gemini://{}", &self.target)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mention_type(&self) -> &MentionType {
|
pub fn mention_type(&self) -> &MentionType {
|
||||||
&self.mention_type
|
&self.mention_type
|
||||||
}
|
}
|
||||||
|
@ -125,12 +149,13 @@ impl Mention {
|
||||||
|
|
||||||
fn valid_gemtext(&self) -> String {
|
fn valid_gemtext(&self) -> String {
|
||||||
let headline = format!("## {} from {}", self.mention_type, self.user);
|
let headline = format!("## {} from {}", self.mention_type, self.user);
|
||||||
|
let header = format!("=> {} In response to this page", self.target_url());
|
||||||
let content = match &self.mention_type {
|
let content = match &self.mention_type {
|
||||||
MentionType::Reply => self.content.clone().unwrap_or(String::from("[no content]")),
|
MentionType::Reply => self.content.clone().unwrap_or(String::from("[no content]")),
|
||||||
MentionType::Like => format!("{} liked this.", self.user),
|
MentionType::Like => format!("{} liked this.", self.user),
|
||||||
};
|
};
|
||||||
|
|
||||||
format!("{}\n\n{}", headline, content)
|
format!("{}\n\n{}\n\n### Content\n{}", headline, header, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn invalid_gemtext(&self) -> String {
|
fn invalid_gemtext(&self) -> String {
|
||||||
|
@ -182,10 +207,10 @@ impl TryFrom<&[u8]> for MentionType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TryFrom<MentionUpload<'a>> for Mention {
|
impl<'a> TryFrom<C2SMentionRequest<'a>> for Mention {
|
||||||
type Error = GementionError;
|
type Error = GementionError;
|
||||||
|
|
||||||
fn try_from(resource: MentionUpload<'a>) -> Result<Self, Self::Error> {
|
fn try_from(resource: C2SMentionRequest<'a>) -> Result<Self, Self::Error> {
|
||||||
if resource.mime() == "text/plain" {
|
if resource.mime() == "text/plain" {
|
||||||
// Be flexible on mention type: if first line isn't a
|
// Be flexible on mention type: if first line isn't a
|
||||||
// mention type, just assume reply.
|
// mention type, just assume reply.
|
||||||
|
@ -204,6 +229,15 @@ impl<'a> TryFrom<MentionUpload<'a>> for Mention {
|
||||||
.username()
|
.username()
|
||||||
.ok_or(GementionError::UsernameNotProvided)?;
|
.ok_or(GementionError::UsernameNotProvided)?;
|
||||||
|
|
||||||
|
let receiving_endpoint = Url::parse(
|
||||||
|
resource
|
||||||
|
.accessed_url()
|
||||||
|
.as_str()
|
||||||
|
.split(&target)
|
||||||
|
.next()
|
||||||
|
.ok_or(GementionError::InvalidBody)?,
|
||||||
|
)?;
|
||||||
|
|
||||||
let content = resource.content()?.to_owned();
|
let content = resource.content()?.to_owned();
|
||||||
let content = if !content.is_empty() {
|
let content = if !content.is_empty() {
|
||||||
Some(content)
|
Some(content)
|
||||||
|
@ -216,6 +250,7 @@ impl<'a> TryFrom<MentionUpload<'a>> for Mention {
|
||||||
mention_type,
|
mention_type,
|
||||||
content,
|
content,
|
||||||
target,
|
target,
|
||||||
|
receiving_endpoint,
|
||||||
verification_status: VerificationStatus::NotYetVerified,
|
verification_status: VerificationStatus::NotYetVerified,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
use crate::error::GementionError;
|
use crate::error::GementionError;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use super::mentions::Mention;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum VerificationStatus {
|
pub(crate) enum VerificationStatus {
|
||||||
Verified {
|
Verified {
|
||||||
|
@ -50,7 +48,7 @@ pub(crate) enum VerificationFailureReason {
|
||||||
|
|
||||||
/// One or more mention links exist, but they are not to this
|
/// One or more mention links exist, but they are not to this
|
||||||
/// endpoint, or for this page.
|
/// endpoint, or for this page.
|
||||||
MentionLinkIncorrect,
|
MentionLinkIncorrect(String),
|
||||||
|
|
||||||
/// There was an error during the verification process.
|
/// There was an error during the verification process.
|
||||||
Error(GementionError),
|
Error(GementionError),
|
||||||
|
@ -60,7 +58,9 @@ impl ToString for VerificationFailureReason {
|
||||||
fn to_string(&self) -> String {
|
fn to_string(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::NoMentionLinkFound => String::from("No mention link found"),
|
Self::NoMentionLinkFound => String::from("No mention link found"),
|
||||||
Self::MentionLinkIncorrect => String::from("Mention link points to wrong target"),
|
Self::MentionLinkIncorrect(endpoint) => {
|
||||||
|
format!("Target URL does not accept gementions from endpoint: {}", endpoint)
|
||||||
|
}
|
||||||
Self::Error(err) => err.to_string(),
|
Self::Error(err) => err.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ impl From<Mention> for MentionResponse {
|
||||||
match mention.verify_status() {
|
match mention.verify_status() {
|
||||||
VerificationStatus::Verified { .. } => Self::VerifiedMention(mention),
|
VerificationStatus::Verified { .. } => Self::VerifiedMention(mention),
|
||||||
VerificationStatus::Invalid(_) => Self::InvalidMention(mention),
|
VerificationStatus::Invalid(_) => Self::InvalidMention(mention),
|
||||||
VerificationStatus::NotYetVerified => Self::InvalidMention(mention)
|
VerificationStatus::NotYetVerified => Self::InvalidMention(mention),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,9 +45,11 @@ impl MentionResponse {
|
||||||
|
|
||||||
pub fn description(&self) -> String {
|
pub fn description(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::VerifiedMention(_) => "You have successfully submitted a gemention.",
|
Self::VerifiedMention(_) => {
|
||||||
|
"You have successfully submitted a gemention. It is shown below."
|
||||||
|
}
|
||||||
Self::InvalidMention(_) => {
|
Self::InvalidMention(_) => {
|
||||||
"There was an error submitting the gemention, detailed below."
|
"There was an error submitting the gemention. The error is detailed below."
|
||||||
}
|
}
|
||||||
Self::NotTitanRequest => "Your request was not a Titan protocol request.",
|
Self::NotTitanRequest => "Your request was not a Titan protocol request.",
|
||||||
}
|
}
|
||||||
|
@ -68,7 +70,7 @@ impl GemBytes for MentionResponse {
|
||||||
let status_line = self.status_line();
|
let status_line = self.status_line();
|
||||||
let body = if self.should_have_body() {
|
let body = if self.should_have_body() {
|
||||||
format!(
|
format!(
|
||||||
"{}\n\n{}\n\n{},",
|
"{}\n\n{}\n\n{}",
|
||||||
self.headline(),
|
self.headline(),
|
||||||
self.description(),
|
self.description(),
|
||||||
self.mention_body()
|
self.mention_body()
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use fluffer::App;
|
use fluffer::App;
|
||||||
use crate::comments::receive_mention;
|
use crate::comments::{receive_c2s_gemention, receive_s2s_gemention};
|
||||||
|
|
||||||
pub(crate) fn create_app() -> App {
|
pub(crate) fn create_app() -> App {
|
||||||
App::default()
|
App::default()
|
||||||
.titan("/receive/*target", receive_mention, 20_000)
|
.titan("/c2s/receive/*target", receive_c2s_gemention, 2048)
|
||||||
|
.titan("/s2s/receive/*target", receive_s2s_gemention, 2048)
|
||||||
.route("/", |_| async {
|
.route("/", |_| async {
|
||||||
"# Welcome\n=> titan://localhost/receive/agnos.is/posts/webmentions-test.gmi Receive Mention"
|
"# Welcome\n=> titan://localhost/c2s/receive/agnos.is/posts/webmentions-test.gmi Receive Mention"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
use crate::{models::mentions::C2SMentionRequest, error::GementionError};
|
||||||
|
use super::Validation;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum C2SValidationError {
|
||||||
|
#[error("a username is required on the client certificate")]
|
||||||
|
UsernameRequired,
|
||||||
|
|
||||||
|
#[error("a client certificate is required")]
|
||||||
|
ClientCertificateRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validation for C2SMentionRequest<'_> {
|
||||||
|
type Error = C2SValidationError;
|
||||||
|
|
||||||
|
fn validate(self) -> Result<Self, Self::Error> {
|
||||||
|
if self.username().is_none() {
|
||||||
|
return Err(C2SValidationError::UsernameRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.certificate().is_none() {
|
||||||
|
return Err(C2SValidationError::ClientCertificateRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
pub(crate) mod c2s;
|
||||||
|
|
||||||
|
pub trait Validation {
|
||||||
|
type Error;
|
||||||
|
|
||||||
|
fn validate(self) -> Result<Self, Self::Error> where Self: Sized;
|
||||||
|
}
|
|
@ -7,13 +7,12 @@ use crate::error::GementionError;
|
||||||
use crate::models::mentions::Mention;
|
use crate::models::mentions::Mention;
|
||||||
use crate::models::verification::*;
|
use crate::models::verification::*;
|
||||||
|
|
||||||
const OUR_ENDPOINT: &'static str = "titan://localhost/receive/";
|
fn is_mention_link(endpoint: &Url, gemtext_link: &str) -> bool {
|
||||||
|
gemtext_link.starts_with(endpoint.as_str())
|
||||||
fn is_mention_link(gemtext_link: &str) -> bool {
|
|
||||||
gemtext_link.starts_with(OUR_ENDPOINT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scan_for_gemention_endpoints(
|
fn scan_for_gemention_endpoints(
|
||||||
|
endpoint: &Url,
|
||||||
meta: GeminiMetadata,
|
meta: GeminiMetadata,
|
||||||
ast: GeminiAst,
|
ast: GeminiAst,
|
||||||
) -> (VerificationSource, Vec<String>) {
|
) -> (VerificationSource, Vec<String>) {
|
||||||
|
@ -29,7 +28,7 @@ fn scan_for_gemention_endpoints(
|
||||||
.inner()
|
.inner()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|node| match node {
|
.filter_map(|node| match node {
|
||||||
GemtextNode::Link { ref to, .. } if is_mention_link(to) => Some(to),
|
GemtextNode::Link { ref to, .. } if is_mention_link(endpoint, to) => Some(to),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
|
@ -38,14 +37,17 @@ fn scan_for_gemention_endpoints(
|
||||||
(VerificationSource::Page, endpoints)
|
(VerificationSource::Page, endpoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_mentions<S: AsRef<str>>(
|
fn verify_mentions(
|
||||||
expected_link: S,
|
mention: &mut Mention,
|
||||||
source_and_endpoints: (VerificationSource, Vec<String>),
|
source_and_endpoints: (VerificationSource, Vec<String>),
|
||||||
) -> VerificationStatus {
|
) -> Result<VerificationStatus, GementionError> {
|
||||||
let (verification_source, mentions) = source_and_endpoints;
|
let (verification_source, mentions) = source_and_endpoints;
|
||||||
|
// TODO need to normalize url from page as well as ours, to make
|
||||||
|
// sure things like ports being in url or not are handled.
|
||||||
|
let expected_link = mention.receiving_endpoint().join(mention.target())?;
|
||||||
let expected_link = expected_link.as_ref();
|
let expected_link = expected_link.as_ref();
|
||||||
|
|
||||||
if mentions.len() > 0 {
|
let verification_status = if mentions.len() > 0 {
|
||||||
// We have links that go to our endpoint. Scan links for the
|
// We have links that go to our endpoint. Scan links for the
|
||||||
// one we expect (i.e. for the target), otherwise we say
|
// one we expect (i.e. for the target), otherwise we say
|
||||||
// incorrect link.
|
// incorrect link.
|
||||||
|
@ -62,16 +64,19 @@ fn verify_mentions<S: AsRef<str>>(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or(VerificationStatus::Invalid(
|
.unwrap_or(VerificationStatus::Invalid(
|
||||||
VerificationFailureReason::MentionLinkIncorrect,
|
VerificationFailureReason::MentionLinkIncorrect(
|
||||||
|
mention.receiving_endpoint().to_string(),
|
||||||
|
),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
VerificationStatus::Invalid(VerificationFailureReason::NoMentionLinkFound)
|
VerificationStatus::Invalid(VerificationFailureReason::NoMentionLinkFound)
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Ok(verification_status)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn verify_mention(mention: &mut Mention) -> Result<(), GementionError> {
|
async fn verify_mention(mention: &mut Mention) -> Result<(), GementionError> {
|
||||||
let url = Url::parse(&format!("gemini://{}", mention.target()))?;
|
let url = Url::parse(&format!("gemini://{}", mention.target()))?;
|
||||||
let expected_link = Url::parse(OUR_ENDPOINT)?.join(mention.target())?;
|
|
||||||
|
|
||||||
let resp = germ_request(&url).await?;
|
let resp = germ_request(&url).await?;
|
||||||
let meta = GeminiMetadata::from_string(resp.meta());
|
let meta = GeminiMetadata::from_string(resp.meta());
|
||||||
|
@ -82,8 +87,9 @@ async fn verify_mention(mention: &mut Mention) -> Result<(), GementionError> {
|
||||||
.ok_or(GementionError::NoContentFoundForTarget)?;
|
.ok_or(GementionError::NoContentFoundForTarget)?;
|
||||||
|
|
||||||
let ast = GeminiAst::from_string(content);
|
let ast = GeminiAst::from_string(content);
|
||||||
let source_and_endpoints = scan_for_gemention_endpoints(meta, ast);
|
let source_and_endpoints =
|
||||||
let status = verify_mentions(expected_link, source_and_endpoints);
|
scan_for_gemention_endpoints(mention.receiving_endpoint(), meta, ast);
|
||||||
|
let status = verify_mentions(mention, source_and_endpoints)?;
|
||||||
|
|
||||||
mention.set_verify_status(status);
|
mention.set_verify_status(status);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
Loading…
Reference in New Issue