Federated private messages.

This commit is contained in:
Dessalines 2020-05-05 22:06:24 -04:00
parent 21407260a4
commit 15f1920b25
13 changed files with 1081 additions and 271 deletions

View file

@ -0,0 +1,21 @@
drop materialized view private_message_mview;
drop view private_message_view;
alter table private_message
drop column ap_id,
drop column local;
create view private_message_view as
select
pm.*,
u.name as creator_name,
u.avatar as creator_avatar,
u2.name as recipient_name,
u2.avatar as recipient_avatar
from private_message pm
inner join user_ u on u.id = pm.creator_id
inner join user_ u2 on u2.id = pm.recipient_id;
create materialized view private_message_mview as select * from private_message_view;
create unique index idx_private_message_mview_id on private_message_mview (id);

View file

@ -0,0 +1,25 @@
alter table private_message
add column ap_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
add column local boolean not null default true
;
drop materialized view private_message_mview;
drop view private_message_view;
create view private_message_view as
select
pm.*,
u.name as creator_name,
u.avatar as creator_avatar,
u.actor_id as creator_actor_id,
u.local as creator_local,
u2.name as recipient_name,
u2.avatar as recipient_avatar,
u2.actor_id as recipient_actor_id,
u2.local as recipient_local
from private_message pm
inner join user_ u on u.id = pm.creator_id
inner join user_ u2 on u2.id = pm.recipient_id;
create materialized view private_message_mview as select * from private_message_view;
create unique index idx_private_message_mview_id on private_message_mview (id);

View file

@ -186,7 +186,7 @@ pub struct PrivateMessagesResponse {
#[derive(Serialize, Deserialize, Clone)]
pub struct PrivateMessageResponse {
message: PrivateMessageView,
pub message: PrivateMessageView,
}
#[derive(Serialize, Deserialize, Debug)]
@ -861,12 +861,15 @@ impl Perform for Oper<MarkAllAsRead> {
for message in &messages {
let private_message_form = PrivateMessageForm {
content: None,
content: message.to_owned().content,
creator_id: message.to_owned().creator_id,
recipient_id: message.to_owned().recipient_id,
deleted: None,
read: Some(true),
updated: None,
ap_id: message.to_owned().ap_id,
local: message.local,
published: None,
};
let _updated_message = match PrivateMessage::update(&conn, message.id, &private_message_form)
@ -1034,19 +1037,23 @@ impl Perform for Oper<CreatePrivateMessage> {
let conn = pool.get()?;
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into());
}
let content_slurs_removed = remove_slurs(&data.content.to_owned());
let private_message_form = PrivateMessageForm {
content: Some(content_slurs_removed.to_owned()),
content: content_slurs_removed.to_owned(),
creator_id: user_id,
recipient_id: data.recipient_id,
deleted: None,
read: None,
updated: None,
ap_id: "changeme".into(),
local: true,
published: None,
};
let inserted_private_message = match PrivateMessage::create(&conn, &private_message_form) {
@ -1056,6 +1063,14 @@ impl Perform for Oper<CreatePrivateMessage> {
}
};
let updated_private_message =
match PrivateMessage::update_ap_id(&conn, inserted_private_message.id) {
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_create_private_message").into()),
};
updated_private_message.send_create(&user, &conn)?;
// Send notifications to the recipient
let recipient_user = User_::read(&conn, data.recipient_id)?;
if recipient_user.send_notifications_to_email {
@ -1099,7 +1114,7 @@ impl Perform for Oper<EditPrivateMessage> {
fn perform(
&self,
pool: Pool<ConnectionManager<PgConnection>>,
_websocket_info: Option<WebsocketInfo>,
websocket_info: Option<WebsocketInfo>,
) -> Result<PrivateMessageResponse, Error> {
let data: &EditPrivateMessage = &self.data;
@ -1115,7 +1130,8 @@ impl Perform for Oper<EditPrivateMessage> {
let orig_private_message = PrivateMessage::read(&conn, data.edit_id)?;
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into());
}
@ -1127,8 +1143,8 @@ impl Perform for Oper<EditPrivateMessage> {
}
let content_slurs_removed = match &data.content {
Some(content) => Some(remove_slurs(content)),
None => None,
Some(content) => remove_slurs(content),
None => orig_private_message.content,
};
let private_message_form = PrivateMessageForm {
@ -1142,17 +1158,41 @@ impl Perform for Oper<EditPrivateMessage> {
} else {
Some(naive_now())
},
ap_id: orig_private_message.ap_id,
local: orig_private_message.local,
published: None,
};
let _updated_private_message =
let updated_private_message =
match PrivateMessage::update(&conn, data.edit_id, &private_message_form) {
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
};
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_private_message.send_delete(&user, &conn)?;
} else {
updated_private_message.send_undo_delete(&user, &conn)?;
}
} else {
updated_private_message.send_update(&user, &conn)?;
}
let message = PrivateMessageView::read(&conn, data.edit_id)?;
Ok(PrivateMessageResponse { message })
let res = PrivateMessageResponse { message };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res.clone(),
recipient_id: orig_private_message.recipient_id,
my_id: ws.id,
});
}
Ok(res)
}
}

View file

@ -5,6 +5,7 @@ pub mod community_inbox;
pub mod fetcher;
pub mod page_extension;
pub mod post;
pub mod private_message;
pub mod shared_inbox;
pub mod signatures;
pub mod user;
@ -46,6 +47,7 @@ use url::Url;
use crate::api::comment::CommentResponse;
use crate::api::post::PostResponse;
use crate::api::site::SearchResponse;
use crate::api::user::PrivateMessageResponse;
use crate::db::comment::{Comment, CommentForm, CommentLike, CommentLikeForm};
use crate::db::comment_view::CommentView;
use crate::db::community::{
@ -55,13 +57,15 @@ use crate::db::community::{
use crate::db::community_view::{CommunityFollowerView, CommunityModeratorView, CommunityView};
use crate::db::post::{Post, PostForm, PostLike, PostLikeForm};
use crate::db::post_view::PostView;
use crate::db::private_message::{PrivateMessage, PrivateMessageForm};
use crate::db::private_message_view::PrivateMessageView;
use crate::db::user::{UserForm, User_};
use crate::db::user_view::UserView;
use crate::db::{activity, Crud, Followable, Joinable, Likeable, SearchType};
use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
use crate::routes::{ChatServerParam, DbPoolParam};
use crate::websocket::{
server::{SendComment, SendPost},
server::{SendComment, SendPost, SendUserRoomMessage},
UserOperation,
};
use crate::{convert_datetime, naive_now, Settings};
@ -85,6 +89,7 @@ pub enum EndpointType {
User,
Post,
Comment,
PrivateMessage,
}
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
@ -120,6 +125,7 @@ pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
// TODO I have to change this else my update advanced_migrations crashes the
// server if a comment exists.
EndpointType::Comment => "comment",
EndpointType::PrivateMessage => "private_message",
};
Url::parse(&format!(

View file

@ -0,0 +1,234 @@
use super::*;
impl ToApub for PrivateMessage {
type Response = Note;
fn to_apub(&self, conn: &PgConnection) -> Result<Note, Error> {
let mut private_message = Note::default();
let oprops: &mut ObjectProperties = private_message.as_mut();
let creator = User_::read(&conn, self.creator_id)?;
let recipient = User_::read(&conn, self.recipient_id)?;
oprops
.set_context_xsd_any_uri(context())?
.set_id(self.ap_id.to_owned())?
.set_published(convert_datetime(self.published))?
.set_content_xsd_string(self.content.to_owned())?
.set_to_xsd_any_uri(recipient.actor_id)?
.set_attributed_to_xsd_any_uri(creator.actor_id)?;
if let Some(u) = self.updated {
oprops.set_updated(convert_datetime(u))?;
}
Ok(private_message)
}
fn to_tombstone(&self) -> Result<Tombstone, Error> {
create_tombstone(
self.deleted,
&self.ap_id,
self.updated,
NoteType.to_string(),
)
}
}
impl FromApub for PrivateMessageForm {
type ApubType = Note;
/// Parse an ActivityPub note received from another instance into a Lemmy Private message
fn from_apub(note: &Note, conn: &PgConnection) -> Result<PrivateMessageForm, Error> {
let oprops = &note.object_props;
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
let recipient_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
let recipient = get_or_fetch_and_upsert_remote_user(&recipient_actor_id, &conn)?;
Ok(PrivateMessageForm {
creator_id: creator.id,
recipient_id: recipient.id,
content: oprops
.get_content_xsd_string()
.map(|c| c.to_string())
.unwrap(),
published: oprops
.get_published()
.map(|u| u.as_ref().to_owned().naive_local()),
updated: oprops
.get_updated()
.map(|u| u.as_ref().to_owned().naive_local()),
deleted: None,
read: None,
ap_id: oprops.get_id().unwrap().to_string(),
local: false,
})
}
}
impl ApubObjectType for PrivateMessage {
/// Send out information about a newly created private message
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(conn)?;
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let mut create = Create::new();
create
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
let to = format!("{}/inbox", recipient.actor_id);
create
.create_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
// Insert the sent activity into the activity table
let activity_form = activity::ActivityForm {
user_id: creator.id,
data: serde_json::to_value(&create)?,
local: true,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
send_activity(
&create,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
vec![to],
)?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community.
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(conn)?;
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let mut update = Update::new();
update
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
let to = format!("{}/inbox", recipient.actor_id);
update
.update_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
// Insert the sent activity into the activity table
let activity_form = activity::ActivityForm {
user_id: creator.id,
data: serde_json::to_value(&update)?,
local: true,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
send_activity(
&update,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
vec![to],
)?;
Ok(())
}
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(conn)?;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let mut delete = Delete::new();
delete
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
let to = format!("{}/inbox", recipient.actor_id);
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
// Insert the sent activity into the activity table
let activity_form = activity::ActivityForm {
user_id: creator.id,
data: serde_json::to_value(&delete)?,
local: true,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
send_activity(
&delete,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
vec![to],
)?;
Ok(())
}
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(conn)?;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let mut delete = Delete::new();
delete
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
let to = format!("{}/inbox", recipient.actor_id);
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
// TODO
// Undo that fake activity
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
undo
.object_props
.set_context_xsd_any_uri(context())?
.set_id(undo_id)?;
undo
.undo_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?;
// Insert the sent activity into the activity table
let activity_form = activity::ActivityForm {
user_id: creator.id,
data: serde_json::to_value(&undo)?,
local: true,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
send_activity(
&undo,
&creator.private_key.as_ref().unwrap(),
&creator.actor_id,
vec![to],
)?;
Ok(())
}
fn send_remove(&self, _mod_: &User_, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
fn send_undo_remove(&self, _mod_: &User_, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
}

View file

@ -3,7 +3,11 @@ use super::*;
#[serde(untagged)]
#[derive(Deserialize, Debug)]
pub enum UserAcceptedObjects {
Accept(Accept),
Accept(Box<Accept>),
Create(Box<Create>),
Update(Box<Update>),
Delete(Box<Delete>),
Undo(Box<Undo>),
}
/// Handler for all incoming activities to user inboxes.
@ -12,7 +16,7 @@ pub async fn user_inbox(
input: web::Json<UserAcceptedObjects>,
path: web::Path<String>,
db: DbPoolParam,
_chat_server: ChatServerParam,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
// TODO: would be nice if we could do the signature check here, but we cant access the actor property
let input = input.into_inner();
@ -21,12 +25,24 @@ pub async fn user_inbox(
debug!("User {} received activity: {:?}", &username, &input);
match input {
UserAcceptedObjects::Accept(a) => handle_accept(&a, &request, &username, &conn),
UserAcceptedObjects::Accept(a) => receive_accept(&a, &request, &username, &conn),
UserAcceptedObjects::Create(c) => {
receive_create_private_message(&c, &request, &conn, chat_server)
}
UserAcceptedObjects::Update(u) => {
receive_update_private_message(&u, &request, &conn, chat_server)
}
UserAcceptedObjects::Delete(d) => {
receive_delete_private_message(&d, &request, &conn, chat_server)
}
UserAcceptedObjects::Undo(u) => {
receive_undo_delete_private_message(&u, &request, &conn, chat_server)
}
}
}
/// Handle accepted follows.
fn handle_accept(
fn receive_accept(
accept: &Accept,
request: &HttpRequest,
username: &str,
@ -65,3 +81,240 @@ fn handle_accept(
// TODO: at this point, indicate to the user that they are following the community
Ok(HttpResponse::Ok().finish())
}
fn receive_create_private_message(
create: &Create,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let note = create
.create_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user_uri = create
.create_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
// Insert the received activity into the activity table
let activity_form = activity::ActivityForm {
user_id: user.id,
data: serde_json::to_value(&create)?,
local: false,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
let inserted_private_message = PrivateMessage::create(&conn, &private_message)?;
let message = PrivateMessageView::read(&conn, inserted_private_message.id)?;
let res = PrivateMessageResponse {
message: message.to_owned(),
};
chat_server.do_send(SendUserRoomMessage {
op: UserOperation::CreatePrivateMessage,
response: res,
recipient_id: message.recipient_id,
my_id: None,
});
Ok(HttpResponse::Ok().finish())
}
fn receive_update_private_message(
update: &Update,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let note = update
.update_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user_uri = update
.update_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
// Insert the received activity into the activity table
let activity_form = activity::ActivityForm {
user_id: user.id,
data: serde_json::to_value(&update)?,
local: false,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
PrivateMessage::update(conn, private_message_id, &private_message)?;
let message = PrivateMessageView::read(&conn, private_message_id)?;
let res = PrivateMessageResponse {
message: message.to_owned(),
};
chat_server.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res,
recipient_id: message.recipient_id,
my_id: None,
});
Ok(HttpResponse::Ok().finish())
}
fn receive_delete_private_message(
delete: &Delete,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let note = delete
.delete_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user_uri = delete
.delete_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
// Insert the received activity into the activity table
let activity_form = activity::ActivityForm {
user_id: user.id,
data: serde_json::to_value(&delete)?,
local: false,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
let private_message_form = PrivateMessageForm {
content: private_message.content,
recipient_id: private_message.recipient_id,
creator_id: private_message.creator_id,
deleted: Some(true),
read: None,
ap_id: private_message.ap_id,
local: private_message.local,
published: None,
updated: Some(naive_now()),
};
PrivateMessage::update(conn, private_message_id, &private_message_form)?;
let message = PrivateMessageView::read(&conn, private_message_id)?;
let res = PrivateMessageResponse {
message: message.to_owned(),
};
chat_server.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res,
recipient_id: message.recipient_id,
my_id: None,
});
Ok(HttpResponse::Ok().finish())
}
fn receive_undo_delete_private_message(
undo: &Undo,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let delete = undo
.undo_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Delete>()?;
let note = delete
.delete_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user_uri = delete
.delete_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user.public_key.unwrap())?;
// Insert the received activity into the activity table
let activity_form = activity::ActivityForm {
user_id: user.id,
data: serde_json::to_value(&delete)?,
local: false,
updated: None,
};
activity::Activity::create(&conn, &activity_form)?;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
let private_message_form = PrivateMessageForm {
content: private_message.content,
recipient_id: private_message.recipient_id,
creator_id: private_message.creator_id,
deleted: Some(false),
read: None,
ap_id: private_message.ap_id,
local: private_message.local,
published: None,
updated: Some(naive_now()),
};
PrivateMessage::update(conn, private_message_id, &private_message_form)?;
let message = PrivateMessageView::read(&conn, private_message_id)?;
let res = PrivateMessageResponse {
message: message.to_owned(),
};
chat_server.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res,
recipient_id: message.recipient_id,
my_id: None,
});
Ok(HttpResponse::Ok().finish())
}

View file

@ -2,6 +2,7 @@
use super::comment::Comment;
use super::community::{Community, CommunityForm};
use super::post::Post;
use super::private_message::PrivateMessage;
use super::user::{UserForm, User_};
use super::*;
use crate::apub::signatures::generate_actor_keypair;
@ -15,6 +16,7 @@ pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), Error> {
community_updates_2020_04_02(conn)?;
post_updates_2020_04_03(conn)?;
comment_updates_2020_04_03(conn)?;
private_message_updates_2020_05_05(conn)?;
Ok(())
}
@ -145,3 +147,23 @@ fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
Ok(())
}
fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), Error> {
use crate::schema::private_message::dsl::*;
info!("Running private_message_updates_2020_05_05");
// Update the ap_id
let incorrect_pms = private_message
.filter(ap_id.eq("changeme"))
.filter(local.eq(true))
.load::<PrivateMessage>(conn)?;
for cpm in &incorrect_pms {
PrivateMessage::update_ap_id(&conn, cpm.id)?;
}
info!("{} private message rows updated.", incorrect_pms.len());
Ok(())
}

View file

@ -1,4 +1,5 @@
use super::*;
use crate::apub::{make_apub_endpoint, EndpointType};
use crate::schema::private_message;
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
@ -12,6 +13,8 @@ pub struct PrivateMessage {
pub read: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub ap_id: String,
pub local: bool,
}
#[derive(Insertable, AsChangeset, Clone)]
@ -19,10 +22,13 @@ pub struct PrivateMessage {
pub struct PrivateMessageForm {
pub creator_id: i32,
pub recipient_id: i32,
pub content: Option<String>,
pub content: String,
pub deleted: Option<bool>,
pub read: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>,
pub ap_id: String,
pub local: bool,
}
impl Crud<PrivateMessageForm> for PrivateMessage {
@ -55,6 +61,28 @@ impl Crud<PrivateMessageForm> for PrivateMessage {
}
}
impl PrivateMessage {
pub fn update_ap_id(conn: &PgConnection, private_message_id: i32) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
let apid = make_apub_endpoint(
EndpointType::PrivateMessage,
&private_message_id.to_string(),
)
.to_string();
diesel::update(private_message.find(private_message_id))
.set(ap_id.eq(apid))
.get_result::<Self>(conn)
}
pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
private_message
.filter(ap_id.eq(object_id))
.first::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use super::super::user::*;
@ -118,12 +146,15 @@ mod tests {
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
let private_message_form = PrivateMessageForm {
content: Some("A test private message".into()),
content: "A test private message".into(),
creator_id: inserted_creator.id,
recipient_id: inserted_recipient.id,
deleted: None,
read: None,
published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_private_message = PrivateMessage::create(&conn, &private_message_form).unwrap();
@ -137,6 +168,8 @@ mod tests {
read: false,
updated: None,
published: inserted_private_message.published,
ap_id: "changeme".into(),
local: true,
};
let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();

View file

@ -12,10 +12,16 @@ table! {
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
ap_id -> Text,
local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
creator_actor_id -> Text,
creator_local -> Bool,
recipient_name -> Varchar,
recipient_avatar -> Nullable<Text>,
recipient_actor_id -> Text,
recipient_local -> Bool,
}
}
@ -29,10 +35,16 @@ table! {
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
ap_id -> Text,
local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
creator_actor_id -> Text,
creator_local -> Bool,
recipient_name -> Varchar,
recipient_avatar -> Nullable<Text>,
recipient_actor_id -> Text,
recipient_local -> Bool,
}
}
@ -49,10 +61,16 @@ pub struct PrivateMessageView {
pub read: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub ap_id: String,
pub local: bool,
pub creator_name: String,
pub creator_avatar: Option<String>,
pub creator_actor_id: String,
pub creator_local: bool,
pub recipient_name: String,
pub recipient_avatar: Option<String>,
pub recipient_actor_id: String,
pub recipient_local: bool,
}
pub struct PrivateMessageQueryBuilder<'a> {

View file

@ -83,6 +83,14 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.route("/like", web::post().to(route_post::<CreateCommentLike>))
.route("/save", web::put().to(route_post::<SaveComment>)),
)
// Private Message
.service(
web::scope("/private_message")
.wrap(rate_limit.message())
.route("/list", web::get().to(route_get::<GetPrivateMessages>))
.route("", web::post().to(route_post::<CreatePrivateMessage>))
.route("", web::put().to(route_post::<EditPrivateMessage>)),
)
// User
.service(
// Account action, I don't like that it's in /user maybe /accounts

View file

@ -1,339 +1,341 @@
table! {
activity (id) {
id -> Int4,
user_id -> Int4,
data -> Jsonb,
local -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
activity (id) {
id -> Int4,
user_id -> Int4,
data -> Jsonb,
local -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
}
table! {
category (id) {
id -> Int4,
name -> Varchar,
}
category (id) {
id -> Int4,
name -> Varchar,
}
}
table! {
comment (id) {
id -> Int4,
creator_id -> Int4,
post_id -> Int4,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
ap_id -> Varchar,
local -> Bool,
}
comment (id) {
id -> Int4,
creator_id -> Int4,
post_id -> Int4,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
ap_id -> Varchar,
local -> Bool,
}
}
table! {
comment_like (id) {
id -> Int4,
user_id -> Int4,
comment_id -> Int4,
post_id -> Int4,
score -> Int2,
published -> Timestamp,
}
comment_like (id) {
id -> Int4,
user_id -> Int4,
comment_id -> Int4,
post_id -> Int4,
score -> Int2,
published -> Timestamp,
}
}
table! {
comment_saved (id) {
id -> Int4,
comment_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
comment_saved (id) {
id -> Int4,
comment_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
community (id) {
id -> Int4,
name -> Varchar,
title -> Varchar,
description -> Nullable<Text>,
category_id -> Int4,
creator_id -> Int4,
removed -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
nsfw -> Bool,
actor_id -> Varchar,
local -> Bool,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
last_refreshed_at -> Timestamp,
}
community (id) {
id -> Int4,
name -> Varchar,
title -> Varchar,
description -> Nullable<Text>,
category_id -> Int4,
creator_id -> Int4,
removed -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
nsfw -> Bool,
actor_id -> Varchar,
local -> Bool,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
last_refreshed_at -> Timestamp,
}
}
table! {
community_follower (id) {
id -> Int4,
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
community_follower (id) {
id -> Int4,
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
community_moderator (id) {
id -> Int4,
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
community_moderator (id) {
id -> Int4,
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
community_user_ban (id) {
id -> Int4,
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
community_user_ban (id) {
id -> Int4,
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
mod_add (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
mod_add (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_add_community (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
community_id -> Int4,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
mod_add_community (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
community_id -> Int4,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_ban (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
reason -> Nullable<Text>,
banned -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
}
mod_ban (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
reason -> Nullable<Text>,
banned -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
}
}
table! {
mod_ban_from_community (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
community_id -> Int4,
reason -> Nullable<Text>,
banned -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
}
mod_ban_from_community (id) {
id -> Int4,
mod_user_id -> Int4,
other_user_id -> Int4,
community_id -> Int4,
reason -> Nullable<Text>,
banned -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
}
}
table! {
mod_lock_post (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
locked -> Nullable<Bool>,
when_ -> Timestamp,
}
mod_lock_post (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
locked -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_remove_comment (id) {
id -> Int4,
mod_user_id -> Int4,
comment_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
mod_remove_comment (id) {
id -> Int4,
mod_user_id -> Int4,
comment_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_remove_community (id) {
id -> Int4,
mod_user_id -> Int4,
community_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
}
mod_remove_community (id) {
id -> Int4,
mod_user_id -> Int4,
community_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
expires -> Nullable<Timestamp>,
when_ -> Timestamp,
}
}
table! {
mod_remove_post (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
mod_remove_post (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
reason -> Nullable<Text>,
removed -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
mod_sticky_post (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
stickied -> Nullable<Bool>,
when_ -> Timestamp,
}
mod_sticky_post (id) {
id -> Int4,
mod_user_id -> Int4,
post_id -> Int4,
stickied -> Nullable<Bool>,
when_ -> Timestamp,
}
}
table! {
password_reset_request (id) {
id -> Int4,
user_id -> Int4,
token_encrypted -> Text,
published -> Timestamp,
}
password_reset_request (id) {
id -> Int4,
user_id -> Int4,
token_encrypted -> Text,
published -> Timestamp,
}
}
table! {
post (id) {
id -> Int4,
name -> Varchar,
url -> Nullable<Text>,
body -> Nullable<Text>,
creator_id -> Int4,
community_id -> Int4,
removed -> Bool,
locked -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
nsfw -> Bool,
stickied -> Bool,
embed_title -> Nullable<Text>,
embed_description -> Nullable<Text>,
embed_html -> Nullable<Text>,
thumbnail_url -> Nullable<Text>,
ap_id -> Varchar,
local -> Bool,
}
post (id) {
id -> Int4,
name -> Varchar,
url -> Nullable<Text>,
body -> Nullable<Text>,
creator_id -> Int4,
community_id -> Int4,
removed -> Bool,
locked -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
nsfw -> Bool,
stickied -> Bool,
embed_title -> Nullable<Text>,
embed_description -> Nullable<Text>,
embed_html -> Nullable<Text>,
thumbnail_url -> Nullable<Text>,
ap_id -> Varchar,
local -> Bool,
}
}
table! {
post_like (id) {
id -> Int4,
post_id -> Int4,
user_id -> Int4,
score -> Int2,
published -> Timestamp,
}
post_like (id) {
id -> Int4,
post_id -> Int4,
user_id -> Int4,
score -> Int2,
published -> Timestamp,
}
}
table! {
post_read (id) {
id -> Int4,
post_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
post_read (id) {
id -> Int4,
post_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
post_saved (id) {
id -> Int4,
post_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
post_saved (id) {
id -> Int4,
post_id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
private_message (id) {
id -> Int4,
creator_id -> Int4,
recipient_id -> Int4,
content -> Text,
deleted -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
private_message (id) {
id -> Int4,
creator_id -> Int4,
recipient_id -> Int4,
content -> Text,
deleted -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
ap_id -> Varchar,
local -> Bool,
}
}
table! {
site (id) {
id -> Int4,
name -> Varchar,
description -> Nullable<Text>,
creator_id -> Int4,
published -> Timestamp,
updated -> Nullable<Timestamp>,
enable_downvotes -> Bool,
open_registration -> Bool,
enable_nsfw -> Bool,
}
site (id) {
id -> Int4,
name -> Varchar,
description -> Nullable<Text>,
creator_id -> Int4,
published -> Timestamp,
updated -> Nullable<Timestamp>,
enable_downvotes -> Bool,
open_registration -> Bool,
enable_nsfw -> Bool,
}
}
table! {
user_ (id) {
id -> Int4,
name -> Varchar,
preferred_username -> Nullable<Varchar>,
password_encrypted -> Text,
email -> Nullable<Text>,
avatar -> Nullable<Text>,
admin -> Bool,
banned -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
show_nsfw -> Bool,
theme -> Varchar,
default_sort_type -> Int2,
default_listing_type -> Int2,
lang -> Varchar,
show_avatars -> Bool,
send_notifications_to_email -> Bool,
matrix_user_id -> Nullable<Text>,
actor_id -> Varchar,
bio -> Nullable<Text>,
local -> Bool,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
last_refreshed_at -> Timestamp,
}
user_ (id) {
id -> Int4,
name -> Varchar,
preferred_username -> Nullable<Varchar>,
password_encrypted -> Text,
email -> Nullable<Text>,
avatar -> Nullable<Text>,
admin -> Bool,
banned -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
show_nsfw -> Bool,
theme -> Varchar,
default_sort_type -> Int2,
default_listing_type -> Int2,
lang -> Varchar,
show_avatars -> Bool,
send_notifications_to_email -> Bool,
matrix_user_id -> Nullable<Text>,
actor_id -> Varchar,
bio -> Nullable<Text>,
local -> Bool,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
last_refreshed_at -> Timestamp,
}
}
table! {
user_ban (id) {
id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
user_ban (id) {
id -> Int4,
user_id -> Int4,
published -> Timestamp,
}
}
table! {
user_mention (id) {
id -> Int4,
recipient_id -> Int4,
comment_id -> Int4,
read -> Bool,
published -> Timestamp,
}
user_mention (id) {
id -> Int4,
recipient_id -> Int4,
comment_id -> Int4,
read -> Bool,
published -> Timestamp,
}
}
joinable!(activity -> user_ (user_id));

View file

@ -18,6 +18,10 @@ import {
GetCommunityResponse,
CommentLikeForm,
CreatePostLikeForm,
PrivateMessageForm,
EditPrivateMessageForm,
PrivateMessageResponse,
PrivateMessagesResponse,
} from '../interfaces';
let lemmyAlphaUrl = 'http://localhost:8540';
@ -158,6 +162,7 @@ describe('main', () => {
body: wrapper(unfollowForm),
}
).then(d => d.json());
expect(unfollowRes.community.local).toBe(false);
// Check that you are unsubscribed to it locally
let followedCommunitiesResAgain: GetFollowedCommunitiesResponse = await fetch(
@ -965,6 +970,143 @@ describe('main', () => {
expect(getCommunityResAgain.community.removed).toBe(false);
});
});
describe('private message', () => {
test('/u/lemmy_alpha creates/updates/deletes/undeletes a private_message to /u/lemmy_beta, its on both instances', async () => {
let content = 'A jest test federated private message';
let privateMessageForm: PrivateMessageForm = {
content,
recipient_id: 3,
auth: lemmyAlphaAuth,
};
let createRes: PrivateMessageResponse = await fetch(
`${lemmyAlphaApiUrl}/private_message`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(privateMessageForm),
}
).then(d => d.json());
expect(createRes.message.content).toBe(content);
expect(createRes.message.local).toBe(true);
expect(createRes.message.creator_local).toBe(true);
expect(createRes.message.recipient_local).toBe(false);
// Get it from beta
let getPrivateMessagesUrl = `${lemmyBetaApiUrl}/private_message/list?auth=${lemmyBetaAuth}&unread_only=false`;
let getPrivateMessagesRes: PrivateMessagesResponse = await fetch(
getPrivateMessagesUrl,
{
method: 'GET',
}
).then(d => d.json());
expect(getPrivateMessagesRes.messages[0].content).toBe(content);
expect(getPrivateMessagesRes.messages[0].local).toBe(false);
expect(getPrivateMessagesRes.messages[0].creator_local).toBe(false);
expect(getPrivateMessagesRes.messages[0].recipient_local).toBe(true);
// lemmy alpha updates the private message
let updatedContent = 'A jest test federated private message edited';
let updatePrivateMessageForm: EditPrivateMessageForm = {
content: updatedContent,
edit_id: createRes.message.id,
auth: lemmyAlphaAuth,
};
let updateRes: PrivateMessageResponse = await fetch(
`${lemmyAlphaApiUrl}/private_message`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(updatePrivateMessageForm),
}
).then(d => d.json());
expect(updateRes.message.content).toBe(updatedContent);
// Fetch from beta again
let getPrivateMessagesUpdatedRes: PrivateMessagesResponse = await fetch(
getPrivateMessagesUrl,
{
method: 'GET',
}
).then(d => d.json());
expect(getPrivateMessagesUpdatedRes.messages[0].content).toBe(
updatedContent
);
// lemmy alpha deletes the private message
let deletePrivateMessageForm: EditPrivateMessageForm = {
deleted: true,
edit_id: createRes.message.id,
auth: lemmyAlphaAuth,
};
let deleteRes: PrivateMessageResponse = await fetch(
`${lemmyAlphaApiUrl}/private_message`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(deletePrivateMessageForm),
}
).then(d => d.json());
expect(deleteRes.message.deleted).toBe(true);
// Fetch from beta again
let getPrivateMessagesDeletedRes: PrivateMessagesResponse = await fetch(
getPrivateMessagesUrl,
{
method: 'GET',
}
).then(d => d.json());
// The GetPrivateMessages filters out deleted,
// even though they are in the actual database.
// no reason to show them
expect(getPrivateMessagesDeletedRes.messages.length).toBe(0);
// lemmy alpha undeletes the private message
let undeletePrivateMessageForm: EditPrivateMessageForm = {
deleted: false,
edit_id: createRes.message.id,
auth: lemmyAlphaAuth,
};
let undeleteRes: PrivateMessageResponse = await fetch(
`${lemmyAlphaApiUrl}/private_message`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(undeletePrivateMessageForm),
}
).then(d => d.json());
expect(undeleteRes.message.deleted).toBe(false);
// Fetch from beta again
let getPrivateMessagesUnDeletedRes: PrivateMessagesResponse = await fetch(
getPrivateMessagesUrl,
{
method: 'GET',
}
).then(d => d.json());
expect(getPrivateMessagesUnDeletedRes.messages[0].deleted).toBe(false);
});
});
});
function wrapper(form: any): string {

View file

@ -273,10 +273,16 @@ export interface PrivateMessage {
read: boolean;
published: string;
updated?: string;
ap_id: string;
local: boolean;
creator_name: string;
creator_avatar?: string;
creator_actor_id: string;
creator_local: boolean;
recipient_name: string;
recipient_avatar?: string;
recipient_actor_id: string;
recipient_local: boolean;
}
export enum BanType {