Merge 002f49d7db
into 61b9f81e39
This commit is contained in:
commit
fe230fa862
25 changed files with 758 additions and 2 deletions
29
client/css/banned-posts.styl
Normal file
29
client/css/banned-posts.styl
Normal file
|
@ -0,0 +1,29 @@
|
|||
@import colors
|
||||
|
||||
.content-wrapper.banned-posts
|
||||
width: 100%
|
||||
max-width: 45em
|
||||
table
|
||||
border-spacing: 0
|
||||
width: 100%
|
||||
tr.default td
|
||||
background: $default-banned-post-background-color
|
||||
td, th
|
||||
padding: .4em
|
||||
&.color
|
||||
input[type=text]
|
||||
width: 8em
|
||||
&.usages
|
||||
text-align: center
|
||||
&.remove, &.set-default
|
||||
white-space: pre
|
||||
th
|
||||
white-space: nowrap
|
||||
&:first-child
|
||||
padding-left: 0
|
||||
&:last-child
|
||||
padding-right: 0
|
||||
tfoot
|
||||
display: none
|
||||
form
|
||||
width: auto
|
13
client/html/banned_post_entry.tpl
Normal file
13
client/html/banned_post_entry.tpl
Normal file
|
@ -0,0 +1,13 @@
|
|||
<tr data-category='<%- ctx.postBan.checksum %>'>
|
||||
<td class='name'>
|
||||
<%- ctx.postBan.checksum %>
|
||||
</td>
|
||||
<td class='time'>
|
||||
<%= ctx.makeRelativeTime(ctx.postBan.time) %>
|
||||
</td>
|
||||
<% if (ctx.canDelete) { %>
|
||||
<td class='remove'>
|
||||
<a href>Unban</a>
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
25
client/html/banned_post_list.tpl
Normal file
25
client/html/banned_post_list.tpl
Normal file
|
@ -0,0 +1,25 @@
|
|||
<div class='content-wrapper banned-posts'>
|
||||
<form>
|
||||
<h1>Banned posts</h1>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class='checksum'>Checksum</th>
|
||||
<th class='time'>Time of ban</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='messages'></div>
|
||||
|
||||
<% if (ctx.canDelete) { %>
|
||||
<div class='buttons'>
|
||||
<input type='submit' class='save' value='Save changes'>
|
||||
</div>
|
||||
<% } %>
|
||||
</form>
|
||||
</div>
|
|
@ -108,7 +108,7 @@
|
|||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canFeaturePosts || ctx.canDeletePosts || ctx.canMergePosts) { %>
|
||||
<% if (ctx.canFeaturePosts || ctx.canDeletePosts || ctx.canMergePosts || ctx.canBanPosts) { %>
|
||||
<section class='management'>
|
||||
<ul>
|
||||
<% if (ctx.canFeaturePosts) { %>
|
||||
|
@ -120,6 +120,9 @@
|
|||
<% if (ctx.canDeletePosts) { %>
|
||||
<li><a href class='delete'>Delete this post</a></li>
|
||||
<% } %>
|
||||
<% if (ctx.canBanPosts) { %>
|
||||
<li><a href class='ban'>Ban this post</a></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</section>
|
||||
<% } %>
|
||||
|
|
59
client/js/controllers/banned_post_controller.js
Normal file
59
client/js/controllers/banned_post_controller.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
"use strict";
|
||||
|
||||
const api = require("../api.js");
|
||||
const BannedPostList = require("../models/banned_post_list.js");
|
||||
const topNavigation = require("../models/top_navigation.js");
|
||||
const BannedPostsView = require("../views/banned_posts_view.js");
|
||||
const EmptyView = require("../views/empty_view.js");
|
||||
|
||||
class BannedPostController {
|
||||
constructor() {
|
||||
if (!api.hasPrivilege("posts:ban:list")) {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(
|
||||
"You don't have privileges to view banned posts."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
topNavigation.activate("banned-posts");
|
||||
topNavigation.setTitle("Listing banned posts");
|
||||
BannedPostList.get().then(
|
||||
(response) => {
|
||||
this._bannedPosts = response.results;
|
||||
this._view = new BannedPostsView({
|
||||
bannedPosts: this._bannedPosts,
|
||||
canDelete: api.hasPrivilege("poolCategories:delete")
|
||||
});
|
||||
this._view.addEventListener("submit", (e) =>
|
||||
this._evtSubmit(e)
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_evtSubmit(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
this._bannedPosts.save().then(
|
||||
() => {
|
||||
this._view.enableForm();
|
||||
this._view.showSuccess("Changes saved.");
|
||||
},
|
||||
(error) => {
|
||||
this._view.enableForm();
|
||||
this._view.showError(error.message);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = (router) => {
|
||||
router.enter(["banned-posts"], (ctx, next) => {
|
||||
ctx.controller = new BannedPostController(ctx, next);
|
||||
});
|
||||
};
|
|
@ -88,6 +88,9 @@ class PostMainController extends BasePostController {
|
|||
this._view.sidebarControl.addEventListener("delete", (e) =>
|
||||
this._evtDeletePost(e)
|
||||
);
|
||||
this._view.sidebarControl.addEventListener("ban", (e) =>
|
||||
this._evtBanPost(e)
|
||||
);
|
||||
this._view.sidebarControl.addEventListener("merge", (e) =>
|
||||
this._evtMergePost(e)
|
||||
);
|
||||
|
@ -165,6 +168,22 @@ class PostMainController extends BasePostController {
|
|||
);
|
||||
}
|
||||
|
||||
_evtBanPost(e) {
|
||||
this._view.sidebarControl.disableForm();
|
||||
this._view.sidebarControl.clearMessages();
|
||||
e.detail.post.ban().then(
|
||||
() => {
|
||||
misc.disableExitConfirmation();
|
||||
const ctx = router.show(uri.formatClientLink("posts"));
|
||||
ctx.controller.showSuccess("Post banned.");
|
||||
},
|
||||
(error) => {
|
||||
this._view.sidebarControl.showError(error.message);
|
||||
this._view.sidebarControl.enableForm();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_evtUpdatePost(e) {
|
||||
this._view.sidebarControl.disableForm();
|
||||
this._view.sidebarControl.clearMessages();
|
||||
|
|
|
@ -53,6 +53,9 @@ class TopNavigationController {
|
|||
if (!api.hasPrivilege("pools:list")) {
|
||||
topNavigation.hide("pools");
|
||||
}
|
||||
if (!api.hasPrivilege("posts:ban:list")) {
|
||||
topNavigation.hide("banned-posts");
|
||||
}
|
||||
if (api.isLoggedIn()) {
|
||||
if (!api.hasPrivilege("users:create:any")) {
|
||||
topNavigation.hide("register");
|
||||
|
|
|
@ -46,6 +46,7 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
"posts:create:anonymous"
|
||||
),
|
||||
canDeletePosts: api.hasPrivilege("posts:delete"),
|
||||
canBanPosts: api.hasPrivilege("posts:ban"),
|
||||
canFeaturePosts: api.hasPrivilege("posts:feature"),
|
||||
canMergePosts: api.hasPrivilege("posts:merge"),
|
||||
})
|
||||
|
@ -186,6 +187,12 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
);
|
||||
}
|
||||
|
||||
if (this._banLinkNode) {
|
||||
this._banLinkNode.addEventListener("click", (e) =>
|
||||
this._evtBanClick(e)
|
||||
);
|
||||
}
|
||||
|
||||
this._postNotesOverlayControl.addEventListener("blur", (e) =>
|
||||
this._evtNoteBlur(e)
|
||||
);
|
||||
|
@ -301,6 +308,19 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
_evtBanClick(e) {
|
||||
e.preventDefault();
|
||||
if (confirm("Are you sure you want to ban this post?")) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("ban", {
|
||||
detail: {
|
||||
post: this._post,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_evtNoteTextChangeRequest(e) {
|
||||
if (this._editedNote) {
|
||||
this._editedNote.text = this._noteTextareaNode.value;
|
||||
|
@ -517,6 +537,11 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
return this._formNode.querySelector(".management .delete");
|
||||
}
|
||||
|
||||
get _banLinkNode() {
|
||||
return this._formNode.querySelector(".management .ban");
|
||||
}
|
||||
|
||||
|
||||
get _addNoteLinkNode() {
|
||||
return this._formNode.querySelector(".notes .add");
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ Promise.resolve()
|
|||
controllers.push(
|
||||
require("./controllers/user_registration_controller.js")
|
||||
);
|
||||
controllers.push(require("./controllers/banned_post_controller.js"));
|
||||
|
||||
// 404 controller needs to be registered last
|
||||
controllers.push(require("./controllers/not_found_controller.js"));
|
||||
|
|
57
client/js/models/banned_post.js
Normal file
57
client/js/models/banned_post.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
"use strict";
|
||||
|
||||
const api = require("../api.js");
|
||||
const uri = require("../util/uri.js");
|
||||
const events = require("../events.js");
|
||||
|
||||
class BannedPost extends events.EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
this._checksum = "";
|
||||
this._time = new Date();
|
||||
}
|
||||
|
||||
get checksum() {
|
||||
return this._checksum;
|
||||
}
|
||||
|
||||
get time() {
|
||||
return this._time;
|
||||
}
|
||||
|
||||
set checksum(value) {
|
||||
this._checksum = value;
|
||||
}
|
||||
|
||||
set time(value) {
|
||||
this._time = value;
|
||||
}
|
||||
|
||||
static fromResponse(response) {
|
||||
const ret = new BannedPost();
|
||||
ret._updateFromResponse(response);
|
||||
return ret;
|
||||
}
|
||||
|
||||
delete() {
|
||||
return api
|
||||
.delete(uri.formatApiLink("post-ban", this._checksum))
|
||||
.then((response) => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("delete", {
|
||||
detail: {
|
||||
bannedPost: this,
|
||||
},
|
||||
})
|
||||
);
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
_updateFromResponse(response) {
|
||||
this._checksum = response.checksum;
|
||||
this.time = response.time;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BannedPost;
|
47
client/js/models/banned_post_list.js
Normal file
47
client/js/models/banned_post_list.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
const api = require("../api.js");
|
||||
const uri = require("../util/uri.js");
|
||||
const AbstractList = require("./abstract_list.js");
|
||||
const BannedPost = require("./banned_post.js");
|
||||
|
||||
class BannedPostList extends AbstractList {
|
||||
constructor() {
|
||||
super();
|
||||
this._deletedBans = [];
|
||||
this.addEventListener("remove", (e) => this._evtBannedPostDeleted(e));
|
||||
}
|
||||
|
||||
static get() {
|
||||
return api
|
||||
.get(uri.formatApiLink("post-ban"))
|
||||
.then((response) => {
|
||||
return Promise.resolve(
|
||||
Object.assign({}, response, {
|
||||
results: BannedPostList.fromResponse(
|
||||
response.results
|
||||
),
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
save() {
|
||||
let promises = [];
|
||||
for (let bannedPost of this._deletedBans) {
|
||||
promises.push(bannedPost.delete());
|
||||
}
|
||||
|
||||
return Promise.all(promises).then((response) => {
|
||||
this._deletedBans = [];
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
_evtBannedPostDeleted(e) {
|
||||
this._deletedBans.push(e.detail.bannedPost);
|
||||
}
|
||||
}
|
||||
|
||||
BannedPostList._itemClass = BannedPost;
|
||||
BannedPostList._itemName = "bannedPost";
|
||||
|
||||
module.exports = BannedPostList;
|
|
@ -334,6 +334,24 @@ class Post extends events.EventTarget {
|
|||
});
|
||||
}
|
||||
|
||||
ban() {
|
||||
return api
|
||||
.post(uri.formatApiLink("post-ban"), {
|
||||
post_id: this.id,
|
||||
version: this._version
|
||||
})
|
||||
.then((response) => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("ban", {
|
||||
detail: {
|
||||
post: this
|
||||
}
|
||||
})
|
||||
);
|
||||
return Promise.resolve();
|
||||
})
|
||||
}
|
||||
|
||||
delete() {
|
||||
return api
|
||||
.delete(uri.formatApiLink("post", this.id), {
|
||||
|
|
|
@ -90,6 +90,7 @@ function _makeTopNavigation() {
|
|||
ret.add("login", new TopNavigationItem("L", "Log in", "login"));
|
||||
ret.add("logout", new TopNavigationItem("O", "Logout", "logout"));
|
||||
ret.add("help", new TopNavigationItem("E", "Help", "help"));
|
||||
ret.add("banned-posts", new TopNavigationItem("B", "Banned posts", "banned-posts"));
|
||||
ret.add(
|
||||
"settings",
|
||||
new TopNavigationItem(null, "<i class='fa fa-cog'></i>", "settings")
|
||||
|
|
103
client/js/views/banned_posts_view.js
Normal file
103
client/js/views/banned_posts_view.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
"use strict";
|
||||
|
||||
const events = require("../events.js");
|
||||
const views = require("../util/views.js");
|
||||
const BannedPost = require("../models/banned_post.js");
|
||||
|
||||
const template = views.getTemplate("banned-post-list");
|
||||
const rowTemplate = views.getTemplate("banned-post-entry");
|
||||
|
||||
class BannedPostsView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
this._ctx = ctx;
|
||||
this._hostNode = document.getElementById("content-holder");
|
||||
|
||||
views.replaceContent(this._hostNode, template(ctx));
|
||||
views.syncScrollPosition();
|
||||
views.decorateValidator(this._formNode);
|
||||
|
||||
const bannedPostsToAdd = Array.from(ctx.bannedPosts);
|
||||
for (let bannedPost of bannedPostsToAdd) {
|
||||
this._addBannedPostRowNode(bannedPost);
|
||||
}
|
||||
|
||||
ctx.bannedPosts.addEventListener("remove", (e) =>
|
||||
this._evtBannedPostDeleted(e)
|
||||
);
|
||||
|
||||
this._formNode.addEventListener("submit", (e) =>
|
||||
this._evtSaveButtonClick(e, ctx)
|
||||
);
|
||||
}
|
||||
|
||||
enableForm() {
|
||||
views.enableForm(this._formNode);
|
||||
}
|
||||
|
||||
disableForm() {
|
||||
views.disableForm(this._formNode);
|
||||
}
|
||||
|
||||
clearMessages() {
|
||||
views.clearMessages(this._hostNode);
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
views.showSuccess(this._hostNode, message);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
views.showError(this._hostNode, message);
|
||||
}
|
||||
|
||||
get _formNode() {
|
||||
return this._hostNode.querySelector("form");
|
||||
}
|
||||
|
||||
get _tableBodyNode() {
|
||||
return this._hostNode.querySelector("tbody");
|
||||
}
|
||||
|
||||
_addBannedPostRowNode(bannedPost) {
|
||||
const rowNode = rowTemplate(
|
||||
Object.assign({}, this._ctx, { postBan: bannedPost })
|
||||
);
|
||||
|
||||
const removeLinkNode = rowNode.querySelector(".remove a");
|
||||
if (removeLinkNode) {
|
||||
removeLinkNode.addEventListener("click", (e) =>
|
||||
this._evtDeleteButtonClick(e, rowNode)
|
||||
);
|
||||
}
|
||||
|
||||
this._tableBodyNode.appendChild(rowNode);
|
||||
|
||||
rowNode._bannedPost = bannedPost;
|
||||
bannedPost._rowNode = rowNode;
|
||||
}
|
||||
|
||||
_removeBannedPostRowNode(bannedPost) {
|
||||
const rowNode = bannedPost._rowNode;
|
||||
rowNode.parentNode.removeChild(rowNode);
|
||||
}
|
||||
|
||||
_evtBannedPostDeleted(e) {
|
||||
this._removeBannedPostRowNode(e.detail.bannedPost);
|
||||
}
|
||||
|
||||
_evtDeleteButtonClick(e, rowNode, link) {
|
||||
e.preventDefault();
|
||||
if (e.target.classList.contains("inactive")) {
|
||||
return;
|
||||
}
|
||||
this._ctx.bannedPosts.remove(rowNode._bannedPost);
|
||||
}
|
||||
|
||||
_evtSaveButtonClick(e, ctx) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent("submit"));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BannedPostsView;
|
98
doc/API.md
98
doc/API.md
|
@ -39,6 +39,7 @@
|
|||
- [Getting post](#getting-post)
|
||||
- [Getting around post](#getting-around-post)
|
||||
- [Deleting post](#deleting-post)
|
||||
- [Banning post](#banning-post)
|
||||
- [Merging posts](#merging-posts)
|
||||
- [Rating post](#rating-post)
|
||||
- [Adding post to favorites](#adding-post-to-favorites)
|
||||
|
@ -911,7 +912,7 @@ data.
|
|||
|
||||
- **Output**
|
||||
|
||||
A [post resource](#post).
|
||||
A [post resource](#banned-post).
|
||||
|
||||
- **Errors**
|
||||
|
||||
|
@ -1004,6 +1005,81 @@ data.
|
|||
|
||||
Deletes existing post. Related posts and tags are kept.
|
||||
|
||||
|
||||
|
||||
## Listing banned posts
|
||||
- **Request**
|
||||
|
||||
`GET /post-ban`
|
||||
|
||||
- **Output**
|
||||
|
||||
An [unpaged search result](#unpaged-search-result) of [banned posts](#postban).
|
||||
|
||||
- **Errors**
|
||||
|
||||
- the post does not exist
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Retrieves information about an existing post.
|
||||
|
||||
## Banning post
|
||||
- **Request**
|
||||
|
||||
`POST /post-ban`
|
||||
|
||||
- **Input**
|
||||
|
||||
```json5
|
||||
{
|
||||
"post_id": <post id>
|
||||
"version": <version>
|
||||
}
|
||||
```
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
{}
|
||||
```
|
||||
|
||||
- **Errors**
|
||||
|
||||
- the version is outdated
|
||||
- the post does not exist
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Deletes existing post, then adds the sha1sum of the post file to a ban list.
|
||||
Users will not be able to upload the file again.
|
||||
|
||||
Related posts and tags are kept.
|
||||
|
||||
|
||||
## Undoing post ban
|
||||
- **Request**
|
||||
|
||||
`DELETE /post-ban/<image_hash>`
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
{}
|
||||
```
|
||||
|
||||
- **Errors**
|
||||
|
||||
- there is no banned image with that hash
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Removes a banned image from the ban list. Takes a SHA-1 hash of the image as input.
|
||||
|
||||
|
||||
## Merging posts
|
||||
- **Request**
|
||||
|
||||
|
@ -2582,6 +2658,26 @@ An ordered list of posts, with a description and category.
|
|||
A [pool resource](#pool) stripped down to `id`, `names`, `category`,
|
||||
`description` and `postCount` fields.
|
||||
|
||||
|
||||
## Banned post
|
||||
**Description**
|
||||
|
||||
A record of a post that has been banned.
|
||||
|
||||
**Structure**
|
||||
|
||||
```json5
|
||||
{
|
||||
"checksum": <sha-hash>,
|
||||
"time": <time-of-ban>
|
||||
}
|
||||
```
|
||||
|
||||
**Field meaning**
|
||||
- `<sha-hash>`: SHA-1 hash of an image that has been banned
|
||||
- `<time-of-ban>`: time the post was banned
|
||||
|
||||
|
||||
## Comment
|
||||
**Description**
|
||||
|
||||
|
|
|
@ -116,6 +116,9 @@ privileges:
|
|||
'posts:bulk-edit:tags': power
|
||||
'posts:bulk-edit:safety': power
|
||||
'posts:bulk-edit:delete': power
|
||||
'posts:ban:create': moderator
|
||||
'posts:ban:delete': moderator
|
||||
'posts:ban:list': moderator
|
||||
|
||||
'tags:create': regular
|
||||
'tags:edit:names': power
|
||||
|
|
|
@ -10,3 +10,4 @@ import szurubooru.api.tag_category_api
|
|||
import szurubooru.api.upload_api
|
||||
import szurubooru.api.user_api
|
||||
import szurubooru.api.user_token_api
|
||||
import szurubooru.api.ban_api
|
58
server/szurubooru/api/ban_api.py
Normal file
58
server/szurubooru/api/ban_api.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from szurubooru.api import post_api
|
||||
from szurubooru.func import posts
|
||||
from szurubooru.model.bans import PostBan
|
||||
|
||||
from szurubooru import db, errors, model, rest, search
|
||||
from szurubooru.func import (
|
||||
auth,
|
||||
bans,
|
||||
serialization,
|
||||
snapshots,
|
||||
versions,
|
||||
)
|
||||
|
||||
def _get_ban_by_hash(hash: str) -> Optional[PostBan]:
|
||||
try:
|
||||
return bans.get_bans_by_hash(hash)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
_search_executor = search.Executor(search.configs.BanSearchConfig())
|
||||
|
||||
|
||||
def _serialize(ctx: rest.Context, ban: model.PostBan) -> rest.Response:
|
||||
return bans.serialize_ban(
|
||||
ban, options=serialization.get_serialization_options(ctx)
|
||||
)
|
||||
|
||||
|
||||
@rest.routes.delete("/post-ban/(?P<image_hash>[^/]+)/?")
|
||||
def unban_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response:
|
||||
auth.verify_privilege(ctx.user, "posts:ban:delete")
|
||||
ban = _get_ban_by_hash(params["image_hash"])
|
||||
bans.delete(ban)
|
||||
ctx.session.commit()
|
||||
return {}
|
||||
|
||||
|
||||
@rest.routes.post("/post-ban/?")
|
||||
def ban_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response:
|
||||
auth.verify_privilege(ctx.user, "posts:ban:create")
|
||||
# post = post_api._get_post(params)
|
||||
post = posts.get_post_by_id(ctx.get_param_as_int("post_id"))
|
||||
versions.verify_version(post, ctx)
|
||||
posts.ban(bans.create_ban(post))
|
||||
snapshots.delete(post, ctx.user)
|
||||
posts.delete(post)
|
||||
ctx.session.commit()
|
||||
return {}
|
||||
|
||||
@rest.routes.get("/post-ban/?")
|
||||
def get_bans(ctx: rest.Context, _params: Dict[str, str] = {}) -> rest.Response:
|
||||
auth.verify_privilege(ctx.user, "posts:ban:list")
|
||||
return _search_executor.execute_and_serialize(
|
||||
ctx, lambda ban: _serialize(ctx, ban)
|
||||
)
|
68
server/szurubooru/func/bans.py
Normal file
68
server/szurubooru/func/bans.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
from datetime import datetime
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from szurubooru import db, errors, model, rest
|
||||
from szurubooru.func import (
|
||||
serialization,
|
||||
)
|
||||
|
||||
class PostBannedError(errors.ValidationError):
|
||||
def __init__(self, message: str = "This file was banned", extra_fields: Dict[str, str] = None) -> None:
|
||||
super().__init__(message, extra_fields)
|
||||
|
||||
|
||||
class HashNotBannedError(errors.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class BanSerializer(serialization.BaseSerializer):
|
||||
def __init__(self, ban: model.PostBan) -> None:
|
||||
self.ban = ban
|
||||
|
||||
def _serializers(self) -> Dict[str, Callable[[], Any]]:
|
||||
return {
|
||||
"checksum": self.serialize_checksum,
|
||||
"time": self.serialize_time
|
||||
}
|
||||
|
||||
def serialize_checksum(self) -> Any:
|
||||
return self.ban.checksum
|
||||
|
||||
def serialize_time(self) -> Any:
|
||||
return self.ban.time
|
||||
|
||||
|
||||
def create_ban(post: model.Post) -> model.PostBan:
|
||||
ban = model.PostBan()
|
||||
ban.checksum = post.checksum
|
||||
ban.time = datetime.utcnow()
|
||||
|
||||
db.session.add(ban)
|
||||
return ban
|
||||
|
||||
|
||||
def try_get_ban_by_checksum(checksum: str) -> Optional[model.PostBan]:
|
||||
return (
|
||||
db.session.query(model.PostBan)
|
||||
.filter(model.PostBan.checksum == checksum)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
|
||||
def get_bans_by_hash(hash: str) -> model.PostBan:
|
||||
ban = try_get_ban_by_checksum(hash)
|
||||
if ban is None:
|
||||
raise HashNotBannedError("Hash %s is not banned" % hash)
|
||||
return ban
|
||||
|
||||
|
||||
def delete(ban: model.PostBan) -> None:
|
||||
db.session.delete(ban)
|
||||
|
||||
|
||||
def serialize_ban(
|
||||
ban: model.PostBan, options: List[str] = []
|
||||
) -> Optional[rest.Response]:
|
||||
if not ban:
|
||||
return None
|
||||
return BanSerializer(ban).serialize(options)
|
|
@ -4,6 +4,7 @@ from datetime import datetime
|
|||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
import sqlalchemy as sa
|
||||
from szurubooru.func.bans import PostBannedError
|
||||
|
||||
from szurubooru import config, db, errors, model, rest
|
||||
from szurubooru.func import (
|
||||
|
@ -50,6 +51,7 @@ class PostAlreadyUploadedError(errors.ValidationError):
|
|||
)
|
||||
|
||||
|
||||
|
||||
class InvalidPostIdError(errors.ValidationError):
|
||||
pass
|
||||
|
||||
|
@ -634,6 +636,7 @@ def update_post_content(post: model.Post, content: Optional[bytes]) -> None:
|
|||
.filter(model.Post.post_id != post.post_id)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
if (
|
||||
other_post
|
||||
and other_post.post_id
|
||||
|
@ -641,6 +644,15 @@ def update_post_content(post: model.Post, content: Optional[bytes]) -> None:
|
|||
):
|
||||
raise PostAlreadyUploadedError(other_post)
|
||||
|
||||
|
||||
post_ban = (db.session.query(model.PostBan)
|
||||
.filter(model.PostBan.checksum == post.checksum)
|
||||
.one_or_none()
|
||||
)
|
||||
if (post_ban):
|
||||
raise PostBannedError()
|
||||
|
||||
|
||||
if update_signature:
|
||||
purge_post_signature(post)
|
||||
post.signature = generate_post_signature(post, content)
|
||||
|
@ -806,6 +818,11 @@ def delete(post: model.Post) -> None:
|
|||
db.session.delete(post)
|
||||
|
||||
|
||||
def ban(ban: model.PostBan) -> None:
|
||||
assert ban
|
||||
db.session.add(ban)
|
||||
|
||||
|
||||
def merge_posts(
|
||||
source_post: model.Post, target_post: model.Post, replace_content: bool
|
||||
) -> None:
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
'''
|
||||
create ban table
|
||||
|
||||
Revision ID: cc2956cb8ee7
|
||||
Created at: 2023-05-12 02:04:22.592006
|
||||
'''
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
|
||||
revision = 'cc2956cb8ee7'
|
||||
down_revision = 'adcd63ff76a2'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"post_ban",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("checksum", sa.Unicode(64), nullable=False),
|
||||
sa.Column("time", sa.DateTime, nullable=False),
|
||||
sa.PrimaryKeyConstraint("id")
|
||||
)
|
||||
op.create_unique_constraint("uq_ban_checksum", "post_ban", ["checksum"])
|
||||
|
||||
def downgrade():
|
||||
op.drop_table("post_ban")
|
|
@ -1,5 +1,6 @@
|
|||
import szurubooru.model.util
|
||||
from szurubooru.model.base import Base
|
||||
from szurubooru.model.bans import PostBan
|
||||
from szurubooru.model.comment import Comment, CommentScore
|
||||
from szurubooru.model.pool import Pool, PoolName, PoolPost
|
||||
from szurubooru.model.pool_category import PoolCategory
|
||||
|
|
12
server/szurubooru/model/bans.py
Normal file
12
server/szurubooru/model/bans.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from typing import List
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from szurubooru.model.base import Base
|
||||
|
||||
class PostBan(Base):
|
||||
__tablename__ = "post_ban"
|
||||
|
||||
ban_id = sa.Column("id", sa.Integer, primary_key=True)
|
||||
checksum = sa.Column("checksum", sa.Unicode(64), nullable=False)
|
||||
time = sa.Column("time", sa.DateTime, nullable=False)
|
|
@ -4,3 +4,4 @@ from .post_search_config import PostSearchConfig
|
|||
from .snapshot_search_config import SnapshotSearchConfig
|
||||
from .tag_search_config import TagSearchConfig
|
||||
from .user_search_config import UserSearchConfig
|
||||
from .ban_search_config import BanSearchConfig
|
67
server/szurubooru/search/configs/ban_search_config.py
Normal file
67
server/szurubooru/search/configs/ban_search_config.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
from typing import Dict, Tuple
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from szurubooru import db, model
|
||||
from szurubooru.func import util
|
||||
from szurubooru.search.configs import util as search_util
|
||||
from szurubooru.search.configs.base_search_config import (
|
||||
BaseSearchConfig,
|
||||
Filter,
|
||||
)
|
||||
from szurubooru.search.typing import SaColumn, SaQuery
|
||||
|
||||
|
||||
class BanSearchConfig(BaseSearchConfig):
|
||||
def create_filter_query(self, _disable_eager_loads: bool) -> SaQuery:
|
||||
return db.session.query(model.PostBan)
|
||||
|
||||
def create_count_query(self, _disable_eager_loads: bool) -> SaQuery:
|
||||
return db.session.query(model.PostBan)
|
||||
|
||||
def create_around_query(self) -> SaQuery:
|
||||
raise NotImplementedError()
|
||||
|
||||
def finalize_query(self, query: SaQuery) -> SaQuery:
|
||||
return query.order_by(model.PostBan.time.asc())
|
||||
|
||||
@property
|
||||
def anonymous_filter(self) -> Filter:
|
||||
return search_util.create_subquery_filter(
|
||||
model.PostBan.checksum,
|
||||
model.PostBan.time,
|
||||
search_util.create_str_filter,
|
||||
)
|
||||
|
||||
@property
|
||||
def named_filters(self) -> Dict[str, Filter]:
|
||||
return util.unalias_dict(
|
||||
[
|
||||
(
|
||||
["time"],
|
||||
search_util.create_date_filter(
|
||||
model.PostBan.time,
|
||||
),
|
||||
),
|
||||
(
|
||||
["checksum"],
|
||||
search_util.create_subquery_filter(
|
||||
model.PostBan.checksum,
|
||||
search_util.create_str_filter,
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]:
|
||||
return util.unalias_dict(
|
||||
[
|
||||
(
|
||||
["random"],
|
||||
(sa.sql.expression.func.random(), self.SORT_NONE),
|
||||
),
|
||||
(["checksum"], (model.PostBan.checksum, self.SORT_ASC)),
|
||||
(["time"], (model.PostBan.time, self.SORT_ASC))
|
||||
]
|
||||
)
|
Reference in a new issue