JS na rozbalování souborů + IP akce

Tento commit je obsažen v:
sneedmaster 2024-02-18 20:25:38 +01:00
rodič a453130797
revize eecc1a191c
23 změnil soubory, kde provedl 565 přidání a 221 odebrání

Zobrazit soubor

@ -195,6 +195,11 @@ impl Board {
update_overboard(ctx, boards).await?;
query("DELETE FROM bans WHERE board = $1")
.bind(&self.id)
.execute(ctx.db())
.await?;
query(&format!("DROP TABLE posts_{}", self.id))
.execute(ctx.db())
.await?;

Zobrazit soubor

@ -197,6 +197,27 @@ impl Post {
Ok(posts)
}
pub async fn read_ip_page(
ctx: &Ctx,
ip: IpAddr,
page: i64,
) -> Result<Vec<Self>, NekrochanError> {
let posts = query_as(
r#"SELECT * FROM overboard
WHERE ip = $1
ORDER BY created DESC
LIMIT $2
OFFSET $3"#,
)
.bind(ip)
.bind(GENERIC_PAGE_SIZE)
.bind((page - 1) * GENERIC_PAGE_SIZE)
.fetch_all(ctx.db())
.await?;
Ok(posts)
}
pub async fn read_replies(&self, ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
let replies = query_as(&format!(
"SELECT * FROM posts_{} WHERE thread = $1 ORDER BY sticky DESC, created ASC",

Zobrazit soubor

@ -63,6 +63,7 @@ async fn run() -> Result<(), Error> {
.service(web::board::board)
.service(web::board_catalog::board_catalog)
.service(web::index::index)
.service(web::ip_posts::ip_posts)
.service(web::edit_posts::edit_posts)
.service(web::login::login_get)
.service(web::login::login_post)

Zobrazit soubor

@ -15,6 +15,7 @@ pub enum Permissions {
BoardConfig,
News,
Jannytext,
ViewIPs,
BypassBans,
BypassBoardLock,
BypassThreadLock,
@ -83,6 +84,10 @@ impl PermissionWrapper {
self.0.contains(Permissions::Jannytext)
}
pub fn view_ips(&self) -> bool {
self.0.contains(Permissions::ViewIPs)
}
pub fn bypass_bans(&self) -> bool {
self.0.contains(Permissions::BypassBans)
}

Zobrazit soubor

@ -1,6 +1,7 @@
use actix_web::{post, web::Data, HttpRequest, HttpResponse};
use chrono::{Duration, Utc};
use ipnetwork::IpNetwork;
use redis::AsyncCommands;
use serde::Deserialize;
use std::{collections::HashSet, fmt::Write, net::IpAddr};
@ -26,6 +27,8 @@ pub struct StaffPostActionsForm {
pub remove_files: Option<String>,
#[serde(rename = "staff_toggle_spoiler")]
pub toggle_spoiler: Option<String>,
pub remove_by_ip_board: Option<String>,
pub remove_by_ip_global: Option<String>,
pub toggle_sticky: Option<String>,
pub toggle_lock: Option<String>,
pub remove_reports: Option<String>,
@ -36,6 +39,7 @@ pub struct StaffPostActionsForm {
pub ban_reason: Option<String>,
pub ban_duration: Option<u64>,
pub ban_range: Option<String>,
pub troll_user: Option<String>,
}
#[post("/actions/staff-post-actions")]
@ -57,6 +61,7 @@ pub async fn staff_post_actions(
let mut locks_toggled = 0;
let mut reports_removed = 0;
let mut bans_issued = 0;
let mut users_trolled = 0;
for post in &posts {
if (form.remove_posts.is_some()
@ -90,6 +95,30 @@ pub async fn staff_post_actions(
spoilers_toggled += post.files.0.len();
}
if form.remove_by_ip_board.is_some() {
let key = format!("by_ip:{}", post.ip);
let ip_posts: Vec<String> = ctx.cache().zrange(key, 0, -1).await?;
let board_ip_posts = ip_posts
.into_iter()
.filter(|p| p.starts_with(&format!("{}/", post.board)))
.collect::<Vec<_>>();
for post in get_posts_from_ids(&ctx, &board_ip_posts).await {
post.delete(&ctx).await?;
posts_removed += 1;
}
}
if form.remove_by_ip_global.is_some() {
let key = format!("by_ip:{}", post.ip);
let ip_posts: Vec<String> = ctx.cache().zrange(key, 0, -1).await?;
for post in get_posts_from_ids(&ctx, &ip_posts).await {
post.delete(&ctx).await?;
posts_removed += 1;
}
}
if form.toggle_sticky.is_some() {
post.update_sticky(&ctx).await?;
stickies_toggled += 1;
@ -99,8 +128,15 @@ pub async fn staff_post_actions(
post.update_lock(&ctx).await?;
locks_toggled += 1;
}
}
for post in &posts {
if form.remove_reports.is_some() {
if !(tcx.perms.owner() || tcx.perms.reports()) {
writeln!(&mut response, "[Chyba] Nemáš přístup k hlášením.").ok();
continue;
}
post.delete_reports(&ctx).await?;
reports_removed += post.reports.0.len();
}
@ -108,7 +144,7 @@ pub async fn staff_post_actions(
let mut already_banned = HashSet::new();
for post in posts {
for post in &posts {
if let (
(Some(_), None) | (None, Some(_)) | (Some(_), Some(_)),
Some(reason),
@ -132,6 +168,11 @@ pub async fn staff_post_actions(
}
if form.ban_reporters.is_some() {
if !(tcx.perms.owner() || tcx.perms.reports()) {
writeln!(&mut response, "[Chyba] Nemáš přístup k hlášením.").ok();
continue;
}
ips_to_ban.extend(
post.reports
.0
@ -178,6 +219,34 @@ pub async fn staff_post_actions(
}
}
for post in &posts {
if form.troll_user.is_some() {
if !(tcx.perms.owner() || tcx.perms.edit_posts()) {
writeln!(&mut response, "[Chyba] Nemáš oprávnění upravovat příspěvky.").ok();
continue;
}
if !(tcx.perms.owner() || tcx.perms.view_ips()) {
writeln!(&mut response, "[Chyba] Nemáš oprávnění zobrazovat IP adresy.").ok();
continue;
}
let content_nomarkup = format!(
"{}\n\n##({})##",
post.content_nomarkup, post.ip
);
let content = format!(
"{}\n\n<span class=\"jannytext\">({})</span>",
post.content, post.ip
);
post.update_content(&ctx, content, content_nomarkup).await?;
users_trolled += 1;
}
}
if posts_removed != 0 {
writeln!(
&mut response,
@ -226,6 +295,14 @@ pub async fn staff_post_actions(
.ok();
}
if users_trolled != 0 {
writeln!(
&mut response,
"[Úspěch] Vytroleni uživatelé: {users_trolled}"
)
.ok();
}
if bans_issued != 0 {
writeln!(&mut response, "[Úspěch] Uděleny bany: {bans_issued}").ok();
}

65
src/web/ip_posts.rs Normální soubor
Zobrazit soubor

@ -0,0 +1,65 @@
use actix_web::{
get,
web::{Data, Path, Query},
HttpRequest, HttpResponse,
};
use askama::Template;
use serde::Deserialize;
use std::{collections::HashMap, net::IpAddr};
use crate::{
ctx::Ctx,
db::models::{Board, Post},
error::NekrochanError,
filters,
web::{tcx::TemplateCtx, template_response},
};
#[derive(Deserialize)]
pub struct IpPostsQuery {
page: i64,
}
#[derive(Template)]
#[template(path = "ip-posts.html")]
struct IpPostsTemplate {
tcx: TemplateCtx,
ip: IpAddr,
boards: HashMap<String, Board>,
posts: Vec<Post>,
page: i64,
}
#[get("/ip-posts/{ip}")]
pub async fn ip_posts(
ctx: Data<Ctx>,
req: HttpRequest,
path: Path<IpAddr>,
query: Option<Query<IpPostsQuery>>,
) -> Result<HttpResponse, NekrochanError> {
let tcx = TemplateCtx::new(&ctx, &req).await?;
if !(tcx.perms.owner() || tcx.perms.view_ips()) {
return Err(NekrochanError::InsufficientPermissionError);
}
let ip = path.into_inner();
let boards = Board::read_all_map(&ctx).await?;
let page = query.map_or(1, |q| q.page);
if page <= 0 {
return Err(NekrochanError::InvalidPageError);
}
let posts = Post::read_ip_page(&ctx, ip, page).await?;
let template = IpPostsTemplate {
tcx,
ip,
boards,
posts,
page,
};
template_response(&template)
}

Zobrazit soubor

@ -14,6 +14,7 @@ pub mod overboard_catalog;
pub mod staff;
pub mod tcx;
pub mod thread;
pub mod ip_posts;
use self::tcx::TemplateCtx;
use crate::{ctx::Ctx, db::models::Ban, error::NekrochanError, filters};

Zobrazit soubor

@ -18,9 +18,10 @@ pub struct UpdatePermissionsForm {
reports: Option<String>,
bans: Option<String>,
banners: Option<String>,
board_config: Option<String>,
news: Option<String>,
jannytext: Option<String>,
board_config: Option<String>,
view_ips: Option<String>,
bypass_bans: Option<String>,
bypass_board_lock: Option<String>,
bypass_thread_lock: Option<String>,
@ -91,6 +92,10 @@ pub async fn update_permissions(
permissions |= Permissions::Jannytext;
}
if form.view_ips.is_some() {
permissions |= Permissions::ViewIPs;
}
if form.bypass_bans.is_some() {
permissions |= Permissions::BypassBans;
}

86
static/js/expand-image.js Normální soubor
Zobrazit soubor

@ -0,0 +1,86 @@
$(function () {
$(".expandable").click(function () {
let src_link = $(this).attr("href");
let is_video = [".mpeg", ".mov", ".mp4", ".webm", ".mkv", ".ogg"].some(
(ext) => src_link.endsWith(ext)
);
if (!is_video) {
toggle_image($(this), src_link);
} else {
toggle_video($(this), src_link);
}
$(this).toggleClass("expanded");
return false;
});
});
function toggle_image(parent, src_link) {
let expanded = parent.hasClass("expanded");
let thumb = parent.find(".thumb");
let src = parent.find(".src");
if (!expanded) {
if (src.length === 0) {
thumb.css("opacity", 0.5);
parent.append(`<img class="src" src="${src_link}">`);
let src = parent.find(".src");
src.hide();
src.on("load", function () {
thumb.hide();
thumb.css("opacity", "");
src.show();
});
} else {
thumb.hide();
src.show();
}
} else {
src.hide();
thumb.show();
}
}
function toggle_video(parent, src_link) {
let expanded = parent.hasClass("expanded");
let thumb = parent.find(".thumb");
let src = parent.parent().find(".src");
if (!expanded) {
if (src.length === 0) {
thumb.css("opacity", 0.5);
parent.append('<span class="closer">[Zavřít]<br></span>')
parent.parent().append(
`<video class="src" src="${src_link}" autoplay="" controls="" loop=""></video>`
);
let src = parent.parent().find(".src");
src.hide();
src.on("loadstart", function () {
thumb.hide();
thumb.css("opacity", "");
src.show();
});
} else {
thumb.hide();
src.show();
src.get(0).play();
parent.find(".closer").show();
}
} else {
src.get(0).pause();
src.hide();
thumb.show();
parent.find(".closer").hide();
}
}

2
static/js/jquery.min.js vendorováno Normální soubor

Rozdílový obsah nebyl zobrazen, protože některé řádky jsou příliš dlouhá

Zobrazit soubor

@ -29,6 +29,7 @@ details:last-of-type {
img,
video {
max-width: 100%;
max-height: 90vh;
}
@ -300,6 +301,7 @@ summary {
}
.post-header input[type="checkbox"] {
vertical-align: middle;
margin: 0;
}

Zobrazit soubor

@ -9,10 +9,10 @@
<meta name="description" content="{{ tcx.cfg.site.description }}">
<link rel="stylesheet" href='/static/themes/{% block theme %}{% include "../theme.txt" %}{% endblock %}'>
<link rel="stylesheet" href="/static/style.css">
<!-- Muh flash of unstyled content -->
<script>0</script>
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/expand-image.js"></script>
</head>
<body>
<body id="top">
<div class="board-links header">
<span class="link-group"><a href="/">domov</a></span>
{% call board_links::board_links() %}
@ -35,7 +35,7 @@
</div>
<div class="main">
{% block content %}{% endblock %}
<div class="footer">
<div id="bottom" class="footer">
<div class="box inline-block">
<a href="https://git.nekrofilie.com/sneedmaster/nekrochan">nekrochan</a> - Projekt <a href="https://nekrofilie.com/">Nekrofilie</a>
<br>

Zobrazit soubor

@ -21,7 +21,9 @@
<div class="box">
<h2 class="headline">
<span>{{ news.title }}</span>
<span class="float-r">{{ news.author }} - {{ news.created|czech_datetime }}</span>
<span class="float-r">
{{ news.author }} - <span title="{{ news.created|czech_humantime }}">{{ news.created|czech_datetime }}</span>
</span>
</h2>
<hr>
<div class="post-content">{{ news.content|safe }}</div>

26
templates/ip-posts.html Normální soubor
Zobrazit soubor

@ -0,0 +1,26 @@
{% import "./macros/post-actions.html" as post_actions %}
{% import "./macros/post.html" as post %}
{% import "./macros/static-pagination.html" as static_pagination %}
{% extends "base.html" %}
{% block title %}Příspěvky od [{{ ip }}]{% endblock %}
{% block content %}
<h1 class="title">Příspěvky od [{{ ip }}]</h1>
<hr>
<form method="post">
<div class="thread">
{% for post in posts %}
<b>Příspěvek z <a href="/boards/{{ post.board }}">/{{ post.board }}/</a></b>
<br>
{% call post::post(boards[post.board.as_str()], post, true) %}
<br>
{% endfor %}
</div>
<hr>
{% call static_pagination::static_pagination("/ip-posts/{}"|format(ip), page) %}
<hr>
{% call post_actions::post_actions() %}
</form>
{% endblock %}

Zobrazit soubor

@ -67,7 +67,7 @@
</table>
</details>
<br>
{% if tcx.perms.owner() || tcx.perms.manage_posts() || tcx.perms.bans() || tcx.perms.edit_posts() %}
{% if tcx.perms.owner() || tcx.perms.edit_posts() || tcx.perms.manage_posts() || tcx.perms.reports() || tcx.perms.bans() %}
<details>
<summary class="box">Uklízečské akce</summary>
<table class="form-table">
@ -96,6 +96,22 @@
</div>
</td>
</tr>
<tr>
<td class="label">Odstranit od IP na nástěnce</td>
<td>
<div class="input-wrapper">
<input name="remove_by_ip_board" type="checkbox">
</div>
</td>
</tr>
<tr>
<td class="label">Odstranit od IP globálně</td>
<td>
<div class="input-wrapper">
<input name="remove_by_ip_global" type="checkbox">
</div>
</td>
</tr>
<tr>
<td class="label">Připnout/odepnout</td>
<td>
@ -177,6 +193,14 @@
</td>
</tr>
{% endif %}
{% if tcx.perms.owner() || (tcx.perms.edit_posts() && tcx.perms.view_ips()) %}
<td class="label">Vytrolit uživatele</td>
<td>
<div class="input-wrapper">
<input name="troll_user" type="checkbox">
</div>
</td>
{% endif %}
<tr>
<td colspan="2">
<input

Zobrazit soubor

@ -2,6 +2,9 @@
<div id="{{ post.id }}" class="post{% if boxed %} box{% endif %}">
<div class="post-header">
<input name="posts[]" type="checkbox" value="{{ post.board }}/{{ post.id }}">&#32;
{% if tcx.perms.owner() || tcx.perms.view_ips() %}
<b><a title="Více příspěvků od uživatele [{{ post.ip }}]" href="/ip-posts/{{ post.ip }}">[+]</a></b>&#32;
{% endif %}
{% if let Some(email) = post.email %}
<a class="name" rel="nofollow" href="mailto:{{ email }}">{{ post.name }}</a>&#32;
{% else %}
@ -11,14 +14,14 @@
<span class="tripcode">{{ tripcode }}</span>&#32;
{% endif %}
{% if let Some(capcode) = post.capcode %}
<span class="capcode">## {{ capcode }} <img class="icon" src="/favicon.ico"></span>&#32;
<span title="Tento uživatel to dělá ZDARMA!" class="capcode">## {{ capcode }} <img class="icon" src="/favicon.ico"></span>&#32;
{% endif %}
{% if tcx.ip == post.ip %}
{# Technically not a tripcode or something but same styling #}
<i class="tripcode">(Ty)</i>&#32;
{% endif %}
{% if board.config.0.flags %}
<img class="icon" src="/static/flags/{{ post.country }}.png">&#32;
<img title="Země: {{ post.country }}" class="icon" src="/static/flags/{{ post.country }}.png">&#32;
{% endif %}
<span title="{{ post.created|czech_datetime }}">{{ post.created|czech_humantime }}</span>&#32;
{% if board.config.0.user_ids %}
@ -26,10 +29,10 @@
{% endif %}
<a class="post-number" href="{{ post.post_url() }}">Č.{{ post.id }}</a>&#32;
{% if post.sticky %}
<img class="icon" src="/static/icons/sticky.png">&#32;
<img title="Připnuto" class="icon" src="/static/icons/sticky.png">&#32;
{% endif %}
{% if post.locked %}
<img class="icon" src="/static/icons/locked.png">&#32;
<img title="Uzamčeno" class="icon" src="/static/icons/locked.png">&#32;
{% endif %}
{% if !boxed %}
@ -40,7 +43,7 @@
<div class="post-files{% if post.files.0.len() > 1 %} multi-files{% endif %}">
{% for file in post.files.0 %}
<div class="post-file">
<a download="{{ file.original_name }}" href="{{ file.file_url() }}">
<a title="Stáhnout ({{ file.original_name }})" download="{{ file.original_name }}" href="{{ file.file_url() }}">
{% if file.spoiler %}
[Spoiler]
{% else %}
@ -50,7 +53,7 @@
<br>
({{ file.size|filesizeformat }}, {{ file.width }}x{{ file.height }})
<br>
<a target="_blank" href="{{ file.file_url() }}">
<a class="expandable" target="_blank" href="{{ file.file_url() }}">
<img class="thumb" src="{{ file.thumb_url() }}">
</a>
</div>

Zobrazit soubor

@ -9,7 +9,9 @@
<div class="news box">
<h2 class="headline">
<span>{{ newspost.title }}</span>
<span class="float-r">{{ newspost.author }} - {{ newspost.created|czech_datetime }}</span>
<span class="float-r">
{{ newspost.author }} - <span title="{{ newspost.created|czech_humantime }}">{{ newspost.created|czech_datetime }}</span>
</span>
</h2>
<hr>
<div class="post-content">{{ newspost.content|safe }}</div>

Zobrazit soubor

@ -24,7 +24,7 @@
<td><input name="accounts[]" type="checkbox" value="{{ account.username }}" {% if !tcx.perms.owner() %}disabled=""{% endif %}></td>
<td>{{ account.username }}</td>
<td>{% if account.owner %}Ano{% else %}Ne{% endif %}</td>
<td>{{ account.created|czech_datetime }}</td>
<td title="{{ account.created|czech_humantime }}">{{ account.created|czech_datetime }}</td>
<td>{{ account.permissions.0 }} <a href="/staff/permissions/{{ account.username }}">[Zobrazit]</a></td>
</tr>
{% endfor %}

Zobrazit soubor

@ -38,8 +38,12 @@
<td>{{ ban.issued_by }}</td>
<td>{% if ban.appealable %}Ano{% else %}Ne{% endif %}</td>
<td>{% if let Some(appeal) = ban.appeal %}<div class="post-content">{{ appeal }}</div>{% else %}-{% endif %}</td>
<td>{{ ban.created|czech_datetime }}</td>
<td>{% if let Some(expires) = ban.expires %}<span title="{{ expires|czech_datetime }}">{{ expires|czech_humantime }}</span>{% else %}<i>Nikdy</i>{% endif %}</td>
<td title="ban.{{ ban.created|czech_humantime }}">{{ ban.created|czech_datetime }}</td>
{% if let Some(expires) = ban.expires %}
<td title="{{ expires|czech_humantime }}">{{ expires|czech_datetime }}</td>
{% else %}
<td>Nikdy</td>
{% endif %}
</tr>
{% endfor %}
</table>

Zobrazit soubor

@ -26,7 +26,7 @@
<td>/{{ board.id }}/</td>
<td>{{ board.name }}</td>
<td>{{ board.description }}</td>
<td>{{ board.created|czech_datetime }}</td>
<td title="{{ board.created|czech_humantime }}">{{ board.created|czech_datetime }}</td>
<td>{% if tcx.perms.owner() || tcx.perms.board_config() %}<a href="/staff/board-config/{{ board.id }}">[Zobrazit]</a>{% else %}-{% endif %}</td>
</tr>
{% endfor %}

Zobrazit soubor

@ -11,7 +11,9 @@
<div class="box">
<h2 class="headline">
<span>{{ newspost.title }}</span>
<span class="float-r">{{ newspost.author }} - {{ newspost.created|czech_datetime }}</span>
<span class="float-r">
{{ newspost.author }} - <span title="{{ newspost.created|czech_humantime }}">{{ newspost.created|czech_datetime }}</span>
</span>
</h2>
<hr>
<textarea class="edit-box" name="{{ newspost.id }}">{{ newspost.content_nomarkup }}</textarea>

Zobrazit soubor

@ -115,6 +115,15 @@
</td>
</tr>
<tr>
<td class="label">Zobrazit IP adresy</td>
<td>
<div class="input-wrapper">
<input name="view_ips" type="checkbox"{% if account.perms().view_ips() %} checked="checked"{% endif %}{% if !tcx.perms.owner() %} disabled=""{% endif %}>
</div>
</td>
</tr>
<tr>
<td class="label">Obejít ban</td>
<td>

Zobrazit soubor

@ -16,6 +16,8 @@
<hr>
<form method="post">
{% for post in posts %}
<b>Příspěvek z <a href="/boards/{{ post.board }}">/{{ post.board }}/</a></b>
<br>
{% call post::post(boards[post.board.as_str()], post, true) %}
<table class="data-table inline-block m-0">
<tr>