Make User follow change of activitystreams
This commit is contained in:
parent
00d31e323f
commit
c37e115470
@ -4,12 +4,15 @@ use crate::{
|
||||
safe_string::SafeString, schema::users, timeline::Timeline, Connection, Error, Result,
|
||||
UserEvent::*, CONFIG, ITEMS_PER_PAGE, USER_CHAN,
|
||||
};
|
||||
use activitypub::{
|
||||
activity::Delete,
|
||||
actor::Person,
|
||||
use activitystreams::{
|
||||
activity::{ActorAndObject, Create, Delete},
|
||||
actor::{ApActor, Endpoints, Person},
|
||||
base::{AnyBase, AsBase, Base},
|
||||
collection::{OrderedCollection, OrderedCollectionPage},
|
||||
object::{Image, Tombstone},
|
||||
Activity, CustomObject, Endpoint,
|
||||
iri,
|
||||
object::{kind::ImageType, ApObject, Image, Tombstone},
|
||||
prelude::*,
|
||||
primitives::OneOrMany, // CustomObject,
|
||||
};
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
|
||||
@ -42,7 +45,7 @@ use std::{
|
||||
use url::Url;
|
||||
use webfinger::*;
|
||||
|
||||
pub type CustomPerson = CustomObject<ApSignature, Person>;
|
||||
// pub type CustomPerson = CustomObject<ApSignature, Person>;
|
||||
|
||||
pub enum Role {
|
||||
Admin = 0,
|
||||
@ -242,13 +245,16 @@ impl User {
|
||||
.ok_or(Error::Webfinger)
|
||||
}
|
||||
|
||||
fn fetch(url: &str) -> Result<CustomPerson> {
|
||||
// fn fetch(url: &str) -> Result<CustomPerson> {
|
||||
fn fetch(url: &str) -> Result<Person> {
|
||||
let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
|
||||
let text = &res.text()?;
|
||||
// without this workaround, publicKey is not correctly deserialized
|
||||
let ap_sign = serde_json::from_str::<ApSignature>(text)?;
|
||||
let mut json = serde_json::from_str::<CustomPerson>(text)?;
|
||||
json.custom_props = ap_sign;
|
||||
// TODO: Implement
|
||||
// let ap_sign = serde_json::from_str::<ApSignature>(text)?;
|
||||
// let mut json = serde_json::from_str::<CustomPerson>(text)?;
|
||||
let json = serde_json::from_str::<Person>(text)?;
|
||||
// json.custom_props = ap_sign; // TODO: implement
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
@ -260,35 +266,69 @@ impl User {
|
||||
User::fetch(&self.ap_url.clone()).and_then(|json| {
|
||||
let avatar = Media::save_remote(
|
||||
conn,
|
||||
json.object
|
||||
.object_props
|
||||
.icon_image()? // FIXME: Fails when icon is not set
|
||||
.object_props
|
||||
.url_string()?,
|
||||
json.icon()
|
||||
.and_then(|icon| {
|
||||
Some(
|
||||
icon.as_one()
|
||||
.expect("only")
|
||||
.extend::<Image, ImageType>()
|
||||
.expect("possible")
|
||||
.expect("exists")
|
||||
.url()
|
||||
.ok_or(Error::MissingApProperty)
|
||||
.ok()?
|
||||
.as_one()
|
||||
.expect("one")
|
||||
.as_xsd_string()
|
||||
.expect("possible"),
|
||||
)
|
||||
})
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.into(), // FIXME: Fails when icon is not set,
|
||||
self,
|
||||
)
|
||||
.ok();
|
||||
|
||||
let person =
|
||||
serde_json::from_value::<ApActor<Person>>(serde_json::to_value(json)?.into())?;
|
||||
|
||||
diesel::update(self)
|
||||
.set((
|
||||
users::username.eq(json.object.ap_actor_props.preferred_username_string()?),
|
||||
users::display_name.eq(json.object.object_props.name_string()?),
|
||||
users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?),
|
||||
users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?),
|
||||
users::username.eq(person
|
||||
.preferred_username()
|
||||
.ok_or(Error::MissingApProperty)?),
|
||||
users::display_name.eq(person
|
||||
.name()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.as_one()
|
||||
.expect("only")
|
||||
.as_xsd_string()
|
||||
.expect("possible")),
|
||||
users::outbox_url.eq(person.outbox().ok_or(Error::MissingApProperty)?.as_str()),
|
||||
users::inbox_url.eq(person.inbox().as_str()),
|
||||
users::summary.eq(SafeString::new(
|
||||
&json
|
||||
.object
|
||||
.object_props
|
||||
.summary_string()
|
||||
&person
|
||||
.summary()
|
||||
.and_then(|summary| {
|
||||
Some(
|
||||
summary
|
||||
.as_one()
|
||||
.expect("only")
|
||||
.as_xsd_string()
|
||||
.expect("possible"),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
)),
|
||||
users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?),
|
||||
users::followers_endpoint
|
||||
.eq(person.followers().ok_or(Error::MissingApProperty)?.as_str()),
|
||||
users::avatar_id.eq(avatar.map(|a| a.id)),
|
||||
users::last_fetched_date.eq(Utc::now().naive_utc()),
|
||||
users::public_key.eq(json
|
||||
.custom_props
|
||||
.public_key_publickey()?
|
||||
.public_key_pem_string()?),
|
||||
// TODO:
|
||||
// users::public_key.eq(json
|
||||
// .custom_props
|
||||
// .public_key_publickey()?
|
||||
// .public_key_pem_string()?),
|
||||
))
|
||||
.execute(conn)
|
||||
.map(|_| ())
|
||||
@ -428,31 +468,32 @@ impl User {
|
||||
.load::<User>(conn)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
|
||||
Ok(ActivityStream::new(self.outbox_collection(conn)?))
|
||||
pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<ApObject<OrderedCollection>>> {
|
||||
Ok(ActivityStream::new(ApObject::new(
|
||||
self.outbox_collection(conn)?,
|
||||
)))
|
||||
}
|
||||
pub fn outbox_collection(&self, conn: &Connection) -> Result<OrderedCollection> {
|
||||
let mut coll = OrderedCollection::default();
|
||||
let mut coll = OrderedCollection::new();
|
||||
let first = &format!("{}?page=1", &self.outbox_url);
|
||||
let last = &format!(
|
||||
"{}?page={}",
|
||||
&self.outbox_url,
|
||||
self.get_activities_count(conn) / i64::from(ITEMS_PER_PAGE) + 1
|
||||
);
|
||||
coll.collection_props.set_first_link(Id::new(first))?;
|
||||
coll.collection_props.set_last_link(Id::new(last))?;
|
||||
coll.collection_props
|
||||
.set_total_items_u64(self.get_activities_count(conn) as u64)?;
|
||||
coll.set_first(iri!(first))
|
||||
.set_last(iri!(last))
|
||||
.set_total_items(self.get_activities_count(conn) as u64);
|
||||
Ok(coll)
|
||||
}
|
||||
pub fn outbox_page(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
(min, max): (i32, i32),
|
||||
) -> Result<ActivityStream<OrderedCollectionPage>> {
|
||||
Ok(ActivityStream::new(
|
||||
) -> Result<ActivityStream<ApObject<OrderedCollectionPage>>> {
|
||||
Ok(ActivityStream::new(ApObject::new(
|
||||
self.outbox_collection_page(conn, (min, max))?,
|
||||
))
|
||||
)))
|
||||
}
|
||||
pub fn outbox_collection_page(
|
||||
&self,
|
||||
@ -461,27 +502,33 @@ impl User {
|
||||
) -> Result<OrderedCollectionPage> {
|
||||
let acts = self.get_activities_page(conn, (min, max))?;
|
||||
let n_acts = self.get_activities_count(conn);
|
||||
let mut coll = OrderedCollectionPage::default();
|
||||
let mut coll = OrderedCollectionPage::new();
|
||||
if n_acts - i64::from(min) >= i64::from(ITEMS_PER_PAGE) {
|
||||
coll.collection_page_props.set_next_link(Id::new(&format!(
|
||||
coll.set_next(iri!(&format!(
|
||||
"{}?page={}",
|
||||
&self.outbox_url,
|
||||
min / ITEMS_PER_PAGE + 2
|
||||
)))?;
|
||||
)));
|
||||
}
|
||||
if min > 0 {
|
||||
coll.collection_page_props.set_prev_link(Id::new(&format!(
|
||||
coll.set_prev(iri!(&format!(
|
||||
"{}?page={}",
|
||||
&self.outbox_url,
|
||||
min / ITEMS_PER_PAGE
|
||||
)))?;
|
||||
)));
|
||||
}
|
||||
coll.collection_props.items = serde_json::to_value(acts)?;
|
||||
coll.collection_page_props
|
||||
.set_part_of_link(Id::new(&self.outbox_url))?;
|
||||
coll.set_many_items(
|
||||
acts.iter().map(|create| {
|
||||
AnyBase::from_base(create.base_ref().into_generic().expect("possible"))
|
||||
}),
|
||||
)
|
||||
.set_part_of(iri!(&self.outbox_url));
|
||||
Ok(coll)
|
||||
}
|
||||
fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
|
||||
fn fetch_outbox_page<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
url: &str,
|
||||
) -> Result<(Vec<ActorAndObject<T>>, Option<String>)> {
|
||||
let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?;
|
||||
let text = &res.text()?;
|
||||
let json: serde_json::Value = serde_json::from_str(text)?;
|
||||
@ -490,12 +537,12 @@ impl User {
|
||||
.unwrap_or(&vec![])
|
||||
.iter()
|
||||
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
||||
.collect::<Vec<T>>();
|
||||
.collect::<Vec<ActorAndObject<T>>>();
|
||||
|
||||
let next = json.get("next").map(|x| x.as_str().unwrap().to_owned());
|
||||
Ok((items, next))
|
||||
}
|
||||
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
|
||||
pub fn fetch_outbox<T: serde::de::DeserializeOwned>(&self) -> Result<Vec<ActorAndObject<T>>> {
|
||||
let mut res = get(
|
||||
&self.outbox_url[..],
|
||||
Self::get_sender(),
|
||||
@ -504,7 +551,7 @@ impl User {
|
||||
let text = &res.text()?;
|
||||
let json: serde_json::Value = serde_json::from_str(text)?;
|
||||
if let Some(first) = json.get("first") {
|
||||
let mut items: Vec<T> = Vec::new();
|
||||
let mut items: Vec<ActorAndObject<T>> = Vec::new();
|
||||
let mut next = first.as_str().unwrap().to_owned();
|
||||
while let Ok((mut page, nxt)) = self.fetch_outbox_page(&next) {
|
||||
if page.is_empty() {
|
||||
@ -527,7 +574,7 @@ impl User {
|
||||
.unwrap_or(&vec![])
|
||||
.iter()
|
||||
.filter_map(|j| serde_json::from_value(j.clone()).ok())
|
||||
.collect::<Vec<T>>())
|
||||
.collect::<Vec<ActorAndObject<T>>>())
|
||||
}
|
||||
}
|
||||
|
||||
@ -561,7 +608,7 @@ impl User {
|
||||
&self,
|
||||
conn: &Connection,
|
||||
(min, max): (i32, i32),
|
||||
) -> Result<Vec<serde_json::Value>> {
|
||||
) -> Result<Vec<Create>> {
|
||||
use crate::schema::post_authors;
|
||||
use crate::schema::posts;
|
||||
let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id);
|
||||
@ -574,12 +621,8 @@ impl User {
|
||||
.load::<Post>(conn)?;
|
||||
Ok(posts
|
||||
.into_iter()
|
||||
.filter_map(|p| {
|
||||
p.create_activity(conn)
|
||||
.ok()
|
||||
.and_then(|a| serde_json::to_value(a).ok())
|
||||
})
|
||||
.collect::<Vec<serde_json::Value>>())
|
||||
.filter_map(|p| p.create_activity(conn).ok())
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn get_followers(&self, conn: &Connection) -> Result<Vec<User>> {
|
||||
@ -739,72 +782,57 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> {
|
||||
let mut actor = Person::default();
|
||||
actor.object_props.set_id_string(self.ap_url.clone())?;
|
||||
actor
|
||||
.object_props
|
||||
.set_name_string(self.display_name.clone())?;
|
||||
actor
|
||||
.object_props
|
||||
.set_summary_string(self.summary_html.get().clone())?;
|
||||
actor.object_props.set_url_string(self.ap_url.clone())?;
|
||||
actor
|
||||
.ap_actor_props
|
||||
.set_inbox_string(self.inbox_url.clone())?;
|
||||
actor
|
||||
.ap_actor_props
|
||||
.set_outbox_string(self.outbox_url.clone())?;
|
||||
actor
|
||||
.ap_actor_props
|
||||
.set_preferred_username_string(self.username.clone())?;
|
||||
actor
|
||||
.ap_actor_props
|
||||
.set_followers_string(self.followers_endpoint.clone())?;
|
||||
// pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> {
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<ApActor<Person>> {
|
||||
let mut actor = ApActor::new(
|
||||
iri!(self.inbox_url),
|
||||
*Person::new()
|
||||
.set_name(self.display_name)
|
||||
.set_summary(self.summary_html.get().to_owned())
|
||||
.set_url(self.ap_url),
|
||||
)
|
||||
.set_outbox(iri!(self.outbox_url))
|
||||
.set_preferred_username(self.username)
|
||||
.set_followers(iri!(self.followers_endpoint));
|
||||
|
||||
if let Some(shared_inbox_url) = self.shared_inbox_url.clone() {
|
||||
let mut endpoints = Endpoint::default();
|
||||
endpoints.set_shared_inbox_string(shared_inbox_url)?;
|
||||
actor.ap_actor_props.set_endpoints_endpoint(endpoints)?;
|
||||
let endpoints = Endpoints {
|
||||
shared_inbox: Some(iri!(shared_inbox_url)),
|
||||
..Endpoints::default()
|
||||
};
|
||||
actor.set_endpoints(endpoints);
|
||||
}
|
||||
|
||||
let mut public_key = PublicKey::default();
|
||||
public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||
public_key.set_owner_string(self.ap_url.clone())?;
|
||||
public_key.set_public_key_pem_string(self.public_key.clone())?;
|
||||
let mut ap_signature = ApSignature::default();
|
||||
ap_signature.set_public_key_publickey(public_key)?;
|
||||
// FIXME
|
||||
// let mut public_key = PublicKey::default();
|
||||
// public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||
// public_key.set_owner_string(self.ap_url.clone())?;
|
||||
// public_key.set_public_key_pem_string(self.public_key.clone())?;
|
||||
// let mut ap_signature = ApSignature::default();
|
||||
// ap_signature.set_public_key_publickey(public_key)?;
|
||||
|
||||
if let Some(avatar_id) = self.avatar_id {
|
||||
let mut avatar = Image::default();
|
||||
avatar
|
||||
.object_props
|
||||
.set_url_string(Media::get(conn, avatar_id)?.url()?)?;
|
||||
actor.object_props.set_icon_object(avatar)?;
|
||||
let avatar = Image::new().set_url(iri!(Media::get(conn, avatar_id)?.url()?));
|
||||
let base = Base::retract(*avatar)?.into_generic()?;
|
||||
actor.set_icon(AnyBase::from_base(base));
|
||||
}
|
||||
|
||||
Ok(CustomPerson::new(actor, ap_signature))
|
||||
// Ok(CustomPerson::new(actor, ap_signature))
|
||||
Ok(*actor)
|
||||
}
|
||||
|
||||
pub fn delete_activity(&self, conn: &Connection) -> Result<Delete> {
|
||||
let mut del = Delete::default();
|
||||
|
||||
let mut tombstone = Tombstone::default();
|
||||
tombstone.object_props.set_id_string(self.ap_url.clone())?;
|
||||
|
||||
del.delete_props
|
||||
.set_actor_link(Id::new(self.ap_url.clone()))?;
|
||||
del.delete_props.set_object_object(tombstone)?;
|
||||
del.object_props
|
||||
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||
del.object_props
|
||||
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILITY)])?;
|
||||
del.object_props.set_cc_link_vec(
|
||||
self.get_followers(conn)?
|
||||
.into_iter()
|
||||
.map(|f| Id::new(f.ap_url))
|
||||
.collect(),
|
||||
)?;
|
||||
let followers = self.get_followers(conn)?;
|
||||
let ccs = Vec::with_capacity(followers.len());
|
||||
for addr in followers.into_iter() {
|
||||
ccs.push(iri!(addr.ap_url));
|
||||
}
|
||||
let tombstone = Tombstone::new()
|
||||
.set_id(iri!(format!("{}#delete", self.ap_url)))
|
||||
.set_many_tos([iri!(PUBLIC_VISIBILITY)])
|
||||
.set_many_ccs(ccs);
|
||||
let base = Base::retract(*tombstone)?.into_generic()?;
|
||||
let del = Delete::new::<_, OneOrMany<AnyBase>>(iri!(self.ap_url), base.into());
|
||||
|
||||
Ok(del)
|
||||
}
|
||||
@ -923,14 +951,16 @@ impl Eq for User {}
|
||||
|
||||
impl FromId<DbConn> for User {
|
||||
type Error = Error;
|
||||
type Object = CustomPerson;
|
||||
// type Object = CustomPerson;
|
||||
type Object = Person;
|
||||
|
||||
fn from_db(conn: &DbConn, id: &str) -> Result<Self> {
|
||||
Self::find_by_ap_url(conn, id)
|
||||
}
|
||||
|
||||
fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> {
|
||||
let url = Url::parse(&acct.object.object_props.id_string()?)?;
|
||||
// fn from_activity(conn: &DbConn, acct: CustomPerson) -> Result<Self> {
|
||||
fn from_activity(conn: &DbConn, acct: Person) -> Result<Self> {
|
||||
let url = Url::parse(&acct.id().ok_or(Error::MissingApProperty)?.as_str())?;
|
||||
let inst = url.host_str().ok_or(Error::Url)?;
|
||||
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
|
||||
Instance::insert(
|
||||
@ -949,8 +979,12 @@ impl FromId<DbConn> for User {
|
||||
},
|
||||
)
|
||||
})?;
|
||||
let person = serde_json::from_value::<ApActor<Person>>(serde_json::to_value(acct)?.into())?;
|
||||
|
||||
let username = acct.object.ap_actor_props.preferred_username_string()?;
|
||||
let username = person
|
||||
.preferred_username()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.to_string();
|
||||
|
||||
if username.contains(&['<', '>', '&', '@', '\'', '"', ' ', '\t'][..]) {
|
||||
return Err(Error::InvalidValue);
|
||||
@ -966,50 +1000,77 @@ impl FromId<DbConn> for User {
|
||||
conn,
|
||||
NewUser {
|
||||
display_name: acct
|
||||
.object
|
||||
.object_props
|
||||
.name_string()
|
||||
.unwrap_or_else(|_| username.clone()),
|
||||
.name()
|
||||
.map(|name| {
|
||||
name.as_one()
|
||||
.expect("only")
|
||||
.as_xsd_string()
|
||||
.expect("exists")
|
||||
})
|
||||
.unwrap_or_else(|| &username)
|
||||
.to_string(),
|
||||
username,
|
||||
outbox_url: acct.object.ap_actor_props.outbox_string()?,
|
||||
inbox_url: acct.object.ap_actor_props.inbox_string()?,
|
||||
outbox_url: person.outbox().ok_or(Error::MissingApProperty)?.to_string(),
|
||||
inbox_url: person.inbox().to_string(),
|
||||
role: 2,
|
||||
summary: acct
|
||||
.object
|
||||
.object_props
|
||||
.summary_string()
|
||||
.unwrap_or_default(),
|
||||
summary: person
|
||||
.summary()
|
||||
.map(|summary| {
|
||||
summary
|
||||
.as_one()
|
||||
.expect("only")
|
||||
.as_xsd_string()
|
||||
.expect("exists")
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
summary_html: SafeString::new(
|
||||
&acct
|
||||
.object
|
||||
.object_props
|
||||
.summary_string()
|
||||
&person
|
||||
.summary()
|
||||
.map(|summary| {
|
||||
summary
|
||||
.as_one()
|
||||
.expect("only")
|
||||
.as_xsd_string()
|
||||
.expect("exists")
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
email: None,
|
||||
hashed_password: None,
|
||||
instance_id: instance.id,
|
||||
ap_url: acct.object.object_props.id_string()?,
|
||||
public_key: acct
|
||||
.custom_props
|
||||
.public_key_publickey()?
|
||||
.public_key_pem_string()?,
|
||||
ap_url: person.id().ok_or(Error::MissingApProperty)?.to_string(),
|
||||
// public_key: acct
|
||||
// .custom_props
|
||||
// .public_key_publickey()?
|
||||
// .public_key_pem_string()?,
|
||||
public_key: "".to_string(), // FIXME
|
||||
private_key: None,
|
||||
shared_inbox_url: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.endpoints_endpoint()
|
||||
.and_then(|e| e.shared_inbox_string())
|
||||
.ok(),
|
||||
followers_endpoint: acct.object.ap_actor_props.followers_string()?,
|
||||
shared_inbox_url: person
|
||||
.endpoints()
|
||||
.and_then(|e| e.shared_inbox.map(|shared_inbox| shared_inbox.to_string())),
|
||||
followers_endpoint: person
|
||||
.followers()
|
||||
.ok_or(Error::MissingApProperty)?
|
||||
.to_string(),
|
||||
fqn,
|
||||
avatar_id: None,
|
||||
},
|
||||
)?;
|
||||
|
||||
if let Ok(icon) = acct.object.object_props.icon_image() {
|
||||
if let Ok(url) = icon.object_props.url_string() {
|
||||
let avatar = Media::save_remote(conn, url, &user);
|
||||
if let Some(icon) = acct.icon() {
|
||||
let icon_image = icon
|
||||
.as_one()
|
||||
.expect("only")
|
||||
.extend::<Image, ImageType>()
|
||||
.expect("possible")
|
||||
.expect("exists");
|
||||
if let Some(url) = icon_image.url() {
|
||||
let avatar = Media::save_remote(
|
||||
conn,
|
||||
url.as_single_xsd_string().expect("exists").into(),
|
||||
&user,
|
||||
);
|
||||
|
||||
if let Ok(avatar) = avatar {
|
||||
user.set_avatar(conn, avatar.id)?;
|
||||
|
Loading…
x
Reference in New Issue
Block a user