mirror of
https://github.com/versia-pub/activitypub.git
synced 2025-12-06 06:38:20 +01:00
feat: basic lysand -> ap fetching api
This commit is contained in:
parent
b1af17c5d2
commit
a840feecb0
|
|
@ -4,18 +4,19 @@ use anyhow::{anyhow, Ok};
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
use chrono::{DateTime, TimeZone, Utc};
|
use chrono::{DateTime, TimeZone, Utc};
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||||
|
use time::OffsetDateTime;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::State,
|
database::State,
|
||||||
entities::{self, post, prelude, user},
|
entities::{self, post, prelude, user},
|
||||||
objects::post::Mention,
|
objects::post::Mention,
|
||||||
utils::{generate_object_id, generate_user_id},
|
utils::{generate_lysand_post_url, generate_object_id, generate_user_id},
|
||||||
API_DOMAIN, DB, FEDERATION_CONFIG, LYSAND_DOMAIN,
|
API_DOMAIN, DB, FEDERATION_CONFIG, LYSAND_DOMAIN,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
objects::{ContentFormat, Note},
|
objects::{CategoryType, ContentEntry, ContentFormat, Note, PublicKey},
|
||||||
superx::request_client,
|
superx::request_client,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -25,6 +26,114 @@ pub async fn fetch_user_from_url(url: Url) -> anyhow::Result<super::objects::Use
|
||||||
Ok(request.json::<super::objects::User>().await?)
|
Ok(request.json::<super::objects::User>().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn lysand_post_from_db(
|
||||||
|
post: entities::post::Model,
|
||||||
|
) -> anyhow::Result<super::objects::Note> {
|
||||||
|
let data = FEDERATION_CONFIG.get().unwrap();
|
||||||
|
let domain = data.domain();
|
||||||
|
let url = generate_lysand_post_url(domain, &post.id)?;
|
||||||
|
let author = Url::parse(&post.creator.to_string())?;
|
||||||
|
let visibility = match post.visibility.as_str() {
|
||||||
|
"public" => super::objects::VisibilityType::Public,
|
||||||
|
"followers" => super::objects::VisibilityType::Followers,
|
||||||
|
"direct" => super::objects::VisibilityType::Direct,
|
||||||
|
"unlisted" => super::objects::VisibilityType::Unlisted,
|
||||||
|
_ => super::objects::VisibilityType::Public,
|
||||||
|
};
|
||||||
|
//let mut mentions = Vec::new();
|
||||||
|
//for obj in post.tag.clone() {
|
||||||
|
// mentions.push(obj.href.clone());
|
||||||
|
//}
|
||||||
|
let mut content = ContentFormat::default();
|
||||||
|
content.x.insert(
|
||||||
|
"text/html".to_string(),
|
||||||
|
ContentEntry::from_string(post.content),
|
||||||
|
);
|
||||||
|
let note = super::objects::Note {
|
||||||
|
rtype: super::objects::LysandType::Note,
|
||||||
|
id: uuid::Uuid::parse_str(&post.id)?,
|
||||||
|
author: author.clone(),
|
||||||
|
uri: url.clone(),
|
||||||
|
created_at: OffsetDateTime::from_unix_timestamp(post.created_at.timestamp()).unwrap(),
|
||||||
|
content: Some(content),
|
||||||
|
mentions: None,
|
||||||
|
category: Some(CategoryType::Microblog),
|
||||||
|
device: None,
|
||||||
|
visibility: Some(visibility),
|
||||||
|
previews: None,
|
||||||
|
replies_to: None,
|
||||||
|
quotes: None,
|
||||||
|
group: None,
|
||||||
|
attachments: None,
|
||||||
|
subject: post.title,
|
||||||
|
is_sensitive: Some(post.sensitive),
|
||||||
|
};
|
||||||
|
Ok(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn lysand_user_from_db(
|
||||||
|
user: entities::user::Model,
|
||||||
|
) -> anyhow::Result<super::objects::User> {
|
||||||
|
let url = Url::parse(&user.url)?;
|
||||||
|
let inbox_url = Url::parse("https://ap.lysand.org/apbridge/lysand/inbox")?;
|
||||||
|
let outbox_url = Url::parse(
|
||||||
|
("https://ap.lysand.org/apbridge/lysand/outbox/".to_string() + &user.id).as_str(),
|
||||||
|
)?;
|
||||||
|
let followers_url = Url::parse(
|
||||||
|
("https://ap.lysand.org/apbridge/lysand/followers/".to_string() + &user.id).as_str(),
|
||||||
|
)?;
|
||||||
|
let following_url = Url::parse(
|
||||||
|
("https://ap.lysand.org/apbridge/lysand/following/".to_string() + &user.id).as_str(),
|
||||||
|
)?;
|
||||||
|
let featured_url = Url::parse(
|
||||||
|
("https://ap.lysand.org/apbridge/lysand/featured/".to_string() + &user.id).as_str(),
|
||||||
|
)?;
|
||||||
|
let likes_url = Url::parse(
|
||||||
|
("https://ap.lysand.org/apbridge/lysand/likes/".to_string() + &user.id).as_str(),
|
||||||
|
)?;
|
||||||
|
let dislikes_url = Url::parse(
|
||||||
|
("https://ap.lysand.org/apbridge/lysand/dislikes/".to_string() + &user.id).as_str(),
|
||||||
|
)?;
|
||||||
|
let og_displayname_ref = user.name.clone();
|
||||||
|
let og_username_ref = user.username.clone();
|
||||||
|
let empty = "".to_owned();
|
||||||
|
// linter was having a stroke
|
||||||
|
let display_name = match og_displayname_ref {
|
||||||
|
og_username_ref => None,
|
||||||
|
empty => None,
|
||||||
|
_ => Some(user.name),
|
||||||
|
};
|
||||||
|
let mut bio = ContentFormat::default();
|
||||||
|
bio.x.insert(
|
||||||
|
"text/html".to_string(),
|
||||||
|
ContentEntry::from_string(user.summary.unwrap_or_default()),
|
||||||
|
);
|
||||||
|
let user = super::objects::User {
|
||||||
|
rtype: super::objects::LysandType::User,
|
||||||
|
id: uuid::Uuid::parse_str(&user.id)?,
|
||||||
|
uri: url.clone(),
|
||||||
|
username: user.username,
|
||||||
|
display_name,
|
||||||
|
inbox: inbox_url,
|
||||||
|
outbox: outbox_url,
|
||||||
|
followers: followers_url,
|
||||||
|
following: following_url,
|
||||||
|
featured: featured_url,
|
||||||
|
likes: likes_url,
|
||||||
|
dislikes: dislikes_url,
|
||||||
|
bio: Some(bio),
|
||||||
|
avatar: None,
|
||||||
|
header: None,
|
||||||
|
fields: None,
|
||||||
|
created_at: OffsetDateTime::from_unix_timestamp(user.created_at.timestamp()).unwrap(),
|
||||||
|
public_key: PublicKey {
|
||||||
|
actor: url.clone(),
|
||||||
|
public_key: user.public_key,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn option_content_format_text(opt: Option<ContentFormat>) -> Option<String> {
|
pub async fn option_content_format_text(opt: Option<ContentFormat>) -> Option<String> {
|
||||||
if let Some(format) = opt {
|
if let Some(format) = opt {
|
||||||
return Some(format.select_rich_text().await.unwrap());
|
return Some(format.select_rich_text().await.unwrap());
|
||||||
|
|
@ -47,7 +156,7 @@ pub async fn db_post_from_url(url: Url) -> anyhow::Result<entities::post::Model>
|
||||||
Ok(post)
|
Ok(post)
|
||||||
} else {
|
} else {
|
||||||
let post = fetch_note_from_url(url.clone()).await?;
|
let post = fetch_note_from_url(url.clone()).await?;
|
||||||
let res = receive_lysand_note(post, "https://ap.lysand.org/example".to_string()).await?;
|
let res = receive_lysand_note(post, "https://ap.lysand.org/example".to_string()).await?; // TODO: Replace user id with actual user id
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +227,6 @@ pub async fn receive_lysand_note(
|
||||||
generate_object_id(data.domain(), ¬e.id.to_string())?.into();
|
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(note.author.clone()).await?;
|
let user = fetch_user_from_url(note.author.clone()).await?;
|
||||||
let data = FEDERATION_CONFIG.get().unwrap();
|
|
||||||
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 {
|
tag.push(Mention {
|
||||||
|
|
@ -145,9 +253,7 @@ pub async fn receive_lysand_note(
|
||||||
vec.append(&mut mentions.clone());
|
vec.append(&mut mentions.clone());
|
||||||
vec
|
vec
|
||||||
}
|
}
|
||||||
super::objects::VisibilityType::Direct => {
|
super::objects::VisibilityType::Direct => mentions.clone(),
|
||||||
mentions.clone()
|
|
||||||
},
|
|
||||||
super::objects::VisibilityType::Unlisted => {
|
super::objects::VisibilityType::Unlisted => {
|
||||||
let mut vec = vec![Url::parse(&user.followers.to_string().as_str())?];
|
let mut vec = vec![Url::parse(&user.followers.to_string().as_str())?];
|
||||||
vec.append(&mut mentions.clone());
|
vec.append(&mut mentions.clone());
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,82 @@
|
||||||
use activitypub_federation::{
|
use activitypub_federation::{
|
||||||
protocol::context::WithContext, traits::Object, FEDERATION_CONTENT_TYPE,
|
fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor},
|
||||||
|
protocol::context::WithContext,
|
||||||
|
traits::Object,
|
||||||
|
FEDERATION_CONTENT_TYPE,
|
||||||
};
|
};
|
||||||
use activitystreams_kinds::{activity::CreateType, object};
|
use activitystreams_kinds::{activity::CreateType, object};
|
||||||
use actix_web::{get, web, HttpResponse};
|
use actix_web::{get, web, HttpResponse};
|
||||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
use sea_orm::{query, ColumnTrait, EntityTrait, QueryFilter};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::State,
|
database::State,
|
||||||
entities::{
|
entities::{
|
||||||
post::{self, Entity},
|
post::{self, Entity},
|
||||||
prelude,
|
prelude, user,
|
||||||
},
|
},
|
||||||
error, objects,
|
error,
|
||||||
|
lysand::conversion::{lysand_post_from_db, lysand_user_from_db},
|
||||||
|
objects,
|
||||||
utils::{base_url_decode, generate_create_id},
|
utils::{base_url_decode, generate_create_id},
|
||||||
Response, DB, FEDERATION_CONFIG,
|
Response, DB, FEDERATION_CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct LysandQuery {
|
||||||
|
// Post url
|
||||||
|
url: Option<Url>,
|
||||||
|
// User handle
|
||||||
|
user: Option<String>,
|
||||||
|
// User URL
|
||||||
|
user_url: Option<Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/apbridge/lysand/query")]
|
||||||
|
async fn query_post(
|
||||||
|
query: web::Query<LysandQuery>,
|
||||||
|
state: web::Data<State>,
|
||||||
|
) -> actix_web::Result<HttpResponse, error::Error> {
|
||||||
|
if query.url.is_none() && query.user.is_none() && query.user_url.is_none() {
|
||||||
|
return Ok(
|
||||||
|
HttpResponse::BadRequest().body("Bad Request. Error code: mrrrmrrrmrrawwawwawwa")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = DB.get().unwrap();
|
||||||
|
let data = FEDERATION_CONFIG.get().unwrap();
|
||||||
|
|
||||||
|
if let Some(user) = query.user.clone() {
|
||||||
|
let target =
|
||||||
|
webfinger_resolve_actor::<State, user::Model>(user.as_str(), &data.to_request_data())
|
||||||
|
.await?;
|
||||||
|
let lysand_user = lysand_user_from_db(target).await?;
|
||||||
|
|
||||||
|
return Ok(HttpResponse::Ok()
|
||||||
|
.content_type("application/json")
|
||||||
|
.json(lysand_user));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(user) = query.user_url.clone() {
|
||||||
|
let target = ObjectId::<user::Model>::from(user)
|
||||||
|
.dereference(&data.to_request_data())
|
||||||
|
.await?;
|
||||||
|
let lysand_user = lysand_user_from_db(target).await?;
|
||||||
|
|
||||||
|
return Ok(HttpResponse::Ok()
|
||||||
|
.content_type("application/json")
|
||||||
|
.json(lysand_user));
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = ObjectId::<post::Model>::from(query.url.clone().unwrap())
|
||||||
|
.dereference(&data.to_request_data())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type("application/json")
|
||||||
|
.json(lysand_post_from_db(target).await?))
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/apbridge/object/{post}")]
|
#[get("/apbridge/object/{post}")]
|
||||||
async fn fetch_post(
|
async fn fetch_post(
|
||||||
path: web::Path<String>,
|
path: web::Path<String>,
|
||||||
|
|
@ -38,6 +99,28 @@ async fn fetch_post(
|
||||||
.json(crate::objects::post::Note::from_db(&post)))
|
.json(crate::objects::post::Note::from_db(&post)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/apbridge/lysand/object/{post}")]
|
||||||
|
async fn fetch_lysand_post(
|
||||||
|
path: web::Path<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.as_str()))
|
||||||
|
.one(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let post = match post {
|
||||||
|
Some(post) => post,
|
||||||
|
None => return Ok(HttpResponse::NotFound().finish()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type("application/json")
|
||||||
|
.json(lysand_post_from_db(post).await?))
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/apbridge/create/{id}/{base64url}")]
|
#[get("/apbridge/create/{id}/{base64url}")]
|
||||||
async fn create_activity(
|
async fn create_activity(
|
||||||
path: web::Path<(String, String)>,
|
path: web::Path<(String, String)>,
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,8 @@ pub enum LysandExtensions {
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct PublicKey {
|
pub struct PublicKey {
|
||||||
public_key: String,
|
pub public_key: String,
|
||||||
actor: Url,
|
pub actor: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
|
@ -108,9 +108,9 @@ pub struct ContentHash {
|
||||||
sha512: Option<String>,
|
sha512: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ContentFormat {
|
pub struct ContentFormat {
|
||||||
x: HashMap<String, ContentEntry>,
|
pub x: HashMap<String, ContentEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentFormat {
|
impl ContentFormat {
|
||||||
|
|
@ -181,7 +181,7 @@ impl<'de> Deserialize<'de> for ContentFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
struct FieldKV {
|
pub struct FieldKV {
|
||||||
key: ContentFormat,
|
key: ContentFormat,
|
||||||
value: ContentFormat,
|
value: ContentFormat,
|
||||||
}
|
}
|
||||||
|
|
@ -198,6 +198,21 @@ pub struct ContentEntry {
|
||||||
height: Option<u64>,
|
height: Option<u64>,
|
||||||
duration: Option<u64>,
|
duration: Option<u64>,
|
||||||
}
|
}
|
||||||
|
impl ContentEntry {
|
||||||
|
pub fn from_string(string: String) -> ContentEntry {
|
||||||
|
ContentEntry {
|
||||||
|
content: string,
|
||||||
|
description: None,
|
||||||
|
size: None,
|
||||||
|
hash: None,
|
||||||
|
blurhash: None,
|
||||||
|
fps: None,
|
||||||
|
width: None,
|
||||||
|
height: None,
|
||||||
|
duration: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use clap::Parser;
|
||||||
use database::Database;
|
use database::Database;
|
||||||
use entities::post;
|
use entities::post;
|
||||||
use http::{http_get_user, http_post_user_inbox, webfinger};
|
use http::{http_get_user, http_post_user_inbox, webfinger};
|
||||||
use lysand::http::{create_activity, fetch_post};
|
use lysand::http::{create_activity, fetch_lysand_post, fetch_post, query_post};
|
||||||
use objects::person::DbUser;
|
use objects::person::DbUser;
|
||||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, Set};
|
use sea_orm::{ActiveModelTrait, DatabaseConnection, Set};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -260,6 +260,8 @@ async fn main() -> actix_web::Result<(), anyhow::Error> {
|
||||||
.service(index)
|
.service(index)
|
||||||
.service(fetch_post)
|
.service(fetch_post)
|
||||||
.service(create_activity)
|
.service(create_activity)
|
||||||
|
.service(query_post)
|
||||||
|
.service(fetch_lysand_post)
|
||||||
})
|
})
|
||||||
.bind(SERVER_URL.to_string())?
|
.bind(SERVER_URL.to_string())?
|
||||||
.workers(num_cpus::get())
|
.workers(num_cpus::get())
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DbUser {
|
pub struct DbUser {
|
||||||
|
|
@ -135,7 +136,7 @@ impl Object for user::Model {
|
||||||
return Ok(user);
|
return Ok(user);
|
||||||
}
|
}
|
||||||
let model = user::ActiveModel {
|
let model = user::ActiveModel {
|
||||||
id: Set(json.id.to_string()),
|
id: Set(Uuid::now_v7().to_string()),
|
||||||
username: Set(json.preferred_username),
|
username: Set(json.preferred_username),
|
||||||
name: Set(json.name),
|
name: Set(json.name),
|
||||||
inbox: Set(json.inbox.to_string()),
|
inbox: Set(json.inbox.to_string()),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ 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;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct DbPost {
|
pub struct DbPost {
|
||||||
|
|
@ -119,7 +120,7 @@ impl Object for post::Model {
|
||||||
let creator = json.attributed_to.dereference(data).await?;
|
let creator = json.attributed_to.dereference(data).await?;
|
||||||
let post: post::ActiveModel = post::ActiveModel {
|
let post: post::ActiveModel = post::ActiveModel {
|
||||||
content: Set(json.content.clone()),
|
content: Set(json.content.clone()),
|
||||||
id: Set(json.id.to_string()),
|
id: Set(Uuid::now_v7().to_string()),
|
||||||
creator: Set(creator.id.to_string()),
|
creator: Set(creator.id.to_string()),
|
||||||
created_at: Set(chrono::Utc::now()), //TODO: make this use the real timestamp
|
created_at: Set(chrono::Utc::now()), //TODO: make this use the real timestamp
|
||||||
content_type: Set("text/plain".to_string()), // TODO: make this use the real content type
|
content_type: Set("text/plain".to_string()), // TODO: make this use the real content type
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,13 @@ pub fn generate_follow_accept_id(domain: &str, db_id: &str) -> Result<Url, Parse
|
||||||
Url::parse(&format!("https://{}/apbridge/follow/{}", domain, db_id))
|
Url::parse(&format!("https://{}/apbridge/follow/{}", domain, db_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_lysand_post_url(domain: &str, db_id: &str) -> Result<Url, ParseError> {
|
||||||
|
Url::parse(&format!(
|
||||||
|
"https://{}/apbridge/lysand/object/{}",
|
||||||
|
domain, db_id
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO for later aprl: needs to be base64url!!!
|
// TODO for later aprl: needs to be base64url!!!
|
||||||
pub fn generate_create_id(
|
pub fn generate_create_id(
|
||||||
domain: &str,
|
domain: &str,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue