Merge branch 'master' into federation_merge_from_master_1
This commit is contained in:
commit
f0026065f5
31 changed files with 3624 additions and 2164 deletions
2
ansible/VERSION
vendored
2
ansible/VERSION
vendored
|
@ -1 +1 @@
|
|||
v0.6.49
|
||||
v0.6.51
|
||||
|
|
2
docker/prod/docker-compose.yml
vendored
2
docker/prod/docker-compose.yml
vendored
|
@ -12,7 +12,7 @@ services:
|
|||
restart: always
|
||||
|
||||
lemmy:
|
||||
image: dessalines/lemmy:v0.6.49
|
||||
image: dessalines/lemmy:v0.6.51
|
||||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
restart: always
|
||||
|
|
2548
server/Cargo.lock
generated
vendored
2548
server/Cargo.lock
generated
vendored
File diff suppressed because it is too large
Load diff
2
server/Cargo.toml
vendored
2
server/Cargo.toml
vendored
|
@ -42,3 +42,5 @@ openssl = "0.10"
|
|||
http = "0.2.1"
|
||||
http-signature-normalization = "0.4.1"
|
||||
base64 = "0.12.0"
|
||||
tokio = "0.2.18"
|
||||
futures = "0.3.4"
|
||||
|
|
6
server/query_testing/apache_bench_report.sh
vendored
6
server/query_testing/apache_bench_report.sh
vendored
|
@ -11,6 +11,12 @@ declare -a arr=(
|
|||
"https://torrents-csv.ml/service/search?q=wheel&page=1&type_=torrent"
|
||||
)
|
||||
|
||||
## check if ab installed
|
||||
if ! [ -x "$(command -v ab)" ]; then
|
||||
echo 'Error: ab (Apache Bench) is not installed. https://httpd.apache.org/docs/2.4/programs/ab.html' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## now loop through the above array
|
||||
for i in "${arr[@]}"
|
||||
do
|
||||
|
|
6
server/query_testing/api_benchmark.sh
vendored
6
server/query_testing/api_benchmark.sh
vendored
|
@ -15,6 +15,12 @@ declare -a arr=(
|
|||
"/api/v1/post/list?sort=Hot&type_=All"
|
||||
)
|
||||
|
||||
## check if ab installed
|
||||
if ! [ -x "$(command -v ab)" ]; then
|
||||
echo 'Error: ab (Apache Bench) is not installed. https://httpd.apache.org/docs/2.4/programs/ab.html' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## now loop through the above array
|
||||
for path in "${arr[@]}"
|
||||
do
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
use super::*;
|
||||
use crate::send_email;
|
||||
use crate::settings::Settings;
|
||||
use diesel::PgConnection;
|
||||
use log::error;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreateComment {
|
||||
|
@ -64,8 +59,14 @@ pub struct GetCommentsResponse {
|
|||
comments: Vec<CommentView>,
|
||||
}
|
||||
|
||||
impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||
impl Perform for Oper<CreateComment> {
|
||||
type Response = CommentResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommentResponse, Error> {
|
||||
let data: &CreateComment = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -77,6 +78,8 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
|||
|
||||
let hostname = &format!("https://{}", Settings::get().hostname);
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Check for a community ban
|
||||
let post = Post::read(&conn, data.post_id)?;
|
||||
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||
|
@ -230,15 +233,35 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
|
|||
|
||||
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
|
||||
|
||||
Ok(CommentResponse {
|
||||
let mut res = CommentResponse {
|
||||
comment: comment_view,
|
||||
recipient_ids,
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendComment {
|
||||
op: UserOperation::CreateComment,
|
||||
comment: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
|
||||
// strip out the recipient_ids, so that
|
||||
// users don't get double notifs
|
||||
res.recipient_ids = Vec::new();
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<CommentResponse> for Oper<EditComment> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||
impl Perform for Oper<EditComment> {
|
||||
type Response = CommentResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommentResponse, Error> {
|
||||
let data: &EditComment = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -248,6 +271,8 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let orig_comment = CommentView::read(&conn, data.edit_id, None)?;
|
||||
|
||||
// You are allowed to mark the comment as read even if you're banned.
|
||||
|
@ -364,15 +389,35 @@ impl Perform<CommentResponse> for Oper<EditComment> {
|
|||
|
||||
let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?;
|
||||
|
||||
Ok(CommentResponse {
|
||||
let mut res = CommentResponse {
|
||||
comment: comment_view,
|
||||
recipient_ids,
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendComment {
|
||||
op: UserOperation::EditComment,
|
||||
comment: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
|
||||
// strip out the recipient_ids, so that
|
||||
// users don't get double notifs
|
||||
res.recipient_ids = Vec::new();
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<CommentResponse> for Oper<SaveComment> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||
impl Perform for Oper<SaveComment> {
|
||||
type Response = CommentResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommentResponse, Error> {
|
||||
let data: &SaveComment = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -387,6 +432,8 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
|
|||
user_id,
|
||||
};
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
if data.save {
|
||||
match CommentSaved::save(&conn, &comment_saved_form) {
|
||||
Ok(comment) => comment,
|
||||
|
@ -408,8 +455,14 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||
impl Perform for Oper<CreateCommentLike> {
|
||||
type Response = CommentResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommentResponse, Error> {
|
||||
let data: &CreateCommentLike = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -421,6 +474,8 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
|||
|
||||
let mut recipient_ids = Vec::new();
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Don't do a downvote if site has downvotes disabled
|
||||
if data.score == -1 {
|
||||
let site = SiteView::read(&conn)?;
|
||||
|
@ -478,15 +533,35 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
|||
// Have to refetch the comment to get the current state
|
||||
let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?;
|
||||
|
||||
Ok(CommentResponse {
|
||||
let mut res = CommentResponse {
|
||||
comment: liked_comment,
|
||||
recipient_ids,
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendComment {
|
||||
op: UserOperation::CreateCommentLike,
|
||||
comment: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
|
||||
// strip out the recipient_ids, so that
|
||||
// users don't get double notifs
|
||||
res.recipient_ids = Vec::new();
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetCommentsResponse> for Oper<GetComments> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetCommentsResponse, Error> {
|
||||
impl Perform for Oper<GetComments> {
|
||||
type Response = GetCommentsResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetCommentsResponse, Error> {
|
||||
let data: &GetComments = &self.data;
|
||||
|
||||
let user_claims: Option<Claims> = match &data.auth {
|
||||
|
@ -505,6 +580,8 @@ impl Perform<GetCommentsResponse> for Oper<GetComments> {
|
|||
let type_ = ListingType::from_str(&data.type_)?;
|
||||
let sort = SortType::from_str(&data.sort)?;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let comments = match CommentQueryBuilder::create(&conn)
|
||||
.listing_type(type_)
|
||||
.sort(&sort)
|
||||
|
@ -518,6 +595,20 @@ impl Perform<GetCommentsResponse> for Oper<GetComments> {
|
|||
Err(_e) => return Err(APIError::err("couldnt_get_comments").into()),
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
// You don't need to join the specific community room, bc this is already handled by
|
||||
// GetCommunity
|
||||
if data.community_id.is_none() {
|
||||
if let Some(id) = ws.id {
|
||||
// 0 is the "all" community
|
||||
ws.chatserver.do_send(JoinCommunityRoom {
|
||||
community_id: 0,
|
||||
id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GetCommentsResponse { comments })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
use super::*;
|
||||
use crate::apub::activities::follow_community;
|
||||
use crate::apub::signatures::generate_actor_keypair;
|
||||
use crate::apub::{make_apub_endpoint, EndpointType};
|
||||
use diesel::PgConnection;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetCommunity {
|
||||
|
@ -58,7 +53,7 @@ pub struct BanFromCommunity {
|
|||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct BanFromCommunityResponse {
|
||||
user: UserView,
|
||||
banned: bool,
|
||||
|
@ -72,7 +67,7 @@ pub struct AddModToCommunity {
|
|||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct AddModToCommunityResponse {
|
||||
moderators: Vec<CommunityModeratorView>,
|
||||
}
|
||||
|
@ -116,8 +111,14 @@ pub struct TransferCommunity {
|
|||
auth: String,
|
||||
}
|
||||
|
||||
impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetCommunityResponse, Error> {
|
||||
impl Perform for Oper<GetCommunity> {
|
||||
type Response = GetCommunityResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetCommunityResponse, Error> {
|
||||
let data: &GetCommunity = &self.data;
|
||||
|
||||
let user_id: Option<i32> = match &data.auth {
|
||||
|
@ -131,6 +132,8 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
|||
None => None,
|
||||
};
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let community = match data.id {
|
||||
Some(id) => Community::read(&conn, id)?,
|
||||
None => {
|
||||
|
@ -160,18 +163,44 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
|||
let creator_user = admins.remove(creator_index);
|
||||
admins.insert(0, creator_user);
|
||||
|
||||
// Return the jwt
|
||||
Ok(GetCommunityResponse {
|
||||
let online = if let Some(ws) = websocket_info {
|
||||
if let Some(id) = ws.id {
|
||||
ws.chatserver.do_send(JoinCommunityRoom {
|
||||
community_id: community.id,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO
|
||||
1
|
||||
// let fut = async {
|
||||
// ws.chatserver.send(GetCommunityUsersOnline {community_id}).await.unwrap()
|
||||
// };
|
||||
// Runtime::new().unwrap().block_on(fut)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let res = GetCommunityResponse {
|
||||
community: community_view,
|
||||
moderators,
|
||||
admins,
|
||||
online: 0,
|
||||
})
|
||||
online,
|
||||
};
|
||||
|
||||
// Return the jwt
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommunityResponse, Error> {
|
||||
impl Perform for Oper<CreateCommunity> {
|
||||
type Response = CommunityResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommunityResponse, Error> {
|
||||
let data: &CreateCommunity = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -195,6 +224,8 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err("site_ban").into());
|
||||
|
@ -256,8 +287,14 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommunityResponse, Error> {
|
||||
impl Perform for Oper<EditCommunity> {
|
||||
type Response = CommunityResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommunityResponse, Error> {
|
||||
let data: &EditCommunity = &self.data;
|
||||
|
||||
if let Err(slurs) = slur_check(&data.name) {
|
||||
|
@ -281,6 +318,8 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err("site_ban").into());
|
||||
|
@ -342,14 +381,36 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
|||
|
||||
let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?;
|
||||
|
||||
Ok(CommunityResponse {
|
||||
let res = CommunityResponse {
|
||||
community: community_view,
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
// Strip out the user id and subscribed when sending to others
|
||||
let mut res_sent = res.clone();
|
||||
res_sent.community.user_id = None;
|
||||
res_sent.community.subscribed = None;
|
||||
|
||||
ws.chatserver.do_send(SendCommunityRoomMessage {
|
||||
op: UserOperation::EditCommunity,
|
||||
response: res_sent,
|
||||
community_id: data.edit_id,
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<ListCommunitiesResponse, Error> {
|
||||
impl Perform for Oper<ListCommunities> {
|
||||
type Response = ListCommunitiesResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<ListCommunitiesResponse, Error> {
|
||||
let data: &ListCommunities = &self.data;
|
||||
|
||||
let user_claims: Option<Claims> = match &data.auth {
|
||||
|
@ -372,6 +433,8 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
|
|||
|
||||
let sort = SortType::from_str(&data.sort)?;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let communities = CommunityQueryBuilder::create(&conn)
|
||||
.sort(&sort)
|
||||
.for_user(user_id)
|
||||
|
@ -385,8 +448,14 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<CommunityResponse, Error> {
|
||||
impl Perform for Oper<FollowCommunity> {
|
||||
type Response = CommunityResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommunityResponse, Error> {
|
||||
let data: &FollowCommunity = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -396,7 +465,9 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let community = Community::read(conn, data.community_id)?;
|
||||
let conn = pool.get()?;
|
||||
|
||||
let community = Community::read(&conn, data.community_id)?;
|
||||
if community.local {
|
||||
let community_follower_form = CommunityFollowerForm {
|
||||
community_id: data.community_id,
|
||||
|
@ -416,8 +487,8 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
|||
}
|
||||
} else {
|
||||
// TODO: still have to implement unfollow
|
||||
let user = User_::read(conn, user_id)?;
|
||||
follow_community(&community, &user, conn)?;
|
||||
let user = User_::read(&conn, user_id)?;
|
||||
follow_community(&community, &user, &conn)?;
|
||||
// TODO: this needs to return a "pending" state, until Accept is received from the remote server
|
||||
}
|
||||
|
||||
|
@ -429,8 +500,14 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetFollowedCommunitiesResponse, Error> {
|
||||
impl Perform for Oper<GetFollowedCommunities> {
|
||||
type Response = GetFollowedCommunitiesResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetFollowedCommunitiesResponse, Error> {
|
||||
let data: &GetFollowedCommunities = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -440,6 +517,8 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let communities: Vec<CommunityFollowerView> =
|
||||
match CommunityFollowerView::for_user(&conn, user_id) {
|
||||
Ok(communities) => communities,
|
||||
|
@ -451,8 +530,14 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<BanFromCommunityResponse, Error> {
|
||||
impl Perform for Oper<BanFromCommunity> {
|
||||
type Response = BanFromCommunityResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<BanFromCommunityResponse, Error> {
|
||||
let data: &BanFromCommunity = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -467,6 +552,8 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
|||
user_id: data.user_id,
|
||||
};
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
if data.ban {
|
||||
match CommunityUserBan::ban(&conn, &community_user_ban_form) {
|
||||
Ok(user) => user,
|
||||
|
@ -497,15 +584,32 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
|||
|
||||
let user_view = UserView::read(&conn, data.user_id)?;
|
||||
|
||||
Ok(BanFromCommunityResponse {
|
||||
let res = BanFromCommunityResponse {
|
||||
user: user_view,
|
||||
banned: data.ban,
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendCommunityRoomMessage {
|
||||
op: UserOperation::BanFromCommunity,
|
||||
response: res.clone(),
|
||||
community_id: data.community_id,
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<AddModToCommunityResponse, Error> {
|
||||
impl Perform for Oper<AddModToCommunity> {
|
||||
type Response = AddModToCommunityResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<AddModToCommunityResponse, Error> {
|
||||
let data: &AddModToCommunity = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -520,6 +624,8 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
|||
user_id: data.user_id,
|
||||
};
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
if data.added {
|
||||
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||
Ok(user) => user,
|
||||
|
@ -543,12 +649,29 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
|||
|
||||
let moderators = CommunityModeratorView::for_community(&conn, data.community_id)?;
|
||||
|
||||
Ok(AddModToCommunityResponse { moderators })
|
||||
let res = AddModToCommunityResponse { moderators };
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendCommunityRoomMessage {
|
||||
op: UserOperation::AddModToCommunity,
|
||||
response: res.clone(),
|
||||
community_id: data.community_id,
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetCommunityResponse, Error> {
|
||||
impl Perform for Oper<TransferCommunity> {
|
||||
type Response = GetCommunityResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetCommunityResponse, Error> {
|
||||
let data: &TransferCommunity = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -558,6 +681,8 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let read_community = Community::read(&conn, data.community_id)?;
|
||||
|
||||
let site_creator_id = Site::read(&conn, 1)?.creator_id;
|
||||
|
|
|
@ -18,12 +18,31 @@ use crate::db::user_mention_view::*;
|
|||
use crate::db::user_view::*;
|
||||
use crate::db::*;
|
||||
use crate::{
|
||||
extract_usernames, fetch_iframely_and_pictshare_data, naive_from_unix, naive_now, remove_slurs,
|
||||
slur_check, slurs_vec_to_str,
|
||||
extract_usernames, fetch_iframely_and_pictshare_data, generate_random_string, naive_from_unix,
|
||||
naive_now, remove_slurs, send_email, slur_check, slurs_vec_to_str,
|
||||
};
|
||||
|
||||
use crate::apub::{
|
||||
activities::{follow_community, post_create, post_update},
|
||||
fetcher::search_by_apub_id,
|
||||
signatures::generate_actor_keypair,
|
||||
{make_apub_endpoint, EndpointType},
|
||||
};
|
||||
use crate::settings::Settings;
|
||||
use crate::websocket::UserOperation;
|
||||
use crate::websocket::{
|
||||
server::{
|
||||
JoinCommunityRoom, JoinPostRoom, JoinUserRoom, SendAllMessage, SendComment,
|
||||
SendCommunityRoomMessage, SendPost, SendUserRoomMessage,
|
||||
},
|
||||
WebsocketInfo,
|
||||
};
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use log::{debug, error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub mod comment;
|
||||
pub mod community;
|
||||
|
@ -55,8 +74,12 @@ impl<T> Oper<T> {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait Perform<T> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<T, Error>
|
||||
where
|
||||
T: Sized;
|
||||
pub trait Perform {
|
||||
type Response: serde::ser::Serialize;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<Self::Response, Error>;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
use super::*;
|
||||
use crate::apub::activities::{post_create, post_update};
|
||||
use diesel::PgConnection;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CreatePost {
|
||||
|
@ -80,8 +77,14 @@ pub struct SavePost {
|
|||
auth: String,
|
||||
}
|
||||
|
||||
impl Perform<PostResponse> for Oper<CreatePost> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
|
||||
impl Perform for Oper<CreatePost> {
|
||||
type Response = PostResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PostResponse, Error> {
|
||||
let data: &CreatePost = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -101,6 +104,8 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Check for a community ban
|
||||
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
||||
return Err(APIError::err("community_ban").into());
|
||||
|
@ -155,7 +160,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
|||
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
|
||||
};
|
||||
|
||||
post_create(&updated_post, &user, conn)?;
|
||||
post_create(&updated_post, &user, &conn)?;
|
||||
|
||||
// They like their own post by default
|
||||
let like_form = PostLikeForm {
|
||||
|
@ -176,12 +181,28 @@ impl Perform<PostResponse> for Oper<CreatePost> {
|
|||
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
||||
};
|
||||
|
||||
Ok(PostResponse { post: post_view })
|
||||
let res = PostResponse { post: post_view };
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendPost {
|
||||
op: UserOperation::CreatePost,
|
||||
post: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetPostResponse> for Oper<GetPost> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetPostResponse, Error> {
|
||||
impl Perform for Oper<GetPost> {
|
||||
type Response = GetPostResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetPostResponse, Error> {
|
||||
let data: &GetPost = &self.data;
|
||||
|
||||
let user_id: Option<i32> = match &data.auth {
|
||||
|
@ -195,6 +216,8 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
|
|||
None => None,
|
||||
};
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let post_view = match PostView::read(&conn, data.id, user_id) {
|
||||
Ok(post) => post,
|
||||
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
||||
|
@ -216,6 +239,24 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
|
|||
let creator_user = admins.remove(creator_index);
|
||||
admins.insert(0, creator_user);
|
||||
|
||||
let online = if let Some(ws) = websocket_info {
|
||||
if let Some(id) = ws.id {
|
||||
ws.chatserver.do_send(JoinPostRoom {
|
||||
post_id: data.id,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO
|
||||
1
|
||||
// let fut = async {
|
||||
// ws.chatserver.send(GetPostUsersOnline {post_id: data.id}).await.unwrap()
|
||||
// };
|
||||
// Runtime::new().unwrap().block_on(fut)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Return the jwt
|
||||
Ok(GetPostResponse {
|
||||
post: post_view,
|
||||
|
@ -223,13 +264,19 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
|
|||
community,
|
||||
moderators,
|
||||
admins,
|
||||
online: 0,
|
||||
online,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetPostsResponse, Error> {
|
||||
impl Perform for Oper<GetPosts> {
|
||||
type Response = GetPostsResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetPostsResponse, Error> {
|
||||
let data: &GetPosts = &self.data;
|
||||
|
||||
let user_claims: Option<Claims> = match &data.auth {
|
||||
|
@ -253,6 +300,8 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
|||
let type_ = ListingType::from_str(&data.type_)?;
|
||||
let sort = SortType::from_str(&data.sort)?;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let posts = match PostQueryBuilder::create(&conn)
|
||||
.listing_type(type_)
|
||||
.sort(&sort)
|
||||
|
@ -267,12 +316,32 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
|||
Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
// You don't need to join the specific community room, bc this is already handled by
|
||||
// GetCommunity
|
||||
if data.community_id.is_none() {
|
||||
if let Some(id) = ws.id {
|
||||
// 0 is the "all" community
|
||||
ws.chatserver.do_send(JoinCommunityRoom {
|
||||
community_id: 0,
|
||||
id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(GetPostsResponse { posts })
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<PostResponse> for Oper<CreatePostLike> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
|
||||
impl Perform for Oper<CreatePostLike> {
|
||||
type Response = PostResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PostResponse, Error> {
|
||||
let data: &CreatePostLike = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -282,6 +351,8 @@ impl Perform<PostResponse> for Oper<CreatePostLike> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Don't do a downvote if site has downvotes disabled
|
||||
if data.score == -1 {
|
||||
let site = SiteView::read(&conn)?;
|
||||
|
@ -324,13 +395,28 @@ impl Perform<PostResponse> for Oper<CreatePostLike> {
|
|||
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
||||
};
|
||||
|
||||
// just output the score
|
||||
Ok(PostResponse { post: post_view })
|
||||
let res = PostResponse { post: post_view };
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendPost {
|
||||
op: UserOperation::CreatePostLike,
|
||||
post: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<PostResponse> for Oper<EditPost> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
|
||||
impl Perform for Oper<EditPost> {
|
||||
type Response = PostResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PostResponse, Error> {
|
||||
let data: &EditPost = &self.data;
|
||||
|
||||
if let Err(slurs) = slur_check(&data.name) {
|
||||
|
@ -350,6 +436,8 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Verify its the creator or a mod or admin
|
||||
let mut editors: Vec<i32> = vec![data.creator_id];
|
||||
editors.append(
|
||||
|
@ -443,16 +531,32 @@ impl Perform<PostResponse> for Oper<EditPost> {
|
|||
ModStickyPost::create(&conn, &form)?;
|
||||
}
|
||||
|
||||
post_update(&updated_post, &user, conn)?;
|
||||
post_update(&updated_post, &user, &conn)?;
|
||||
|
||||
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
|
||||
|
||||
Ok(PostResponse { post: post_view })
|
||||
let res = PostResponse { post: post_view };
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendPost {
|
||||
op: UserOperation::EditPost,
|
||||
post: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<PostResponse> for Oper<SavePost> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
|
||||
impl Perform for Oper<SavePost> {
|
||||
type Response = PostResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PostResponse, Error> {
|
||||
let data: &SavePost = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -467,6 +571,8 @@ impl Perform<PostResponse> for Oper<SavePost> {
|
|||
user_id,
|
||||
};
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
if data.save {
|
||||
match PostSaved::save(&conn, &post_saved_form) {
|
||||
Ok(post) => post,
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
use super::user::Register;
|
||||
use super::*;
|
||||
use crate::api::user::Register;
|
||||
use crate::api::{Oper, Perform};
|
||||
use crate::apub::fetcher::search_by_apub_id;
|
||||
use crate::settings::Settings;
|
||||
use diesel::PgConnection;
|
||||
use log::{debug, info};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ListCategories {}
|
||||
|
@ -79,7 +73,7 @@ pub struct EditSite {
|
|||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetSite {}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct SiteResponse {
|
||||
site: SiteView,
|
||||
}
|
||||
|
@ -114,10 +108,18 @@ pub struct SaveSiteConfig {
|
|||
auth: String,
|
||||
}
|
||||
|
||||
impl Perform<ListCategoriesResponse> for Oper<ListCategories> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<ListCategoriesResponse, Error> {
|
||||
impl Perform for Oper<ListCategories> {
|
||||
type Response = ListCategoriesResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<ListCategoriesResponse, Error> {
|
||||
let _data: &ListCategories = &self.data;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let categories: Vec<Category> = Category::list_all(&conn)?;
|
||||
|
||||
// Return the jwt
|
||||
|
@ -125,10 +127,18 @@ impl Perform<ListCategoriesResponse> for Oper<ListCategories> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<GetModlogResponse> for Oper<GetModlog> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetModlogResponse, Error> {
|
||||
impl Perform for Oper<GetModlog> {
|
||||
type Response = GetModlogResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetModlogResponse, Error> {
|
||||
let data: &GetModlog = &self.data;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let removed_posts = ModRemovePostView::list(
|
||||
&conn,
|
||||
data.community_id,
|
||||
|
@ -198,8 +208,14 @@ impl Perform<GetModlogResponse> for Oper<GetModlog> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<SiteResponse> for Oper<CreateSite> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<SiteResponse, Error> {
|
||||
impl Perform for Oper<CreateSite> {
|
||||
type Response = SiteResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<SiteResponse, Error> {
|
||||
let data: &CreateSite = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -219,6 +235,8 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Make sure user is an admin
|
||||
if !UserView::read(&conn, user_id)?.admin {
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
|
@ -245,8 +263,13 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<SiteResponse> for Oper<EditSite> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<SiteResponse, Error> {
|
||||
impl Perform for Oper<EditSite> {
|
||||
type Response = SiteResponse;
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<SiteResponse, Error> {
|
||||
let data: &EditSite = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -266,6 +289,8 @@ impl Perform<SiteResponse> for Oper<EditSite> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Make sure user is an admin
|
||||
if !UserView::read(&conn, user_id)?.admin {
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
|
@ -290,14 +315,33 @@ impl Perform<SiteResponse> for Oper<EditSite> {
|
|||
|
||||
let site_view = SiteView::read(&conn)?;
|
||||
|
||||
Ok(SiteResponse { site: site_view })
|
||||
let res = SiteResponse { site: site_view };
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendAllMessage {
|
||||
op: UserOperation::EditSite,
|
||||
response: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetSiteResponse> for Oper<GetSite> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetSiteResponse, Error> {
|
||||
impl Perform for Oper<GetSite> {
|
||||
type Response = GetSiteResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetSiteResponse, Error> {
|
||||
let _data: &GetSite = &self.data;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// TODO refactor this a little
|
||||
let site_view = if let Ok(_site) = Site::read(&conn, 1) {
|
||||
Some(SiteView::read(&conn)?)
|
||||
} else if let Some(setup) = Settings::get().setup.as_ref() {
|
||||
|
@ -309,7 +353,7 @@ impl Perform<GetSiteResponse> for Oper<GetSite> {
|
|||
admin: true,
|
||||
show_nsfw: true,
|
||||
};
|
||||
let login_response = Oper::new(register).perform(&conn)?;
|
||||
let login_response = Oper::new(register).perform(pool.clone(), websocket_info.clone())?;
|
||||
info!("Admin {} created", setup.admin_username);
|
||||
|
||||
let create_site = CreateSite {
|
||||
|
@ -320,7 +364,7 @@ impl Perform<GetSiteResponse> for Oper<GetSite> {
|
|||
enable_nsfw: false,
|
||||
auth: login_response.jwt,
|
||||
};
|
||||
Oper::new(create_site).perform(&conn)?;
|
||||
Oper::new(create_site).perform(pool, websocket_info.clone())?;
|
||||
info!("Site {} created", setup.site_name);
|
||||
Some(SiteView::read(&conn)?)
|
||||
} else {
|
||||
|
@ -342,21 +386,41 @@ impl Perform<GetSiteResponse> for Oper<GetSite> {
|
|||
|
||||
let banned = UserView::banned(&conn)?;
|
||||
|
||||
let online = if let Some(_ws) = websocket_info {
|
||||
// TODO
|
||||
1
|
||||
// let fut = async {
|
||||
// ws.chatserver.send(GetUsersOnline).await.unwrap()
|
||||
// };
|
||||
// Runtime::new().unwrap().block_on(fut)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Ok(GetSiteResponse {
|
||||
site: site_view,
|
||||
admins,
|
||||
banned,
|
||||
online: 0,
|
||||
online,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<SearchResponse> for Oper<Search> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<SearchResponse, Error> {
|
||||
impl Perform for Oper<Search> {
|
||||
type Response = SearchResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<SearchResponse, Error> {
|
||||
let data: &Search = &self.data;
|
||||
|
||||
dbg!(&data);
|
||||
match search_by_apub_id(&data.q, conn) {
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
match search_by_apub_id(&data.q, &conn) {
|
||||
Ok(r) => return Ok(r),
|
||||
Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
|
||||
}
|
||||
|
@ -475,8 +539,14 @@ impl Perform<SearchResponse> for Oper<Search> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetSiteResponse, Error> {
|
||||
impl Perform for Oper<TransferSite> {
|
||||
type Response = GetSiteResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetSiteResponse, Error> {
|
||||
let data: &TransferSite = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -486,6 +556,8 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let read_site = Site::read(&conn, 1)?;
|
||||
|
||||
// Make sure user is the creator
|
||||
|
@ -538,8 +610,14 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<GetSiteConfigResponse> for Oper<GetSiteConfig> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetSiteConfigResponse, Error> {
|
||||
impl Perform for Oper<GetSiteConfig> {
|
||||
type Response = GetSiteConfigResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetSiteConfigResponse, Error> {
|
||||
let data: &GetSiteConfig = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -549,6 +627,8 @@ impl Perform<GetSiteConfigResponse> for Oper<GetSiteConfig> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Only let admins read this
|
||||
let admins = UserView::admins(&conn)?;
|
||||
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
|
||||
|
@ -563,8 +643,14 @@ impl Perform<GetSiteConfigResponse> for Oper<GetSiteConfig> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<GetSiteConfigResponse> for Oper<SaveSiteConfig> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetSiteConfigResponse, Error> {
|
||||
impl Perform for Oper<SaveSiteConfig> {
|
||||
type Response = GetSiteConfigResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetSiteConfigResponse, Error> {
|
||||
let data: &SaveSiteConfig = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -574,6 +660,8 @@ impl Perform<GetSiteConfigResponse> for Oper<SaveSiteConfig> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Only let admins read this
|
||||
let admins = UserView::admins(&conn)?;
|
||||
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
use super::*;
|
||||
use crate::apub::signatures::generate_actor_keypair;
|
||||
use crate::apub::{make_apub_endpoint, EndpointType};
|
||||
use crate::settings::Settings;
|
||||
use crate::{generate_random_string, send_email};
|
||||
use bcrypt::verify;
|
||||
use diesel::PgConnection;
|
||||
use log::error;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Login {
|
||||
|
@ -91,7 +84,7 @@ pub struct AddAdmin {
|
|||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct AddAdminResponse {
|
||||
admins: Vec<UserView>,
|
||||
}
|
||||
|
@ -105,7 +98,7 @@ pub struct BanUser {
|
|||
auth: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct BanUserResponse {
|
||||
user: UserView,
|
||||
banned: bool,
|
||||
|
@ -206,10 +199,18 @@ pub struct UserJoinResponse {
|
|||
pub user_id: i32,
|
||||
}
|
||||
|
||||
impl Perform<LoginResponse> for Oper<Login> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
|
||||
impl Perform for Oper<Login> {
|
||||
type Response = LoginResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<LoginResponse, Error> {
|
||||
let data: &Login = &self.data;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Fetch that username / email
|
||||
let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
|
||||
Ok(user) => user,
|
||||
|
@ -227,10 +228,18 @@ impl Perform<LoginResponse> for Oper<Login> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<LoginResponse> for Oper<Register> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
|
||||
impl Perform for Oper<Register> {
|
||||
type Response = LoginResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<LoginResponse, Error> {
|
||||
let data: &Register = &self.data;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Make sure site has open registration
|
||||
if let Ok(site) = SiteView::read(&conn) {
|
||||
if !site.open_registration {
|
||||
|
@ -357,8 +366,14 @@ impl Perform<LoginResponse> for Oper<Register> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
|
||||
impl Perform for Oper<SaveUserSettings> {
|
||||
type Response = LoginResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<LoginResponse, Error> {
|
||||
let data: &SaveUserSettings = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -368,6 +383,8 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let read_user = User_::read(&conn, user_id)?;
|
||||
|
||||
let email = match &data.email {
|
||||
|
@ -450,10 +467,18 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetUserDetailsResponse, Error> {
|
||||
impl Perform for Oper<GetUserDetails> {
|
||||
type Response = GetUserDetailsResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetUserDetailsResponse, Error> {
|
||||
let data: &GetUserDetails = &self.data;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let user_claims: Option<Claims> = match &data.auth {
|
||||
Some(auth) => match Claims::decode(&auth) {
|
||||
Ok(claims) => Some(claims.claims),
|
||||
|
@ -547,8 +572,14 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<AddAdminResponse, Error> {
|
||||
impl Perform for Oper<AddAdmin> {
|
||||
type Response = AddAdminResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<AddAdminResponse, Error> {
|
||||
let data: &AddAdmin = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -558,6 +589,8 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Make sure user is an admin
|
||||
if !UserView::read(&conn, user_id)?.admin {
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
|
@ -583,12 +616,28 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
|
|||
let creator_user = admins.remove(creator_index);
|
||||
admins.insert(0, creator_user);
|
||||
|
||||
Ok(AddAdminResponse { admins })
|
||||
let res = AddAdminResponse { admins };
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendAllMessage {
|
||||
op: UserOperation::AddAdmin,
|
||||
response: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<BanUserResponse> for Oper<BanUser> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<BanUserResponse, Error> {
|
||||
impl Perform for Oper<BanUser> {
|
||||
type Response = BanUserResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<BanUserResponse, Error> {
|
||||
let data: &BanUser = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -598,6 +647,8 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Make sure user is an admin
|
||||
if !UserView::read(&conn, user_id)?.admin {
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
|
@ -626,15 +677,31 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
|
|||
|
||||
let user_view = UserView::read(&conn, data.user_id)?;
|
||||
|
||||
Ok(BanUserResponse {
|
||||
let res = BanUserResponse {
|
||||
user: user_view,
|
||||
banned: data.ban,
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendAllMessage {
|
||||
op: UserOperation::BanUser,
|
||||
response: res.clone(),
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetRepliesResponse, Error> {
|
||||
impl Perform for Oper<GetReplies> {
|
||||
type Response = GetRepliesResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetRepliesResponse, Error> {
|
||||
let data: &GetReplies = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -646,6 +713,8 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
|||
|
||||
let sort = SortType::from_str(&data.sort)?;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let replies = ReplyQueryBuilder::create(&conn, user_id)
|
||||
.sort(&sort)
|
||||
.unread_only(data.unread_only)
|
||||
|
@ -657,8 +726,14 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetUserMentionsResponse, Error> {
|
||||
impl Perform for Oper<GetUserMentions> {
|
||||
type Response = GetUserMentionsResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetUserMentionsResponse, Error> {
|
||||
let data: &GetUserMentions = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -670,6 +745,8 @@ impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
|
|||
|
||||
let sort = SortType::from_str(&data.sort)?;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let mentions = UserMentionQueryBuilder::create(&conn, user_id)
|
||||
.sort(&sort)
|
||||
.unread_only(data.unread_only)
|
||||
|
@ -681,8 +758,14 @@ impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<UserMentionResponse, Error> {
|
||||
impl Perform for Oper<EditUserMention> {
|
||||
type Response = UserMentionResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<UserMentionResponse, Error> {
|
||||
let data: &EditUserMention = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -692,6 +775,8 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let user_mention = UserMention::read(&conn, data.user_mention_id)?;
|
||||
|
||||
let user_mention_form = UserMentionForm {
|
||||
|
@ -714,8 +799,14 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<GetRepliesResponse, Error> {
|
||||
impl Perform for Oper<MarkAllAsRead> {
|
||||
type Response = GetRepliesResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetRepliesResponse, Error> {
|
||||
let data: &MarkAllAsRead = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -725,6 +816,8 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let replies = ReplyQueryBuilder::create(&conn, user_id)
|
||||
.unread_only(true)
|
||||
.page(1)
|
||||
|
@ -787,8 +880,14 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
|
||||
impl Perform for Oper<DeleteAccount> {
|
||||
type Response = LoginResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<LoginResponse, Error> {
|
||||
let data: &DeleteAccount = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -798,6 +897,8 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let user: User_ = User_::read(&conn, user_id)?;
|
||||
|
||||
// Verify the password
|
||||
|
@ -839,10 +940,18 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PasswordResetResponse, Error> {
|
||||
impl Perform for Oper<PasswordReset> {
|
||||
type Response = PasswordResetResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PasswordResetResponse, Error> {
|
||||
let data: &PasswordReset = &self.data;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Fetch that email
|
||||
let user: User_ = match User_::find_by_email(&conn, &data.email) {
|
||||
Ok(user) => user,
|
||||
|
@ -870,10 +979,18 @@ impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<LoginResponse> for Oper<PasswordChange> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
|
||||
impl Perform for Oper<PasswordChange> {
|
||||
type Response = LoginResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<LoginResponse, Error> {
|
||||
let data: &PasswordChange = &self.data;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Fetch the user_id from the token
|
||||
let user_id = PasswordResetRequest::read_from_token(&conn, &data.token)?.user_id;
|
||||
|
||||
|
@ -895,8 +1012,14 @@ impl Perform<LoginResponse> for Oper<PasswordChange> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<PrivateMessageResponse> for Oper<CreatePrivateMessage> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
|
||||
impl Perform for Oper<CreatePrivateMessage> {
|
||||
type Response = PrivateMessageResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PrivateMessageResponse, Error> {
|
||||
let data: &CreatePrivateMessage = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -908,6 +1031,8 @@ impl Perform<PrivateMessageResponse> for Oper<CreatePrivateMessage> {
|
|||
|
||||
let hostname = &format!("https://{}", Settings::get().hostname);
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
// Check for a site ban
|
||||
if UserView::read(&conn, user_id)?.banned {
|
||||
return Err(APIError::err("site_ban").into());
|
||||
|
@ -953,12 +1078,29 @@ impl Perform<PrivateMessageResponse> for Oper<CreatePrivateMessage> {
|
|||
|
||||
let message = PrivateMessageView::read(&conn, inserted_private_message.id)?;
|
||||
|
||||
Ok(PrivateMessageResponse { message })
|
||||
let res = PrivateMessageResponse { message };
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
ws.chatserver.do_send(SendUserRoomMessage {
|
||||
op: UserOperation::CreatePrivateMessage,
|
||||
response: res.clone(),
|
||||
recipient_id: recipient_user.id,
|
||||
my_id: ws.id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform<PrivateMessageResponse> for Oper<EditPrivateMessage> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
|
||||
impl Perform for Oper<EditPrivateMessage> {
|
||||
type Response = PrivateMessageResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PrivateMessageResponse, Error> {
|
||||
let data: &EditPrivateMessage = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -968,6 +1110,8 @@ impl Perform<PrivateMessageResponse> for Oper<EditPrivateMessage> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let orig_private_message = PrivateMessage::read(&conn, data.edit_id)?;
|
||||
|
||||
// Check for a site ban
|
||||
|
@ -1012,8 +1156,14 @@ impl Perform<PrivateMessageResponse> for Oper<EditPrivateMessage> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<PrivateMessagesResponse> for Oper<GetPrivateMessages> {
|
||||
fn perform(&self, conn: &PgConnection) -> Result<PrivateMessagesResponse, Error> {
|
||||
impl Perform for Oper<GetPrivateMessages> {
|
||||
type Response = PrivateMessagesResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PrivateMessagesResponse, Error> {
|
||||
let data: &GetPrivateMessages = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -1023,6 +1173,8 @@ impl Perform<PrivateMessagesResponse> for Oper<GetPrivateMessages> {
|
|||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let messages = PrivateMessageQueryBuilder::create(&conn, user_id)
|
||||
.page(data.page)
|
||||
.limit(data.limit)
|
||||
|
@ -1033,8 +1185,14 @@ impl Perform<PrivateMessagesResponse> for Oper<GetPrivateMessages> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Perform<UserJoinResponse> for Oper<UserJoin> {
|
||||
fn perform(&self, _conn: &PgConnection) -> Result<UserJoinResponse, Error> {
|
||||
impl Perform for Oper<UserJoin> {
|
||||
type Response = UserJoinResponse;
|
||||
|
||||
fn perform(
|
||||
&self,
|
||||
_pool: Pool<ConnectionManager<PgConnection>>,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<UserJoinResponse, Error> {
|
||||
let data: &UserJoin = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
|
@ -1043,6 +1201,13 @@ impl Perform<UserJoinResponse> for Oper<UserJoin> {
|
|||
};
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
if let Some(ws) = websocket_info {
|
||||
if let Some(id) = ws.id {
|
||||
ws.chatserver.do_send(JoinUserRoom { user_id, id });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(UserJoinResponse { user_id })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ pub extern crate strum;
|
|||
pub mod api;
|
||||
pub mod apub;
|
||||
pub mod db;
|
||||
pub mod rate_limit;
|
||||
pub mod routes;
|
||||
pub mod schema;
|
||||
pub mod settings;
|
||||
|
@ -36,7 +37,8 @@ pub mod version;
|
|||
pub mod websocket;
|
||||
|
||||
use crate::settings::Settings;
|
||||
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime};
|
||||
use actix_web::dev::ConnectionInfo;
|
||||
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
|
||||
use isahc::prelude::*;
|
||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::smtp::extension::ClientId;
|
||||
|
@ -50,6 +52,16 @@ use rand::{thread_rng, Rng};
|
|||
use regex::{Regex, RegexBuilder};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub type ConnectionId = usize;
|
||||
pub type PostId = i32;
|
||||
pub type CommunityId = i32;
|
||||
pub type UserId = i32;
|
||||
pub type IPAddr = String;
|
||||
|
||||
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
|
||||
DateTime::<Utc>::from_utc(ndt, Utc)
|
||||
}
|
||||
|
||||
pub fn naive_now() -> NaiveDateTime {
|
||||
chrono::prelude::Utc::now().naive_utc()
|
||||
}
|
||||
|
@ -227,6 +239,16 @@ pub fn markdown_to_html(text: &str) -> String {
|
|||
comrak::markdown_to_html(text, &comrak::ComrakOptions::default())
|
||||
}
|
||||
|
||||
pub fn get_ip(conn_info: &ConnectionInfo) -> String {
|
||||
conn_info
|
||||
.remote()
|
||||
.unwrap_or("127.0.0.1:12345")
|
||||
.split(':')
|
||||
.next()
|
||||
.unwrap_or("127.0.0.1")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{extract_usernames, is_email_regex, remove_slurs, slur_check, slurs_vec_to_str};
|
||||
|
|
|
@ -7,10 +7,15 @@ use actix_web::*;
|
|||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use lemmy_server::db::code_migrations::run_advanced_migrations;
|
||||
use lemmy_server::routes::{api, federation, feeds, index, nodeinfo, webfinger, websocket};
|
||||
use lemmy_server::settings::Settings;
|
||||
use lemmy_server::websocket::server::*;
|
||||
use lemmy_server::{
|
||||
db::code_migrations::run_advanced_migrations,
|
||||
rate_limit::{rate_limiter::RateLimiter, RateLimit},
|
||||
routes::{api, federation, feeds, index, nodeinfo, webfinger},
|
||||
settings::Settings,
|
||||
websocket::server::*,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
embed_migrations!();
|
||||
|
||||
|
@ -31,8 +36,13 @@ async fn main() -> Result<(), Error> {
|
|||
embedded_migrations::run(&conn).unwrap();
|
||||
run_advanced_migrations(&conn).unwrap();
|
||||
|
||||
// Set up the rate limiter
|
||||
let rate_limiter = RateLimit {
|
||||
rate_limiter: Arc::new(Mutex::new(RateLimiter::default())),
|
||||
};
|
||||
|
||||
// Set up websocket server
|
||||
let server = ChatServer::startup(pool.clone()).start();
|
||||
let server = ChatServer::startup(pool.clone(), rate_limiter.clone()).start();
|
||||
|
||||
println!(
|
||||
"Starting http server at {}:{}",
|
||||
|
@ -43,18 +53,18 @@ async fn main() -> Result<(), Error> {
|
|||
Ok(
|
||||
HttpServer::new(move || {
|
||||
let settings = Settings::get();
|
||||
let rate_limiter = rate_limiter.clone();
|
||||
App::new()
|
||||
.wrap(middleware::Logger::default())
|
||||
.data(pool.clone())
|
||||
.data(server.clone())
|
||||
// The routes
|
||||
.configure(api::config)
|
||||
.configure(move |cfg| api::config(cfg, &rate_limiter))
|
||||
.configure(federation::config)
|
||||
.configure(feeds::config)
|
||||
.configure(index::config)
|
||||
.configure(nodeinfo::config)
|
||||
.configure(webfinger::config)
|
||||
.configure(websocket::config)
|
||||
// static files
|
||||
.service(actix_files::Files::new(
|
||||
"/static",
|
||||
|
|
194
server/src/rate_limit/mod.rs
Normal file
194
server/src/rate_limit/mod.rs
Normal file
|
@ -0,0 +1,194 @@
|
|||
pub mod rate_limiter;
|
||||
|
||||
use super::{IPAddr, Settings};
|
||||
use crate::api::APIError;
|
||||
use crate::get_ip;
|
||||
use crate::settings::RateLimitConfig;
|
||||
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
|
||||
use failure::Error;
|
||||
use futures::future::{ok, Ready};
|
||||
use log::debug;
|
||||
use rate_limiter::{RateLimitType, RateLimiter};
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::SystemTime;
|
||||
use strum::IntoEnumIterator;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RateLimit {
|
||||
pub rate_limiter: Arc<Mutex<RateLimiter>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RateLimited {
|
||||
rate_limiter: Arc<Mutex<RateLimiter>>,
|
||||
type_: RateLimitType,
|
||||
}
|
||||
|
||||
pub struct RateLimitedMiddleware<S> {
|
||||
rate_limited: RateLimited,
|
||||
service: S,
|
||||
}
|
||||
|
||||
impl RateLimit {
|
||||
pub fn message(&self) -> RateLimited {
|
||||
self.kind(RateLimitType::Message)
|
||||
}
|
||||
|
||||
pub fn post(&self) -> RateLimited {
|
||||
self.kind(RateLimitType::Post)
|
||||
}
|
||||
|
||||
pub fn register(&self) -> RateLimited {
|
||||
self.kind(RateLimitType::Register)
|
||||
}
|
||||
|
||||
fn kind(&self, type_: RateLimitType) -> RateLimited {
|
||||
RateLimited {
|
||||
rate_limiter: self.rate_limiter.clone(),
|
||||
type_,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RateLimited {
|
||||
pub async fn wrap<T, E>(
|
||||
self,
|
||||
ip_addr: String,
|
||||
fut: impl Future<Output = Result<T, E>>,
|
||||
) -> Result<T, E>
|
||||
where
|
||||
E: From<failure::Error>,
|
||||
{
|
||||
let rate_limit: RateLimitConfig = actix_web::web::block(move || {
|
||||
// needs to be in a web::block because the RwLock in settings is from stdlib
|
||||
Ok(Settings::get().rate_limit) as Result<_, failure::Error>
|
||||
})
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
actix_web::error::BlockingError::Error(e) => e,
|
||||
_ => APIError::err("Operation canceled").into(),
|
||||
})?;
|
||||
|
||||
// before
|
||||
{
|
||||
let mut limiter = self.rate_limiter.lock().await;
|
||||
|
||||
match self.type_ {
|
||||
RateLimitType::Message => {
|
||||
limiter.check_rate_limit_full(
|
||||
self.type_,
|
||||
&ip_addr,
|
||||
rate_limit.message,
|
||||
rate_limit.message_per_second,
|
||||
false,
|
||||
)?;
|
||||
|
||||
return fut.await;
|
||||
}
|
||||
RateLimitType::Post => {
|
||||
limiter.check_rate_limit_full(
|
||||
self.type_.clone(),
|
||||
&ip_addr,
|
||||
rate_limit.post,
|
||||
rate_limit.post_per_second,
|
||||
true,
|
||||
)?;
|
||||
}
|
||||
RateLimitType::Register => {
|
||||
limiter.check_rate_limit_full(
|
||||
self.type_,
|
||||
&ip_addr,
|
||||
rate_limit.register,
|
||||
rate_limit.register_per_second,
|
||||
true,
|
||||
)?;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let res = fut.await;
|
||||
|
||||
// after
|
||||
{
|
||||
let mut limiter = self.rate_limiter.lock().await;
|
||||
if res.is_ok() {
|
||||
match self.type_ {
|
||||
RateLimitType::Post => {
|
||||
limiter.check_rate_limit_full(
|
||||
self.type_,
|
||||
&ip_addr,
|
||||
rate_limit.post,
|
||||
rate_limit.post_per_second,
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
RateLimitType::Register => {
|
||||
limiter.check_rate_limit_full(
|
||||
self.type_,
|
||||
&ip_addr,
|
||||
rate_limit.register,
|
||||
rate_limit.register_per_second,
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Transform<S> for RateLimited
|
||||
where
|
||||
S: Service<Request = ServiceRequest, Response = ServiceResponse, Error = actix_web::Error>,
|
||||
S::Future: 'static,
|
||||
{
|
||||
type Request = S::Request;
|
||||
type Response = S::Response;
|
||||
type Error = actix_web::Error;
|
||||
type InitError = ();
|
||||
type Transform = RateLimitedMiddleware<S>;
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ok(RateLimitedMiddleware {
|
||||
rate_limited: self.clone(),
|
||||
service,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type FutResult<T, E> = dyn Future<Output = Result<T, E>>;
|
||||
|
||||
impl<S> Service for RateLimitedMiddleware<S>
|
||||
where
|
||||
S: Service<Request = ServiceRequest, Response = ServiceResponse, Error = actix_web::Error>,
|
||||
S::Future: 'static,
|
||||
{
|
||||
type Request = S::Request;
|
||||
type Response = S::Response;
|
||||
type Error = actix_web::Error;
|
||||
type Future = Pin<Box<FutResult<Self::Response, Self::Error>>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.service.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: S::Request) -> Self::Future {
|
||||
let ip_addr = get_ip(&req.connection_info());
|
||||
|
||||
let fut = self
|
||||
.rate_limited
|
||||
.clone()
|
||||
.wrap(ip_addr, self.service.call(req));
|
||||
|
||||
Box::pin(async move { fut.await.map_err(actix_web::Error::from) })
|
||||
}
|
||||
}
|
101
server/src/rate_limit/rate_limiter.rs
Normal file
101
server/src/rate_limit/rate_limiter.rs
Normal file
|
@ -0,0 +1,101 @@
|
|||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RateLimitBucket {
|
||||
last_checked: SystemTime,
|
||||
allowance: f64,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Debug, EnumIter, Copy, Clone)]
|
||||
pub enum RateLimitType {
|
||||
Message,
|
||||
Register,
|
||||
Post,
|
||||
}
|
||||
|
||||
/// Rate limiting based on rate type and IP addr
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RateLimiter {
|
||||
pub buckets: HashMap<RateLimitType, HashMap<IPAddr, RateLimitBucket>>,
|
||||
}
|
||||
|
||||
impl Default for RateLimiter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buckets: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RateLimiter {
|
||||
fn insert_ip(&mut self, ip: &str) {
|
||||
for rate_limit_type in RateLimitType::iter() {
|
||||
if self.buckets.get(&rate_limit_type).is_none() {
|
||||
self.buckets.insert(rate_limit_type, HashMap::new());
|
||||
}
|
||||
|
||||
if let Some(bucket) = self.buckets.get_mut(&rate_limit_type) {
|
||||
if bucket.get(ip).is_none() {
|
||||
bucket.insert(
|
||||
ip.to_string(),
|
||||
RateLimitBucket {
|
||||
last_checked: SystemTime::now(),
|
||||
allowance: -2f64,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::float_cmp)]
|
||||
pub(super) fn check_rate_limit_full(
|
||||
&mut self,
|
||||
type_: RateLimitType,
|
||||
ip: &str,
|
||||
rate: i32,
|
||||
per: i32,
|
||||
check_only: bool,
|
||||
) -> Result<(), Error> {
|
||||
self.insert_ip(ip);
|
||||
if let Some(bucket) = self.buckets.get_mut(&type_) {
|
||||
if let Some(rate_limit) = bucket.get_mut(ip) {
|
||||
let current = SystemTime::now();
|
||||
let time_passed = current.duration_since(rate_limit.last_checked)?.as_secs() as f64;
|
||||
|
||||
// The initial value
|
||||
if rate_limit.allowance == -2f64 {
|
||||
rate_limit.allowance = rate as f64;
|
||||
};
|
||||
|
||||
rate_limit.last_checked = current;
|
||||
rate_limit.allowance += time_passed * (rate as f64 / per as f64);
|
||||
if !check_only && rate_limit.allowance > rate as f64 {
|
||||
rate_limit.allowance = rate as f64;
|
||||
}
|
||||
|
||||
if rate_limit.allowance < 1.0 {
|
||||
debug!(
|
||||
"Rate limited IP: {}, time_passed: {}, allowance: {}",
|
||||
ip, time_passed, rate_limit.allowance
|
||||
);
|
||||
Err(
|
||||
APIError {
|
||||
message: format!("Too many requests. {} per {} seconds", rate, per),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
if !check_only {
|
||||
rate_limit.allowance -= 1.0;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,105 +1,185 @@
|
|||
use super::*;
|
||||
use crate::api::comment::*;
|
||||
use crate::api::community::*;
|
||||
use crate::api::post::*;
|
||||
use crate::api::site::*;
|
||||
use crate::api::user::*;
|
||||
use crate::api::{Oper, Perform};
|
||||
use actix_web::{web, HttpResponse};
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use serde::Serialize;
|
||||
use crate::rate_limit::RateLimit;
|
||||
use actix_web::guard;
|
||||
|
||||
type DbParam = web::Data<Pool<ConnectionManager<PgConnection>>>;
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg
|
||||
// Site
|
||||
.route("/api/v1/site", web::get().to(route_get::<GetSite, GetSiteResponse>))
|
||||
.route("/api/v1/categories", web::get().to(route_get::<ListCategories, ListCategoriesResponse>))
|
||||
.route("/api/v1/modlog", web::get().to(route_get::<GetModlog, GetModlogResponse>))
|
||||
.route("/api/v1/search", web::get().to(route_get::<Search, SearchResponse>))
|
||||
// Community
|
||||
.route("/api/v1/community", web::post().to(route_post::<CreateCommunity, CommunityResponse>))
|
||||
.route("/api/v1/community", web::get().to(route_get::<GetCommunity, GetCommunityResponse>))
|
||||
.route("/api/v1/community", web::put().to(route_post::<EditCommunity, CommunityResponse>))
|
||||
.route("/api/v1/community/list", web::get().to(route_get::<ListCommunities, ListCommunitiesResponse>))
|
||||
.route("/api/v1/community/follow", web::post().to(route_post::<FollowCommunity, CommunityResponse>))
|
||||
// Post
|
||||
.route("/api/v1/post", web::post().to(route_post::<CreatePost, PostResponse>))
|
||||
.route("/api/v1/post", web::put().to(route_post::<EditPost, PostResponse>))
|
||||
.route("/api/v1/post", web::get().to(route_get::<GetPost, GetPostResponse>))
|
||||
.route("/api/v1/post/list", web::get().to(route_get::<GetPosts, GetPostsResponse>))
|
||||
.route("/api/v1/post/like", web::post().to(route_post::<CreatePostLike, PostResponse>))
|
||||
.route("/api/v1/post/save", web::put().to(route_post::<SavePost, PostResponse>))
|
||||
// Comment
|
||||
.route("/api/v1/comment", web::post().to(route_post::<CreateComment, CommentResponse>))
|
||||
.route("/api/v1/comment", web::put().to(route_post::<EditComment, CommentResponse>))
|
||||
.route("/api/v1/comment/like", web::post().to(route_post::<CreateCommentLike, CommentResponse>))
|
||||
.route("/api/v1/comment/save", web::put().to(route_post::<SaveComment, CommentResponse>))
|
||||
// User
|
||||
.route("/api/v1/user", web::get().to(route_get::<GetUserDetails, GetUserDetailsResponse>))
|
||||
.route("/api/v1/user/mention", web::get().to(route_get::<GetUserMentions, GetUserMentionsResponse>))
|
||||
.route("/api/v1/user/mention", web::put().to(route_post::<EditUserMention, UserMentionResponse>))
|
||||
.route("/api/v1/user/replies", web::get().to(route_get::<GetReplies, GetRepliesResponse>))
|
||||
.route("/api/v1/user/followed_communities", web::get().to(route_get::<GetFollowedCommunities, GetFollowedCommunitiesResponse>))
|
||||
// Mod actions
|
||||
.route("/api/v1/community/transfer", web::post().to(route_post::<TransferCommunity, GetCommunityResponse>))
|
||||
.route("/api/v1/community/ban_user", web::post().to(route_post::<BanFromCommunity, BanFromCommunityResponse>))
|
||||
.route("/api/v1/community/mod", web::post().to(route_post::<AddModToCommunity, AddModToCommunityResponse>))
|
||||
// Admin actions
|
||||
.route("/api/v1/site", web::post().to(route_post::<CreateSite, SiteResponse>))
|
||||
.route("/api/v1/site", web::put().to(route_post::<EditSite, SiteResponse>))
|
||||
.route("/api/v1/site/transfer", web::post().to(route_post::<TransferSite, GetSiteResponse>))
|
||||
.route("/api/v1/site/config", web::get().to(route_get::<GetSiteConfig, GetSiteConfigResponse>))
|
||||
.route("/api/v1/site/config", web::put().to(route_post::<SaveSiteConfig, GetSiteConfigResponse>))
|
||||
.route("/api/v1/admin/add", web::post().to(route_post::<AddAdmin, AddAdminResponse>))
|
||||
.route("/api/v1/user/ban", web::post().to(route_post::<BanUser, BanUserResponse>))
|
||||
// User account actions
|
||||
.route("/api/v1/user/login", web::post().to(route_post::<Login, LoginResponse>))
|
||||
.route("/api/v1/user/register", web::post().to(route_post::<Register, LoginResponse>))
|
||||
.route("/api/v1/user/delete_account", web::post().to(route_post::<DeleteAccount, LoginResponse>))
|
||||
.route("/api/v1/user/password_reset", web::post().to(route_post::<PasswordReset, PasswordResetResponse>))
|
||||
.route("/api/v1/user/password_change", web::post().to(route_post::<PasswordChange, LoginResponse>))
|
||||
.route("/api/v1/user/mark_all_as_read", web::post().to(route_post::<MarkAllAsRead, GetRepliesResponse>))
|
||||
.route("/api/v1/user/save_user_settings", web::put().to(route_post::<SaveUserSettings, LoginResponse>));
|
||||
pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
||||
cfg.service(
|
||||
web::scope("/api/v1")
|
||||
// Websockets
|
||||
.service(web::resource("/ws").to(super::websocket::chat_route))
|
||||
// Site
|
||||
.service(
|
||||
web::scope("/site")
|
||||
.wrap(rate_limit.message())
|
||||
.route("", web::get().to(route_get::<GetSite>))
|
||||
// Admin Actions
|
||||
.route("", web::post().to(route_post::<CreateSite>))
|
||||
.route("", web::put().to(route_post::<EditSite>))
|
||||
.route("/transfer", web::post().to(route_post::<TransferSite>))
|
||||
.route("/config", web::get().to(route_get::<GetSiteConfig>))
|
||||
.route("/config", web::put().to(route_post::<SaveSiteConfig>)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/categories")
|
||||
.wrap(rate_limit.message())
|
||||
.route(web::get().to(route_get::<ListCategories>)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/modlog")
|
||||
.wrap(rate_limit.message())
|
||||
.route(web::get().to(route_get::<GetModlog>)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/search")
|
||||
.wrap(rate_limit.message())
|
||||
.route(web::get().to(route_get::<Search>)),
|
||||
)
|
||||
// Community
|
||||
.service(
|
||||
web::resource("/community")
|
||||
.guard(guard::Post())
|
||||
.wrap(rate_limit.register())
|
||||
.route(web::post().to(route_post::<CreateCommunity>)),
|
||||
)
|
||||
.service(
|
||||
web::scope("/community")
|
||||
.wrap(rate_limit.message())
|
||||
.route("", web::get().to(route_get::<GetCommunity>))
|
||||
.route("", web::put().to(route_post::<EditCommunity>))
|
||||
.route("/list", web::get().to(route_get::<ListCommunities>))
|
||||
.route("/follow", web::post().to(route_post::<FollowCommunity>))
|
||||
// Mod Actions
|
||||
.route("/transfer", web::post().to(route_post::<TransferCommunity>))
|
||||
.route("/ban_user", web::post().to(route_post::<BanFromCommunity>))
|
||||
.route("/mod", web::post().to(route_post::<AddModToCommunity>)),
|
||||
)
|
||||
// Post
|
||||
.service(
|
||||
// Handle POST to /post separately to add the post() rate limitter
|
||||
web::resource("/post")
|
||||
.guard(guard::Post())
|
||||
.wrap(rate_limit.post())
|
||||
.route(web::post().to(route_post::<CreatePost>)),
|
||||
)
|
||||
.service(
|
||||
web::scope("/post")
|
||||
.wrap(rate_limit.message())
|
||||
.route("", web::get().to(route_get::<GetPost>))
|
||||
.route("", web::put().to(route_post::<EditPost>))
|
||||
.route("/list", web::get().to(route_get::<GetPosts>))
|
||||
.route("/like", web::post().to(route_post::<CreatePostLike>))
|
||||
.route("/save", web::put().to(route_post::<SavePost>)),
|
||||
)
|
||||
// Comment
|
||||
.service(
|
||||
web::scope("/comment")
|
||||
.wrap(rate_limit.message())
|
||||
.route("", web::post().to(route_post::<CreateComment>))
|
||||
.route("", web::put().to(route_post::<EditComment>))
|
||||
.route("/like", web::post().to(route_post::<CreateCommentLike>))
|
||||
.route("/save", web::put().to(route_post::<SaveComment>)),
|
||||
)
|
||||
// User
|
||||
.service(
|
||||
// Account action, I don't like that it's in /user maybe /accounts
|
||||
// Handle /user/register separately to add the register() rate limitter
|
||||
web::resource("/user/register")
|
||||
.guard(guard::Post())
|
||||
.wrap(rate_limit.register())
|
||||
.route(web::post().to(route_post::<Register>)),
|
||||
)
|
||||
// User actions
|
||||
.service(
|
||||
web::scope("/user")
|
||||
.wrap(rate_limit.message())
|
||||
.route("", web::get().to(route_get::<GetUserDetails>))
|
||||
.route("/mention", web::get().to(route_get::<GetUserMentions>))
|
||||
.route("/mention", web::put().to(route_post::<EditUserMention>))
|
||||
.route("/replies", web::get().to(route_get::<GetReplies>))
|
||||
.route(
|
||||
"/followed_communities",
|
||||
web::get().to(route_get::<GetFollowedCommunities>),
|
||||
)
|
||||
// Admin action. I don't like that it's in /user
|
||||
.route("/ban", web::post().to(route_post::<BanUser>))
|
||||
// Account actions. I don't like that they're in /user maybe /accounts
|
||||
.route("/login", web::post().to(route_post::<Login>))
|
||||
.route(
|
||||
"/delete_account",
|
||||
web::post().to(route_post::<DeleteAccount>),
|
||||
)
|
||||
.route(
|
||||
"/password_reset",
|
||||
web::post().to(route_post::<PasswordReset>),
|
||||
)
|
||||
.route(
|
||||
"/password_change",
|
||||
web::post().to(route_post::<PasswordChange>),
|
||||
)
|
||||
// mark_all_as_read feels off being in this section as well
|
||||
.route(
|
||||
"/mark_all_as_read",
|
||||
web::post().to(route_post::<MarkAllAsRead>),
|
||||
)
|
||||
.route(
|
||||
"/save_user_settings",
|
||||
web::put().to(route_post::<SaveUserSettings>),
|
||||
),
|
||||
)
|
||||
// Admin Actions
|
||||
.service(
|
||||
web::resource("/admin/add")
|
||||
.wrap(rate_limit.message())
|
||||
.route(web::post().to(route_post::<AddAdmin>)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fn perform<Request, Response>(data: Request, db: DbParam) -> Result<HttpResponse, Error>
|
||||
fn perform<Request>(
|
||||
data: Request,
|
||||
db: DbPoolParam,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, Error>
|
||||
where
|
||||
Response: Serialize,
|
||||
Oper<Request>: Perform<Response>,
|
||||
Oper<Request>: Perform,
|
||||
{
|
||||
let conn = match db.get() {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(format_err!("{}", e)),
|
||||
let ws_info = WebsocketInfo {
|
||||
chatserver: chat_server.get_ref().to_owned(),
|
||||
id: None,
|
||||
};
|
||||
|
||||
let oper: Oper<Request> = Oper::new(data);
|
||||
let response = oper.perform(&conn);
|
||||
Ok(HttpResponse::Ok().json(response?))
|
||||
|
||||
let res = oper.perform(db.get_ref().to_owned(), Some(ws_info));
|
||||
|
||||
Ok(HttpResponse::Ok().json(res?))
|
||||
}
|
||||
|
||||
async fn route_get<Data, Response>(
|
||||
async fn route_get<Data>(
|
||||
data: web::Query<Data>,
|
||||
db: DbParam,
|
||||
db: DbPoolParam,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, Error>
|
||||
where
|
||||
Data: Serialize,
|
||||
Response: Serialize,
|
||||
Oper<Data>: Perform<Response>,
|
||||
Oper<Data>: Perform,
|
||||
{
|
||||
perform::<Data, Response>(data.0, db)
|
||||
perform::<Data>(data.0, db, chat_server)
|
||||
}
|
||||
|
||||
async fn route_post<Data, Response>(
|
||||
async fn route_post<Data>(
|
||||
data: web::Json<Data>,
|
||||
db: DbParam,
|
||||
db: DbPoolParam,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, Error>
|
||||
where
|
||||
Data: Serialize,
|
||||
Response: Serialize,
|
||||
Oper<Data>: Perform<Response>,
|
||||
Oper<Data>: Perform,
|
||||
{
|
||||
perform::<Data, Response>(data.0, db)
|
||||
perform::<Data>(data.0, db, chat_server)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use super::*;
|
||||
use crate::apub;
|
||||
use crate::settings::Settings;
|
||||
use actix_web::web;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
if Settings::get().federation.enabled {
|
||||
|
|
|
@ -6,16 +6,6 @@ use crate::db::site_view::SiteView;
|
|||
use crate::db::user::{Claims, User_};
|
||||
use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView};
|
||||
use crate::db::{ListingType, SortType};
|
||||
use crate::{markdown_to_html, Settings};
|
||||
use actix_web::{web, HttpResponse, Result};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
|
||||
use serde::Deserialize;
|
||||
use std::str::FromStr;
|
||||
use strum::ParseError;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Params {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
use crate::settings::Settings;
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::web;
|
||||
use super::*;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg
|
||||
|
|
|
@ -1,3 +1,34 @@
|
|||
use crate::api::{Oper, Perform};
|
||||
use crate::apub::get_apub_protocol_string;
|
||||
use crate::db::site_view::SiteView;
|
||||
use crate::rate_limit::rate_limiter::RateLimiter;
|
||||
use crate::websocket::{server::ChatServer, WebsocketInfo};
|
||||
use crate::{get_ip, markdown_to_html, version, Settings};
|
||||
use actix::prelude::*;
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::{body::Body, web::Query, *};
|
||||
use actix_web_actors::ws;
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use diesel::{
|
||||
r2d2::{ConnectionManager, Pool},
|
||||
PgConnection,
|
||||
};
|
||||
use failure::Error;
|
||||
use log::{error, info};
|
||||
use regex::Regex;
|
||||
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use strum::ParseError;
|
||||
use url::Url;
|
||||
|
||||
pub type DbPoolParam = web::Data<Pool<ConnectionManager<PgConnection>>>;
|
||||
pub type RateLimitParam = web::Data<Arc<Mutex<RateLimiter>>>;
|
||||
pub type ChatServerParam = web::Data<Addr<ChatServer>>;
|
||||
|
||||
pub mod api;
|
||||
pub mod federation;
|
||||
pub mod feeds;
|
||||
|
|
|
@ -1,16 +1,4 @@
|
|||
use crate::apub::get_apub_protocol_string;
|
||||
use crate::db::site_view::SiteView;
|
||||
use crate::version;
|
||||
use crate::Settings;
|
||||
use actix_web::body::Body;
|
||||
use actix_web::web;
|
||||
use actix_web::HttpResponse;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use url::Url;
|
||||
use super::*;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
use super::*;
|
||||
use crate::db::community::Community;
|
||||
use crate::Settings;
|
||||
use actix_web::web;
|
||||
use actix_web::web::Query;
|
||||
use actix_web::HttpResponse;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Params {
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
use super::*;
|
||||
use crate::websocket::server::*;
|
||||
use actix::prelude::*;
|
||||
use actix_web::web;
|
||||
use actix_web::*;
|
||||
use actix_web_actors::ws;
|
||||
use log::{error, info};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::resource("/api/v1/ws").to(chat_route));
|
||||
}
|
||||
use actix_web::{Error, Result};
|
||||
|
||||
/// How often heartbeat pings are sent
|
||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||
|
@ -16,25 +8,17 @@ const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
|||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Entry point for our route
|
||||
async fn chat_route(
|
||||
pub async fn chat_route(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
chat_server: web::Data<Addr<ChatServer>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
// TODO not sure if the blocking should be here or not
|
||||
ws::start(
|
||||
WSSession {
|
||||
cs_addr: chat_server.get_ref().to_owned(),
|
||||
id: 0,
|
||||
hb: Instant::now(),
|
||||
ip: req
|
||||
.connection_info()
|
||||
.remote()
|
||||
.unwrap_or("127.0.0.1:12345")
|
||||
.split(':')
|
||||
.next()
|
||||
.unwrap_or("127.0.0.1")
|
||||
.to_string(),
|
||||
ip: get_ip(&req.connection_info()),
|
||||
},
|
||||
&req,
|
||||
stream,
|
||||
|
@ -135,10 +119,9 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WSSession {
|
|||
.into_actor(self)
|
||||
.then(|res, _, ctx| {
|
||||
match res {
|
||||
Ok(res) => ctx.text(res),
|
||||
Err(e) => {
|
||||
error!("{}", &e);
|
||||
}
|
||||
Ok(Ok(res)) => ctx.text(res),
|
||||
Ok(Err(e)) => match e {},
|
||||
Err(e) => error!("{}", &e),
|
||||
}
|
||||
actix::fut::ready(())
|
||||
})
|
||||
|
|
|
@ -1 +1 @@
|
|||
pub const VERSION: &str = "v0.6.49";
|
||||
pub const VERSION: &str = "v0.6.51";
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
pub mod server;
|
||||
|
||||
#[derive(EnumString, ToString, Debug)]
|
||||
use crate::ConnectionId;
|
||||
use actix::prelude::*;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use log::{error, info};
|
||||
use rand::{rngs::ThreadRng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use server::ChatServer;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(EnumString, ToString, Debug, Clone)]
|
||||
pub enum UserOperation {
|
||||
Login,
|
||||
Register,
|
||||
|
@ -49,3 +62,9 @@ pub enum UserOperation {
|
|||
GetSiteConfig,
|
||||
SaveSiteConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WebsocketInfo {
|
||||
pub chatserver: Addr<ChatServer>,
|
||||
pub id: Option<ConnectionId>,
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
2
ui/src/components/sponsors.tsx
vendored
2
ui/src/components/sponsors.tsx
vendored
|
@ -10,7 +10,7 @@ let general = [
|
|||
'Andre Vallestero',
|
||||
'NotTooHighToHack',
|
||||
];
|
||||
let highlighted = ['Alex Benishek'];
|
||||
let highlighted = ['Oskenso Kashi', 'Alex Benishek'];
|
||||
// let silver = [];
|
||||
// let gold = [];
|
||||
// let latinum = [];
|
||||
|
|
2
ui/src/version.ts
vendored
2
ui/src/version.ts
vendored
|
@ -1 +1 @@
|
|||
export const version: string = 'v0.6.49';
|
||||
export const version: string = 'v0.6.51';
|
||||
|
|
39
ui/translations/ja.json
vendored
39
ui/translations/ja.json
vendored
|
@ -24,7 +24,7 @@
|
|||
"number_of_communities": "{{count}} 個のコミュニティ",
|
||||
"community_reqs": "英小文字、アンダースコア、空白なし。",
|
||||
"create_private_message": "プライベートメッセージの作成",
|
||||
"send_secure_message": "メッセージを安全に送信",
|
||||
"send_secure_message": "セキュアメッセージを送信",
|
||||
"send_message": "メッセージを送信",
|
||||
"message": "メッセージ",
|
||||
"edit": "編集",
|
||||
|
@ -133,7 +133,7 @@
|
|||
"no_email_setup": "サーバーのメール設定が正しくありません。",
|
||||
"email": "メールアドレス",
|
||||
"matrix_user_id": "Matrix のユーザーアカウント",
|
||||
"private_message_disclaimer": "Lemmy でのプライベートメッセージは安全ではありません。安全なメッセージを送るには <1>Riot.im</1> でアカウントを作成してください。",
|
||||
"private_message_disclaimer": "警告:Lemmy でのプライベートメッセージは安全ではありません。安全なメッセージを送るには <1>Riot.im</1> でアカウントを作成してください。",
|
||||
"send_notifications_to_email": "通知をメール送信",
|
||||
"optional": "任意",
|
||||
"language": "言語",
|
||||
|
@ -200,5 +200,38 @@
|
|||
"sorting_help": "並び順のヘルプ",
|
||||
"block_leaving": "このページから離れてもよろしいですか?",
|
||||
"number_of_upvotes": "{{count}} 票の賛成",
|
||||
"number_of_downvotes": "{{count}} 票の反対"
|
||||
"number_of_downvotes": "{{count}} 票の反対",
|
||||
"show_context": "前後を表示",
|
||||
"both": "両方",
|
||||
"expires": "期限切れ",
|
||||
"downvotes_disabled": "反対票を無効化",
|
||||
"chat": "会話",
|
||||
"sponsors_of_lemmy": "Lemmyのスポンサー",
|
||||
"from": "から",
|
||||
"couldnt_create_post": "投稿ができない。",
|
||||
"couldnt_update_comment": "コメントが更新されない。",
|
||||
"couldnt_save_comment": "コメントが保存されない。",
|
||||
"couldnt_get_comments": "コメントが取得されない。",
|
||||
"no_comment_edit_allowed": "コメントの編集権限がありません。",
|
||||
"no_post_edit_allowed": "投稿の編集権限がありません。",
|
||||
"no_community_edit_allowed": "コミュニティの編集許可がありません。",
|
||||
"couldnt_update_community": "コミュニティが更新されない。",
|
||||
"community_already_exists": "コミュニティは既に存在します。",
|
||||
"community_moderator_already_exists": "コミュニティ管理人は既に存在します。",
|
||||
"community_follower_already_exists": "コミュニティフォロワーは既に存在します。",
|
||||
"community_user_already_banned": "コミュニティユーザーは既に禁止されています。",
|
||||
"post_title_too_long": "投稿のタイトルが長すぎます。",
|
||||
"couldnt_update_post": "投稿が更新されない",
|
||||
"couldnt_get_posts": "投稿が取得できない",
|
||||
"couldnt_save_post": "投稿が保存されない。",
|
||||
"no_slurs": "悪口禁止。",
|
||||
"not_an_admin": "管理者ではありません。",
|
||||
"site_already_exists": "サイトは既に存在します。",
|
||||
"couldnt_update_site": "サイトが更新されない。",
|
||||
"user_already_exists": "ユーザーは既に存在します。",
|
||||
"couldnt_update_user": "ユーザーが更新されない。",
|
||||
"system_err_login": "システムエラーが発生しました。一度ログアウトして、再度ログインをお試しください。",
|
||||
"couldnt_create_private_message": "プライベートメッセージが作成されない。",
|
||||
"no_private_message_edit_allowed": "プライベートメッセージの編集許可がありません。",
|
||||
"couldnt_update_private_message": "プライベートメッセージが更新されない。"
|
||||
}
|
||||
|
|
193
ui/translations/zh.json
vendored
193
ui/translations/zh.json
vendored
|
@ -1,25 +1,25 @@
|
|||
{
|
||||
"post": "帖子",
|
||||
"remove_post": "移除帖子",
|
||||
"no_posts": "没有帖子.",
|
||||
"no_posts": "没有帖子。",
|
||||
"create_a_post": "创建新帖子",
|
||||
"create_post": "创建帖子",
|
||||
"number_of_posts": "{{count}} 帖子",
|
||||
"number_of_posts": "{{count}} 个帖子",
|
||||
"posts": "帖子",
|
||||
"related_posts": "相关的帖子",
|
||||
"related_posts": "这些帖子可能相关",
|
||||
"comments": "评论",
|
||||
"number_of_comments": "{{count}} 评论",
|
||||
"number_of_comments": "{{count}} 条评论",
|
||||
"remove_comment": "移除评论",
|
||||
"communities": "节点",
|
||||
"create_a_community": "创建新节点",
|
||||
"create_community": "创建节点",
|
||||
"remove_community": "移除节点",
|
||||
"subscribed_to_communities": "订阅新 <1>节点</1>",
|
||||
"trending_communities": "<1>节点</1>趋势",
|
||||
"list_of_communities": "节点列表",
|
||||
"community_reqs": "包含小写与下划线且没有空格的字符串.",
|
||||
"communities": "社群",
|
||||
"create_a_community": "创建新社群",
|
||||
"create_community": "创建社群",
|
||||
"remove_community": "移除社群",
|
||||
"subscribed_to_communities": "订阅新 <1>社群</1>",
|
||||
"trending_communities": "热门<1>社群</1>",
|
||||
"list_of_communities": "社群列表",
|
||||
"community_reqs": "小写字母、下划线(_)且不含空格。",
|
||||
"edit": "编辑",
|
||||
"reply": "回应",
|
||||
"reply": "回复",
|
||||
"cancel": "取消",
|
||||
"unlock": "解锁",
|
||||
"lock": "加锁",
|
||||
|
@ -52,9 +52,9 @@
|
|||
"create": "创建",
|
||||
"username": "用户名",
|
||||
"email_or_username": "邮箱或用户名",
|
||||
"number_of_users": "{{count}} 用户",
|
||||
"number_of_subscribers": "{{count}} 订阅",
|
||||
"number_of_points": "{{count}} 分",
|
||||
"number_of_users": "{{count}} 名用户",
|
||||
"number_of_subscribers": "{{count}} 名订阅者",
|
||||
"number_of_points": "{{count}} 点数",
|
||||
"name": "名字",
|
||||
"title": "标题",
|
||||
"category": "分类",
|
||||
|
@ -90,7 +90,7 @@
|
|||
"login_sign_up": "登录/注册",
|
||||
"login": "登录",
|
||||
"sign_up": "注册",
|
||||
"notifications_error": "你的浏览器不支持桌面通知,尝试 Firefox 或 Chrome",
|
||||
"notifications_error": "你的浏览器不支持桌面通知,尝试 Firefox 或 Chrome。",
|
||||
"unread_messages": "未读消息",
|
||||
"password": "密码",
|
||||
"verify_password": "确认密码",
|
||||
|
@ -106,57 +106,134 @@
|
|||
"chat": "聊天",
|
||||
"no_results": "没有结果.",
|
||||
"setup": "设置",
|
||||
"lemmy_instance_setup": "Lemmy Instance Setup",
|
||||
"lemmy_instance_setup": "创设 Lemmy 实例",
|
||||
"setup_admin": "设置管理员",
|
||||
"your_site": "你的站点",
|
||||
"modified": "修改",
|
||||
"sponsors": "发起人",
|
||||
"sponsors_of_lemmy": "Lemmy 的发起人",
|
||||
"sponsor_message":
|
||||
"Lemmy is free, <1>open-source</1> software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Thank you to the following people:",
|
||||
"support_on_patreon": "在 Patreon 赞助",
|
||||
"support_on_liberapay": "在 on 赞助",
|
||||
"general_sponsors":
|
||||
"General Sponsors are those that pledged $10 to $39 to Lemmy.",
|
||||
"crypto": "加密",
|
||||
"sponsors": "赞助者",
|
||||
"sponsors_of_lemmy": "Lemmy 的赞助者",
|
||||
"sponsor_message": "Lemmy 是自由的<1>开源</1>软件,这意味着它决不会有广告、盈利行为和风险投资介入。您的捐款将被直接用于支持项目的开发。感谢以下诸位:",
|
||||
"support_on_patreon": "在 Patreon 支持",
|
||||
"support_on_liberapay": "在 Liberapay支持",
|
||||
"general_sponsors": "向Lemmy捐助10至39美元者为普通赞助人。",
|
||||
"crypto": "加密货币",
|
||||
"bitcoin": "比特币",
|
||||
"ethereum": "以太币",
|
||||
"code": "代码",
|
||||
"joined": "已加入",
|
||||
"powered_by": "保留所有权利",
|
||||
"landing_0":
|
||||
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
|
||||
"landing_0": "Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
|
||||
"not_logged_in": "未登录.",
|
||||
"community_ban": "你被此节点禁止.",
|
||||
"site_ban": "你被此站点禁止",
|
||||
"community_ban": "你已被此社群拉黑。",
|
||||
"site_ban": "你已被本站拉黑",
|
||||
"couldnt_create_comment": "不能创建评论.",
|
||||
"couldnt_like_comment": "不能收藏评论.",
|
||||
"couldnt_like_comment": "无法点赞评论。",
|
||||
"couldnt_update_comment": "不能更新评论.",
|
||||
"couldnt_save_comment": "不能保存评论.",
|
||||
"no_comment_edit_allowed": "不允许编辑评论.",
|
||||
"no_post_edit_allowed": "不运行编辑帖子.",
|
||||
"no_community_edit_allowed": "不允许编辑节点.",
|
||||
"couldnt_find_community": "不能找到节点.",
|
||||
"couldnt_update_community": "不能更新节点.",
|
||||
"community_already_exists": "节点已存在.",
|
||||
"community_moderator_already_exists": "节点监管人已存在.",
|
||||
"community_follower_already_exists": "节点追随者已存在.",
|
||||
"community_user_already_banned": "节点用户已禁止.",
|
||||
"couldnt_create_post": "不能创建帖子.",
|
||||
"couldnt_like_post": "不能收藏帖子.",
|
||||
"couldnt_find_post": "不能搜寻帖子.",
|
||||
"couldnt_get_posts": "不能获取帖子",
|
||||
"couldnt_update_post": "不能更新帖子",
|
||||
"couldnt_save_post": "不能保持帖子.",
|
||||
"no_slurs": "和谐.",
|
||||
"not_an_admin": "不是管理员.",
|
||||
"site_already_exists": "站点已存在.",
|
||||
"couldnt_update_site": "不能更新站点.",
|
||||
"couldnt_find_that_username_or_email": "用户名/邮箱不存在.",
|
||||
"password_incorrect": "密码不正确.",
|
||||
"passwords_dont_match": "密码不匹配.",
|
||||
"admin_already_created": "抱歉,管理员已存在.",
|
||||
"user_already_exists": "用户已存在.",
|
||||
"couldnt_update_user": "不可以更新用户.",
|
||||
"system_err_login": "系统错误. 尝试注销再登录"
|
||||
"no_comment_edit_allowed": "没有编辑评论的权限。",
|
||||
"no_post_edit_allowed": "没有编辑帖子的权限。",
|
||||
"no_community_edit_allowed": "没有编辑节点的权限。",
|
||||
"couldnt_find_community": "无法找到节点。",
|
||||
"couldnt_update_community": "无法更新节点。",
|
||||
"community_already_exists": "节点已存在。",
|
||||
"community_moderator_already_exists": "节点监管人已存在。",
|
||||
"community_follower_already_exists": "节点追随者已存在。",
|
||||
"community_user_already_banned": "节点用户已禁止。",
|
||||
"couldnt_create_post": "无法创建帖子。",
|
||||
"couldnt_like_post": "无法点赞帖子。",
|
||||
"couldnt_find_post": "无法找到帖子。",
|
||||
"couldnt_get_posts": "无法获取帖子",
|
||||
"couldnt_update_post": "无法更新帖子",
|
||||
"couldnt_save_post": "无法保存帖子。",
|
||||
"no_slurs": "文明语言。",
|
||||
"not_an_admin": "不是管理员。",
|
||||
"site_already_exists": "站点已存在。",
|
||||
"couldnt_update_site": "无法更新站点。",
|
||||
"couldnt_find_that_username_or_email": "用户名/邮箱不存在。",
|
||||
"password_incorrect": "密码不正确。",
|
||||
"passwords_dont_match": "密码不匹配。",
|
||||
"admin_already_created": "抱歉,管理员已存在。",
|
||||
"user_already_exists": "用户已存在。",
|
||||
"couldnt_update_user": "无法更新用户。",
|
||||
"system_err_login": "系统错误。请尝试注销后重新登入。",
|
||||
"nsfw": "少儿不宜",
|
||||
"show_nsfw": "显示少儿不宜内容",
|
||||
"theme": "主题",
|
||||
"from": "来自",
|
||||
"donate_to_lemmy": "向Lemmy捐赠",
|
||||
"donate": "捐赠",
|
||||
"monero": "门罗币",
|
||||
"to": "致",
|
||||
"are_you_sure": "你确定吗?",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"logged_in": "已登录。",
|
||||
"message": "信息",
|
||||
"create_private_message": "创建私信",
|
||||
"send_secure_message": "发送安全信息",
|
||||
"send_message": "发送信息",
|
||||
"more": "更多",
|
||||
"preview": "预览",
|
||||
"upload_image": "上传图片",
|
||||
"enable_nsfw": "允许少儿不宜内容",
|
||||
"show_avatars": "显示头像",
|
||||
"avatar": "头像",
|
||||
"formatting_help": "格式帮助",
|
||||
"sorting_help": "排序帮助",
|
||||
"view_source": "显示来源",
|
||||
"sticky": "固定",
|
||||
"unsticky": "取消固定",
|
||||
"archive_link": "链接归档",
|
||||
"settings": "设定",
|
||||
"stickied": "已固定",
|
||||
"delete_account": "删除账号",
|
||||
"delete_account_confirm": "警告:此操作将永久删除你的数据。输入密码进行确认。",
|
||||
"cross_posts": "此链接也已被发布到:",
|
||||
"cross_post": "复数发布",
|
||||
"cross_posted_to": "已被复数发布到: ",
|
||||
"users": "用户",
|
||||
"banned": "已被禁止",
|
||||
"creator": "发布者",
|
||||
"number_of_communities": "{{count}} 个社群",
|
||||
"old": "最旧",
|
||||
"docs": "文档",
|
||||
"upload_avatar": "上传头像",
|
||||
"replies": "回复",
|
||||
"number_online": "{{count}}名在线用户",
|
||||
"mentions": "提到",
|
||||
"message_sent": "已发送信息",
|
||||
"old_password": "当前密码",
|
||||
"forgot_password": "忘记密码",
|
||||
"reset_password_mail_sent": "发送邮件重置密码。",
|
||||
"password_change": "修改密码",
|
||||
"new_password": "新密码",
|
||||
"messages": "信息",
|
||||
"no_email_setup": "此服务器还未正确设定邮箱。",
|
||||
"matrix_user_id": "Matrix用户",
|
||||
"private_message_disclaimer": "警告:Lemmy的私信功能并不安全。想要进行安全的信息传递,请在 <1>Riot.im</1>上创建账号。",
|
||||
"send_notifications_to_email": "向邮箱发送通知",
|
||||
"language": "语言",
|
||||
"browser_default": "默认浏览器",
|
||||
"downvotes_disabled": "点踩功能已禁用",
|
||||
"enable_downvotes": "启用点踩功能",
|
||||
"upvote": "点赞",
|
||||
"number_of_upvotes": "{{count}} 个赞",
|
||||
"downvote": "点踩",
|
||||
"number_of_downvotes": "{{count}} 个踩",
|
||||
"open_registration": "开放注册",
|
||||
"registration_closed": "注册功能已关闭",
|
||||
"recent_comments": "最新评论",
|
||||
"by": "来自",
|
||||
"transfer_community": "节点转让",
|
||||
"transfer_site": "站点转让",
|
||||
"post_title_too_long": "帖子标题过长。",
|
||||
"couldnt_get_comments": "无法获取评论。",
|
||||
"email_already_exists": "该邮箱已被占用。",
|
||||
"couldnt_create_private_message": "无法创建私信。",
|
||||
"no_private_message_edit_allowed": "没有编辑私信的权限。",
|
||||
"couldnt_update_private_message": "无法更新私信。",
|
||||
"time": "时间",
|
||||
"action": "行动",
|
||||
"block_leaving": "确定要离开吗?",
|
||||
"show_context": "显示上下文"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue