mirror of
https://github.com/versia-pub/activitypub.git
synced 2025-12-06 06:38:20 +01:00
[feat]: lysand to ap for posts and users
This commit is contained in:
parent
bcab516a1f
commit
1174f92915
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
person::DbUser,
|
person::DbUser,
|
||||||
post::{DbPost, Note},
|
post::{DbPost, Note},
|
||||||
},
|
},
|
||||||
utils::generate_object_id,
|
utils::generate_random_object_id,
|
||||||
};
|
};
|
||||||
use activitypub_federation::{
|
use activitypub_federation::{
|
||||||
activity_sending::SendActivityTask,
|
activity_sending::SendActivityTask,
|
||||||
|
|
@ -39,7 +39,7 @@ impl CreatePost {
|
||||||
to: note.to.clone(),
|
to: note.to.clone(),
|
||||||
object: note,
|
object: note,
|
||||||
kind: CreateType::Create,
|
kind: CreateType::Create,
|
||||||
id: generate_object_id(data.domain())?,
|
id: generate_random_object_id(data.domain())?,
|
||||||
};
|
};
|
||||||
let create_with_context = WithContext::new_default(create);
|
let create_with_context = WithContext::new_default(create);
|
||||||
let sends = SendActivityTask::prepare(
|
let sends = SendActivityTask::prepare(
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use crate::{
|
||||||
database::StateHandle,
|
database::StateHandle,
|
||||||
entities::{follow_relation, post, user},
|
entities::{follow_relation, post, user},
|
||||||
error,
|
error,
|
||||||
utils::{generate_follow_accept_id, generate_object_id},
|
utils::{generate_follow_accept_id, generate_random_object_id},
|
||||||
DB,
|
DB,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ impl Follow {
|
||||||
actor: local_user.clone(),
|
actor: local_user.clone(),
|
||||||
object: followee.clone(),
|
object: followee.clone(),
|
||||||
kind: FollowType::Follow,
|
kind: FollowType::Follow,
|
||||||
id: generate_object_id(data.domain())?,
|
id: generate_random_object_id(data.domain())?,
|
||||||
};
|
};
|
||||||
let create_with_context = WithContext::new_default(create);
|
let create_with_context = WithContext::new_default(create);
|
||||||
let sends = SendActivityTask::prepare(
|
let sends = SendActivityTask::prepare(
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ pub struct Model {
|
||||||
pub created_at: chrono::DateTime<Utc>,
|
pub created_at: chrono::DateTime<Utc>,
|
||||||
#[sea_orm(column_type = "Timestamp")]
|
#[sea_orm(column_type = "Timestamp")]
|
||||||
pub updated_at: Option<chrono::DateTime<Utc>>,
|
pub updated_at: Option<chrono::DateTime<Utc>>,
|
||||||
#[sea_orm(column_type = "Timestamp")]
|
pub reblog_id: Option<String>,
|
||||||
pub reblog_id: Option<chrono::DateTime<Utc>>,
|
|
||||||
pub content_type: String,
|
pub content_type: String,
|
||||||
pub visibility: String,
|
pub visibility: String,
|
||||||
pub reply_id: Option<String>,
|
pub reply_id: Option<String>,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use activitypub_federation::{fetch::object_id::ObjectId, http_signatures::generate_actor_keypair};
|
use activitypub_federation::{fetch::object_id::ObjectId, http_signatures::generate_actor_keypair};
|
||||||
use activitystreams_kinds::public;
|
use activitystreams_kinds::public;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, TimeZone, Utc};
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||||
use anyhow::anyhow;
|
use anyhow::{anyhow, Ok};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{database::State, entities::{self, post, prelude, user}, objects::post::Mention, utils::{generate_object_id, generate_user_id}, API_DOMAIN, DB, FEDERATION_CONFIG, LYSAND_DOMAIN};
|
use crate::{database::State, entities::{self, post, prelude, user}, objects::post::Mention, utils::{generate_object_id, generate_user_id}, API_DOMAIN, DB, FEDERATION_CONFIG, LYSAND_DOMAIN};
|
||||||
|
|
@ -27,7 +27,7 @@ pub async fn db_user_from_url(url: Url) -> anyhow::Result<entities::user::Model>
|
||||||
if !url.domain().eq(&Some(LYSAND_DOMAIN.as_str())) {
|
if !url.domain().eq(&Some(LYSAND_DOMAIN.as_str())) {
|
||||||
return Err(anyhow!("not lysands domain"));
|
return Err(anyhow!("not lysands domain"));
|
||||||
}
|
}
|
||||||
let user_res = prelude::User::find().filter(entities::user::Column::Url.eq(url.to_string())).one(DB.get().unwrap()).await?;
|
let user_res: Option<user::Model> = prelude::User::find().filter(entities::user::Column::Url.eq(url.to_string())).one(DB.get().unwrap()).await?;
|
||||||
|
|
||||||
if let Some(user) = user_res {
|
if let Some(user) = user_res {
|
||||||
Ok(user)
|
Ok(user)
|
||||||
|
|
@ -64,8 +64,8 @@ pub async fn fetch_note_from_url(url: Url) -> anyhow::Result<super::objects::Not
|
||||||
Ok(request.json::<super::objects::Note>().await?)
|
Ok(request.json::<super::objects::Note>().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn receive_lysand_note(note: Note, db_id: String) -> anyhow::Result<()> {
|
pub async fn receive_lysand_note(note: Note, db_id: String) -> anyhow::Result<crate::objects::post::Note> {
|
||||||
let author: entities::user::Model = db_user_from_url(note.author.clone()).await?;
|
let lysand_author: entities::user::Model = db_user_from_url(note.author.clone()).await?;
|
||||||
let user_res = prelude::User::find_by_id(db_id).one(DB.get().unwrap()).await;
|
let user_res = prelude::User::find_by_id(db_id).one(DB.get().unwrap()).await;
|
||||||
if user_res.is_err() {
|
if user_res.is_err() {
|
||||||
println!("{}", user_res.as_ref().unwrap_err());
|
println!("{}", user_res.as_ref().unwrap_err());
|
||||||
|
|
@ -75,23 +75,45 @@ pub async fn receive_lysand_note(note: Note, db_id: String) -> anyhow::Result<()
|
||||||
let data = FEDERATION_CONFIG.get().unwrap();
|
let data = FEDERATION_CONFIG.get().unwrap();
|
||||||
let id: ObjectId<post::Model> = generate_object_id(data.domain(), ¬e.id.to_string())?.into();
|
let id: ObjectId<post::Model> = generate_object_id(data.domain(), ¬e.id.to_string())?.into();
|
||||||
let user_id = generate_user_id(data.domain(), &target.id.to_string())?;
|
let user_id = generate_user_id(data.domain(), &target.id.to_string())?;
|
||||||
let user = fetch_user_from_url(user_id).await?;
|
let user = fetch_user_from_url(note.author.clone()).await?;
|
||||||
let mut tag: Vec<Mention> = Vec::new();
|
let mut tag: Vec<Mention> = Vec::new();
|
||||||
for l_tag in note.mentions.clone().unwrap_or_default() {
|
for l_tag in note.mentions.clone().unwrap_or_default() {
|
||||||
tag.push(Mention { href: l_tag, //todo convert to ap url
|
tag.push(Mention { href: l_tag, //TODO convert to ap url
|
||||||
kind: Default::default(), })
|
kind: Default::default(), })
|
||||||
}
|
}
|
||||||
let to = match note.visibility.clone().unwrap_or(super::objects::VisibilityType::Public) {
|
let to = match note.visibility.clone().unwrap_or(super::objects::VisibilityType::Public) {
|
||||||
super::objects::VisibilityType::Public => vec![public(), Url::parse(&author.followers.unwrap_or_default())?],
|
super::objects::VisibilityType::Public => vec![public(), Url::parse(&user.followers.to_string().as_str())?],
|
||||||
super::objects::VisibilityType::Followers => vec![Url::parse(&author.followers.unwrap_or_default())?],
|
super::objects::VisibilityType::Followers => vec![Url::parse(&user.followers.to_string().as_str())?],
|
||||||
super::objects::VisibilityType::Direct => note.mentions.unwrap_or_default(),
|
super::objects::VisibilityType::Direct => note.mentions.unwrap_or_default(),
|
||||||
super::objects::VisibilityType::Unlisted => vec![Url::parse(&author.followers.unwrap_or_default())?],
|
super::objects::VisibilityType::Unlisted => vec![Url::parse(&user.followers.to_string().as_str())?],
|
||||||
};
|
};
|
||||||
let cc = match note.visibility.unwrap_or(super::objects::VisibilityType::Public) {
|
let cc = match note.visibility.clone().unwrap_or(super::objects::VisibilityType::Public) {
|
||||||
super::objects::VisibilityType::Unlisted => Some(vec![public()]),
|
super::objects::VisibilityType::Unlisted => Some(vec![public()]),
|
||||||
_ => None
|
_ => None
|
||||||
};
|
};
|
||||||
let reply: Option<ObjectId<entities::post::Model>> = if let Some(rep) = note.replies_to {
|
let reply: Option<ObjectId<entities::post::Model>> = if let Some(rep) = note.replies_to.clone() {
|
||||||
|
let note = fetch_note_from_url(rep).await?;
|
||||||
|
let fake_rep_url = Url::parse(&format!(
|
||||||
|
"https://{}/lysand/apnote/{}",
|
||||||
|
API_DOMAIN.to_string(),
|
||||||
|
¬e.id.to_string()
|
||||||
|
))?;
|
||||||
|
Some(fake_rep_url.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let reply_string: Option<String> = if let Some(rep) = note.replies_to {
|
||||||
|
let note = fetch_note_from_url(rep).await?;
|
||||||
|
let fake_rep_url = Url::parse(&format!(
|
||||||
|
"https://{}/lysand/apnote/{}",
|
||||||
|
API_DOMAIN.to_string(),
|
||||||
|
¬e.id.to_string()
|
||||||
|
))?;
|
||||||
|
Some(fake_rep_url.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let quote_string: Option<String> = if let Some(rep) = note.quotes.clone() {
|
||||||
let note = fetch_note_from_url(rep).await?;
|
let note = fetch_note_from_url(rep).await?;
|
||||||
let fake_rep_url = Url::parse(&format!(
|
let fake_rep_url = Url::parse(&format!(
|
||||||
"https://{}/lysand/apnote/{}",
|
"https://{}/lysand/apnote/{}",
|
||||||
|
|
@ -109,12 +131,38 @@ pub async fn receive_lysand_note(note: Note, db_id: String) -> anyhow::Result<()
|
||||||
cc,
|
cc,
|
||||||
to,
|
to,
|
||||||
tag,
|
tag,
|
||||||
attributed_to: Url::parse(author.url.clone().as_str()).unwrap().into(),
|
attributed_to: Url::parse(user.uri.clone().as_str()).unwrap().into(),
|
||||||
content: option_content_format_text(note.content).await.unwrap_or_default(),
|
content: option_content_format_text(note.content).await.unwrap_or_default(),
|
||||||
in_reply_to: reply
|
in_reply_to: reply
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
let visibility = match note.visibility.clone().unwrap_or(super::objects::VisibilityType::Public) {
|
||||||
|
super::objects::VisibilityType::Public => "public",
|
||||||
|
super::objects::VisibilityType::Followers => "followers",
|
||||||
|
super::objects::VisibilityType::Direct => "direct",
|
||||||
|
super::objects::VisibilityType::Unlisted => "unlisted",
|
||||||
|
};
|
||||||
|
|
||||||
Ok(())
|
let post = entities::post::ActiveModel {
|
||||||
|
id: Set(note.id.to_string()),
|
||||||
|
creator: Set(lysand_author.id.clone()),
|
||||||
|
content: Set(ap_note.content.clone()),
|
||||||
|
sensitive: Set(ap_note.sensitive),
|
||||||
|
created_at: Set(Utc.timestamp_micros(note.created_at.unix_timestamp()).unwrap()),
|
||||||
|
local: Set(true),
|
||||||
|
updated_at: Set(Some(Utc::now())),
|
||||||
|
content_type: Set("Note".to_string()),
|
||||||
|
visibility: Set(visibility.to_string()),
|
||||||
|
title: Set(note.subject.clone()),
|
||||||
|
url: Set(ap_note.id.to_string()),
|
||||||
|
reply_id: Set(reply_string),
|
||||||
|
quoting_id: Set(quote_string),
|
||||||
|
spoiler_text: Set(note.subject),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
post.insert(DB.get().unwrap()).await?;
|
||||||
|
Ok(ap_note)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("User not found"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
0
src/lysand/funcs.rs
Normal file
0
src/lysand/funcs.rs
Normal file
24
src/lysand/http.rs
Normal file
24
src/lysand/http.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
use std::os::linux::raw::stat;
|
||||||
|
|
||||||
|
use activitypub_federation::{traits::Object, FEDERATION_CONTENT_TYPE};
|
||||||
|
use actix_web::{get, web, HttpResponse};
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||||
|
|
||||||
|
use crate::{database::State, entities::{post::{self, Entity}, prelude}, error, Response, DB, FEDERATION_CONFIG};
|
||||||
|
|
||||||
|
#[get("/apbridge/object/{post}")]
|
||||||
|
async fn post_manually(
|
||||||
|
path: web::Path<(String, String)>,
|
||||||
|
state: web::Data<State>,
|
||||||
|
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||||
|
let db = DB.get().unwrap();
|
||||||
|
|
||||||
|
let post = prelude::Post::find()
|
||||||
|
.filter(post::Column::Id.eq(path.0.as_str()))
|
||||||
|
.one(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let post = post.unwrap();
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().content_type(FEDERATION_CONTENT_TYPE).json(post.into_json(&FEDERATION_CONFIG.get().unwrap().to_request_data()).await?))
|
||||||
|
}
|
||||||
|
|
@ -2,3 +2,5 @@ pub mod objects;
|
||||||
pub mod superx;
|
pub mod superx;
|
||||||
pub mod test;
|
pub mod test;
|
||||||
pub mod conversion;
|
pub mod conversion;
|
||||||
|
pub mod funcs;
|
||||||
|
pub mod http;
|
||||||
|
|
@ -62,6 +62,7 @@ pub enum CategoryType {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum VisibilityType {
|
pub enum VisibilityType {
|
||||||
Public,
|
Public,
|
||||||
Unlisted,
|
Unlisted,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,19 @@ use crate::lysand::objects::SortAlphabetically;
|
||||||
|
|
||||||
use super::superx::request_client;
|
use super::superx::request_client;
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn test_user_serial() {
|
||||||
|
let client = request_client();
|
||||||
|
let response = client
|
||||||
|
.get("https://social.lysand.org/users/018ec082-0ae1-761c-b2c5-22275a611771")
|
||||||
|
.send()
|
||||||
|
.await.unwrap();
|
||||||
|
let user = super::superx::deserialize_user(response.text().await.unwrap()).await.unwrap();
|
||||||
|
let response_outbox = client.get(user.outbox.as_str()).send().await.unwrap();
|
||||||
|
let outbox = super::superx::deserialize_outbox(response_outbox.text().await.unwrap()).await.unwrap();
|
||||||
|
assert!(outbox.items.len() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn main() -> anyhow::Result<()> {
|
pub async fn main() -> anyhow::Result<()> {
|
||||||
let client = request_client();
|
let client = request_client();
|
||||||
|
|
||||||
|
|
@ -30,5 +43,13 @@ pub async fn main() -> anyhow::Result<()> {
|
||||||
println!("\n\n\nOutbox: ");
|
println!("\n\n\nOutbox: ");
|
||||||
print!("{:#?}", outbox);
|
print!("{:#?}", outbox);
|
||||||
|
|
||||||
|
println!("\n\n\nas AP:");
|
||||||
|
for item in outbox.items {
|
||||||
|
let ap_item = super::conversion::receive_lysand_note(item, "https://ap.lysand.org/example".to_string()).await?;
|
||||||
|
println!("{:#?}", ap_item);
|
||||||
|
let ap_json = serde_json::to_string_pretty(&SortAlphabetically(&ap_item))?;
|
||||||
|
println!("{}", ap_json);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
src/main.rs
13
src/main.rs
|
|
@ -28,7 +28,7 @@ use tokio::signal;
|
||||||
use tracing::{info, instrument::WithSubscriber};
|
use tracing::{info, instrument::WithSubscriber};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::utils::generate_object_id;
|
use crate::utils::generate_random_object_id;
|
||||||
use crate::{
|
use crate::{
|
||||||
activities::create_post::CreatePost,
|
activities::create_post::CreatePost,
|
||||||
database::{Config, State},
|
database::{Config, State},
|
||||||
|
|
@ -84,7 +84,8 @@ async fn post_manually(
|
||||||
href: Url::parse(&target.id)?,
|
href: Url::parse(&target.id)?,
|
||||||
kind: Default::default(),
|
kind: Default::default(),
|
||||||
};
|
};
|
||||||
let id: ObjectId<post::Model> = generate_object_id(data.domain())?.into();
|
// TODO change
|
||||||
|
let id: ObjectId<post::Model> = generate_random_object_id(data.domain())?.into();
|
||||||
let note = Note {
|
let note = Note {
|
||||||
kind: Default::default(),
|
kind: Default::default(),
|
||||||
id,
|
id,
|
||||||
|
|
@ -153,10 +154,6 @@ static FEDERATION_CONFIG: OnceLock<FederationConfig<State>> = OnceLock::new();
|
||||||
async fn main() -> actix_web::Result<(), anyhow::Error> {
|
async fn main() -> actix_web::Result<(), anyhow::Error> {
|
||||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
//TODO remove this
|
|
||||||
lysand::test::main().await?;
|
|
||||||
return Ok(());
|
|
||||||
|
|
||||||
let ap_id = Url::parse(&format!(
|
let ap_id = Url::parse(&format!(
|
||||||
"https://{}/{}",
|
"https://{}/{}",
|
||||||
API_DOMAIN.to_string(),
|
API_DOMAIN.to_string(),
|
||||||
|
|
@ -246,6 +243,10 @@ async fn main() -> actix_web::Result<(), anyhow::Error> {
|
||||||
.keep_alive(KeepAlive::Os)
|
.keep_alive(KeepAlive::Os)
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
|
//TODO remove this
|
||||||
|
lysand::test::main().await?;
|
||||||
|
return Ok(());
|
||||||
|
|
||||||
tokio::spawn(http_server);
|
tokio::spawn(http_server);
|
||||||
|
|
||||||
match signal::ctrl_c().await {
|
match signal::ctrl_c().await {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
activities::create_post::CreatePost,
|
activities::create_post::CreatePost, database::StateHandle, entities::{post, user}, error::Error, lysand::conversion::db_user_from_url, objects::person::DbUser, utils::generate_object_id
|
||||||
database::StateHandle,
|
|
||||||
entities::{post, user},
|
|
||||||
error::Error,
|
|
||||||
objects::person::DbUser,
|
|
||||||
utils::generate_object_id,
|
|
||||||
};
|
};
|
||||||
use activitypub_federation::{
|
use activitypub_federation::{
|
||||||
config::Data,
|
config::Data,
|
||||||
|
|
@ -14,7 +9,7 @@ use activitypub_federation::{
|
||||||
traits::{Actor, Object},
|
traits::{Actor, Object},
|
||||||
};
|
};
|
||||||
use activitystreams_kinds::link::MentionType;
|
use activitystreams_kinds::link::MentionType;
|
||||||
use sea_orm::{ActiveModelTrait, Set};
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
@ -57,14 +52,35 @@ impl Object for post::Model {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
async fn read_from_id(
|
async fn read_from_id(
|
||||||
_object_id: Url,
|
object_id: Url,
|
||||||
_data: &Data<Self::DataType>,
|
data: &Data<Self::DataType>,
|
||||||
) -> Result<Option<Self>, Self::Error> {
|
) -> Result<Option<Self>, Self::Error> {
|
||||||
|
let post = crate::entities::prelude::Post::find()
|
||||||
|
.filter(post::Column::Id.eq(object_id.to_string()))
|
||||||
|
.one(data.app_data().database_connection.clone().as_ref()).await;
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
||||||
todo!()
|
let creator = db_user_from_url(Url::parse(self.creator.as_str()).unwrap()).await?;
|
||||||
|
let to = match self.visibility.as_str() {
|
||||||
|
"public" => vec![public(), Url::parse(creator.followers.unwrap().as_str()).unwrap()],
|
||||||
|
"followers" => vec![Url::parse(creator.followers.unwrap().as_str()).unwrap()],
|
||||||
|
"direct" => vec![], //TODO: implement this
|
||||||
|
"unlisted" => vec![Url::parse(creator.followers.unwrap().as_str()).unwrap(), public()],
|
||||||
|
_ => vec![public()],
|
||||||
|
};
|
||||||
|
Ok(Note {
|
||||||
|
kind: Default::default(),
|
||||||
|
id: Url::parse(self.url.as_str()).unwrap().into(),
|
||||||
|
attributed_to: Url::parse(self.creator.as_str()).unwrap().into(),
|
||||||
|
to: to.clone(),
|
||||||
|
content: self.content,
|
||||||
|
in_reply_to: None,
|
||||||
|
tag: vec![],
|
||||||
|
sensitive: self.sensitive,
|
||||||
|
cc: Some(to),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn verify(
|
async fn verify(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue