diff --git a/src/lemmy/mod.rs b/src/lemmy/mod.rs new file mode 100644 index 0000000..bb29919 --- /dev/null +++ b/src/lemmy/mod.rs @@ -0,0 +1,346 @@ +use std::collections::HashMap; +use std::env; +use std::env::VarError; +use std::error::Error; +use chrono::NaiveDateTime; +use lemmy_api_common::community::{ListCommunities, ListCommunitiesResponse}; +use lemmy_api_common::lemmy_db_views::structs::PostView; +use lemmy_api_common::lemmy_db_views_actor::structs::CommunityView; +use lemmy_api_common::person::{Login, LoginResponse}; +use lemmy_api_common::post::{CreatePost, FeaturePost, GetPosts, GetPostsResponse}; +use lemmy_api_common::sensitive::Sensitive; +use lemmy_db_schema::newtypes::{CommunityId, DbUrl, InstanceId, LanguageId, PersonId, PostId}; +use lemmy_db_schema::{ListingType, PostFeatureType, SubscribedType}; +use reqwest::StatusCode; +use serde_derive::{Deserialize, Serialize}; +use serde_json::json; +use url::Url; +use crate::HTTP_CLIENT; + +pub(crate) struct Credentials { + username: String, + password: String +} + +impl Credentials { + pub(crate) fn get_username(&self) -> Sensitive { + return Sensitive::new(self.username.clone()); + } + + pub(crate) fn get_password(&self) -> Sensitive { + return Sensitive::new(self.password.clone()); + } + + pub(crate) fn set_credentials() -> Result { + let username = env::var("LEMMY_USERNAME")?; + let password = env::var("LEMMY_PASSWORD")?; + Ok(Credentials { + username, + password, + }) + } +} + +pub(crate) struct Lemmy { + jwt_token: Sensitive, + instance: String, +} + +pub(crate) async fn login(credentials: &Credentials, instance: &str) -> Result> { + let login_params = Login { + username_or_email: credentials.get_username(), + password: credentials.get_password(), + totp_2fa_token: None, + }; + + let response = HTTP_CLIENT + .post(instance.to_string() + "/api/v3/user/login") + .json(&login_params) + .send() + .await?; + + match response.status() { + StatusCode::OK => { + let data: LoginResponse = response.json().await.expect("Successful Login Request should return JSON"); + match data.jwt { + Some(token) => Ok(Lemmy { + jwt_token: token.clone(), + instance: instance.to_string(), + }), + None => Err(panic!("Login did not return JWT token. Are the credentials valid?")) + } + }, + status => Err(panic!("Unexpected HTTP Status '{}' during Login", status.to_string())) + } +} + +impl Lemmy { + pub(crate) async fn post(&self, post: CustomCreatePost) -> Result> { + let response = HTTP_CLIENT + .post(format!("{}/api/v3/post", &self.instance)) + .bearer_auth(&self.jwt_token.to_string()) + .json(&post) + .send() + .await? + .text() + .await?; + + let json_data = serde_json::from_str::>(&response)? + .remove("post_view") + .expect("Element should be present"); + + Ok(json_data.post.id) + } + + async fn feature(&self, params: CustomFeaturePost) -> Result> { + let response = HTTP_CLIENT + .post(format!("{}/api/v3/post/feature", &self.instance)) + .bearer_auth(&self.jwt_token.to_string()) + .json(¶ms) + .send() + .await? + .text() + .await?; + + let json_data = serde_json::from_str::>(&response)? + .remove("post_view") + .expect("Element should be present"); + Ok(json_data) + } + + pub(crate) async fn unpin(&self, post_id: PostId, location: PostFeatureType) -> Result> { + let pin_params = CustomFeaturePost { + post_id, + featured: false, + feature_type: location, + }; + self.feature(pin_params).await + } + + pub(crate) async fn pin(&self, post_id: PostId, location: PostFeatureType) -> Result> { + let pin_params = CustomFeaturePost { + post_id, + featured: true, + feature_type: location, + }; + self.feature(pin_params).await + } + + pub(crate) async fn get_community_pinned(&self, community: CommunityId) -> Result, Box> { + let list_params = GetPosts { + community_id: Some(community), + type_: Some(ListingType::Local), + ..Default::default() + }; + + let response = HTTP_CLIENT + .get(format!("{}/api/v3/post/list", &self.instance)) + .bearer_auth(&self.jwt_token.to_string()) + .query(&list_params) + .send() + .await? + .text() + .await?; + + let json_data: CustomGetPostsResponse = serde_json::from_str(&response)?; + + Ok(json_data.posts.iter().filter(|post| { + post.post.featured_community + }) + .cloned() + .collect() + ) + } + + pub(crate) async fn get_local_pinned(&self) -> Result, Box> { + let list_params = GetPosts { + type_: Some(ListingType::Local), + ..Default::default() + }; + + let response = HTTP_CLIENT + .get(format!("{}/api/v3/post/list", &self.instance)) + .bearer_auth(&self.jwt_token.to_string()) + .query(&list_params) + .send() + .await? + .text() + .await?; + + let json_data: CustomGetPostsResponse = serde_json::from_str(&response)?; + + Ok(json_data.posts.iter().filter(|post| { + post.post.featured_local + }) + .cloned() + .collect() + ) + } + + pub(crate) async fn get_communities(&self) -> Result, Box> { + let list_params = ListCommunities { + type_: Some(ListingType::Local), + ..Default::default() + }; + + let response = HTTP_CLIENT + .get(format!("{}/api/v3/community/list", &self.instance)) + .bearer_auth(&self.jwt_token.to_string()) + .query(&list_params) + .send() + .await? + .text() + .await?; + + let json_data: CustomListCommunitiesResponse = serde_json::from_str(&response)?; + + let mut communities: HashMap = HashMap::new(); + for community_view in json_data.communities { + let community = community_view.community; + communities.insert(community.name, community.id); + } + + return Ok(communities) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub(crate) struct CustomCreatePost { + pub(crate) name: String, + pub(crate) community_id: CommunityId, + pub(crate) url: Option, + pub(crate) body: Option, + pub(crate) honeypot: Option, + pub(crate) nsfw: Option, + pub(crate) language_id: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub(crate) struct CustomFeaturePost { + pub(crate) post_id: PostId, + pub(crate) featured: bool, + pub(crate) feature_type: PostFeatureType, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub(crate) struct CustomListCommunitiesResponse { + pub(crate) communities: Vec +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct CustomCommunityView { + pub(crate) community: CustomCommunity, + pub(crate) subscribed: SubscribedType, + pub(crate) blocked: bool, + pub(crate) counts: CustomCommunityAggregates +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct CustomCommunity { + pub(crate) actor_id: String, + pub(crate) banner: Option, + pub(crate) deleted: bool, + pub(crate) description: Option, + pub(crate) hidden: bool, + pub(crate) icon: Option, + pub(crate) id: CommunityId, + pub(crate) instance_id: i32, + pub(crate) local: bool, + pub(crate) name: String, + pub(crate) nsfw: bool, + pub(crate) posting_restricted_to_mods: bool, + pub(crate) published: String, + pub(crate) removed: bool, + pub(crate) title: String, + pub(crate) updated: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct CustomCommunityAggregates { + pub(crate) comments: i64, + pub(crate) community_id: i32, + pub(crate) posts: i64, + pub(crate) published: String, + pub(crate) subscribers: i64, + pub(crate) users_active_day: i64, + pub(crate) users_active_half_year: i64, + pub(crate) users_active_month: i64, + pub(crate) users_active_week: i64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct CustomPostView { + pub(crate) community: CustomCommunity, + pub(crate) counts: CustomPostAggregates, + pub(crate) creator: CustomPerson, + pub(crate) creator_banned_from_community: bool, + pub(crate) creator_blocked: bool, + pub(crate) creator_is_admin: bool, + pub(crate) creator_is_moderator: bool, + pub(crate) my_vote: Option, + pub(crate) post: CustomPost, + pub(crate) read: bool, + pub(crate) saved: bool, + pub(crate) subscribed: SubscribedType, + pub(crate) unread_comments: i64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct CustomPostAggregates { + pub(crate) comments: i64, + pub(crate) downvotes: i64, + pub(crate) post_id: PostId, + pub(crate) published: String, + pub(crate) score: i64, + pub(crate) upvotes: i64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct CustomPerson { + pub(crate) actor_id: String, + pub(crate) avatar: Option, + pub(crate) ban_expires: Option, + pub(crate) banned: bool, + pub(crate) banner: Option, + pub(crate) bio: Option, + pub(crate) bot_account: bool, + pub(crate) deleted: bool, + pub(crate) display_name: Option, + pub(crate) id: PersonId, + pub(crate) instance_id : InstanceId, + pub(crate) local: bool, + pub(crate) matrix_user_id: Option, + pub(crate) name: String, + pub(crate) published: String, + pub(crate) updated: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct CustomPost { + pub(crate) id: PostId, + pub(crate) name: String, + pub(crate) url: Option, + pub(crate) body: Option, + pub(crate) creator_id: PersonId, + pub(crate) community_id: CommunityId, + pub(crate) removed: bool, + pub(crate) locked: bool, + pub(crate) published: String, + pub(crate) updated: Option, + pub(crate) deleted: bool, + pub(crate) nsfw: bool, + pub(crate) embed_title: Option, + pub(crate) embed_description: Option, + pub(crate) thumbnail_url: Option, + pub(crate) ap_id: String, + pub(crate) local: bool, + pub(crate) embed_video_url: Option, + pub(crate) language_id: LanguageId, + pub(crate) featured_community: bool, + pub(crate) featured_local: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct CustomGetPostsResponse { + pub(crate) posts: Vec, +}