mirror of
https://github.com/LemmyNet/lemmy.git
synced 2025-09-02 11:13:51 +00:00
Community post tags (part 2: API methods) (#5389)
* post tags part 2
* post tags part 2 (api methods)
* re-remove tracing
* api tests
* update according to comments
* update lints and api package
* validate tags, fix compile
* insert many simultaneously
* o
* temp revert for tests
* update dep
* Revert "temp revert for tests"
This reverts commit 62f17de60d
.
* fix deps
* limit tag length
* fix comments
* rename display_name
* lint
* lint
* bullshit
* clarify comment
* fix after merge
* lint.sh
* upgrade dep
* rename
* dep
---------
Co-authored-by: Dessalines <tyhou13@gmx.com>
This commit is contained in:
parent
9bfaad8a0d
commit
74701af627
35 changed files with 698 additions and 145 deletions
|
@ -12,7 +12,8 @@ variables:
|
|||
- &install_binstall "wget -O- https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xvz -C /usr/local/cargo/bin"
|
||||
- install_diesel_cli: &install_diesel_cli
|
||||
- apt-get update && apt-get install -y postgresql-client
|
||||
- cargo install --locked diesel_cli --no-default-features --features postgres
|
||||
# diesel_cli@2.2.8 is the last version that supports rust 1.81, which we are currently locked on due to perf regressions on rust 1.82+ :(
|
||||
- cargo install --locked diesel_cli@2.2.8 --no-default-features --features postgres
|
||||
- export PATH="$CARGO_HOME/bin:$PATH"
|
||||
- &slow_check_paths
|
||||
- event: pull_request
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"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",
|
||||
|
@ -18,18 +18,19 @@
|
|||
"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": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/joi": "^17.2.3",
|
||||
"@types/node": "^22.13.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.24.0",
|
||||
"@typescript-eslint/parser": "^8.24.0",
|
||||
"eslint": "^9.20.0",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"jest": "^29.5.0",
|
||||
"lemmy-js-client": "1.0.0-site-person-ban.1",
|
||||
"lemmy-js-client": "npm:@phiresky/lemmy-js-client@1.0.0-post-tags.7",
|
||||
"prettier": "^3.5.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"tsoa": "^6.6.0",
|
||||
|
|
|
@ -8,12 +8,12 @@ importers:
|
|||
|
||||
.:
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: ^9.21.0
|
||||
version: 9.23.0
|
||||
'@types/jest':
|
||||
specifier: ^29.5.12
|
||||
version: 29.5.14
|
||||
'@types/joi':
|
||||
specifier: ^17.2.3
|
||||
version: 17.2.3
|
||||
'@types/node':
|
||||
specifier: ^22.13.1
|
||||
version: 22.13.1
|
||||
|
@ -33,8 +33,8 @@ importers:
|
|||
specifier: ^29.5.0
|
||||
version: 29.7.0(@types/node@22.13.1)
|
||||
lemmy-js-client:
|
||||
specifier: 1.0.0-site-person-ban.1
|
||||
version: 1.0.0-site-person-ban.1
|
||||
specifier: npm:@phiresky/lemmy-js-client@1.0.0-post-tags.7
|
||||
version: '@phiresky/lemmy-js-client@1.0.0-post-tags.7'
|
||||
prettier:
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0
|
||||
|
@ -254,6 +254,10 @@ packages:
|
|||
resolution: {integrity: sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/js@9.23.0':
|
||||
resolution: {integrity: sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@eslint/object-schema@2.1.5':
|
||||
resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
@ -309,9 +313,6 @@ packages:
|
|||
'@hapi/hoek@11.0.7':
|
||||
resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==}
|
||||
|
||||
'@hapi/hoek@9.3.0':
|
||||
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
|
||||
|
||||
'@hapi/iron@7.0.1':
|
||||
resolution: {integrity: sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==}
|
||||
|
||||
|
@ -344,9 +345,6 @@ packages:
|
|||
resolution: {integrity: sha512-05HumSy3LWfXpmJ9cr6HzwhAavrHkJ1ZRCmNE2qJMihdM5YcWreWPfyN0yKT2ZjCM92au3ZkuodjBxOibxM67A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
'@hapi/topo@5.1.0':
|
||||
resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==}
|
||||
|
||||
'@hapi/topo@6.0.2':
|
||||
resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==}
|
||||
|
||||
|
@ -487,6 +485,9 @@ packages:
|
|||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@phiresky/lemmy-js-client@1.0.0-post-tags.7':
|
||||
resolution: {integrity: sha512-zuGyIx54ykPW1nI/0T7RG99D+MP1Ix8lYwbWydLNYY4nhcr37baXq0e+LJjsiCJIv2KptfJI/6pndUkUQjr8JQ==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
@ -495,15 +496,6 @@ packages:
|
|||
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@sideway/address@4.1.5':
|
||||
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
|
||||
|
||||
'@sideway/formula@3.0.1':
|
||||
resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==}
|
||||
|
||||
'@sideway/pinpoint@2.0.0':
|
||||
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
|
||||
|
||||
'@sinclair/typebox@0.27.8':
|
||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||
|
||||
|
@ -579,10 +571,6 @@ packages:
|
|||
'@types/jest@29.5.14':
|
||||
resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==}
|
||||
|
||||
'@types/joi@17.2.3':
|
||||
resolution: {integrity: sha512-dGjs/lhrWOa+eO0HwgxCSnDm5eMGCsXuvLglMghJq32F6q5LyyNuXb41DHzrg501CKNOSSAHmfB7FDGeUnDmzw==}
|
||||
deprecated: This is a stub types definition. joi provides its own type definitions, so you do not need this installed.
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
|
@ -1482,9 +1470,6 @@ packages:
|
|||
node-notifier:
|
||||
optional: true
|
||||
|
||||
joi@17.13.3:
|
||||
resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
|
@ -1528,9 +1513,6 @@ packages:
|
|||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
lemmy-js-client@1.0.0-site-person-ban.1:
|
||||
resolution: {integrity: sha512-CcTxrjh1RuTDRDEzG4xPO2VbRA7XEHUD6EJSOAqciyFdgXx7Dwk63cG71FTM2ohpdLycPFI8IhgzJHZIY4jOyQ==}
|
||||
|
||||
leven@3.1.0:
|
||||
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -2416,6 +2398,8 @@ snapshots:
|
|||
|
||||
'@eslint/js@9.20.0': {}
|
||||
|
||||
'@eslint/js@9.23.0': {}
|
||||
|
||||
'@eslint/object-schema@2.1.5': {}
|
||||
|
||||
'@eslint/plugin-kit@0.2.5':
|
||||
|
@ -2503,8 +2487,6 @@ snapshots:
|
|||
|
||||
'@hapi/hoek@11.0.7': {}
|
||||
|
||||
'@hapi/hoek@9.3.0': {}
|
||||
|
||||
'@hapi/iron@7.0.1':
|
||||
dependencies:
|
||||
'@hapi/b64': 6.0.1
|
||||
|
@ -2569,10 +2551,6 @@ snapshots:
|
|||
|
||||
'@hapi/teamwork@6.0.0': {}
|
||||
|
||||
'@hapi/topo@5.1.0':
|
||||
dependencies:
|
||||
'@hapi/hoek': 9.3.0
|
||||
|
||||
'@hapi/topo@6.0.2':
|
||||
dependencies:
|
||||
'@hapi/hoek': 11.0.7
|
||||
|
@ -2815,19 +2793,17 @@ snapshots:
|
|||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.19.0
|
||||
|
||||
'@phiresky/lemmy-js-client@1.0.0-post-tags.7':
|
||||
dependencies:
|
||||
'@tsoa/runtime': 6.6.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@pkgr/core@0.1.1': {}
|
||||
|
||||
'@sideway/address@4.1.5':
|
||||
dependencies:
|
||||
'@hapi/hoek': 9.3.0
|
||||
|
||||
'@sideway/formula@3.0.1': {}
|
||||
|
||||
'@sideway/pinpoint@2.0.0': {}
|
||||
|
||||
'@sinclair/typebox@0.27.8': {}
|
||||
|
||||
'@sinonjs/commons@3.0.1':
|
||||
|
@ -2949,10 +2925,6 @@ snapshots:
|
|||
expect: 29.7.0
|
||||
pretty-format: 29.7.0
|
||||
|
||||
'@types/joi@17.2.3':
|
||||
dependencies:
|
||||
joi: 17.13.3
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/keygrip@1.0.6': {}
|
||||
|
@ -4126,14 +4098,6 @@ snapshots:
|
|||
- supports-color
|
||||
- ts-node
|
||||
|
||||
joi@17.13.3:
|
||||
dependencies:
|
||||
'@hapi/hoek': 9.3.0
|
||||
'@hapi/topo': 5.1.0
|
||||
'@sideway/address': 4.1.5
|
||||
'@sideway/formula': 3.0.1
|
||||
'@sideway/pinpoint': 2.0.0
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@3.14.1:
|
||||
|
@ -4169,8 +4133,6 @@ snapshots:
|
|||
|
||||
kleur@3.0.3: {}
|
||||
|
||||
lemmy-js-client@1.0.0-site-person-ban.1: {}
|
||||
|
||||
leven@3.1.0: {}
|
||||
|
||||
levn@0.4.1:
|
||||
|
|
149
api_tests/src/tags.spec.ts
Normal file
149
api_tests/src/tags.spec.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
jest.setTimeout(120000);
|
||||
|
||||
import {
|
||||
alpha,
|
||||
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 { EditPost } from "lemmy-js-client";
|
||||
|
||||
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);
|
||||
let createForm: CreateCommunityTag = {
|
||||
display_name: tagName,
|
||||
community_id: communityId,
|
||||
};
|
||||
let createRes = await alpha.createCommunityTag(createForm);
|
||||
expect(createRes.id).toBeDefined();
|
||||
expect(createRes.display_name).toBe(tagName);
|
||||
expect(createRes.community_id).toBe(communityId);
|
||||
|
||||
// Update the tag
|
||||
const newTagName = randomString(10);
|
||||
let updateForm: UpdateCommunityTag = {
|
||||
tag_id: createRes.id,
|
||||
display_name: newTagName,
|
||||
};
|
||||
let updateRes = await alpha.updateCommunityTag(updateForm);
|
||||
expect(updateRes.id).toBe(createRes.id);
|
||||
expect(updateRes.display_name).toBe(newTagName);
|
||||
expect(updateRes.community_id).toBe(communityId);
|
||||
|
||||
// List tags
|
||||
let listRes = await alpha.getCommunity({ id: communityId });
|
||||
expect(listRes.community_view.post_tags.length).toBe(1);
|
||||
expect(
|
||||
listRes.community_view.post_tags.find(t => t.id === createRes.id)
|
||||
?.display_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.getCommunity({ id: communityId });
|
||||
expect(
|
||||
listRes.community_view.post_tags.find(t => t.id === createRes.id),
|
||||
).toBeUndefined();
|
||||
expect(listRes.community_view.post_tags.length).toBe(0);
|
||||
});
|
||||
|
||||
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);
|
||||
let createForm1: CreateCommunityTag = {
|
||||
display_name: tag1Name,
|
||||
community_id: communityId,
|
||||
};
|
||||
let tag1Res = await alpha.createCommunityTag(createForm1);
|
||||
expect(tag1Res.id).toBeDefined();
|
||||
|
||||
const tag2Name = randomString(10);
|
||||
let createForm2: CreateCommunityTag = {
|
||||
display_name: tag2Name,
|
||||
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: EditPost = {
|
||||
post_id: postRes.post_view.post.id,
|
||||
tags: [tag1Res.id, tag2Res.id],
|
||||
};
|
||||
let updateRes = await alpha.editPost(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.editPost(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);
|
||||
let createForm: CreateCommunityTag = {
|
||||
display_name: tagName,
|
||||
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: EditPost = {
|
||||
post_id: postRes.post_view.post.id,
|
||||
tags: [tagRes.id],
|
||||
};
|
||||
let updateRes = await alpha.editPost(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);
|
||||
});
|
|
@ -25,12 +25,12 @@ lemmy_api_common = { workspace = true, features = ["full"] }
|
|||
lemmy_db_schema_file = { workspace = true }
|
||||
lemmy_email = { workspace = true }
|
||||
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 }
|
||||
regex = { workspace = true }
|
||||
|
|
|
@ -4,4 +4,5 @@ pub mod block;
|
|||
pub mod follow;
|
||||
pub mod pending_follows;
|
||||
pub mod random;
|
||||
pub mod tag;
|
||||
pub mod transfer;
|
||||
|
|
87
crates/api/src/community/tag.rs
Normal file
87
crates/api/src/community/tag.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use chrono::Utc;
|
||||
use lemmy_api_common::{
|
||||
community::{CreateCommunityTag, DeleteCommunityTag, UpdateCommunityTag},
|
||||
context::LemmyContext,
|
||||
utils::check_community_mod_action,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::Community,
|
||||
tag::{Tag, TagInsertForm, TagUpdateForm},
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::{error::LemmyResult, utils::validation::tag_name_length_check};
|
||||
|
||||
pub async fn create_community_tag(
|
||||
data: Json<CreateCommunityTag>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<Tag>> {
|
||||
let community = Community::read(&mut context.pool(), data.community_id).await?;
|
||||
|
||||
tag_name_length_check(&data.display_name)?;
|
||||
// Verify that only mods can create tags
|
||||
check_community_mod_action(&local_user_view, &community, false, &mut context.pool()).await?;
|
||||
|
||||
// Create the tag
|
||||
let tag_form = TagInsertForm {
|
||||
display_name: data.display_name.clone(),
|
||||
community_id: data.community_id,
|
||||
ap_id: community.build_tag_ap_id(&data.display_name)?,
|
||||
};
|
||||
|
||||
let tag = Tag::create(&mut context.pool(), &tag_form).await?;
|
||||
|
||||
Ok(Json(tag))
|
||||
}
|
||||
|
||||
pub async fn update_community_tag(
|
||||
data: Json<UpdateCommunityTag>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<Tag>> {
|
||||
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, &community, false, &mut context.pool()).await?;
|
||||
|
||||
tag_name_length_check(&data.display_name)?;
|
||||
// Update the tag
|
||||
let tag_form = TagUpdateForm {
|
||||
display_name: Some(data.display_name.clone()),
|
||||
updated: Some(Some(Utc::now())),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let tag = Tag::update(&mut context.pool(), data.tag_id, &tag_form).await?;
|
||||
|
||||
Ok(Json(tag))
|
||||
}
|
||||
|
||||
pub async fn delete_community_tag(
|
||||
data: Json<DeleteCommunityTag>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<Tag>> {
|
||||
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, &community, false, &mut context.pool()).await?;
|
||||
|
||||
// Soft delete the tag
|
||||
let tag_form = TagUpdateForm {
|
||||
updated: Some(Some(Utc::now())),
|
||||
deleted: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let tag = Tag::update(&mut context.pool(), data.tag_id, &tag_form).await?;
|
||||
|
||||
Ok(Json(tag))
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use lemmy_db_schema::{
|
||||
newtypes::{CommunityId, LanguageId, PersonId},
|
||||
newtypes::{CommunityId, LanguageId, PersonId, TagId},
|
||||
source::site::Site,
|
||||
};
|
||||
use lemmy_db_schema_file::enums::{CommunityVisibility, ListingType};
|
||||
|
@ -15,6 +15,35 @@ use serde_with::skip_serializing_none;
|
|||
#[cfg(feature = "full")]
|
||||
use ts_rs::TS;
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
#[cfg_attr(feature = "full", derive(TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Create a tag for a community.
|
||||
pub struct CreateCommunityTag {
|
||||
pub community_id: CommunityId,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
#[cfg_attr(feature = "full", derive(TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Update a community tag.
|
||||
pub struct UpdateCommunityTag {
|
||||
pub tag_id: TagId,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
#[cfg_attr(feature = "full", derive(TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Delete a community tag.
|
||||
pub struct DeleteCommunityTag {
|
||||
pub tag_id: TagId,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "full", derive(TS))]
|
||||
|
|
|
@ -22,6 +22,8 @@ pub mod send_activity;
|
|||
pub mod site;
|
||||
pub mod tagline;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod tags;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod utils;
|
||||
|
||||
pub extern crate lemmy_db_schema;
|
||||
|
|
|
@ -175,11 +175,11 @@ pub struct EditPost {
|
|||
/// Instead of fetching a thumbnail, use a custom one.
|
||||
#[cfg_attr(feature = "full", ts(optional))]
|
||||
pub custom_thumbnail: Option<String>,
|
||||
#[cfg_attr(feature = "full", ts(optional))]
|
||||
pub tags: Option<Vec<TagId>>,
|
||||
/// Time when this post should be scheduled. Null means publish immediately.
|
||||
#[cfg_attr(feature = "full", ts(optional))]
|
||||
pub scheduled_publish_time: Option<i64>,
|
||||
#[cfg_attr(feature = "full", ts(optional))]
|
||||
pub tags: Option<Vec<TagId>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
|
||||
|
|
43
crates/api_common/src/tags.rs
Normal file
43
crates/api_common/src/tags.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use crate::{context::LemmyContext, utils::check_community_mod_action};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::TagId,
|
||||
source::{post::Post, post_tag::PostTag, tag::PostTagInsertForm},
|
||||
};
|
||||
use lemmy_db_views::structs::{CommunityView, LocalUserView};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub async fn update_post_tags(
|
||||
context: &LemmyContext,
|
||||
post: &Post,
|
||||
community: &CommunityView,
|
||||
tags: &[TagId],
|
||||
local_user_view: &LocalUserView,
|
||||
) -> LemmyResult<()> {
|
||||
let is_author = Post::is_post_creator(local_user_view.person.id, post.creator_id);
|
||||
|
||||
if !is_author {
|
||||
// Check if user is either the post author or a community mod
|
||||
check_community_mod_action(
|
||||
local_user_view,
|
||||
&community.community,
|
||||
false,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
// validate tags
|
||||
let valid_tags: HashSet<TagId> = community.post_tags.0.iter().map(|t| t.id).collect();
|
||||
if !valid_tags.is_superset(&tags.iter().copied().collect()) {
|
||||
return Err(LemmyErrorType::TagNotInCommunity.into());
|
||||
}
|
||||
let insert_tags = tags
|
||||
.iter()
|
||||
.map(|tag_id| PostTagInsertForm {
|
||||
post_id: post.id,
|
||||
tag_id: *tag_id,
|
||||
})
|
||||
.collect();
|
||||
PostTag::set(&mut context.pool(), post.id, insert_tags).await?;
|
||||
Ok(())
|
||||
}
|
|
@ -946,7 +946,7 @@ pub fn read_auth_token(req: &HttpRequest) -> LemmyResult<Option<String>> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn send_webmention(post: Post, community: Community) {
|
||||
pub fn send_webmention(post: Post, community: &Community) {
|
||||
if let Some(url) = post.url.clone() {
|
||||
if community.visibility.can_view_without_login() {
|
||||
spawn_try_task(async move {
|
||||
|
|
|
@ -9,6 +9,7 @@ use lemmy_api_common::{
|
|||
post::{CreatePost, PostResponse},
|
||||
request::generate_post_link_metadata,
|
||||
send_activity::SendActivityData,
|
||||
tags::update_post_tags,
|
||||
utils::{
|
||||
check_community_user_action,
|
||||
check_nsfw_allowed,
|
||||
|
@ -22,14 +23,11 @@ use lemmy_api_common::{
|
|||
use lemmy_db_schema::{
|
||||
impls::actor_language::validate_post_language,
|
||||
newtypes::PostOrCommentId,
|
||||
source::{
|
||||
community::Community,
|
||||
post::{Post, PostActions, PostInsertForm, PostLikeForm, PostReadForm},
|
||||
},
|
||||
source::post::{Post, PostActions, PostInsertForm, PostLikeForm, PostReadForm},
|
||||
traits::{Crud, Likeable, Readable},
|
||||
utils::diesel_url_create,
|
||||
};
|
||||
use lemmy_db_views::structs::{CommunityModeratorView, LocalUserView, SiteView};
|
||||
use lemmy_db_views::structs::{CommunityModeratorView, CommunityView, LocalUserView, SiteView};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
utils::{
|
||||
|
@ -81,8 +79,15 @@ pub async fn create_post(
|
|||
is_valid_body_field(body, true)?;
|
||||
}
|
||||
|
||||
let community = Community::read(&mut context.pool(), data.community_id).await?;
|
||||
check_community_user_action(&local_user_view, &community, &mut context.pool()).await?;
|
||||
let community_view = CommunityView::read(
|
||||
&mut context.pool(),
|
||||
data.community_id,
|
||||
Some(&local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let community = &community_view.community;
|
||||
check_community_user_action(&local_user_view, community, &mut context.pool()).await?;
|
||||
|
||||
// If its an NSFW community, then use that as a default
|
||||
let nsfw = data.nsfw.or(Some(community.nsfw));
|
||||
|
@ -113,7 +118,7 @@ pub async fn create_post(
|
|||
alt_text: data.alt_text.clone(),
|
||||
nsfw,
|
||||
language_id: Some(language_id),
|
||||
federation_pending: Some(community_use_pending(&community, &context).await),
|
||||
federation_pending: Some(community_use_pending(community, &context).await),
|
||||
scheduled_publish_time,
|
||||
..PostInsertForm::new(
|
||||
data.name.trim().to_string(),
|
||||
|
@ -127,8 +132,20 @@ pub async fn create_post(
|
|||
let inserted_post = Post::create(&mut context.pool(), &post_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
|
||||
|
||||
plugin_hook_after("after_create_local_post", &inserted_post)?;
|
||||
|
||||
if let Some(tags) = &data.tags {
|
||||
update_post_tags(
|
||||
&context,
|
||||
&inserted_post,
|
||||
&community_view,
|
||||
tags,
|
||||
&local_user_view,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let community_id = community.id;
|
||||
let federate_post = if scheduled_publish_time.is_none() {
|
||||
send_webmention(inserted_post.clone(), community);
|
||||
|
|
|
@ -9,6 +9,7 @@ use lemmy_api_common::{
|
|||
post::{EditPost, PostResponse},
|
||||
request::generate_post_link_metadata,
|
||||
send_activity::SendActivityData,
|
||||
tags::update_post_tags,
|
||||
utils::{
|
||||
check_community_user_action,
|
||||
check_nsfw_allowed,
|
||||
|
@ -28,7 +29,7 @@ use lemmy_db_schema::{
|
|||
traits::Crud,
|
||||
utils::{diesel_string_update, diesel_url_update},
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, PostView, SiteView};
|
||||
use lemmy_db_views::structs::{CommunityView, LocalUserView, PostView, SiteView};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
utils::{
|
||||
|
@ -98,6 +99,20 @@ pub async fn update_post(
|
|||
|
||||
check_community_user_action(&local_user_view, &orig_post.community, &mut context.pool()).await?;
|
||||
|
||||
if let Some(tags) = &data.tags {
|
||||
// post view does not include communityview.post_tags
|
||||
let community_view =
|
||||
CommunityView::read(&mut context.pool(), orig_post.community.id, None, false).await?;
|
||||
update_post_tags(
|
||||
&context,
|
||||
&orig_post.post,
|
||||
&community_view,
|
||||
tags,
|
||||
&local_user_view,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Verify that only the creator can edit
|
||||
if !Post::is_post_creator(local_user_view.person.id, orig_post.post.creator_id) {
|
||||
Err(LemmyErrorType::NoPostEditAllowed)?
|
||||
|
@ -166,7 +181,7 @@ pub async fn update_post(
|
|||
// schedule was removed, send create activity and webmention
|
||||
(Some(_), None) => {
|
||||
let community = Community::read(&mut context.pool(), orig_post.community.id).await?;
|
||||
send_webmention(updated_post.clone(), community);
|
||||
send_webmention(updated_post.clone(), &community);
|
||||
generate_post_link_metadata(
|
||||
updated_post.clone(),
|
||||
custom_thumbnail.flatten().map(Into::into),
|
||||
|
|
|
@ -44,6 +44,8 @@ use lemmy_utils::{
|
|||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
settings::structs::Settings,
|
||||
};
|
||||
use regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
use url::Url;
|
||||
|
||||
impl Crud for Community {
|
||||
|
@ -258,6 +260,20 @@ impl Community {
|
|||
.and(community::deleted.eq(false))
|
||||
}
|
||||
|
||||
pub fn build_tag_ap_id(&self, tag_name: &str) -> LemmyResult<DbUrl> {
|
||||
#[allow(clippy::expect_used)]
|
||||
// convert a readable name to an id slug that is appended to the community URL to get a unique
|
||||
// tag url (ap_id).
|
||||
static VALID_ID_SLUG: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"[^a-z0-9_-]+").expect("compile regex"));
|
||||
let tag_name_lower = tag_name.to_lowercase();
|
||||
let id_slug = VALID_ID_SLUG.replace_all(&tag_name_lower, "-");
|
||||
if id_slug.is_empty() {
|
||||
Err(LemmyErrorType::InvalidUrl)?
|
||||
}
|
||||
Ok(Url::parse(&format!("{}/tag/{}", self.ap_id, &id_slug))?.into())
|
||||
}
|
||||
|
||||
pub fn local_url(name: &str, settings: &Settings) -> LemmyResult<DbUrl> {
|
||||
let domain = settings.get_protocol_and_hostname();
|
||||
Ok(Url::parse(&format!("{domain}/c/{name}"))?.into())
|
||||
|
|
|
@ -28,6 +28,7 @@ pub mod person_comment_mention;
|
|||
pub mod person_post_mention;
|
||||
pub mod post;
|
||||
pub mod post_report;
|
||||
pub mod post_tag;
|
||||
pub mod private_message;
|
||||
pub mod private_message_report;
|
||||
pub mod registration_application;
|
||||
|
|
71
crates/db_schema/src/impls/post_tag.rs
Normal file
71
crates/db_schema/src/impls/post_tag.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use crate::{
|
||||
diesel::SelectableHelper,
|
||||
newtypes::{PostId, TagId},
|
||||
source::{
|
||||
post_tag::{PostTag, PostTagForm},
|
||||
tag::PostTagInsertForm,
|
||||
},
|
||||
traits::Crud,
|
||||
utils::{get_conn, DbPool},
|
||||
};
|
||||
use diesel::{delete, insert_into, ExpressionMethods, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_db_schema_file::schema::post_tag;
|
||||
|
||||
impl PostTag {
|
||||
pub async fn set(
|
||||
pool: &mut DbPool<'_>,
|
||||
post_id: PostId,
|
||||
tags: Vec<PostTagInsertForm>,
|
||||
) -> Result<Vec<Self>, diesel::result::Error> {
|
||||
PostTag::delete_for_post(pool, post_id).await?;
|
||||
PostTag::create_many(pool, tags).await
|
||||
}
|
||||
async fn delete_for_post(
|
||||
pool: &mut DbPool<'_>,
|
||||
post_id: PostId,
|
||||
) -> Result<usize, diesel::result::Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
delete(post_tag::table.filter(post_tag::post_id.eq(post_id)))
|
||||
.execute(conn)
|
||||
.await
|
||||
}
|
||||
pub async fn create_many(
|
||||
pool: &mut DbPool<'_>,
|
||||
forms: Vec<PostTagInsertForm>,
|
||||
) -> Result<Vec<Self>, diesel::result::Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
insert_into(post_tag::table)
|
||||
.values(forms)
|
||||
.returning(Self::as_select())
|
||||
.get_results(conn)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Crud for PostTag {
|
||||
type InsertForm = PostTagInsertForm;
|
||||
type UpdateForm = PostTagForm;
|
||||
type IdType = (PostId, TagId);
|
||||
|
||||
async fn create(
|
||||
pool: &mut DbPool<'_>,
|
||||
form: &PostTagInsertForm,
|
||||
) -> Result<Self, diesel::result::Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
insert_into(post_tag::table)
|
||||
.values(form)
|
||||
.get_result::<Self>(conn)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update(
|
||||
_pool: &mut DbPool<'_>,
|
||||
_id: Self::IdType,
|
||||
_form: &Self::UpdateForm,
|
||||
) -> Result<Self, diesel::result::Error> {
|
||||
Err(diesel::result::Error::QueryBuilderError(
|
||||
"PostTag does not support (create+delete only)".into(),
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,18 +1,31 @@
|
|||
use crate::{
|
||||
newtypes::TagId,
|
||||
source::tag::{PostTagInsertForm, Tag, TagInsertForm},
|
||||
newtypes::{CommunityId, TagId},
|
||||
source::tag::{PostTagInsertForm, Tag, TagInsertForm, TagUpdateForm},
|
||||
traits::Crud,
|
||||
utils::{get_conn, DbPool},
|
||||
};
|
||||
use diesel::{insert_into, result::Error, QueryDsl};
|
||||
use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_db_schema_file::schema::{post_tag, tag};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
impl Tag {
|
||||
pub async fn get_by_community(
|
||||
pool: &mut DbPool<'_>,
|
||||
search_community_id: CommunityId,
|
||||
) -> Result<Vec<Self>, Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
tag::table
|
||||
.filter(tag::community_id.eq(search_community_id))
|
||||
.filter(tag::deleted.eq(false))
|
||||
.load::<Self>(conn)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Crud for Tag {
|
||||
type InsertForm = TagInsertForm;
|
||||
|
||||
type UpdateForm = TagInsertForm;
|
||||
type UpdateForm = TagUpdateForm;
|
||||
|
||||
type IdType = TagId;
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ pub mod person_comment_mention;
|
|||
pub mod person_post_mention;
|
||||
pub mod post;
|
||||
pub mod post_report;
|
||||
pub mod post_tag;
|
||||
pub mod private_message;
|
||||
pub mod private_message_report;
|
||||
pub mod registration_application;
|
||||
|
|
32
crates/db_schema/src/source/post_tag.rs
Normal file
32
crates/db_schema/src/source/post_tag.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use crate::newtypes::{PostId, TagId};
|
||||
use chrono::{DateTime, Utc};
|
||||
#[cfg(feature = "full")]
|
||||
use lemmy_db_schema_file::schema::post_tag;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(
|
||||
feature = "full",
|
||||
derive(Queryable, Selectable, Associations, Identifiable)
|
||||
)]
|
||||
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))]
|
||||
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::tag::Tag)))]
|
||||
#[cfg_attr(feature = "full", diesel(table_name = post_tag))]
|
||||
#[cfg_attr(feature = "full", diesel(primary_key(post_id, tag_id)))]
|
||||
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||
/// An association between a post and a tag. Created/updated by the post author or mods of a
|
||||
/// community. In the future, more access controls could be added, for example that specific tag
|
||||
/// types can only be added by mods.
|
||||
pub struct PostTag {
|
||||
pub post_id: PostId,
|
||||
pub tag_id: TagId,
|
||||
pub published: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
|
||||
#[cfg_attr(feature = "full", diesel(table_name = post_tag))]
|
||||
pub struct PostTagForm {
|
||||
pub post_id: PostId,
|
||||
pub tag_id: TagId,
|
||||
}
|
|
@ -7,26 +7,28 @@ use serde_with::skip_serializing_none;
|
|||
#[cfg(feature = "full")]
|
||||
use ts_rs::TS;
|
||||
|
||||
/// A tag that can be assigned to a post within a community.
|
||||
/// The tag object is created by the community moderators.
|
||||
/// The assignment happens by the post creator and can be updated by the community moderators.
|
||||
///
|
||||
/// A tag is a federatable object that gives additional context to another object, which can be
|
||||
/// displayed and filtered on currently, we only have community post tags, which is a tag that is
|
||||
/// created by post authors as well as mods of a community, to categorize a post. in the future we
|
||||
/// may add more tag types, depending on the requirements, this will lead to either expansion of
|
||||
/// this table (community_id optional, addition of tag_type enum) or split of this table / creation
|
||||
/// of new tables.
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable, Identifiable))]
|
||||
#[cfg_attr(feature = "full", diesel(table_name = tag))]
|
||||
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// A tag that can be assigned to a post within a community.
|
||||
/// The tag object is created by the community moderators.
|
||||
/// The assignment happens by the post creator and can be updated by the community moderators.
|
||||
///
|
||||
/// A tag is a federatable object that gives additional context to another object, which can be
|
||||
/// displayed and filtered on. Currently, we only have community post tags, which is a tag that is
|
||||
/// created by the mods of a community, then assigned to posts by post authors as well as mods of a
|
||||
/// community, to categorize a post.
|
||||
///
|
||||
/// In the future we may add more tag types, depending on the requirements, this will lead to either
|
||||
/// expansion of this table (community_id optional, addition of tag_type enum) or split of this
|
||||
/// table / creation of new tables.
|
||||
pub struct Tag {
|
||||
pub id: TagId,
|
||||
pub ap_id: DbUrl,
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
/// the community that owns this tag
|
||||
pub community_id: CommunityId,
|
||||
pub published: DateTime<Utc>,
|
||||
|
@ -40,12 +42,20 @@ pub struct Tag {
|
|||
#[cfg_attr(feature = "full", diesel(table_name = tag))]
|
||||
pub struct TagInsertForm {
|
||||
pub ap_id: DbUrl,
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
pub community_id: CommunityId,
|
||||
// default now
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[cfg_attr(feature = "full", derive(AsChangeset))]
|
||||
#[cfg_attr(feature = "full", diesel(table_name = tag))]
|
||||
pub struct TagUpdateForm {
|
||||
pub ap_id: Option<DbUrl>,
|
||||
pub display_name: Option<String>,
|
||||
pub community_id: Option<CommunityId>,
|
||||
pub published: Option<DateTime<Utc>>,
|
||||
pub updated: Option<DateTime<Utc>>,
|
||||
pub deleted: bool,
|
||||
pub updated: Option<Option<DateTime<Utc>>>,
|
||||
pub deleted: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -1051,7 +1051,7 @@ diesel::table! {
|
|||
tag (id) {
|
||||
id -> Int4,
|
||||
ap_id -> Text,
|
||||
name -> Text,
|
||||
display_name -> Text,
|
||||
community_id -> Int4,
|
||||
published -> Timestamptz,
|
||||
updated -> Nullable<Timestamptz>,
|
||||
|
|
|
@ -366,6 +366,7 @@ impl InternalToCombinedView for InboxCombinedViewInternal {
|
|||
creator_local_instance_actions: v.creator_local_instance_actions,
|
||||
creator_community_actions: v.creator_community_actions,
|
||||
creator_is_admin: v.item_creator_is_admin,
|
||||
post_tags: v.post_tags,
|
||||
can_mod: v.can_mod,
|
||||
creator_banned: v.creator_banned,
|
||||
}))
|
||||
|
@ -413,6 +414,7 @@ impl InternalToCombinedView for InboxCombinedViewInternal {
|
|||
image_details: v.image_details,
|
||||
creator_community_actions: v.creator_community_actions,
|
||||
creator_is_admin: v.item_creator_is_admin,
|
||||
post_tags: v.post_tags,
|
||||
can_mod: v.can_mod,
|
||||
creator_banned: v.creator_banned,
|
||||
}))
|
||||
|
|
|
@ -224,6 +224,7 @@ impl InternalToCombinedView for PersonContentCombinedViewInternal {
|
|||
creator_local_instance_actions: v.creator_local_instance_actions,
|
||||
creator_community_actions: v.creator_community_actions,
|
||||
creator_is_admin: v.item_creator_is_admin,
|
||||
post_tags: v.post_tags,
|
||||
can_mod: v.can_mod,
|
||||
creator_banned: v.creator_banned,
|
||||
}))
|
||||
|
@ -241,6 +242,7 @@ impl InternalToCombinedView for PersonContentCombinedViewInternal {
|
|||
creator_local_instance_actions: v.creator_local_instance_actions,
|
||||
creator_community_actions: v.creator_community_actions,
|
||||
creator_is_admin: v.item_creator_is_admin,
|
||||
tags: v.post_tags,
|
||||
can_mod: v.can_mod,
|
||||
creator_banned: v.creator_banned,
|
||||
}))
|
||||
|
|
|
@ -211,6 +211,7 @@ impl InternalToCombinedView for PersonSavedCombinedViewInternal {
|
|||
creator_local_instance_actions: v.creator_local_instance_actions,
|
||||
creator_community_actions: v.creator_community_actions,
|
||||
creator_is_admin: v.item_creator_is_admin,
|
||||
post_tags: v.post_tags,
|
||||
can_mod: v.can_mod,
|
||||
creator_banned: v.creator_banned,
|
||||
}))
|
||||
|
@ -228,6 +229,7 @@ impl InternalToCombinedView for PersonSavedCombinedViewInternal {
|
|||
creator_local_instance_actions: v.creator_local_instance_actions,
|
||||
creator_community_actions: v.creator_community_actions,
|
||||
creator_is_admin: v.item_creator_is_admin,
|
||||
tags: v.post_tags,
|
||||
can_mod: v.can_mod,
|
||||
creator_banned: v.creator_banned,
|
||||
}))
|
||||
|
|
|
@ -360,6 +360,7 @@ impl InternalToCombinedView for SearchCombinedViewInternal {
|
|||
person_actions: v.person_actions,
|
||||
comment_actions: v.comment_actions,
|
||||
creator_is_admin: v.item_creator_is_admin,
|
||||
post_tags: v.post_tags,
|
||||
can_mod: v.can_mod,
|
||||
creator_banned: v.creator_banned,
|
||||
}))
|
||||
|
@ -379,6 +380,7 @@ impl InternalToCombinedView for SearchCombinedViewInternal {
|
|||
creator_community_actions: v.creator_community_actions,
|
||||
person_actions: v.person_actions,
|
||||
post_actions: v.post_actions,
|
||||
tags: v.post_tags,
|
||||
can_mod: v.can_mod,
|
||||
creator_banned: v.creator_banned,
|
||||
}))
|
||||
|
@ -388,6 +390,7 @@ impl InternalToCombinedView for SearchCombinedViewInternal {
|
|||
community_actions: v.community_actions,
|
||||
instance_actions: v.instance_actions,
|
||||
can_mod: v.can_mod,
|
||||
post_tags: v.community_post_tags,
|
||||
}))
|
||||
} else if let Some(person) = v.item_creator {
|
||||
Some(SearchCombinedView::Person(PersonView {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//! see post_view.rs for the reason for this json decoding
|
||||
use crate::structs::PostTags;
|
||||
use crate::structs::TagsView;
|
||||
use diesel::{
|
||||
deserialize::FromSql,
|
||||
pg::{Pg, PgValue},
|
||||
|
@ -7,22 +7,22 @@ use diesel::{
|
|||
sql_types::{self, Nullable},
|
||||
};
|
||||
|
||||
impl FromSql<Nullable<sql_types::Json>, Pg> for PostTags {
|
||||
impl FromSql<Nullable<sql_types::Json>, Pg> for TagsView {
|
||||
fn from_sql(bytes: PgValue) -> diesel::deserialize::Result<Self> {
|
||||
let value = <serde_json::Value as FromSql<sql_types::Json, Pg>>::from_sql(bytes)?;
|
||||
Ok(serde_json::from_value::<PostTags>(value)?)
|
||||
Ok(serde_json::from_value::<TagsView>(value)?)
|
||||
}
|
||||
fn from_nullable_sql(
|
||||
bytes: Option<<Pg as diesel::backend::Backend>::RawValue<'_>>,
|
||||
) -> diesel::deserialize::Result<Self> {
|
||||
match bytes {
|
||||
Some(bytes) => Self::from_sql(bytes),
|
||||
None => Ok(Self { tags: vec![] }),
|
||||
None => Ok(Self(vec![])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql<Nullable<sql_types::Json>, Pg> for PostTags {
|
||||
impl ToSql<Nullable<sql_types::Json>, Pg> for TagsView {
|
||||
fn to_sql(&self, out: &mut diesel::serialize::Output<Pg>) -> diesel::serialize::Result {
|
||||
let value = serde_json::to_value(self)?;
|
||||
<serde_json::Value as ToSql<sql_types::Json, Pg>>::to_sql(&value, &mut out.reborrow())
|
||||
|
|
|
@ -609,8 +609,8 @@ mod tests {
|
|||
post: Post,
|
||||
bot_post: Post,
|
||||
post_with_tags: Post,
|
||||
_tag_1: Tag,
|
||||
_tag_2: Tag,
|
||||
tag_1: Tag,
|
||||
tag_2: Tag,
|
||||
site: Site,
|
||||
}
|
||||
|
||||
|
@ -692,11 +692,8 @@ mod tests {
|
|||
pool,
|
||||
&TagInsertForm {
|
||||
ap_id: Url::parse(&format!("{}/tags/test_tag1", community.ap_id))?.into(),
|
||||
name: "Test Tag 1".into(),
|
||||
display_name: "Test Tag 1".into(),
|
||||
community_id: community.id,
|
||||
published: None,
|
||||
updated: None,
|
||||
deleted: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
@ -704,11 +701,8 @@ mod tests {
|
|||
pool,
|
||||
&TagInsertForm {
|
||||
ap_id: Url::parse(&format!("{}/tags/test_tag2", community.ap_id))?.into(),
|
||||
name: "Test Tag 2".into(),
|
||||
display_name: "Test Tag 2".into(),
|
||||
community_id: community.id,
|
||||
published: None,
|
||||
updated: None,
|
||||
deleted: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
@ -796,8 +790,8 @@ mod tests {
|
|||
post,
|
||||
bot_post,
|
||||
post_with_tags,
|
||||
_tag_1: tag_1,
|
||||
_tag_2: tag_2,
|
||||
tag_1,
|
||||
tag_2,
|
||||
site,
|
||||
})
|
||||
}
|
||||
|
@ -2215,32 +2209,31 @@ mod tests {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
#[test_context(Data)]
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn post_tags_present(data: &mut Data) -> LemmyResult<()> {
|
||||
let pool = &data.pool();
|
||||
let pool = &mut pool.into();
|
||||
|
||||
// TODO add these back in later
|
||||
// #[test_context(Data)]
|
||||
// #[tokio::test]
|
||||
// #[serial]
|
||||
// async fn post_tags_present(data: &mut Data) -> LemmyResult<()> {
|
||||
// let pool = &data.pool();
|
||||
// let pool = &mut pool.into();
|
||||
let post_view = PostView::read(
|
||||
pool,
|
||||
data.post_with_tags.id,
|
||||
Some(&data.tegan_local_user_view.local_user),
|
||||
data.instance.id,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// let post_view = PostView::read(
|
||||
// pool,
|
||||
// data.post_with_tags.id,
|
||||
// Some(&data.tegan_local_user_view.local_user),
|
||||
// false,
|
||||
// )
|
||||
// .await?;
|
||||
assert_eq!(2, post_view.tags.0.len());
|
||||
assert_eq!(data.tag_1.display_name, post_view.tags.0[0].display_name);
|
||||
assert_eq!(data.tag_2.display_name, post_view.tags.0[1].display_name);
|
||||
|
||||
// assert_eq!(2, post_view.tags.tags.len());
|
||||
// assert_eq!(data.tag_1.name, post_view.tags.tags[0].name);
|
||||
// assert_eq!(data.tag_2.name, post_view.tags.tags[1].name);
|
||||
let all_posts = data.default_post_query().list(&data.site, pool).await?;
|
||||
assert_eq!(2, all_posts[0].tags.0.len()); // post with tags
|
||||
assert_eq!(0, all_posts[1].tags.0.len()); // bot post
|
||||
assert_eq!(0, all_posts[2].tags.0.len()); // normal post
|
||||
|
||||
// let all_posts = data.default_post_query().list(&data.site, pool).await?;
|
||||
// assert_eq!(2, all_posts[0].tags.tags.len()); // post with tags
|
||||
// assert_eq!(0, all_posts[1].tags.tags.len()); // bot post
|
||||
// assert_eq!(0, all_posts[2].tags.tags.len()); // normal post
|
||||
|
||||
// Ok(())
|
||||
// }
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
use crate::utils::{
|
||||
comment_creator_is_admin,
|
||||
comment_select_remove_deletes,
|
||||
community_post_tags_fragment,
|
||||
creator_banned,
|
||||
creator_community_actions_select,
|
||||
creator_home_instance_actions_select,
|
||||
|
@ -13,6 +14,7 @@ use crate::utils::{
|
|||
person1_select,
|
||||
person2_select,
|
||||
post_creator_is_admin,
|
||||
post_tags_fragment,
|
||||
};
|
||||
#[cfg(feature = "full")]
|
||||
use diesel::{
|
||||
|
@ -208,6 +210,12 @@ pub struct CommentView {
|
|||
)
|
||||
)]
|
||||
pub creator_is_admin: bool,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = post_tags_fragment()
|
||||
)
|
||||
)]
|
||||
pub post_tags: TagsView,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = local_user_can_mod()
|
||||
|
@ -405,6 +413,12 @@ pub struct PostView {
|
|||
)
|
||||
)]
|
||||
pub creator_is_admin: bool,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = post_tags_fragment()
|
||||
)
|
||||
)]
|
||||
pub tags: TagsView,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = local_user_can_mod()
|
||||
|
@ -645,6 +659,12 @@ pub(crate) struct PersonContentCombinedViewInternal {
|
|||
)
|
||||
)]
|
||||
pub item_creator_is_admin: bool,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = post_tags_fragment()
|
||||
)
|
||||
)]
|
||||
pub post_tags: TagsView,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = local_user_can_mod()
|
||||
|
@ -717,6 +737,12 @@ pub(crate) struct PersonSavedCombinedViewInternal {
|
|||
)
|
||||
)]
|
||||
pub item_creator_is_admin: bool,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = post_tags_fragment()
|
||||
)
|
||||
)]
|
||||
pub post_tags: TagsView,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = local_user_can_mod()
|
||||
|
@ -796,6 +822,12 @@ pub struct CommunityView {
|
|||
)
|
||||
)]
|
||||
pub can_mod: bool,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = community_post_tags_fragment()
|
||||
)
|
||||
)]
|
||||
pub post_tags: TagsView,
|
||||
}
|
||||
|
||||
/// The community sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html
|
||||
|
@ -879,6 +911,7 @@ pub struct PersonPostMentionView {
|
|||
pub creator_local_instance_actions: Option<InstanceActions>,
|
||||
#[cfg_attr(feature = "full", ts(optional))]
|
||||
pub creator_community_actions: Option<CommunityActions>,
|
||||
pub post_tags: TagsView,
|
||||
pub creator_is_admin: bool,
|
||||
pub can_mod: bool,
|
||||
pub creator_banned: bool,
|
||||
|
@ -913,6 +946,7 @@ pub struct CommentReplyView {
|
|||
#[cfg_attr(feature = "full", ts(optional))]
|
||||
pub creator_community_actions: Option<CommunityActions>,
|
||||
pub creator_is_admin: bool,
|
||||
pub post_tags: TagsView,
|
||||
pub can_mod: bool,
|
||||
pub creator_banned: bool,
|
||||
}
|
||||
|
@ -1044,6 +1078,12 @@ pub struct InboxCombinedViewInternal {
|
|||
)
|
||||
)]
|
||||
pub item_creator_is_admin: bool,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = post_tags_fragment()
|
||||
)
|
||||
)]
|
||||
pub post_tags: TagsView,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = local_user_can_mod()
|
||||
|
@ -1435,6 +1475,20 @@ pub(crate) struct SearchCombinedViewInternal {
|
|||
)
|
||||
)]
|
||||
pub item_creator_is_admin: bool,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = post_tags_fragment()
|
||||
)
|
||||
)]
|
||||
/// tags of this post
|
||||
pub post_tags: TagsView,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = community_post_tags_fragment()
|
||||
)
|
||||
)]
|
||||
/// available tags in this community
|
||||
pub community_post_tags: TagsView,
|
||||
#[cfg_attr(feature = "full",
|
||||
diesel(
|
||||
select_expression = local_user_can_mod()
|
||||
|
@ -1462,10 +1516,8 @@ pub enum SearchCombinedView {
|
|||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "full", derive(TS, FromSqlRow, AsExpression))]
|
||||
#[serde(transparent)]
|
||||
#[cfg_attr(feature = "full", derive(TS, FromSqlRow, AsExpression))]
|
||||
#[cfg_attr(feature = "full", diesel(sql_type = Nullable<sql_types::Json>))]
|
||||
/// we wrap this in a struct so we can implement FromSqlRow<Json> for it
|
||||
pub struct PostTags {
|
||||
pub tags: Vec<Tag>,
|
||||
}
|
||||
pub struct TagsView(pub Vec<Tag>);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use diesel::{
|
||||
dsl::{case_when, exists, not, Nullable},
|
||||
expression::SqlLiteral,
|
||||
helper_types::{Eq, NotEq},
|
||||
sql_types::Json,
|
||||
BoolExpressionMethods,
|
||||
ExpressionMethods,
|
||||
JoinOnDsl,
|
||||
|
@ -38,6 +40,8 @@ use lemmy_db_schema_file::{
|
|||
person_actions,
|
||||
post,
|
||||
post_actions,
|
||||
post_tag,
|
||||
tag,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -179,6 +183,29 @@ pub(crate) fn comment_select_remove_deletes() -> _ {
|
|||
)
|
||||
}
|
||||
|
||||
#[diesel::dsl::auto_type]
|
||||
// Gets the post tags set on a specific post
|
||||
pub(crate) fn post_tags_fragment() -> _ {
|
||||
let sel: SqlLiteral<Json> = diesel::dsl::sql::<diesel::sql_types::Json>("json_agg(tag.*)");
|
||||
post_tag::table
|
||||
.inner_join(tag::table)
|
||||
.select(sel)
|
||||
.filter(post_tag::post_id.eq(post::id))
|
||||
.filter(tag::deleted.eq(false))
|
||||
.single_value()
|
||||
}
|
||||
|
||||
#[diesel::dsl::auto_type]
|
||||
/// Gets the post tags available within a specific community
|
||||
pub(crate) fn community_post_tags_fragment() -> _ {
|
||||
let sel: SqlLiteral<Json> = diesel::dsl::sql::<diesel::sql_types::Json>("json_agg(tag.*)");
|
||||
tag::table
|
||||
.select(sel)
|
||||
.filter(tag::community_id.eq(community::id))
|
||||
.filter(tag::deleted.eq(false))
|
||||
.single_value()
|
||||
}
|
||||
|
||||
/// The select for the person1 alias.
|
||||
pub(crate) fn person1_select() -> Person1AliasAllColumnsTuple {
|
||||
person1.fields(person::all_columns)
|
||||
|
|
|
@ -463,7 +463,7 @@ async fn publish_scheduled_posts(context: &Data<LemmyContext>) -> LemmyResult<()
|
|||
// send out post via federation and webmention
|
||||
let send_activity = SendActivityData::CreatePost(post.clone());
|
||||
ActivityChannel::submit_activity(send_activity, context)?;
|
||||
send_webmention(post, community);
|
||||
send_webmention(post, &community);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -140,6 +140,8 @@ pub enum LemmyErrorType {
|
|||
BanExpirationInPast,
|
||||
InvalidUnixTime,
|
||||
InvalidBotAction,
|
||||
InvalidTagName,
|
||||
TagNotInCommunity,
|
||||
CantBlockLocalInstance,
|
||||
Unknown(String),
|
||||
UrlLengthOverflow,
|
||||
|
|
|
@ -26,6 +26,8 @@ const ALT_TEXT_MAX_LENGTH: usize = 1500;
|
|||
const SITE_NAME_MAX_LENGTH: usize = 20;
|
||||
const SITE_NAME_MIN_LENGTH: usize = 1;
|
||||
const SITE_DESCRIPTION_MAX_LENGTH: usize = 150;
|
||||
const TAG_NAME_MIN_LENGTH: usize = 3;
|
||||
const TAG_NAME_MAX_LENGTH: usize = 100;
|
||||
//Invisible unicode characters, taken from https://invisible-characters.com/
|
||||
const FORBIDDEN_DISPLAY_CHARS: [char; 53] = [
|
||||
'\u{0009}',
|
||||
|
@ -197,6 +199,19 @@ pub fn site_or_community_description_length_check(description: &str) -> LemmyRes
|
|||
)
|
||||
}
|
||||
|
||||
pub fn tag_name_length_check(tag_name: &str) -> LemmyResult<()> {
|
||||
min_length_check(
|
||||
tag_name,
|
||||
TAG_NAME_MIN_LENGTH,
|
||||
LemmyErrorType::InvalidTagName,
|
||||
)?;
|
||||
max_length_check(
|
||||
tag_name,
|
||||
TAG_NAME_MAX_LENGTH,
|
||||
LemmyErrorType::InvalidTagName,
|
||||
)
|
||||
}
|
||||
|
||||
/// Check minimum and maximum length of input string. If the string is too short or too long, the
|
||||
/// corresponding error is returned.
|
||||
///
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
CREATE TABLE tag (
|
||||
id serial PRIMARY KEY,
|
||||
ap_id text NOT NULL UNIQUE,
|
||||
name text NOT NULL,
|
||||
display_name text NOT NULL,
|
||||
community_id int NOT NULL REFERENCES community (id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
published timestamptz NOT NULL DEFAULT now(),
|
||||
updated timestamptz,
|
||||
|
|
|
@ -17,6 +17,7 @@ use lemmy_api::{
|
|||
list::get_pending_follows_list,
|
||||
},
|
||||
random::get_random_community,
|
||||
tag::{create_community_tag, delete_community_tag, update_community_tag},
|
||||
transfer::transfer_community,
|
||||
},
|
||||
local_user::{
|
||||
|
@ -228,6 +229,9 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) {
|
|||
.route("/icon", delete().to(delete_community_icon))
|
||||
.route("/banner", post().to(upload_community_banner))
|
||||
.route("/banner", delete().to(delete_community_banner))
|
||||
.route("/tag", post().to(create_community_tag))
|
||||
.route("/tag", put().to(update_community_tag))
|
||||
.route("/tag", delete().to(delete_community_tag))
|
||||
.service(
|
||||
scope("/pending_follows")
|
||||
.route("/count", get().to(get_pending_follows_count))
|
||||
|
|
Loading…
Reference in a new issue