Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Community post tags (part 2: API methods) #5389

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions api_tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@
"scripts": {
"lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'",
"fix": "prettier --write src && eslint --fix src",
"api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i private_community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ",
"api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i private_community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts && jest -i tags.spec.ts",
"api-test-follow": "jest -i follow.spec.ts",
"api-test-comment": "jest -i comment.spec.ts",
"api-test-post": "jest -i post.spec.ts",
"api-test-user": "jest -i user.spec.ts",
"api-test-community": "jest -i community.spec.ts",
"api-test-private-community": "jest -i private_community.spec.ts",
"api-test-private-message": "jest -i private_message.spec.ts",
"api-test-image": "jest -i image.spec.ts"
"api-test-image": "jest -i image.spec.ts",
"api-test-tags": "jest -i tags.spec.ts"
},
"devDependencies": {
"@types/jest": "^29.5.12",
Expand Down
155 changes: 155 additions & 0 deletions api_tests/src/tags.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
jest.setTimeout(120000);

import {
alpha,
beta,
setupLogins,
createCommunity,
unfollows,
randomString,
createPost,
} from "./shared";
import { CreateCommunityTag } from "lemmy-js-client/dist/types/CreateCommunityTag";
import { UpdateCommunityTag } from "lemmy-js-client/dist/types/UpdateCommunityTag";
import { DeleteCommunityTag } from "lemmy-js-client/dist/types/DeleteCommunityTag";
import { ListCommunityTags } from "lemmy-js-client/dist/types/ListCommunityTags";
import { UpdatePostTags } from "lemmy-js-client/dist/types/UpdatePostTags";

beforeAll(setupLogins);
afterAll(unfollows);

test("Create, update, delete community tag", async () => {
// Create a community first
let communityRes = await createCommunity(alpha);
const communityId = communityRes.community_view.community.id;

// Create a tag
const tagName = randomString(10);
const tagSlug = tagName.toLowerCase();
let createForm: CreateCommunityTag = {
name: tagName,
id_slug: tagSlug,
community_id: communityId,
};
let createRes = await alpha.createCommunityTag(createForm);
expect(createRes.id).toBeDefined();
expect(createRes.name).toBe(tagName);
expect(createRes.community_id).toBe(communityId);

// Update the tag
const newTagName = randomString(10);
let updateForm: UpdateCommunityTag = {
tag_id: createRes.id,
name: newTagName,
};
let updateRes = await alpha.updateCommunityTag(updateForm);
expect(updateRes.id).toBe(createRes.id);
expect(updateRes.name).toBe(newTagName);
expect(updateRes.community_id).toBe(communityId);

// List tags
let listForm: ListCommunityTags = {
community_id: communityId,
};
let listRes = await alpha.listCommunityTags(listForm);
expect(listRes.tags.length).toBeGreaterThan(0);
expect(listRes.tags.find(t => t.id === createRes.id)?.name).toBe(newTagName);

// Delete the tag
let deleteForm: DeleteCommunityTag = {
tag_id: createRes.id,
};
let deleteRes = await alpha.deleteCommunityTag(deleteForm);
expect(deleteRes.id).toBe(createRes.id);

// Verify tag is deleted
listRes = await alpha.listCommunityTags(listForm);
expect(listRes.tags.find(t => t.id === createRes.id)).toBeUndefined();
});

test("Update post tags", async () => {
// Create a community
let communityRes = await createCommunity(alpha);
const communityId = communityRes.community_view.community.id;

// Create two tags
const tag1Name = randomString(10);
const tag1Slug = tag1Name.toLowerCase();
let createForm1: CreateCommunityTag = {
name: tag1Name,
id_slug: tag1Slug,
community_id: communityId,
};
let tag1Res = await alpha.createCommunityTag(createForm1);
expect(tag1Res.id).toBeDefined();

const tag2Name = randomString(10);
const tag2Slug = tag2Name.toLowerCase();
let createForm2: CreateCommunityTag = {
name: tag2Name,
id_slug: tag2Slug,
community_id: communityId,
};
let tag2Res = await alpha.createCommunityTag(createForm2);
expect(tag2Res.id).toBeDefined();

// Create a post
let postRes = await alpha.createPost({
name: randomString(10),
community_id: communityId,
});
expect(postRes.post_view.post.id).toBeDefined();

// Update post tags
let updateForm: UpdatePostTags = {
post_id: postRes.post_view.post.id,
tags: [tag1Res.id, tag2Res.id],
};
let updateRes = await alpha.updatePostTags(updateForm);
expect(updateRes.post_view.post.id).toBe(postRes.post_view.post.id);
expect(updateRes.post_view.tags?.length).toBe(2);
expect(updateRes.post_view.tags?.map(t => t.id).sort()).toEqual([tag1Res.id, tag2Res.id].sort());

// Update post to remove one tag
updateForm.tags = [tag1Res.id];
updateRes = await alpha.updatePostTags(updateForm);
expect(updateRes.post_view.post.id).toBe(postRes.post_view.post.id);
expect(updateRes.post_view.tags?.length).toBe(1);
expect(updateRes.post_view.tags?.[0].id).toBe(tag1Res.id);
});


test("Post author can update post tags", async () => {
// Create a community
let communityRes = await createCommunity(alpha);
const communityId = communityRes.community_view.community.id;

// Create a tag
const tagName = randomString(10);
const tagSlug = tagName.toLowerCase();
let createForm: CreateCommunityTag = {
name: tagName,
id_slug: tagSlug,
community_id: communityId,
};
let tagRes = await alpha.createCommunityTag(createForm);
expect(tagRes.id).toBeDefined();

let postRes = await createPost(
alpha,
communityId,
"https://example.com/",
"post with tags",
);
expect(postRes.post_view.post.id).toBeDefined();

// Alpha should be able to update tags on their own post
let updateForm: UpdatePostTags = {
post_id: postRes.post_view.post.id,
tags: [tagRes.id],
};
let updateRes = await alpha.updatePostTags(updateForm);
expect(updateRes.post_view.post.id).toBe(postRes.post_view.post.id);
expect(updateRes.post_view.tags?.length).toBe(1);
expect(updateRes.post_view.tags?.[0].id).toBe(tagRes.id);
});
2 changes: 1 addition & 1 deletion crates/api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ lemmy_db_schema = { workspace = true, features = ["full"] }
lemmy_db_views = { workspace = true, features = ["full"] }
lemmy_api_common = { workspace = true, features = ["full"] }
activitypub_federation = { workspace = true }
tracing = { workspace = true }
bcrypt = { workspace = true }
actix-web = { workspace = true }
base64 = { workspace = true }
captcha = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
chrono = { workspace = true }
url = { workspace = true }
hound = "3.5.1"
Expand Down
1 change: 1 addition & 0 deletions crates/api/src/community/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ pub mod follow;
pub mod hide;
pub mod pending_follows;
pub mod random;
pub mod tag;
pub mod transfer;
161 changes: 161 additions & 0 deletions crates/api/src/community/tag.rs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would probably be better to split each action into its own file, like we've done with the other API actions.

Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use activitypub_federation::config::Data;
use actix_web::web::{Json, Query};
use lemmy_api_common::{
community::{
CommunityTagResponse,
CreateCommunityTag,
DeleteCommunityTag,
ListCommunityTags,
ListCommunityTagsResponse,
UpdateCommunityTag,
},
context::LemmyContext,
utils::check_community_mod_action,
};
use lemmy_db_schema::{
source::{
community::Community,
tag::{Tag, TagInsertForm},
},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, utils::validation::is_valid_tag_slug};
use url::Url;

#[tracing::instrument(skip(context))]
phiresky marked this conversation as resolved.
Show resolved Hide resolved
pub async fn create_community_tag(
data: Json<CreateCommunityTag>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityTagResponse>> {
let community = Community::read(&mut context.pool(), data.community_id).await?;

// Verify that only mods can create tags
check_community_mod_action(
&local_user_view.person,
&community,
false,
&mut context.pool(),
)
.await?;

is_valid_tag_slug(&data.id_slug)?;

// Create the tag
let tag_form = TagInsertForm {
name: data.name.clone(),
community_id: data.community_id,
ap_id: Url::parse(&format!("{}/tag/{}", community.actor_id, &data.id_slug))?.into(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add this as a function to the community impl? Then you can do community.build_tag_ap_id(...) . Or at least as a helper function somewhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or better yet a db trigger

#5409

published: None, // defaults to now
updated: None,
deleted: false,
};

let tag = Tag::create(&mut context.pool(), &tag_form).await?;

Ok(Json(CommunityTagResponse {
id: tag.id,
ap_id: tag.ap_id,
name: tag.name,
community_id: tag.community_id,
}))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern as for the update.

}

#[tracing::instrument(skip(context))]
pub async fn update_community_tag(
data: Json<UpdateCommunityTag>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityTagResponse>> {
let tag = Tag::read(&mut context.pool(), data.tag_id).await?;
let community = Community::read(&mut context.pool(), tag.community_id).await?;

// Verify that only mods can update tags
check_community_mod_action(
&local_user_view.person,
&community,
false,
&mut context.pool(),
)
.await?;

// Update the tag
let tag_form = TagInsertForm {
name: data.name.clone(),
community_id: tag.community_id,
ap_id: tag.ap_id,
published: None,
updated: Some(chrono::Utc::now()),
deleted: false,
phiresky marked this conversation as resolved.
Show resolved Hide resolved
};

let tag = Tag::update(&mut context.pool(), data.tag_id, &tag_form).await?;

Ok(Json(CommunityTagResponse {
id: tag.id,
ap_id: tag.ap_id,
name: tag.name,
community_id: tag.community_id,
}))
phiresky marked this conversation as resolved.
Show resolved Hide resolved
}

#[tracing::instrument(skip(context))]
pub async fn delete_community_tag(
data: Json<DeleteCommunityTag>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityTagResponse>> {
let tag = Tag::read(&mut context.pool(), data.tag_id).await?;
let community = Community::read(&mut context.pool(), tag.community_id).await?;

// Verify that only mods can delete tags
check_community_mod_action(
&local_user_view.person,
&community,
false,
&mut context.pool(),
)
.await?;

// Soft delete the tag
let tag_form = TagInsertForm {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be a TagUpdateForm, with the fields as optional. Check out some of the other community/delete.rs for an example.

name: tag.name.clone(),
community_id: tag.community_id,
ap_id: tag.ap_id,
published: None,
updated: Some(chrono::Utc::now()),
deleted: true,
};

let tag = Tag::update(&mut context.pool(), data.tag_id, &tag_form).await?;

Ok(Json(CommunityTagResponse {
id: tag.id,
ap_id: tag.ap_id,
name: tag.name,
community_id: tag.community_id,
}))
}

#[tracing::instrument(skip(context))]
pub async fn list_community_tags(
phiresky marked this conversation as resolved.
Show resolved Hide resolved
data: Query<ListCommunityTags>,
context: Data<LemmyContext>,
) -> LemmyResult<Json<ListCommunityTagsResponse>> {
let tags = Tag::get_by_community(&mut context.pool(), data.community_id).await?;

let tag_responses = tags
.into_iter()
.map(|t| CommunityTagResponse {
id: t.id,
ap_id: t.ap_id,
name: t.name,
community_id: t.community_id,
})
.collect();
phiresky marked this conversation as resolved.
Show resolved Hide resolved

Ok(Json(ListCommunityTagsResponse {
tags: tag_responses,
}))
}
1 change: 1 addition & 0 deletions crates/api/src/post/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ pub mod lock;
pub mod mark_many_read;
pub mod mark_read;
pub mod save;
pub mod tags;
Loading