Compare commits

..

No commits in common. "master" and "2.5" have entirely different histories.
master ... 2.5

91 changed files with 609 additions and 1206 deletions

5
.gitattributes vendored
View file

@ -1,5 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto
# Shell scripts require LF
*.sh text eol=lf

View file

@ -1,108 +0,0 @@
name: Build Docker containers
on:
push:
branches:
- master
jobs:
build-client:
name: Build and push client/ Docker container
runs-on: ubuntu-latest
steps:
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Determine metadata
run: |
CLOSEST_VER="$(git describe --tags --abbrev=0 $GITHUB_SHA)"
CLOSEST_MAJOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f1)"
CLOSEST_MINOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f2)"
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c1-8)
BUILD_INFO="v${CLOSEST_VER}-${SHORT_COMMIT}"
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
echo "major_tag=${CLOSEST_MAJOR_VER}" >> $GITHUB_ENV
echo "minor_tag=${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}" >> $GITHUB_ENV
echo "build_info=${BUILD_INFO}" >> $GITHUB_ENV
echo "build_date=${BUILD_DATE}" >> $GITHUB_ENV
echo "Build Info: ${BUILD_INFO}"
echo "Build Date: ${BUILD_DATE}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build container
run: >
docker buildx build --push
--platform linux/amd64,linux/arm/v7,linux/arm64/v8
--build-arg BUILD_INFO=${{ env.build_info }}
--build-arg BUILD_DATE=${{ env.build_date }}
--build-arg SOURCE_COMMIT=$GITHUB_SHA
--build-arg DOCKER_REPO=szurubooru/client
-t "szurubooru/client:latest"
-t "szurubooru/client:${{ env.major_tag }}"
-t "szurubooru/client:${{ env.minor_tag }}"
./client
build-server:
name: Build and push server/ Docker container
runs-on: ubuntu-latest
steps:
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Determine metadata
run: |
CLOSEST_VER="$(git describe --tags --abbrev=0 $GITHUB_SHA)"
CLOSEST_MAJOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f1)"
CLOSEST_MINOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f2)"
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c1-8)
BUILD_INFO="v${CLOSEST_VER}-${SHORT_COMMIT}"
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
echo "major_tag=${CLOSEST_MAJOR_VER}" >> $GITHUB_ENV
echo "minor_tag=${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}" >> $GITHUB_ENV
echo "build_info=${BUILD_INFO}" >> $GITHUB_ENV
echo "build_date=${BUILD_DATE}" >> $GITHUB_ENV
echo "Build Info: ${BUILD_INFO}"
echo "Build Date: ${BUILD_DATE}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build container
run: >
docker buildx build --push
--platform linux/amd64,linux/arm/v7,linux/arm64/v8
--build-arg BUILD_DATE=${{ env.build_date }}
--build-arg SOURCE_COMMIT=$GITHUB_SHA
--build-arg DOCKER_REPO=szurubooru/server
-t "szurubooru/server:latest"
-t "szurubooru/server:${{ env.major_tag }}"
-t "szurubooru/server:${{ env.minor_tag }}"
./server

View file

@ -1,28 +0,0 @@
name: Run unit tests
on: [push, pull_request]
jobs:
test-server:
name: Run pytest for server/
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build test container
run: >
docker buildx build --load
--platform linux/amd64 --target testing
-t test_container
./server
- name: Run unit tests
run: >
docker run --rm -t test_container
--color=no
--cov-report=term-missing:skip-covered
--cov=szurubooru
szurubooru/

3
.gitignore vendored
View file

@ -13,6 +13,3 @@ server/**/lib/
server/**/bin/ server/**/bin/
server/**/pyvenv.cfg server/**/pyvenv.cfg
__pycache__/ __pycache__/
data/
sql/

View file

@ -1,29 +1,28 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v3.2.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-yaml - id: check-yaml
- id: mixed-line-ending - id: mixed-line-ending
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.4.2 rev: v1.1.9
hooks: hooks:
- id: remove-tabs - id: remove-tabs
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: '23.1.0' rev: 20.8b1
hooks: hooks:
- id: black - id: black
files: 'server/' files: 'server/'
types: [python] types: [python]
language_version: python3.9 language_version: python3.8
- repo: https://github.com/PyCQA/isort - repo: https://github.com/timothycrosley/isort
rev: '5.12.0' rev: '5.4.2'
hooks: hooks:
- id: isort - id: isort
files: 'server/' files: 'server/'
@ -32,8 +31,8 @@ repos:
additional_dependencies: additional_dependencies:
- toml - toml
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/prettier/prettier
rev: v2.7.1 rev: '2.1.1'
hooks: hooks:
- id: prettier - id: prettier
files: client/js/ files: client/js/
@ -41,7 +40,7 @@ repos:
args: ['--config', 'client/.prettierrc.yml'] args: ['--config', 'client/.prettierrc.yml']
- repo: https://github.com/pre-commit/mirrors-eslint - repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.33.0 rev: v7.8.0
hooks: hooks:
- id: eslint - id: eslint
files: client/js/ files: client/js/
@ -49,8 +48,8 @@ repos:
additional_dependencies: additional_dependencies:
- eslint-config-prettier - eslint-config-prettier
- repo: https://github.com/PyCQA/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: '6.0.0' rev: '3.8.3'
hooks: hooks:
- id: flake8 - id: flake8
files: server/szurubooru/ files: server/szurubooru/
@ -58,5 +57,44 @@ repos:
- flake8-print - flake8-print
args: ['--config=server/.flake8'] args: ['--config=server/.flake8']
- repo: local
hooks:
- id: docker-build-client
name: Docker - build client
entry: bash -c 'docker build client/'
language: system
types: [file]
files: client/
pass_filenames: false
- id: docker-build-server
name: Docker - build server
entry: bash -c 'docker build server/'
language: system
types: [file]
files: server/
pass_filenames: false
- id: pytest
name: pytest
entry: bash -c 'docker run --rm -t $(docker build --target testing -q server/) szurubooru/'
language: system
types: [python]
files: server/szurubooru/
exclude: server/szurubooru/migrations/
pass_filenames: false
stages: [push]
- id: pytest-cov
name: pytest
entry: bash -c 'docker run --rm -t $(docker build --target testing -q server/) --cov-report=term-missing:skip-covered --cov=szurubooru szurubooru/'
language: system
types: [python]
files: server/szurubooru/
exclude: server/szurubooru/migrations/
pass_filenames: false
verbose: true
stages: [manual]
fail_fast: true fail_fast: true
exclude: LICENSE.md exclude: LICENSE.md

View file

@ -3,12 +3,12 @@
Szurubooru is an image board engine inspired by services such as Danbooru, Szurubooru is an image board engine inspired by services such as Danbooru,
Gelbooru and Moebooru dedicated for small and medium communities. Its name [has Gelbooru and Moebooru dedicated for small and medium communities. Its name [has
its roots in Polish language and has onomatopeic meaning of scraping or its roots in Polish language and has onomatopeic meaning of scraping or
scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*. scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
## Features ## Features
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations - Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
- Ability to retrieve web video content using [yt-dlp](https://github.com/yt-dlp/yt-dlp) - Ability to retrieve web video content using [youtube-dl](https://github.com/ytdl-org/youtube-dl)
- Post comments - Post comments
- Post notes / annotations, including arbitrary polygons - Post notes / annotations, including arbitrary polygons
- Rich JSON REST API ([see documentation](doc/API.md)) - Rich JSON REST API ([see documentation](doc/API.md))

View file

@ -1,7 +1,8 @@
FROM --platform=$BUILDPLATFORM node:lts as builder FROM node:lts as builder
WORKDIR /opt/app WORKDIR /opt/app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm install -g npm@lts
RUN npm install RUN npm install
COPY . ./ COPY . ./
@ -11,7 +12,7 @@ ARG CLIENT_BUILD_ARGS=""
RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS} RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS}
FROM --platform=$BUILDPLATFORM scratch as approot FROM scratch as approot
COPY docker-start.sh / COPY docker-start.sh /

View file

@ -61,4 +61,4 @@ $safety-sketchy = #F3D75F
$safety-unsafe = #F3985F $safety-unsafe = #F3985F
$scrollbar-thumb-color = $main-color $scrollbar-thumb-color = $main-color
$scrollbar-bg-color = $input-enabled-background-color $scrollbar-bg-color = $input-enabled-background-color
$transparency-grid-square-color = #F0F0F0 $transparency-grid-square-color = #F0F0F0

View file

@ -127,10 +127,6 @@ $comment-border-color = #DDD
color: mix($main-color, $inactive-link-color-darktheme) color: mix($main-color, $inactive-link-color-darktheme)
.comment-content .comment-content
p
word-wrap: normal
word-break: break-word
ul, ol ul, ol
list-style-position: inside list-style-position: inside
margin: 1em 0 margin: 1em 0

View file

@ -240,9 +240,6 @@ nav
background: $focused-tab-background-color-darktheme background: $focused-tab-background-color-darktheme
&#top-navigation &#top-navigation
background: $top-navigation-color-darktheme background: $top-navigation-color-darktheme
ul
#mobile-navigation-toggle
color: $text-color-darktheme
a .access-key a .access-key
text-decoration: underline text-decoration: underline
@ -278,7 +275,7 @@ a .access-key
background: darken($message-error-background-color, 60%) background: darken($message-error-background-color, 60%)
&.success &.success
border: 1px solid darken($message-success-border-color, 30%) border: 1px solid darken($message-success-border-color, 30%)
background: darken($message-success-background-color, 80%) background: darken($message-success-background-color, 60%)
.thumbnail .thumbnail
/*background-image: attr(data-src url)*/ /* not available yet */ /*background-image: attr(data-src url)*/ /* not available yet */

View file

@ -22,7 +22,7 @@
z-index: 1 z-index: 1
span span
position: relative position: relative
background: $window-color background: white
padding: 0 1em padding: 0 1em
z-index: 2 z-index: 2
@ -31,5 +31,3 @@
.page-header .page-header
&:before &:before
background: $top-navigation-color-darktheme background: $top-navigation-color-darktheme
span
background: $window-color-darktheme

View file

@ -114,29 +114,6 @@
&[data-disabled] &[data-disabled]
background: rgba(200, 200, 200, 0.7) background: rgba(200, 200, 200, 0.7)
.delete-flipper
display: inline-block
padding: 0.5em
box-sizing: border-box
border: 0
&:after
display: inline-block
width: 1em
height: 1em
text-align: center
line-height: 1em
font-size: 2.2em
&.delete
background: rgba(255, 0, 0, 0.7)
&:after
color: white
font-family: FontAwesome;
content: "\f1f8"; // fa-trash
&:not(.delete)
background: rgba(200, 200, 200, 0.7)
&:after
color: white
content: '-'
.thumbnail .thumbnail
width: 100% width: 100%
@ -187,9 +164,6 @@
vertical-align: top vertical-align: top
@media (max-width: 1000px) @media (max-width: 1000px)
display: block display: block
&.bulk-edit-tags:not(.opened), &.bulk-edit-safety:not(.opened)
float: left
margin-right: 1em
input input
margin-bottom: 0.25em margin-bottom: 0.25em
margin-right: 0.25em margin-right: 0.25em
@ -241,19 +215,7 @@
.append .append
@media (max-width: 1000px) @media (max-width: 1000px)
margin-left: 0 margin-left: 0
.bulk-edit-delete
&.opened
.start
@media (max-width: 1000px)
margin-left: 0
&:not(.opened)
.start
display: none
.append.open
@media (max-width: 1000px)
margin-left: 0
.start
margin-left: 1em
.safety .safety
margin-right: 0.25em margin-right: 0.25em
&.safety-safe &.safety-safe

View file

@ -15,53 +15,39 @@
border: 0 border: 0
outline: 0 outline: 0
>.sidebar>nav.buttons, >.content nav.buttons nav.buttons
margin-top: 0 margin-top: 0
display: flex display: flex
flex-wrap: wrap flex-wrap: wrap
article article
flex: 1 0 33% flex: 1 0 33%
a a
display: inline-block display: inline-block
width: 100% width: 100%
padding: 0.3em 0 padding: 0.3em 0
text-align: center
vertical-align: middle
transition: background 0.2s linear
&:not(.inactive):hover
background: lighten($main-color, 90%)
i
font-size: 140%
text-align: center text-align: center
vertical-align: middle @media (max-width: 800px)
transition: background 0.2s linear, box-shadow 0.2s linear margin-top: 2em
&:not(.inactive):hover
background: lighten($main-color, 90%)
i
font-size: 140%
text-align: center
@media (max-width: 800px)
margin-top: 0.6em
margin-bottom: 0.6em
>.content >.content
width: 100% width: 100%
.post-container .post-container
margin-bottom: 0.6em margin-bottom: 2em
.post-content .post-content
margin: 0 margin: 0
.after-mobile-controls
width: 100%
.darktheme .post-view
>.sidebar, >.content
nav.buttons
article
a:not(.inactive):hover
background: unset
box-shadow: inset 0 0 0 0.3em $main-color
@media (max-width: 800px) @media (max-width: 800px)
.post-view .post-view
flex-wrap: wrap flex-wrap: wrap
>.after-mobile-controls
order: 3
>.sidebar >.sidebar
order: 2 order: 2
min-width: 100% min-width: 100%
@ -119,6 +105,7 @@
h1 h1
margin-bottom: 0.5em margin-bottom: 0.5em
.thumbnail .thumbnail
background-position: 50% 30%
width: 4em width: 4em
height: 3em height: 3em
li li

View file

@ -13,12 +13,8 @@ $cancel-button-color = tomato
&.inactive input[type=submit], &.inactive input[type=submit],
&.inactive .skip-duplicates &.inactive .skip-duplicates
&.inactive .always-upload-similar
&.inactive .pause-remain-on-error
&.uploading input[type=submit], &.uploading input[type=submit],
&.uploading .skip-duplicates, &.uploading .skip-duplicates,
&.uploading .always-upload-similar
&.uploading .pause-remain-on-error
&:not(.uploading) .cancel &:not(.uploading) .cancel
display: none display: none
@ -43,12 +39,6 @@ $cancel-button-color = tomato
.skip-duplicates .skip-duplicates
margin-left: 1em margin-left: 1em
.always-upload-similar
margin-left: 1em
.pause-remain-on-error
margin-left: 1em
form>.messages form>.messages
margin-top: 1em margin-top: 1em
@ -62,14 +52,6 @@ $cancel-button-color = tomato
margin: 0 0 1.2em 0 margin: 0 0 1.2em 0
padding-left: 13em padding-left: 13em
img
width: 100%
height: 100%
video
width: 100%
height: 100%
&>.thumbnail-wrapper &>.thumbnail-wrapper
float: left float: left
width: 12em width: 12em

View file

@ -86,12 +86,6 @@ div.tag-input
font-size: 90% font-size: 90%
unselectable() unselectable()
@keyframes tag-added-to-post
from
max-height: 0
to
max-height: 5em
ul.compact-tags ul.compact-tags
width: 100% width: 100%
margin: 0.5em 0 0 0 margin: 0.5em 0 0 0
@ -109,30 +103,18 @@ ul.compact-tags
a:focus a:focus
outline: 0 outline: 0
box-shadow: inset 0 0 0 2px $main-color box-shadow: inset 0 0 0 2px $main-color
// these 3 added when tag is added to ul
&.added, &.new, &.implication
animation: tag-added-to-post 1s ease forwards
&.implication &.implication
background: $implied-tag-background-color
color: $implied-tag-text-color color: $implied-tag-text-color
background-color: $implied-tag-background-color
&.new &.new
background: $new-tag-background-color
color: $new-tag-text-color color: $new-tag-text-color
background-color: $new-tag-background-color
&.duplicate &.duplicate
background: $duplicate-tag-background-color
color: $duplicate-tag-text-color color: $duplicate-tag-text-color
background-color: $duplicate-tag-background-color
i i
padding-right: 0.4em padding-right: 0.4em
.darktheme ul.compact-tags
li
&.new
background-color: darken($new-tag-background-color, 80%)
&.implication
background-color: darken($implied-tag-background-color, 85%)
&.duplicate
background-color: darken($duplicate-tag-background-color, 80%)
div.tag-input, ul.compact-tags div.tag-input, ul.compact-tags
.tag-usages, .tag-weight, .remove-tag .tag-usages, .tag-weight, .remove-tag
color: $inactive-link-color color: $inactive-link-color
@ -152,8 +134,6 @@ div.tag-input, ul.compact-tags
background: $window-color-darktheme background: $window-color-darktheme
ul li:last-child ul li:last-child
border-bottom: 0.5em solid alpha($window-color-darktheme, 0) border-bottom: 0.5em solid alpha($window-color-darktheme, 0)
p
background: darken($tag-suggestions-header-color, 80%)
.append .append
color: $inactive-link-color-darktheme color: $inactive-link-color-darktheme
div.tag-input, ul.compact-tags div.tag-input, ul.compact-tags

View file

@ -21,11 +21,10 @@
.details .details
font-size: 90% font-size: 90%
line-height: 130% line-height: 130%
.image
margin: 0.25em 0.6em 0.25em 0
.thumbnail .thumbnail
width: 3em width: 3em
height: 3em height: 3em
margin: 0.25em 0.6em 0 0
.darktheme .user-list .darktheme .user-list
ul li ul li

16
client/hooks/build Executable file
View file

@ -0,0 +1,16 @@
#!/bin/sh
CLOSEST_VER=$(git describe --tags --abbrev=0 ${SOURCE_COMMIT})
if git describe --exact-match --abbrev=0 ${SOURCE_COMMIT} 2> /dev/null; then
BUILD_INFO="v${CLOSEST_VER}"
else
BUILD_INFO="v${CLOSEST_VER}-edge-$(git rev-parse --short ${SOURCE_COMMIT})"
fi
echo "Using BUILD_INFO=${BUILD_INFO}"
docker build \
--build-arg BUILD_INFO=${BUILD_INFO} \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg SOURCE_COMMIT \
--build-arg DOCKER_REPO \
-f $DOCKERFILE_PATH -t $IMAGE_NAME .

19
client/hooks/post_push Executable file
View file

@ -0,0 +1,19 @@
#!/bin/sh
add_tag() {
echo "Also tagging image as ${DOCKER_REPO}:${1}"
docker tag $IMAGE_NAME $DOCKER_REPO:$1
docker push $DOCKER_REPO:$1
}
CLOSEST_VER=$(git describe --tags --abbrev=0)
CLOSEST_MAJOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f1)
CLOSEST_MINOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f2)
add_tag "${CLOSEST_MAJOR_VER}-edge"
add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}-edge"
if git describe --exact-match --abbrev=0 2> /dev/null; then
add_tag "${CLOSEST_MAJOR_VER}"
add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}"
fi

View file

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset='utf-8'/> <meta charset='utf-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'> <meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'/>
<meta name='theme-color' content='#24aadd'/> <meta name='theme-color' content='#24aadd'/>
<meta name='apple-mobile-web-app-capable' content='yes'/> <meta name='apple-mobile-web-app-capable' content='yes'/>
<meta name='apple-mobile-web-app-status-bar-style' content='black'/> <meta name='apple-mobile-web-app-status-bar-style' content='black'/>

View file

@ -29,7 +29,6 @@
<span class='vim-nav-hint'>Next post &gt;</span> <span class='vim-nav-hint'>Next post &gt;</span>
</a> </a>
</article> </article>
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
<article class='edit-post'> <article class='edit-post'>
<% if (ctx.editMode) { %> <% if (ctx.editMode) { %>
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'> <a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
@ -37,13 +36,16 @@
<span class='vim-nav-hint'>Back to view mode</span> <span class='vim-nav-hint'>Back to view mode</span>
</a> </a>
<% } else { %> <% } else { %>
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'> <% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
<i class='fa fa-pencil'></i> <a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
<span class='vim-nav-hint'>Edit post</span> <% } else { %>
<a class='inactive'>
<% } %>
<i class='fa fa-pencil'></i>
<span class='vim-nav-hint'>Edit post</span>
</a> </a>
<% } %> <% } %>
</article> </article>
<% } %>
</nav> </nav>
<div class='sidebar-container'></div> <div class='sidebar-container'></div>
@ -52,16 +54,13 @@
<div class='content'> <div class='content'>
<div class='post-container'></div> <div class='post-container'></div>
<div class='after-mobile-controls'> <% if (ctx.canListComments) { %>
<div class='description'></div> <div class='comments-container'></div>
<% if (ctx.canCreateComments) { %> <% } %>
<h2>Add comment</h2>
<div class='comment-form-container'></div>
<% } %>
<% if (ctx.canListComments) { %> <% if (ctx.canCreateComments) { %>
<div class='comments-container'></div> <h2>Add comment</h2>
<% } %> <div class='comment-form-container'></div>
</div> <% } %>
</div> </div>
</div> </div>

View file

@ -42,7 +42,6 @@
'image/heic': 'HEIC', 'image/heic': 'HEIC',
'video/webm': 'WEBM', 'video/webm': 'WEBM',
'video/mp4': 'MPEG-4', 'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF', 'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] + }[ctx.post.mimeType] +
' (' + ' (' +

View file

@ -15,10 +15,9 @@
'image/heic': 'HEIC', 'image/heic': 'HEIC',
'video/webm': 'WEBM', 'video/webm': 'WEBM',
'video/mp4': 'MPEG-4', 'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF', 'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] %><!-- }[ctx.post.mimeType] %>
--></a> </a>
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>) (<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
<% if (ctx.post.flags.length) { %><!-- <% if (ctx.post.flags.length) { %><!--
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!-- --><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
@ -58,7 +57,7 @@
Search on Search on
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> &middot; <a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> &middot;
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> &middot; <a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> &middot;
<a href='https://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a> <a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
</section> </section>
<section class='social'> <section class='social'>
@ -99,10 +98,10 @@
--><% if (ctx.canListPosts) { %><!-- --><% if (ctx.canListPosts) { %><!--
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!-- --><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
--><% } %><!-- --><% } %><!--
--><%- ctx.getPrettyName(tag.names[0]) %><!-- --><%- ctx.getPrettyName(tag.names[0]) %>&#32;<!--
--><% if (ctx.canListPosts) { %><!-- --><% if (ctx.canListPosts) { %><!--
--></a><!-- --></a><!--
--><% } %>&#32;<!-- --><% } %><!--
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!-- --><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
--></li><!-- --></li><!--
--><% } %><!-- --><% } %><!--

View file

@ -7,28 +7,12 @@
<span class='skip-duplicates'> <span class='skip-duplicates'>
<%= ctx.makeCheckbox({ <%= ctx.makeCheckbox({
text: 'Skip duplicate', text: 'Skip duplicates',
name: 'skip-duplicates', name: 'skip-duplicates',
checked: false, checked: false,
}) %> }) %>
</span> </span>
<span class='always-upload-similar'>
<%= ctx.makeCheckbox({
text: 'Force upload similar',
name: 'always-upload-similar',
checked: false,
}) %>
</span>
<span class='pause-remain-on-error'>
<%= ctx.makeCheckbox({
text: 'Pause on error',
name: 'pause-remain-on-error',
checked: true,
}) %>
</span>
<input type='button' value='Cancel' class='cancel'/> <input type='button' value='Cancel' class='cancel'/>
</div> </div>

View file

@ -61,7 +61,6 @@
text: 'Upload anonymously', text: 'Upload anonymously',
name: 'anonymous', name: 'anonymous',
checked: ctx.uploadable.anonymous, checked: ctx.uploadable.anonymous,
readonly: ctx.uploadable.forceAnonymous,
}) %> }) %>
</div> </div>
<% } %> <% } %>

View file

@ -16,6 +16,7 @@
%><form class='horizontal bulk-edit bulk-edit-tags'><% %><form class='horizontal bulk-edit bulk-edit-tags'><%
%><span class='append hint'>Tagging with:</span><% %><span class='append hint'>Tagging with:</span><%
%><a href class='mousetrap button append open'>Mass tag</a><% %><a href class='mousetrap button append open'>Mass tag</a><%
%><wbr/><%
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><% %><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
%><input class='mousetrap start' type='submit' value='Start tagging'/><% %><input class='mousetrap start' type='submit' value='Start tagging'/><%
%><a href class='mousetrap button append close'>Stop tagging</a><% %><a href class='mousetrap button append close'>Stop tagging</a><%
@ -27,11 +28,4 @@
%><a href class='mousetrap button append close'>Stop editing safety</a><% %><a href class='mousetrap button append close'>Stop editing safety</a><%
%></form><% %></form><%
%><% } %><% %><% } %><%
%><% if (ctx.canBulkDelete) { %><%
%><form class='horizontal bulk-edit bulk-edit-delete'><%
%><a href class='mousetrap button append open'>Mass delete</a><%
%><input class='mousetrap start' type='submit' value='Delete selected posts'/><%
%><a href class='mousetrap button append close'>Stop deleting</a><%
%></form><%
%><% } %><%
%></div> %></div>

View file

@ -50,10 +50,6 @@
<% } %> <% } %>
</span> </span>
<% } %> <% } %>
<% if (ctx.canBulkDelete && ctx.parameters && ctx.parameters.delete) { %>
<a href class='delete-flipper'>
</a>
<% } %>
</span> </span>
</li> </li>
<% } %> <% } %>

View file

@ -17,7 +17,7 @@ class HomeController {
buildDate: config.meta.buildDate, buildDate: config.meta.buildDate,
canListSnapshots: api.hasPrivilege("snapshots:list"), canListSnapshots: api.hasPrivilege("snapshots:list"),
canListPosts: api.hasPrivilege("posts:list"), canListPosts: api.hasPrivilege("posts:list"),
isDevelopmentMode: config.environment == "development", isDevelopmentMode: config.environment == "development"
}); });
Info.get().then( Info.get().then(

View file

@ -91,16 +91,16 @@ class PoolController {
_evtUpdate(e) { _evtUpdate(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
if (e.detail.names !== undefined && e.detail.names !== null) { if (e.detail.names !== undefined) {
e.detail.pool.names = e.detail.names; e.detail.pool.names = e.detail.names;
} }
if (e.detail.category !== undefined && e.detail.category !== null) { if (e.detail.category !== undefined) {
e.detail.pool.category = e.detail.category; e.detail.pool.category = e.detail.category;
} }
if (e.detail.description !== undefined && e.detail.description !== null) { if (e.detail.description !== undefined) {
e.detail.pool.description = e.detail.description; e.detail.pool.description = e.detail.description;
} }
if (e.detail.posts !== undefined && e.detail.posts !== null) { if (e.detail.posts !== undefined) {
e.detail.pool.posts.clear(); e.detail.pool.posts.clear();
for (let postId of e.detail.posts) { for (let postId of e.detail.posts) {
e.detail.pool.posts.add( e.detail.pool.posts.add(

View file

@ -43,8 +43,6 @@ class PoolListController {
this._headerView.addEventListener( this._headerView.addEventListener(
"submit", "submit",
(e) => this._evtSubmit(e), (e) => this._evtSubmit(e),
);
this._headerView.addEventListener(
"navigate", "navigate",
(e) => this._evtNavigate(e) (e) => this._evtNavigate(e)
); );

View file

@ -44,7 +44,6 @@ class PostListController {
enableSafety: api.safetyEnabled(), enableSafety: api.safetyEnabled(),
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"), canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"), canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"),
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
bulkEdit: { bulkEdit: {
tags: this._bulkEditTags, tags: this._bulkEditTags,
}, },
@ -53,16 +52,6 @@ class PostListController {
this._evtNavigate(e) this._evtNavigate(e)
); );
if (this._headerView._bulkDeleteEditor) {
this._headerView._bulkDeleteEditor.addEventListener(
"deleteSelectedPosts",
(e) => {
this._evtDeleteSelectedPosts(e);
}
);
}
this._postsMarkedForDeletion = [];
this._syncPageController(); this._syncPageController();
} }
@ -102,38 +91,6 @@ class PostListController {
e.detail.post.save().catch((error) => window.alert(error.message)); e.detail.post.save().catch((error) => window.alert(error.message));
} }
_evtMarkForDeletion(e) {
const postId = e.detail;
// Add or remove post from delete list
if (e.detail.delete) {
this._postsMarkedForDeletion.push(e.detail.post);
} else {
this._postsMarkedForDeletion = this._postsMarkedForDeletion.filter(
(x) => x.id != e.detail.post.id
);
}
}
_evtDeleteSelectedPosts(e) {
if (this._postsMarkedForDeletion.length == 0) return;
if (
confirm(
`Are you sure you want to delete ${this._postsMarkedForDeletion.length} posts?`
)
) {
Promise.all(
this._postsMarkedForDeletion.map((post) => post.delete())
)
.catch((error) => window.alert(error.message))
.then(() => {
this._postsMarkedForDeletion = [];
this._headerView._navigate();
});
}
}
_syncPageController() { _syncPageController() {
this._pageController.run({ this._pageController.run({
parameters: this._ctx.parameters, parameters: this._ctx.parameters,
@ -160,10 +117,8 @@ class PostListController {
canBulkEditSafety: api.hasPrivilege( canBulkEditSafety: api.hasPrivilege(
"posts:bulk-edit:safety" "posts:bulk-edit:safety"
), ),
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
bulkEdit: { bulkEdit: {
tags: this._bulkEditTags, tags: this._bulkEditTags,
markedForDeletion: this._postsMarkedForDeletion,
}, },
postFlow: settings.get().postFlow, postFlow: settings.get().postFlow,
}); });
@ -173,9 +128,6 @@ class PostListController {
view.addEventListener("changeSafety", (e) => view.addEventListener("changeSafety", (e) =>
this._evtChangeSafety(e) this._evtChangeSafety(e)
); );
view.addEventListener("markForDeletion", (e) =>
this._evtMarkForDeletion(e)
);
return view; return view;
}, },
}); });

View file

@ -169,27 +169,24 @@ class PostMainController extends BasePostController {
this._view.sidebarControl.disableForm(); this._view.sidebarControl.disableForm();
this._view.sidebarControl.clearMessages(); this._view.sidebarControl.clearMessages();
const post = e.detail.post; const post = e.detail.post;
if (e.detail.safety !== undefined && e.detail.safety !== null) { if (e.detail.safety !== undefined) {
post.safety = e.detail.safety; post.safety = e.detail.safety;
} }
if (e.detail.flags !== undefined && e.detail.flags !== null) { if (e.detail.flags !== undefined) {
post.flags = e.detail.flags; post.flags = e.detail.flags;
} }
if (e.detail.relations !== undefined && e.detail.relations !== null) { if (e.detail.relations !== undefined) {
post.relations = e.detail.relations; post.relations = e.detail.relations;
} }
if (e.detail.content !== undefined && e.detail.content !== null) { if (e.detail.content !== undefined) {
post.newContent = e.detail.content; post.newContent = e.detail.content;
} }
if (e.detail.thumbnail !== undefined && e.detail.thumbnail !== null) { if (e.detail.thumbnail !== undefined) {
post.newThumbnail = e.detail.thumbnail; post.newThumbnail = e.detail.thumbnail;
} }
if (e.detail.source !== undefined && e.detail.source !== null) { if (e.detail.source !== undefined) {
post.source = e.detail.source; post.source = e.detail.source;
} }
if (e.detail.desc !== undefined && e.detail.desc !== null) {
post.desc = e.detail.desc;
}
post.save().then( post.save().then(
() => { () => {
this._view.sidebarControl.showSuccess("Post saved."); this._view.sidebarControl.showSuccess("Post saved.");

View file

@ -12,7 +12,7 @@ const PostUploadView = require("../views/post_upload_view.js");
const EmptyView = require("../views/empty_view.js"); const EmptyView = require("../views/empty_view.js");
const genericErrorMessage = const genericErrorMessage =
"One or more posts needs your attention; " + "One of the posts needs your attention; " +
'click "resume upload" when you\'re ready.'; 'click "resume upload" when you\'re ready.';
class PostUploadController { class PostUploadController {
@ -55,7 +55,6 @@ class PostUploadController {
_evtSubmit(e) { _evtSubmit(e) {
this._view.disableForm(); this._view.disableForm();
this._view.clearMessages(); this._view.clearMessages();
let anyFailures = false;
e.detail.uploadables e.detail.uploadables
.reduce( .reduce(
@ -63,45 +62,11 @@ class PostUploadController {
promise.then(() => promise.then(() =>
this._uploadSinglePost( this._uploadSinglePost(
uploadable, uploadable,
e.detail.skipDuplicates, e.detail.skipDuplicates
e.detail.alwaysUploadSimilar )
).catch((error) => {
anyFailures = true;
if (error.uploadable) {
if (error.similarPosts) {
error.uploadable.lookalikes =
error.similarPosts;
this._view.updateUploadable(
error.uploadable
);
this._view.showInfo(
error.message,
error.uploadable
);
} else {
this._view.showError(
error.message,
error.uploadable
);
}
} else {
this._view.showError(
error.message,
uploadable
);
}
if (e.detail.pauseRemainOnError) {
return Promise.reject();
}
})
), ),
Promise.resolve() Promise.resolve()
) )
.then(() => {
if (anyFailures) {
return Promise.reject();
}
})
.then( .then(
() => { () => {
this._view.clearMessages(); this._view.clearMessages();
@ -110,13 +75,31 @@ class PostUploadController {
ctx.controller.showSuccess("Posts uploaded."); ctx.controller.showSuccess("Posts uploaded.");
}, },
(error) => { (error) => {
this._view.showError(genericErrorMessage); if (error.uploadable) {
if (error.similarPosts) {
error.uploadable.lookalikes = error.similarPosts;
this._view.updateUploadable(error.uploadable);
this._view.showInfo(genericErrorMessage);
this._view.showInfo(
error.message,
error.uploadable
);
} else {
this._view.showError(genericErrorMessage);
this._view.showError(
error.message,
error.uploadable
);
}
} else {
this._view.showError(error.message);
}
this._view.enableForm(); this._view.enableForm();
} }
); );
} }
_uploadSinglePost(uploadable, skipDuplicates, alwaysUploadSimilar) { _uploadSinglePost(uploadable, skipDuplicates) {
progress.start(); progress.start();
let reverseSearchPromise = Promise.resolve(); let reverseSearchPromise = Promise.resolve();
if (!uploadable.lookalikesConfirmed) { if (!uploadable.lookalikesConfirmed) {
@ -145,10 +128,7 @@ class PostUploadController {
} }
// notify about similar posts // notify about similar posts
if ( if (searchResult.similarPosts.length) {
searchResult.similarPosts.length &&
!alwaysUploadSimilar
) {
let error = new Error( let error = new Error(
`Found ${searchResult.similarPosts.length} similar ` + `Found ${searchResult.similarPosts.length} similar ` +
"posts.\nYou can resume or discard this upload." "posts.\nYou can resume or discard this upload."

View file

@ -95,13 +95,13 @@ class TagController {
_evtUpdate(e) { _evtUpdate(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
if (e.detail.names !== undefined && e.detail.names !== null) { if (e.detail.names !== undefined) {
e.detail.tag.names = e.detail.names; e.detail.tag.names = e.detail.names;
} }
if (e.detail.category !== undefined && e.detail.category !== null) { if (e.detail.category !== undefined) {
e.detail.tag.category = e.detail.category; e.detail.tag.category = e.detail.category;
} }
if (e.detail.description !== undefined && e.detail.description !== null) { if (e.detail.description !== undefined) {
e.detail.tag.description = e.detail.description; e.detail.tag.description = e.detail.description;
} }
e.detail.tag.save().then( e.detail.tag.save().then(

View file

@ -31,8 +31,9 @@ class UserController {
userTokenPromise = UserToken.get(userName).then( userTokenPromise = UserToken.get(userName).then(
(userTokens) => { (userTokens) => {
return userTokens.map((token) => { return userTokens.map((token) => {
token.isCurrentAuthToken = token.isCurrentAuthToken = api.isCurrentAuthToken(
api.isCurrentAuthToken(token); token
);
return token; return token;
}); });
}, },
@ -175,21 +176,21 @@ class UserController {
const isLoggedIn = api.isLoggedIn(e.detail.user); const isLoggedIn = api.isLoggedIn(e.detail.user);
const infix = isLoggedIn ? "self" : "any"; const infix = isLoggedIn ? "self" : "any";
if (e.detail.name !== undefined && e.detail.name !== null) { if (e.detail.name !== undefined) {
e.detail.user.name = e.detail.name; e.detail.user.name = e.detail.name;
} }
if (e.detail.email !== undefined && e.detail.email !== null) { if (e.detail.email !== undefined) {
e.detail.user.email = e.detail.email; e.detail.user.email = e.detail.email;
} }
if (e.detail.rank !== undefined && e.detail.rank !== null) { if (e.detail.rank !== undefined) {
e.detail.user.rank = e.detail.rank; e.detail.user.rank = e.detail.rank;
} }
if (e.detail.password !== undefined && e.detail.password !== null) { if (e.detail.password !== undefined) {
e.detail.user.password = e.detail.password; e.detail.user.password = e.detail.password;
} }
if (e.detail.avatarStyle !== undefined && e.detail.avatarStyle !== null) { if (e.detail.avatarStyle !== undefined) {
e.detail.user.avatarStyle = e.detail.avatarStyle; e.detail.user.avatarStyle = e.detail.avatarStyle;
if (e.detail.avatarContent) { if (e.detail.avatarContent) {
e.detail.user.avatarContent = e.detail.avatarContent; e.detail.user.avatarContent = e.detail.avatarContent;
@ -302,7 +303,7 @@ class UserController {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
if (e.detail.note !== undefined && e.detail.note !== null) { if (e.detail.note !== undefined) {
e.detail.userToken.note = e.detail.note; e.detail.userToken.note = e.detail.note;
} }

View file

@ -45,8 +45,9 @@ class ExpanderControl {
// eslint-disable-next-line accessor-pairs // eslint-disable-next-line accessor-pairs
set title(newTitle) { set title(newTitle) {
if (this._expanderNode) { if (this._expanderNode) {
this._expanderNode.querySelector("header span").textContent = this._expanderNode.querySelector(
newTitle; "header span"
).textContent = newTitle;
} }
} }

View file

@ -48,12 +48,6 @@ class FileDropperControl extends events.EventTarget {
this._urlInputNode.addEventListener("keydown", (e) => this._urlInputNode.addEventListener("keydown", (e) =>
this._evtUrlInputKeyDown(e) this._evtUrlInputKeyDown(e)
); );
this._urlInputNode.addEventListener("paste", (e) => {
// document.onpaste is used on the post-upload page.
// And this event is used on the post edit page.
if (document.getElementById("post-upload")) return;
this._evtPaste(e)
});
} }
if (this._urlConfirmButtonNode) { if (this._urlConfirmButtonNode) {
this._urlConfirmButtonNode.addEventListener("click", (e) => this._urlConfirmButtonNode.addEventListener("click", (e) =>
@ -61,11 +55,6 @@ class FileDropperControl extends events.EventTarget {
); );
} }
document.onpaste = (e) => {
if (!document.getElementById("post-upload")) return;
this._evtPaste(e)
}
this._originalHtml = this._dropperNode.innerHTML; this._originalHtml = this._dropperNode.innerHTML;
views.replaceContent(target, source); views.replaceContent(target, source);
} }
@ -140,17 +129,6 @@ class FileDropperControl extends events.EventTarget {
this._emitFiles(e.dataTransfer.files); this._emitFiles(e.dataTransfer.files);
} }
_evtPaste(e) {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
const fileList = Array.from(items).map((x) => x.getAsFile()).filter(f => f);
if (!this._options.allowMultiple && fileList.length > 1) {
window.alert("Cannot select multiple files.");
} else if (fileList.length > 0) {
this._emitFiles(fileList);
}
}
_evtUrlInputKeyDown(e) { _evtUrlInputKeyDown(e) {
if (e.which !== KEY_RETURN) { if (e.which !== KEY_RETURN) {
return; return;

View file

@ -103,30 +103,6 @@ class PostContentControl {
} }
_refreshSize() { _refreshSize() {
if (window.innerWidth <= 800) {
const buttons = document.querySelector(".sidebar > .buttons");
if (buttons) {
const content = document.querySelector(".content");
content.insertBefore(buttons, content.querySelector(".post-container + *"));
const afterControls = document.querySelector(".content > .after-mobile-controls");
if (afterControls) {
afterControls.parentElement.parentElement.appendChild(afterControls);
}
}
} else {
const buttons = document.querySelector(".content > .buttons");
if (buttons) {
const sidebar = document.querySelector(".sidebar");
sidebar.insertBefore(buttons, sidebar.firstElementChild);
}
const afterControls = document.querySelector(".content + .after-mobile-controls");
if (afterControls) {
document.querySelector(".content").appendChild(afterControls);
}
}
this._currentFitFunction(); this._currentFitFunction();
} }

View file

@ -203,8 +203,9 @@ class PostEditSidebarControl extends events.EventTarget {
); );
if (this._formNode) { if (this._formNode) {
const inputNodes = const inputNodes = this._formNode.querySelectorAll(
this._formNode.querySelectorAll("input, textarea"); "input, textarea"
);
for (let node of inputNodes) { for (let node of inputNodes) {
node.addEventListener("change", (e) => node.addEventListener("change", (e) =>
this.dispatchEvent(new CustomEvent("change")) this.dispatchEvent(new CustomEvent("change"))
@ -427,7 +428,7 @@ class PostEditSidebarControl extends events.EventTarget {
: undefined, : undefined,
thumbnail: thumbnail:
this._newPostThumbnail !== undefined && this._newPostThumbnail !== null this._newPostThumbnail !== undefined
? this._newPostThumbnail ? this._newPostThumbnail
: undefined, : undefined,

View file

@ -727,8 +727,9 @@ class PostNotesOverlayControl extends events.EventTarget {
} }
_showNoteText(note) { _showNoteText(note) {
this._textNode.querySelector(".wrapper").innerHTML = this._textNode.querySelector(
misc.formatMarkdown(note.text); ".wrapper"
).innerHTML = misc.formatMarkdown(note.text);
this._textNode.style.display = "block"; this._textNode.style.display = "block";
const bodyRect = document.body.getBoundingClientRect(); const bodyRect = document.body.getBoundingClientRect();
const noteRect = this._textNode.getBoundingClientRect(); const noteRect = this._textNode.getBoundingClientRect();

View file

@ -196,10 +196,9 @@ class TagInputControl extends events.EventTarget {
const listItemNode = this._createListItemNode(tag); const listItemNode = this._createListItemNode(tag);
if (!tag.category) { if (!tag.category) {
listItemNode.classList.add("new"); listItemNode.classList.add("new");
} else if (source === SOURCE_IMPLICATION) { }
if (source === SOURCE_IMPLICATION) {
listItemNode.classList.add("implication"); listItemNode.classList.add("implication");
} else {
listItemNode.classList.add("added");
} }
this._tagListNode.prependChild(listItemNode); this._tagListNode.prependChild(listItemNode);
_fadeOutListItemNodeStatus(listItemNode); _fadeOutListItemNodeStatus(listItemNode);

View file

@ -4,12 +4,12 @@ const config = require("./config.js");
if (config.environment == "development") { if (config.environment == "development") {
var ws = new WebSocket("ws://" + location.hostname + ":8080"); var ws = new WebSocket("ws://" + location.hostname + ":8080");
ws.addEventListener("open", function (event) { ws.addEventListener('open', function (event) {
console.log("Live-reloading websocket connected."); console.log("Live-reloading websocket connected.");
}); });
ws.addEventListener("message", (event) => { ws.addEventListener('message', (event) => {
console.log(event); console.log(event);
if (event.data == "reload") { if (event.data == 'reload'){
location.reload(); location.reload();
} }
}); });

View file

@ -114,10 +114,6 @@ class Post extends events.EventTarget {
return this._notes; return this._notes;
} }
get desc() {
return this._desc;
}
get comments() { get comments() {
return this._comments; return this._comments;
} }
@ -275,15 +271,12 @@ class Post extends events.EventTarget {
if (this._newContent) { if (this._newContent) {
files.content = this._newContent; files.content = this._newContent;
} }
if (this._newThumbnail !== undefined && this._newThumbnail !== null) { if (this._newThumbnail !== undefined) {
files.thumbnail = this._newThumbnail; files.thumbnail = this._newThumbnail;
} }
if (this._source !== this._orig._source) { if (this._source !== this._orig._source) {
detail.source = this._source; detail.source = this._source;
} }
if (this._desc !== this._orig._desc) {
detail.desc = this._desc;
}
let apiPromise = this._id let apiPromise = this._id
? api.put(uri.formatApiLink("post", this.id), detail, files) ? api.put(uri.formatApiLink("post", this.id), detail, files)

View file

@ -11,13 +11,13 @@ const defaultSettings = {
upscaleSmallPosts: false, upscaleSmallPosts: false,
endlessScroll: false, endlessScroll: false,
keyboardShortcuts: true, keyboardShortcuts: true,
transparencyGrid: false, transparencyGrid: true,
fitMode: "fit-both", fitMode: "fit-both",
tagSuggestions: true, tagSuggestions: true,
autoplayVideos: false, autoplayVideos: false,
postsPerPage: 42, postsPerPage: 42,
tagUnderscoresAsSpaces: false, tagUnderscoresAsSpaces: false,
darkTheme: true, darkTheme: false,
postFlow: false, postFlow: false,
}; };

View file

@ -65,6 +65,17 @@ class TagPermalinkFixWrapper extends BaseMarkdownWrapper {
// post, user and tags permalinks // post, user and tags permalinks
class EntityPermalinkWrapper extends BaseMarkdownWrapper { class EntityPermalinkWrapper extends BaseMarkdownWrapper {
preprocess(text) { preprocess(text) {
// URL-based permalinks
text = text.replace(new RegExp("\\b/post/(\\d+)/?\\b", "g"), "@$1");
text = text.replace(
new RegExp("\\b/tag/([a-zA-Z0-9_-]+?)/?", "g"),
"#$1"
);
text = text.replace(
new RegExp("\\b/user/([a-zA-Z0-9_-]+?)/?", "g"),
"+$1"
);
text = text.replace( text = text.replace(
/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g, /(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g,
"$1[$2]($2)" "$1[$2]($2)"
@ -125,8 +136,12 @@ function createRenderer() {
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
renderer.image = (href, title, alt) => { renderer.image = (href, title, alt) => {
let [_, url, width, height] = let [
/^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href); _,
url,
width,
height,
] = /^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href);
let res = '<img src="' + sanitize(url) + '" alt="' + sanitize(alt); let res = '<img src="' + sanitize(url) + '" alt="' + sanitize(alt);
if (width) { if (width) {
res += '" width="' + width; res += '" width="' + width;
@ -159,7 +174,7 @@ function formatMarkdown(text) {
for (let wrapper of wrappers) { for (let wrapper of wrappers) {
text = wrapper.preprocess(text); text = wrapper.preprocess(text);
} }
text = marked.parse(text, options); text = marked(text, options);
wrappers.reverse(); wrappers.reverse();
for (let wrapper of wrappers) { for (let wrapper of wrappers) {
text = wrapper.postprocess(text); text = wrapper.postprocess(text);
@ -185,7 +200,7 @@ function formatInlineMarkdown(text) {
for (let wrapper of wrappers) { for (let wrapper of wrappers) {
text = wrapper.preprocess(text); text = wrapper.preprocess(text);
} }
text = marked.parseInline(text, options); text = marked.inlineLexer(text, [], options);
wrappers.reverse(); wrappers.reverse();
for (let wrapper of wrappers) { for (let wrapper of wrappers) {
text = wrapper.postprocess(text); text = wrapper.postprocess(text);

View file

@ -209,13 +209,13 @@ function makePostLink(id, includeHash) {
} }
function makeTagLink(name, includeHash, includeCount, tag) { function makeTagLink(name, includeHash, includeCount, tag) {
const category = tag && tag.category ? tag.category : "unknown"; const category = tag ? tag.category : "unknown";
let text = misc.getPrettyName(name); let text = misc.getPrettyName(name);
if (includeHash === true) { if (includeHash === true) {
text = "#" + text; text = "#" + text;
} }
if (includeCount === true) { if (includeCount === true) {
text += " (" + (tag && tag.postCount ? tag.postCount : 0) + ")"; text += " (" + (tag ? tag.postCount : 0) + ")";
} }
return api.hasPrivilege("tags:view") return api.hasPrivilege("tags:view")
? makeElement( ? makeElement(
@ -234,15 +234,15 @@ function makeTagLink(name, includeHash, includeCount, tag) {
} }
function makePoolLink(id, includeHash, includeCount, pool, name) { function makePoolLink(id, includeHash, includeCount, pool, name) {
const category = pool && pool.category ? pool.category : "unknown"; const category = pool ? pool.category : "unknown";
let text = misc.getPrettyName( let text = misc.getPrettyName(
name ? name : pool && pool.names ? pool.names[0] : "pool " + id name ? name : pool ? pool.names[0] : "unknown"
); );
if (includeHash === true) { if (includeHash === true) {
text = "#" + text; text = "#" + text;
} }
if (includeCount === true) { if (includeCount === true) {
text += " (" + (pool && pool.postCount ? pool.postCount : 0) + ")"; text += " (" + (pool ? pool.postCount : 0) + ")";
} }
return api.hasPrivilege("pools:view") return api.hasPrivilege("pools:view")
? makeElement( ? makeElement(
@ -264,7 +264,7 @@ function makeUserLink(user) {
let text = makeThumbnail(user ? user.avatarUrl : null); let text = makeThumbnail(user ? user.avatarUrl : null);
text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous"; text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous";
const link = const link =
user && user.name && api.hasPrivilege("users:view") user && api.hasPrivilege("users:view")
? makeElement( ? makeElement(
"a", "a",
{ href: uri.formatClientLink("user", user.name) }, { href: uri.formatClientLink("user", user.name) },

View file

@ -25,8 +25,9 @@ class PostMainView {
views.replaceContent(this._hostNode, sourceNode); views.replaceContent(this._hostNode, sourceNode);
views.syncScrollPosition(); views.syncScrollPosition();
const topNavigationNode = const topNavigationNode = document.body.querySelector(
document.body.querySelector("#top-navigation"); "#top-navigation"
);
this._postContentControl = new PostContentControl( this._postContentControl = new PostContentControl(
postContainerNode, postContainerNode,

View file

@ -1,7 +1,6 @@
"use strict"; "use strict";
const events = require("../events.js"); const events = require("../events.js");
const api = require("../api.js");
const views = require("../util/views.js"); const views = require("../util/views.js");
const FileDropperControl = require("../controls/file_dropper_control.js"); const FileDropperControl = require("../controls/file_dropper_control.js");
@ -22,7 +21,6 @@ function _mimeTypeToPostType(mimeType) {
"image/heic": "image", "image/heic": "image",
"video/mp4": "video", "video/mp4": "video",
"video/webm": "video", "video/webm": "video",
"video/quicktime": "video",
}[mimeType] || "unknown" }[mimeType] || "unknown"
); );
} }
@ -36,8 +34,7 @@ class Uploadable extends events.EventTarget {
this.flags = []; this.flags = [];
this.tags = []; this.tags = [];
this.relations = []; this.relations = [];
this.anonymous = !api.isLoggedIn(); this.anonymous = false;
this.forceAnonymous = !api.isLoggedIn();
} }
destroy() {} destroy() {}
@ -121,7 +118,6 @@ class Url extends Uploadable {
heif: "image/heif", heif: "image/heif",
heic: "image/heic", heic: "image/heic",
mp4: "video/mp4", mp4: "video/mp4",
mov: "video/quicktime",
webm: "video/webm", webm: "video/webm",
}; };
for (let extension of Object.keys(mime)) { for (let extension of Object.keys(mime)) {
@ -287,7 +283,7 @@ class PostUploadView extends events.EventTarget {
for (let uploadable of this._uploadables) { for (let uploadable of this._uploadables) {
this._updateUploadableFromDom(uploadable); this._updateUploadableFromDom(uploadable);
} }
this._submitButtonNode.value = "Resume"; this._submitButtonNode.value = "Resume upload";
this._emit("submit"); this._emit("submit");
} }
@ -362,10 +358,6 @@ class PostUploadView extends events.EventTarget {
detail: { detail: {
uploadables: this._uploadables, uploadables: this._uploadables,
skipDuplicates: this._skipDuplicatesCheckboxNode.checked, skipDuplicates: this._skipDuplicatesCheckboxNode.checked,
alwaysUploadSimilar:
this._alwaysUploadSimilarCheckboxNode.checked,
pauseRemainOnError:
this._pauseRemainOnErrorCheckboxNode.checked,
}, },
}) })
); );
@ -429,18 +421,6 @@ class PostUploadView extends events.EventTarget {
return this._hostNode.querySelector("form [name=skip-duplicates]"); return this._hostNode.querySelector("form [name=skip-duplicates]");
} }
get _alwaysUploadSimilarCheckboxNode() {
return this._hostNode.querySelector(
"form [name=always-upload-similar]"
);
}
get _pauseRemainOnErrorCheckboxNode() {
return this._hostNode.querySelector(
"form [name=pause-remain-on-error]"
);
}
get _submitButtonNode() { get _submitButtonNode() {
return this._hostNode.querySelector("form [type=submit]"); return this._hostNode.querySelector("form [type=submit]");
} }

View file

@ -141,34 +141,6 @@ class BulkTagEditor extends BulkEditor {
} }
} }
class BulkDeleteEditor extends BulkEditor {
constructor(hostNode) {
super(hostNode);
this._hostNode.addEventListener("submit", (e) =>
this._evtFormSubmit(e)
);
}
_evtFormSubmit(e) {
e.preventDefault();
this.dispatchEvent(
new CustomEvent("deleteSelectedPosts", { detail: {} })
);
}
_evtOpenLinkClick(e) {
e.preventDefault();
this.toggleOpen(true);
this.dispatchEvent(new CustomEvent("open", { detail: {} }));
}
_evtCloseLinkClick(e) {
e.preventDefault();
this.toggleOpen(false);
this.dispatchEvent(new CustomEvent("close", { detail: {} }));
}
}
class PostsHeaderView extends events.EventTarget { class PostsHeaderView extends events.EventTarget {
constructor(ctx) { constructor(ctx) {
super(); super();
@ -214,13 +186,6 @@ class PostsHeaderView extends events.EventTarget {
this._bulkEditors.push(this._bulkSafetyEditor); this._bulkEditors.push(this._bulkSafetyEditor);
} }
if (this._bulkEditDeleteNode) {
this._bulkDeleteEditor = new BulkDeleteEditor(
this._bulkEditDeleteNode
);
this._bulkEditors.push(this._bulkDeleteEditor);
}
for (let editor of this._bulkEditors) { for (let editor of this._bulkEditors) {
editor.addEventListener("submit", (e) => { editor.addEventListener("submit", (e) => {
this._navigate(); this._navigate();
@ -239,8 +204,6 @@ class PostsHeaderView extends events.EventTarget {
this._openBulkEditor(this._bulkTagEditor); this._openBulkEditor(this._bulkTagEditor);
} else if (ctx.parameters.safety && this._bulkSafetyEditor) { } else if (ctx.parameters.safety && this._bulkSafetyEditor) {
this._openBulkEditor(this._bulkSafetyEditor); this._openBulkEditor(this._bulkSafetyEditor);
} else if (ctx.parameters.delete && this._bulkDeleteEditor) {
this._openBulkEditor(this._bulkDeleteEditor);
} }
} }
@ -264,10 +227,6 @@ class PostsHeaderView extends events.EventTarget {
return this._hostNode.querySelector(".bulk-edit-safety"); return this._hostNode.querySelector(".bulk-edit-safety");
} }
get _bulkEditDeleteNode() {
return this._hostNode.querySelector(".bulk-edit-delete");
}
_openBulkEditor(editor) { _openBulkEditor(editor) {
editor.toggleOpen(true); editor.toggleOpen(true);
this._hideBulkEditorsExcept(editor); this._hideBulkEditorsExcept(editor);
@ -294,8 +253,9 @@ class PostsHeaderView extends events.EventTarget {
e.target.classList.toggle("disabled"); e.target.classList.toggle("disabled");
const safety = e.target.getAttribute("data-safety"); const safety = e.target.getAttribute("data-safety");
let browsingSettings = settings.get(); let browsingSettings = settings.get();
browsingSettings.listPosts[safety] = browsingSettings.listPosts[safety] = !browsingSettings.listPosts[
!browsingSettings.listPosts[safety]; safety
];
settings.save(browsingSettings, true); settings.save(browsingSettings, true);
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("navigate", { new CustomEvent("navigate", {
@ -334,10 +294,6 @@ class PostsHeaderView extends events.EventTarget {
this._bulkSafetyEditor && this._bulkSafetyEditor.opened this._bulkSafetyEditor && this._bulkSafetyEditor.opened
? "1" ? "1"
: null; : null;
parameters.delete =
this._bulkDeleteEditor && this._bulkDeleteEditor.opened
? "1"
: null;
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("navigate", { detail: { parameters: parameters } }) new CustomEvent("navigate", { detail: { parameters: parameters } })
); );

View file

@ -39,13 +39,6 @@ class PostsPageView extends events.EventTarget {
); );
} }
} }
const deleteFlipperNode = this._getDeleteFlipperNode(listItemNode);
if (deleteFlipperNode) {
deleteFlipperNode.addEventListener("click", (e) =>
this._evtBulkToggleDeleteClick(e, post)
);
}
} }
this._syncBulkEditorsHighlights(); this._syncBulkEditorsHighlights();
@ -63,10 +56,6 @@ class PostsPageView extends events.EventTarget {
return listItemNode.querySelector(".safety-flipper"); return listItemNode.querySelector(".safety-flipper");
} }
_getDeleteFlipperNode(listItemNode) {
return listItemNode.querySelector(".delete-flipper");
}
_evtPostChange(e) { _evtPostChange(e) {
const listItemNode = this._postIdToListItemNode[e.detail.post.id]; const listItemNode = this._postIdToListItemNode[e.detail.post.id];
for (let node of listItemNode.querySelectorAll("[data-disabled]")) { for (let node of listItemNode.querySelectorAll("[data-disabled]")) {
@ -110,20 +99,6 @@ class PostsPageView extends events.EventTarget {
); );
} }
_evtBulkToggleDeleteClick(e, post) {
e.preventDefault();
const linkNode = e.target;
linkNode.classList.toggle("delete");
this.dispatchEvent(
new CustomEvent("markForDeletion", {
detail: {
post,
delete: linkNode.classList.contains("delete"),
},
})
);
}
_syncBulkEditorsHighlights() { _syncBulkEditorsHighlights() {
for (let listItemNode of this._listItemNodes) { for (let listItemNode of this._listItemNodes) {
const postId = listItemNode.getAttribute("data-post-id"); const postId = listItemNode.getAttribute("data-post-id");
@ -148,16 +123,6 @@ class PostsPageView extends events.EventTarget {
); );
} }
} }
const deleteFlipperNode = this._getDeleteFlipperNode(listItemNode);
if (deleteFlipperNode) {
deleteFlipperNode.classList.toggle(
"delete",
this._ctx.bulkEdit.markedForDeletion.some(
(x) => x.id == postId
)
);
}
} }
} }
} }

View file

@ -72,8 +72,9 @@ class UserTokenView extends events.EventTarget {
_evtDelete(e) { _evtDelete(e) {
e.preventDefault(); e.preventDefault();
const userToken = const userToken = this._tokens[
this._tokens[parseInt(e.target.getAttribute("data-token-id"))]; parseInt(e.target.getAttribute("data-token-id"))
];
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("delete", { new CustomEvent("delete", {
detail: { detail: {
@ -109,8 +110,9 @@ class UserTokenView extends events.EventTarget {
_evtChangeNoteClick(e) { _evtChangeNoteClick(e) {
e.preventDefault(); e.preventDefault();
const userToken = const userToken = this._tokens[
this._tokens[parseInt(e.target.getAttribute("data-token-id"))]; parseInt(e.target.getAttribute("data-token-id"))
];
const text = window.prompt( const text = window.prompt(
"Please enter the new name:", "Please enter the new name:",
userToken.note !== null ? userToken.note : undefined userToken.note !== null ? userToken.note : undefined

274
client/package-lock.json generated
View file

@ -10,7 +10,7 @@
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"ios-inner-height": "^1.0.3", "ios-inner-height": "^1.0.3",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"marked": "^4.0.10", "marked": "^0.7.0",
"mousetrap": "^1.6.2", "mousetrap": "^1.6.2",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"superagent": "^3.8.3" "superagent": "^3.8.3"
@ -28,7 +28,7 @@
"jimp": "^0.13.0", "jimp": "^0.13.0",
"pretty-error": "^3.0.3", "pretty-error": "^3.0.3",
"stylus": "^0.54.8", "stylus": "^0.54.8",
"terser": "^4.8.1", "terser": "^3.7.7",
"underscore": "^1.12.1", "underscore": "^1.12.1",
"watchify": "^4.0.0", "watchify": "^4.0.0",
"ws": "^7.4.6" "ws": "^7.4.6"
@ -476,6 +476,24 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/array-filter": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz",
"integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=",
"dev": true
},
"node_modules/array-map": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
"integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=",
"dev": true
},
"node_modules/array-reduce": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
"integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
"dev": true
},
"node_modules/asn1.js": { "node_modules/asn1.js": {
"version": "4.10.1", "version": "4.10.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
@ -1488,15 +1506,16 @@
"dev": true "dev": true
}, },
"node_modules/cached-path-relative": { "node_modules/cached-path-relative": {
"version": "1.1.0", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz", "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz",
"integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==", "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==",
"dev": true "dev": true
}, },
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2" "get-intrinsic": "^1.0.2"
@ -1663,9 +1682,9 @@
"dev": true "dev": true
}, },
"node_modules/cookiejar": { "node_modules/cookiejar": {
"version": "2.1.4", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
}, },
"node_modules/core-js": { "node_modules/core-js": {
"version": "2.5.7", "version": "2.5.7",
@ -1831,9 +1850,9 @@
} }
}, },
"node_modules/decode-uri-component": { "node_modules/decode-uri-component": {
"version": "0.2.2", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
@ -2235,7 +2254,8 @@
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
}, },
"node_modules/get-assigned-identifiers": { "node_modules/get-assigned-identifiers": {
"version": "1.2.0", "version": "1.2.0",
@ -2247,6 +2267,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
@ -2330,6 +2351,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.1" "function-bind": "^1.1.1"
}, },
@ -2362,6 +2384,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -2836,9 +2859,9 @@
"dev": true "dev": true
}, },
"node_modules/jpeg-js": { "node_modules/jpeg-js": {
"version": "0.4.4", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.0.tgz",
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "integrity": "sha512-960VHmtN1vTpasX/1LupLohdP5odwAT7oK/VSm6mW0M58LbrBnowLAPWAZhWGhDAGjzbMnPXZxzB/QYgBwkN0w==",
"dev": true "dev": true
}, },
"node_modules/js-cookie": { "node_modules/js-cookie": {
@ -2974,14 +2997,14 @@
"dev": true "dev": true
}, },
"node_modules/marked": { "node_modules/marked": {
"version": "4.0.10", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
"integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==", "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==",
"bin": { "bin": {
"marked": "bin/marked.js" "marked": "bin/marked"
}, },
"engines": { "engines": {
"node": ">= 12" "node": ">=0.10.0"
} }
}, },
"node_modules/md5.js": { "node_modules/md5.js": {
@ -3085,9 +3108,9 @@
} }
}, },
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.6", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true "dev": true
}, },
"node_modules/mkdirp": { "node_modules/mkdirp": {
@ -3201,6 +3224,7 @@
"version": "1.10.3", "version": "1.10.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
"dev": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@ -3361,9 +3385,9 @@
} }
}, },
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true "dev": true
}, },
"node_modules/path-platform": { "node_modules/path-platform": {
@ -3483,17 +3507,11 @@
"dev": true "dev": true
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.11.0", "version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/querystring": { "node_modules/querystring": {
@ -3752,22 +3770,15 @@
} }
}, },
"node_modules/shell-quote": { "node_modules/shell-quote": {
"version": "1.7.3", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
"dev": true "dev": true,
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dependencies": { "dependencies": {
"call-bind": "^1.0.0", "array-filter": "~0.0.0",
"get-intrinsic": "^1.0.2", "array-map": "~0.0.0",
"object-inspect": "^1.9.0" "array-reduce": "~0.0.0",
}, "jsonify": "~0.0.0"
"funding": {
"url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/simple-concat": { "node_modules/simple-concat": {
@ -3972,6 +3983,12 @@
"minimist": "^1.1.0" "minimist": "^1.1.0"
} }
}, },
"node_modules/subarg/node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"node_modules/superagent": { "node_modules/superagent": {
"version": "3.8.3", "version": "3.8.3",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
@ -4011,26 +4028,26 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "4.8.1", "version": "3.7.7",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-3.7.7.tgz",
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", "integrity": "sha512-RRLIxE7S52vSOI9cEbOaisgBd2y6MNgfg2ihUkidsFnuP1eDmZ79+lBWbyvgfFTAc/r8nSjL0k3cpZDDIYiYiA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"commander": "^2.20.0", "commander": "~2.14.1",
"source-map": "~0.6.1", "source-map": "~0.6.1",
"source-map-support": "~0.5.12" "source-map-support": "~0.5.6"
}, },
"bin": { "bin": {
"terser": "bin/terser" "terser": "bin/uglifyjs"
}, },
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=0.8.0"
} }
}, },
"node_modules/terser/node_modules/commander": { "node_modules/terser/node_modules/commander": {
"version": "2.20.3", "version": "2.14.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
"dev": true "dev": true
}, },
"node_modules/terser/node_modules/source-map": { "node_modules/terser/node_modules/source-map": {
@ -4043,9 +4060,9 @@
} }
}, },
"node_modules/terser/node_modules/source-map-support": { "node_modules/terser/node_modules/source-map-support": {
"version": "0.5.21", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"buffer-from": "^1.0.0", "buffer-from": "^1.0.0",
@ -5030,6 +5047,24 @@
"picomatch": "^2.0.4" "picomatch": "^2.0.4"
} }
}, },
"array-filter": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz",
"integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=",
"dev": true
},
"array-map": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
"integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=",
"dev": true
},
"array-reduce": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
"integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
"dev": true
},
"asn1.js": { "asn1.js": {
"version": "4.10.1", "version": "4.10.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
@ -6018,15 +6053,16 @@
"dev": true "dev": true
}, },
"cached-path-relative": { "cached-path-relative": {
"version": "1.1.0", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz", "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz",
"integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==", "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==",
"dev": true "dev": true
}, },
"call-bind": { "call-bind": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2" "get-intrinsic": "^1.0.2"
@ -6175,9 +6211,9 @@
"dev": true "dev": true
}, },
"cookiejar": { "cookiejar": {
"version": "2.1.4", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
}, },
"core-js": { "core-js": {
"version": "2.5.7", "version": "2.5.7",
@ -6327,9 +6363,9 @@
} }
}, },
"decode-uri-component": { "decode-uri-component": {
"version": "0.2.2", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true "dev": true
}, },
"define-properties": { "define-properties": {
@ -6661,7 +6697,8 @@
"function-bind": { "function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
}, },
"get-assigned-identifiers": { "get-assigned-identifiers": {
"version": "1.2.0", "version": "1.2.0",
@ -6673,6 +6710,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
@ -6740,6 +6778,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": { "requires": {
"function-bind": "^1.1.1" "function-bind": "^1.1.1"
} }
@ -6762,7 +6801,8 @@
"has-symbols": { "has-symbols": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
"dev": true
}, },
"hash-base": { "hash-base": {
"version": "3.0.4", "version": "3.0.4",
@ -7118,9 +7158,9 @@
} }
}, },
"jpeg-js": { "jpeg-js": {
"version": "0.4.4", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.0.tgz",
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "integrity": "sha512-960VHmtN1vTpasX/1LupLohdP5odwAT7oK/VSm6mW0M58LbrBnowLAPWAZhWGhDAGjzbMnPXZxzB/QYgBwkN0w==",
"dev": true "dev": true
}, },
"js-cookie": { "js-cookie": {
@ -7240,9 +7280,9 @@
"dev": true "dev": true
}, },
"marked": { "marked": {
"version": "4.0.10", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
"integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==" "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg=="
}, },
"md5.js": { "md5.js": {
"version": "1.3.4", "version": "1.3.4",
@ -7324,9 +7364,9 @@
} }
}, },
"minimist": { "minimist": {
"version": "1.2.6", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true "dev": true
}, },
"mkdirp": { "mkdirp": {
@ -7426,7 +7466,8 @@
"object-inspect": { "object-inspect": {
"version": "1.10.3", "version": "1.10.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
"dev": true
}, },
"object-keys": { "object-keys": {
"version": "1.1.1", "version": "1.1.1",
@ -7566,9 +7607,9 @@
"dev": true "dev": true
}, },
"path-parse": { "path-parse": {
"version": "1.0.7", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true "dev": true
}, },
"path-platform": { "path-platform": {
@ -7664,12 +7705,9 @@
"dev": true "dev": true
}, },
"qs": { "qs": {
"version": "6.11.0", "version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
"requires": {
"side-channel": "^1.0.4"
}
}, },
"querystring": { "querystring": {
"version": "0.2.0", "version": "0.2.0",
@ -7898,19 +7936,15 @@
} }
}, },
"shell-quote": { "shell-quote": {
"version": "1.7.3", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
"dev": true "dev": true,
},
"side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"requires": { "requires": {
"call-bind": "^1.0.0", "array-filter": "~0.0.0",
"get-intrinsic": "^1.0.2", "array-map": "~0.0.0",
"object-inspect": "^1.9.0" "array-reduce": "~0.0.0",
"jsonify": "~0.0.0"
} }
}, },
"simple-concat": { "simple-concat": {
@ -8082,6 +8116,14 @@
"dev": true, "dev": true,
"requires": { "requires": {
"minimist": "^1.1.0" "minimist": "^1.1.0"
},
"dependencies": {
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
}
} }
}, },
"superagent": { "superagent": {
@ -8117,20 +8159,20 @@
} }
}, },
"terser": { "terser": {
"version": "4.8.1", "version": "3.7.7",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-3.7.7.tgz",
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", "integrity": "sha512-RRLIxE7S52vSOI9cEbOaisgBd2y6MNgfg2ihUkidsFnuP1eDmZ79+lBWbyvgfFTAc/r8nSjL0k3cpZDDIYiYiA==",
"dev": true, "dev": true,
"requires": { "requires": {
"commander": "^2.20.0", "commander": "~2.14.1",
"source-map": "~0.6.1", "source-map": "~0.6.1",
"source-map-support": "~0.5.12" "source-map-support": "~0.5.6"
}, },
"dependencies": { "dependencies": {
"commander": { "commander": {
"version": "2.20.3", "version": "2.14.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
"dev": true "dev": true
}, },
"source-map": { "source-map": {
@ -8140,9 +8182,9 @@
"dev": true "dev": true
}, },
"source-map-support": { "source-map-support": {
"version": "0.5.21", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==",
"dev": true, "dev": true,
"requires": { "requires": {
"buffer-from": "^1.0.0", "buffer-from": "^1.0.0",

View file

@ -3,15 +3,14 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build": "node build.js", "build": "node build.js",
"watch": "node build.js --watch", "watch": "node build.js --watch"
"build-container": "docker build -t szurubooru/client:dev ."
}, },
"dependencies": { "dependencies": {
"dompurify": "^2.0.17", "dompurify": "^2.0.17",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"ios-inner-height": "^1.0.3", "ios-inner-height": "^1.0.3",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"marked": "^4.0.10", "marked": "^0.7.0",
"mousetrap": "^1.6.2", "mousetrap": "^1.6.2",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"superagent": "^3.8.3" "superagent": "^3.8.3"
@ -29,7 +28,7 @@
"jimp": "^0.13.0", "jimp": "^0.13.0",
"pretty-error": "^3.0.3", "pretty-error": "^3.0.3",
"stylus": "^0.54.8", "stylus": "^0.54.8",
"terser": "^4.8.1", "terser": "^3.7.7",
"underscore": "^1.12.1", "underscore": "^1.12.1",
"watchify": "^4.0.0", "watchify": "^4.0.0",
"ws": "^7.4.6" "ws": "^7.4.6"

View file

@ -37,7 +37,6 @@
- [Creating post](#creating-post) - [Creating post](#creating-post)
- [Updating post](#updating-post) - [Updating post](#updating-post)
- [Getting post](#getting-post) - [Getting post](#getting-post)
- [Getting around post](#getting-around-post)
- [Deleting post](#deleting-post) - [Deleting post](#deleting-post)
- [Merging posts](#merging-posts) - [Merging posts](#merging-posts)
- [Rating post](#rating-post) - [Rating post](#rating-post)
@ -54,7 +53,7 @@
- [Deleting pool category](#deleting-pool-category) - [Deleting pool category](#deleting-pool-category)
- [Setting default pool category](#setting-default-pool-category) - [Setting default pool category](#setting-default-pool-category)
- Pools - Pools
- [Listing pools](#listing-pools) - [Listing pools](#listing-pool)
- [Creating pool](#creating-pool) - [Creating pool](#creating-pool)
- [Updating pool](#updating-pool) - [Updating pool](#updating-pool)
- [Getting pool](#getting-pool) - [Getting pool](#getting-pool)
@ -165,9 +164,9 @@ way. The files, however, should be passed as regular fields appended with a
accepts a file named `content`, the client should pass accepts a file named `content`, the client should pass
`{"contentUrl":"http://example.com/file.jpg"}` as a part of the JSON message `{"contentUrl":"http://example.com/file.jpg"}` as a part of the JSON message
body. When creating or updating post content using this method, the server can body. When creating or updating post content using this method, the server can
also be configured to employ [yt-dlp](https://github.com/yt-dlp/yt-dlp) to also be configured to employ [youtube-dl](https://github.com/ytdl-org/youtube-dl)
download content from popular sites such as youtube, gfycat, etc. Access to to download content from popular sites such as youtube, gfycat, etc. Access to
yt-dlp can be configured with the `'uploads:use_downloader'` permission youtube-dl can be configured with the `'uploads:use_downloader'` permission
Finally, in some cases the user might want to reuse one file between the Finally, in some cases the user might want to reuse one file between the
requests to save the bandwidth (for example, reverse search + consecutive requests to save the bandwidth (for example, reverse search + consecutive
@ -323,7 +322,7 @@ data.
{ {
"name": <name>, "name": <name>,
"color": <color>, "color": <color>,
"order": <order> "order": <order> // optional
} }
``` ```
@ -952,29 +951,6 @@ data.
Retrieves information about an existing post. Retrieves information about an existing post.
## Getting around post
- **Request**
`GET /post/<id>/around`
- **Output**
```json5
{
"prev": <post-resource>,
"next": <post-resource>
}
```
- **Errors**
- the post does not exist
- privileges are too low
- **Description**
Retrieves information about posts that are before or after an existing post.
## Deleting post ## Deleting post
- **Request** - **Request**
@ -1389,7 +1365,7 @@ data.
## Creating pool ## Creating pool
- **Request** - **Request**
`POST /pool` `POST /pools/create`
- **Input** - **Input**
@ -2491,7 +2467,7 @@ One file together with its metadata posted to the site.
## Micro post ## Micro post
**Description** **Description**
A [post resource](#post) stripped down to `id` and `thumbnailUrl` fields. A [post resource](#post) stripped down to `name` and `thumbnailUrl` fields.
## Note ## Note
**Description** **Description**

View file

@ -34,79 +34,33 @@ and Docker Compose (version 1.6.0 or greater) already installed.
Read the comments to guide you. Note that `.env` should be in the root Read the comments to guide you. Note that `.env` should be in the root
directory of this repository. directory of this repository.
4. Pull the containers: ### Running the Application
This pulls the latest containers from docker.io: Download containers:
```console ```console
user@host:szuru$ docker-compose pull user@host:szuru$ docker-compose pull
``` ```
If you have modified the application's source and would like to manually For first run, it is recommended to start the database separately:
build it, follow the instructions in [**Building**](#Building) instead, ```console
then read here once you're done. user@host:szuru$ docker-compose up -d sql
```
5. Run it! To start all containers:
```console
user@host:szuru$ docker-compose up -d
```
For first run, it is recommended to start the database separately: To view/monitor the application logs:
```console ```console
user@host:szuru$ docker-compose up -d sql user@host:szuru$ docker-compose logs -f
``` # (CTRL+C to exit)
```
To start all containers:
```console
user@host:szuru$ docker-compose up -d
```
To view/monitor the application logs:
```console
user@host:szuru$ docker-compose logs -f
# (CTRL+C to exit)
```
### Building
1. Edit `docker-compose.yml` to tell Docker to build instead of pull containers:
```diff yaml
...
server:
- image: szurubooru/server:latest
+ build: server
...
client:
- image: szurubooru/client:latest
+ build: client
...
```
You can choose to build either one from source.
2. Build the containers:
```console
user@host:szuru$ docker-compose build
```
That will attempt to build both containers, but you can specify `client`
or `server` to make it build only one.
If `docker-compose build` spits out:
```
ERROR: Service 'server' failed to build: failed to parse platform : "" is an invalid component of "": platform specifier component must match "^[A-Za-z0-9_-]+$": invalid argument
```
...you will need to export Docker BuildKit flags:
```console
user@host:szuru$ export DOCKER_BUILDKIT=1; export COMPOSE_DOCKER_CLI_BUILD=1
```
...and run `docker-compose build` again.
*Note: If your changes are not taking effect in your builds, consider building
with `--no-cache`.*
To stop all containers:
```console
user@host:szuru$ docker-compose down
```
### Additional Features ### Additional Features

View file

@ -10,12 +10,6 @@ BUILD_INFO=latest
# otherwise the port specified here will be publicly accessible # otherwise the port specified here will be publicly accessible
PORT=8080 PORT=8080
# How many waitress threads to start
# 4 is the default amount of threads. If you experience performance
# degradation with a large number of posts, increasing this may
# improve performance, since waitress is most likely clogging up with Tasks.
THREADS=4
# URL base to run szurubooru under # URL base to run szurubooru under
# See "Additional Features" section in INSTALL.md # See "Additional Features" section in INSTALL.md
BASE_URL=/ BASE_URL=/

View file

@ -2,16 +2,14 @@
## ##
## Use this as a template to set up docker-compose, or as guide to set up other ## Use this as a template to set up docker-compose, or as guide to set up other
## orchestration services ## orchestration services
#version: '2' version: '2'
services: services:
server: server:
# image: szurubooru/server:latest image: szurubooru/server:latest
build: server
depends_on: depends_on:
- sql - sql
restart: unless-stopped
environment: environment:
## These should be the names of the dependent containers listed below, ## These should be the names of the dependent containers listed below,
## or FQDNs/IP addresses if these services are running outside of Docker ## or FQDNs/IP addresses if these services are running outside of Docker
@ -23,15 +21,12 @@ services:
#POSTGRES_DB: defaults to same as POSTGRES_USER #POSTGRES_DB: defaults to same as POSTGRES_USER
#POSTGRES_PORT: 5432 #POSTGRES_PORT: 5432
#LOG_SQL: 0 (1 for verbose SQL logs) #LOG_SQL: 0 (1 for verbose SQL logs)
THREADS:
volumes: volumes:
- "${MOUNT_DATA}:/data" - "${MOUNT_DATA}:/data"
- "./server/config.yaml:/opt/app/config.yaml" - "./server/config.yaml:/opt/app/config.yaml"
client: client:
# image: szurubooru/client:latest image: szurubooru/client:latest
build: client
restart: unless-stopped
depends_on: depends_on:
- server - server
environment: environment:
@ -48,7 +43,5 @@ services:
environment: environment:
POSTGRES_USER: POSTGRES_USER:
POSTGRES_PASSWORD: POSTGRES_PASSWORD:
# ports:
# - 5432:5432
volumes: volumes:
- "${MOUNT_SQL}:/var/lib/postgresql/data" - "${MOUNT_SQL}:/var/lib/postgresql/data"

View file

@ -7,13 +7,8 @@ WORKDIR /opt/app
RUN apk --no-cache add \ RUN apk --no-cache add \
python3 \ python3 \
python3-dev \ python3-dev \
py3-pip \
build-base \
libheif \
libheif-dev \
libavif \
libavif-dev \
ffmpeg \ ffmpeg \
py3-pip \
# from requirements.txt: # from requirements.txt:
py3-yaml \ py3-yaml \
py3-psycopg2 \ py3-psycopg2 \
@ -23,21 +18,26 @@ RUN apk --no-cache add \
py3-pillow \ py3-pillow \
py3-pynacl \ py3-pynacl \
py3-tz \ py3-tz \
py3-pyrfc3339 py3-pyrfc3339 \
RUN pip3 install --no-cache-dir --disable-pip-version-check \ build-base \
"alembic>=0.8.5" \ && apk --no-cache add \
libheif \
libavif \
libheif-dev \
libavif-dev \
&& pip3 install --no-cache-dir --disable-pip-version-check \
alembic \
"coloredlogs==5.0" \ "coloredlogs==5.0" \
"pyheif==0.6.1" \ youtube_dl \
"heif-image-plugin>=0.3.2" \ pillow-avif-plugin \
yt-dlp \ pyheif-pillow-opener \
"pillow-avif-plugin~=1.1.0" && apk --no-cache del py3-pip
RUN apk --no-cache del py3-pip
COPY ./ /opt/app/ COPY ./ /opt/app/
RUN rm -rf /opt/app/szurubooru/tests RUN rm -rf /opt/app/szurubooru/tests
FROM --platform=$BUILDPLATFORM prereqs as testing FROM prereqs as testing
WORKDIR /opt/app WORKDIR /opt/app
RUN apk --no-cache add \ RUN apk --no-cache add \
@ -83,9 +83,6 @@ ARG PORT=6666
ENV PORT=${PORT} ENV PORT=${PORT}
EXPOSE ${PORT} EXPOSE ${PORT}
ARG THREADS=4
ENV THREADS=${THREADS}
VOLUME ["/data/"] VOLUME ["/data/"]
ARG DOCKER_REPO ARG DOCKER_REPO

View file

@ -103,7 +103,6 @@ privileges:
'posts:edit:content': power 'posts:edit:content': power
'posts:edit:flags': regular 'posts:edit:flags': regular
'posts:edit:notes': regular 'posts:edit:notes': regular
'posts:edit:desc': regular
'posts:edit:relations': regular 'posts:edit:relations': regular
'posts:edit:safety': power 'posts:edit:safety': power
'posts:edit:source': regular 'posts:edit:source': regular
@ -116,7 +115,6 @@ privileges:
'posts:favorite': regular 'posts:favorite': regular
'posts:bulk-edit:tags': power 'posts:bulk-edit:tags': power
'posts:bulk-edit:safety': power 'posts:bulk-edit:safety': power
'posts:bulk-edit:delete': power
'tags:create': regular 'tags:create': regular
'tags:edit:names': power 'tags:edit:names': power

View file

@ -4,5 +4,5 @@ cd /opt/app
alembic upgrade head alembic upgrade head
echo "Starting szurubooru API on port ${PORT} - Running on ${THREADS} threads" echo "Starting szurubooru API on port ${PORT}"
exec waitress-serve-3 --port ${PORT} --threads ${THREADS} szurubooru.facade:app exec waitress-serve-3 --port ${PORT} szurubooru.facade:app

7
server/hooks/build Executable file
View file

@ -0,0 +1,7 @@
#!/bin/sh
docker build \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg SOURCE_COMMIT \
--build-arg DOCKER_REPO \
-f $DOCKERFILE_PATH -t $IMAGE_NAME .

19
server/hooks/post_push Executable file
View file

@ -0,0 +1,19 @@
#!/bin/sh
add_tag() {
echo "Also tagging image as ${DOCKER_REPO}:${1}"
docker tag $IMAGE_NAME $DOCKER_REPO:$1
docker push $DOCKER_REPO:$1
}
CLOSEST_VER=$(git describe --tags --abbrev=0)
CLOSEST_MAJOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f1)
CLOSEST_MINOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f2)
add_tag "${CLOSEST_MAJOR_VER}-edge"
add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}-edge"
if git describe --exact-match --abbrev=0 2> /dev/null; then
add_tag "${CLOSEST_MAJOR_VER}"
add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}"
fi

8
server/hooks/test Executable file
View file

@ -0,0 +1,8 @@
#!/bin/sh
set -e
docker run --rm \
-t $(docker build --target testing -q .) \
--color=no szurubooru/
exit $?

View file

@ -1,15 +1,14 @@
alembic>=0.8.5 alembic>=0.8.5
certifi>=2017.11.5
coloredlogs==5.0
heif-image-plugin==0.3.2
numpy>=1.8.2
pillow-avif-plugin~=1.1.0
pillow>=4.3.0
psycopg2-binary>=2.6.1
pyheif==0.6.1
pynacl>=1.2.1
pyRFC3339>=1.0
pytz>=2018.3
pyyaml>=3.11 pyyaml>=3.11
psycopg2-binary>=2.6.1
SQLAlchemy>=1.0.12, <1.4 SQLAlchemy>=1.0.12, <1.4
yt-dlp coloredlogs==5.0
certifi>=2017.11.5
numpy>=1.8.2
pillow>=4.3.0
pynacl>=1.2.1
pytz>=2018.3
pyRFC3339>=1.0
pillow-avif-plugin>=1.1.0
pyheif-pillow-opener>=0.1.0
youtube_dl

View file

@ -91,15 +91,6 @@ def reset_filenames() -> None:
rename_in_dir("posts/custom-thumbnails/") rename_in_dir("posts/custom-thumbnails/")
def regenerate_thumbnails() -> None:
for post in db.session.query(model.Post).all():
print("Generating tumbnail for post %d ..." % post.post_id, end="\r")
try:
postfuncs.generate_post_thumbnail(post)
except Exception:
pass
def main() -> None: def main() -> None:
parser_top = ArgumentParser( parser_top = ArgumentParser(
description="Collection of CLI commands for an administrator to use", description="Collection of CLI commands for an administrator to use",
@ -123,12 +114,6 @@ def main() -> None:
help="reset and rename the content and thumbnail " help="reset and rename the content and thumbnail "
"filenames in case of a lost/changed secret key", "filenames in case of a lost/changed secret key",
) )
parser.add_argument(
"--regenerate-thumbnails",
action="store_true",
help="regenerate the thumbnails for posts if the "
"thumbnail files are missing",
)
command = parser_top.parse_args() command = parser_top.parse_args()
try: try:
@ -138,8 +123,6 @@ def main() -> None:
check_audio() check_audio()
elif command.reset_filenames: elif command.reset_filenames:
reset_filenames() reset_filenames()
elif command.regenerate_thumbnails:
regenerate_thumbnails()
except errors.BaseError as e: except errors.BaseError as e:
print(e, file=stderr) print(e, file=stderr)

View file

@ -72,7 +72,6 @@ def create_post(
source = ctx.get_param_as_string("contentUrl", default="") source = ctx.get_param_as_string("contentUrl", default="")
relations = ctx.get_param_as_int_list("relations", default=[]) relations = ctx.get_param_as_int_list("relations", default=[])
notes = ctx.get_param_as_list("notes", default=[]) notes = ctx.get_param_as_list("notes", default=[])
desc = ctx.get_param_as_string("desc", default="")
flags = ctx.get_param_as_string_list( flags = ctx.get_param_as_string_list(
"flags", default=posts.get_default_flags(content) "flags", default=posts.get_default_flags(content)
) )
@ -86,7 +85,6 @@ def create_post(
posts.update_post_source(post, source) posts.update_post_source(post, source)
posts.update_post_relations(post, relations) posts.update_post_relations(post, relations)
posts.update_post_notes(post, notes) posts.update_post_notes(post, notes)
posts.update_post_desc(post, desc)
posts.update_post_flags(post, flags) posts.update_post_flags(post, flags)
if ctx.has_file("thumbnail"): if ctx.has_file("thumbnail"):
posts.update_post_thumbnail(post, ctx.get_file("thumbnail")) posts.update_post_thumbnail(post, ctx.get_file("thumbnail"))
@ -161,9 +159,6 @@ def update_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response:
if ctx.has_param("notes"): if ctx.has_param("notes"):
auth.verify_privilege(ctx.user, "posts:edit:notes") auth.verify_privilege(ctx.user, "posts:edit:notes")
posts.update_post_notes(post, ctx.get_param_as_list("notes")) posts.update_post_notes(post, ctx.get_param_as_list("notes"))
if ctx.has_param("desc"):
auth.verify_privilege(ctx.user, "posts:edit:desc")
posts.update_post_desc(post, ctx.get_param_as_string("desc"))
if ctx.has_param("flags"): if ctx.has_param("flags"):
auth.verify_privilege(ctx.user, "posts:edit:flags") auth.verify_privilege(ctx.user, "posts:edit:flags")
posts.update_post_flags(post, ctx.get_param_as_string_list("flags")) posts.update_post_flags(post, ctx.get_param_as_string_list("flags"))

View file

@ -21,7 +21,7 @@ def _merge(left: Dict, right: Dict) -> Dict:
return left return left
def _container_config() -> Dict: def _docker_config() -> Dict:
if "TEST_ENVIRONMENT" not in os.environ: if "TEST_ENVIRONMENT" not in os.environ:
for key in ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_HOST"]: for key in ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_HOST"]:
if key not in os.environ: if key not in os.environ:
@ -33,7 +33,7 @@ def _container_config() -> Dict:
"show_sql": int(os.getenv("LOG_SQL", 0)), "show_sql": int(os.getenv("LOG_SQL", 0)),
"data_url": os.getenv("DATA_URL", "data/"), "data_url": os.getenv("DATA_URL", "data/"),
"data_dir": "/data/", "data_dir": "/data/",
"database": "postgresql://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s" "database": "postgres://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s"
% { % {
"user": os.getenv("POSTGRES_USER"), "user": os.getenv("POSTGRES_USER"),
"pass": os.getenv("POSTGRES_PASSWORD"), "pass": os.getenv("POSTGRES_PASSWORD"),
@ -49,15 +49,6 @@ def _file_config(filename: str) -> Dict:
return yaml.load(handle.read(), Loader=yaml.SafeLoader) or {} return yaml.load(handle.read(), Loader=yaml.SafeLoader) or {}
def _running_inside_container() -> bool:
env = os.environ.keys()
return (
os.path.exists("/.dockerenv")
or "KUBERNETES_SERVICE_HOST" in env
or "container" in env # set by lxc/podman
)
def _read_config() -> Dict: def _read_config() -> Dict:
ret = _file_config("config.yaml.dist") ret = _file_config("config.yaml.dist")
if os.path.isfile("config.yaml"): if os.path.isfile("config.yaml"):
@ -66,8 +57,8 @@ def _read_config() -> Dict:
logger.warning( logger.warning(
"'config.yaml' should be a file, not a directory, skipping" "'config.yaml' should be a file, not a directory, skipping"
) )
if _running_inside_container(): if os.path.exists("/.dockerenv"):
ret = _merge(ret, _container_config()) ret = _merge(ret, _docker_config())
return ret return ret

View file

@ -135,7 +135,7 @@ _live_migrations = (
def create_app() -> Callable[[Any, Any], Any]: def create_app() -> Callable[[Any, Any], Any]:
"""Create a WSGI compatible App object.""" """ Create a WSGI compatible App object. """
validate_config() validate_config()
coloredlogs.install(fmt="[%(asctime)-15s] %(name)s %(message)s") coloredlogs.install(fmt="[%(asctime)-15s] %(name)s %(message)s")
if config.config["debug"]: if config.config["debug"]:

View file

@ -25,7 +25,7 @@ RANK_MAP = OrderedDict(
def get_password_hash(salt: str, password: str) -> Tuple[str, int]: def get_password_hash(salt: str, password: str) -> Tuple[str, int]:
"""Retrieve argon2id password hash.""" """ Retrieve argon2id password hash. """
return ( return (
pwhash.argon2id.str( pwhash.argon2id.str(
(config.config["secret"] + salt + password).encode("utf8") (config.config["secret"] + salt + password).encode("utf8")
@ -37,7 +37,7 @@ def get_password_hash(salt: str, password: str) -> Tuple[str, int]:
def get_sha256_legacy_password_hash( def get_sha256_legacy_password_hash(
salt: str, password: str salt: str, password: str
) -> Tuple[str, int]: ) -> Tuple[str, int]:
"""Retrieve old-style sha256 password hash.""" """ Retrieve old-style sha256 password hash. """
digest = hashlib.sha256() digest = hashlib.sha256()
digest.update(config.config["secret"].encode("utf8")) digest.update(config.config["secret"].encode("utf8"))
digest.update(salt.encode("utf8")) digest.update(salt.encode("utf8"))
@ -46,7 +46,7 @@ def get_sha256_legacy_password_hash(
def get_sha1_legacy_password_hash(salt: str, password: str) -> Tuple[str, int]: def get_sha1_legacy_password_hash(salt: str, password: str) -> Tuple[str, int]:
"""Retrieve old-style sha1 password hash.""" """ Retrieve old-style sha1 password hash. """
digest = hashlib.sha1() digest = hashlib.sha1()
digest.update(b"1A2/$_4xVa") digest.update(b"1A2/$_4xVa")
digest.update(salt.encode("utf8")) digest.update(salt.encode("utf8"))
@ -125,7 +125,7 @@ def verify_privilege(user: model.User, privilege_name: str) -> None:
def generate_authentication_token(user: model.User) -> str: def generate_authentication_token(user: model.User) -> str:
"""Generate nonguessable challenge (e.g. links in password reminder).""" """ Generate nonguessable challenge (e.g. links in password reminder). """
assert user assert user
digest = hashlib.md5() digest = hashlib.md5()
digest.update(config.config["secret"].encode("utf8")) digest.update(config.config["secret"].encode("utf8"))

View file

@ -4,10 +4,12 @@ from datetime import datetime
from io import BytesIO from io import BytesIO
from typing import Any, Callable, List, Optional, Set, Tuple from typing import Any, Callable, List, Optional, Set, Tuple
import HeifImagePlugin
import numpy as np import numpy as np
import pillow_avif
from PIL import Image from PIL import Image
import pillow_avif
import pyheif
from pyheif_pillow_opener import register_heif_opener
register_heif_opener()
from szurubooru import config, errors from szurubooru import config, errors
@ -42,7 +44,7 @@ def _preprocess_image(content: bytes) -> NpMatrix:
try: try:
img = Image.open(BytesIO(content)) img = Image.open(BytesIO(content))
return np.asarray(img.convert("L"), dtype=np.uint8) return np.asarray(img.convert("L"), dtype=np.uint8)
except (IOError, ValueError): except IOError:
raise errors.ProcessingError( raise errors.ProcessingError(
"Unable to generate a signature hash " "for this image." "Unable to generate a signature hash " "for this image."
) )

View file

@ -6,9 +6,6 @@ import shlex
import subprocess import subprocess
from io import BytesIO from io import BytesIO
from typing import List from typing import List
import HeifImagePlugin
import pillow_avif
from PIL import Image as PILImage from PIL import Image as PILImage
from szurubooru import errors from szurubooru import errors
@ -20,7 +17,7 @@ logger = logging.getLogger(__name__)
def convert_heif_to_png(content: bytes) -> bytes: def convert_heif_to_png(content: bytes) -> bytes:
img = PILImage.open(BytesIO(content)) img = PILImage.open(BytesIO(content))
img_byte_arr = BytesIO() img_byte_arr = BytesIO()
img.save(img_byte_arr, format="PNG") img.save(img_byte_arr, format='PNG')
return img_byte_arr.getvalue() return img_byte_arr.getvalue()
@ -99,7 +96,7 @@ class Image:
"-f", "-f",
"lavfi", "lavfi",
"-i", "-i",
"color=black:s=%dx%d" % (self.width, self.height), "color=white:s=%dx%d" % (self.width, self.height),
"-i", "-i",
"{path}", "{path}",
"-f", "-f",
@ -279,10 +276,10 @@ class Image:
proc = subprocess.Popen( proc = subprocess.Popen(
cli, cli,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL, stdin=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
) )
out, err = proc.communicate() out, err = proc.communicate(input=self.content)
if proc.returncode != 0: if proc.returncode != 0:
logger.warning( logger.warning(
"Failed to execute ffmpeg command (cli=%r, err=%r)", "Failed to execute ffmpeg command (cli=%r, err=%r)",

View file

@ -36,12 +36,9 @@ def get_mime_type(content: bytes) -> str:
if content[0:4] == b"\x1A\x45\xDF\xA3": if content[0:4] == b"\x1A\x45\xDF\xA3":
return "video/webm" return "video/webm"
if content[4:12] in (b"ftypisom", b"ftypiso5", b"ftypiso6", b"ftypmp42", b"ftypM4V "): if content[4:12] in (b"ftypisom", b"ftypiso5", b"ftypmp42", b"ftypM4V "):
return "video/mp4" return "video/mp4"
if content[4:12] == b"ftypqt ":
return "video/quicktime"
return "application/octet-stream" return "application/octet-stream"
@ -57,7 +54,6 @@ def get_extension(mime_type: str) -> Optional[str]:
"image/heif": "heif", "image/heif": "heif",
"image/heic": "heic", "image/heic": "heic",
"video/mp4": "mp4", "video/mp4": "mp4",
"video/quicktime": "mov",
"video/webm": "webm", "video/webm": "webm",
"application/octet-stream": "dat", "application/octet-stream": "dat",
} }
@ -69,12 +65,7 @@ def is_flash(mime_type: str) -> bool:
def is_video(mime_type: str) -> bool: def is_video(mime_type: str) -> bool:
return mime_type.lower() in ( return mime_type.lower() in ("application/ogg", "video/mp4", "video/webm")
"application/ogg",
"video/mp4",
"video/quicktime",
"video/webm",
)
def is_image(mime_type: str) -> bool: def is_image(mime_type: str) -> bool:
@ -97,7 +88,6 @@ def is_animated_gif(content: bytes) -> bool:
and len(re.findall(pattern, content)) > 1 and len(re.findall(pattern, content)) > 1
) )
def is_heif(mime_type: str) -> bool: def is_heif(mime_type: str) -> bool:
return mime_type.lower() in ( return mime_type.lower() in (
"image/heif", "image/heif",

View file

@ -39,20 +39,13 @@ def download(url: str, use_video_downloader: bool = False) -> bytes:
length_tally = 0 length_tally = 0
try: try:
with urllib.request.urlopen(request) as handle: with urllib.request.urlopen(request) as handle:
while chunk := handle.read(_dl_chunk_size): while (chunk := handle.read(_dl_chunk_size)) :
length_tally += len(chunk) length_tally += len(chunk)
if length_tally > config.config["max_dl_filesize"]: if length_tally > config.config["max_dl_filesize"]:
raise DownloadTooLargeError( raise DownloadTooLargeError(url)
"Download target exceeds maximum. (%d)"
% (config.config["max_dl_filesize"]),
extra_fields={"URL": url},
)
content_buffer += chunk content_buffer += chunk
except urllib.error.HTTPError as ex: except urllib.error.HTTPError as ex:
raise DownloadError( raise DownloadError(url) from ex
"Download target returned HTTP %d. (%s)" % (ex.code, ex.reason),
extra_fields={"URL": url},
) from ex
if ( if (
youtube_dl_error youtube_dl_error
@ -64,7 +57,7 @@ def download(url: str, use_video_downloader: bool = False) -> bytes:
def _get_youtube_dl_content_url(url: str) -> str: def _get_youtube_dl_content_url(url: str) -> str:
cmd = ["yt-dlp", "--format", "best", "--no-playlist"] cmd = ["youtube-dl", "--format", "best", "--no-playlist"]
if config.config["user_agent"]: if config.config["user_agent"]:
cmd.extend(["--user-agent", config.config["user_agent"]]) cmd.extend(["--user-agent", config.config["user_agent"]])
cmd.extend(["--get-url", url]) cmd.extend(["--get-url", url])
@ -76,8 +69,7 @@ def _get_youtube_dl_content_url(url: str) -> str:
) )
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
raise errors.ThirdPartyError( raise errors.ThirdPartyError(
"Could not extract content location from URL.", "Could not extract content location from %s" % (url)
extra_fields={"URL": url},
) from None ) from None

View file

@ -197,7 +197,6 @@ class PostSerializer(serialization.BaseSerializer):
"favoritedBy": self.serialize_favorited_by, "favoritedBy": self.serialize_favorited_by,
"hasCustomThumbnail": self.serialize_has_custom_thumbnail, "hasCustomThumbnail": self.serialize_has_custom_thumbnail,
"notes": self.serialize_notes, "notes": self.serialize_notes,
"desc": self.serialize_desc,
"comments": self.serialize_comments, "comments": self.serialize_comments,
"pools": self.serialize_pools, "pools": self.serialize_pools,
} }
@ -329,9 +328,6 @@ class PostSerializer(serialization.BaseSerializer):
key=lambda x: x["polygon"], key=lambda x: x["polygon"],
) )
def serialize_desc(self) -> Any:
return self.post.desc
def serialize_comments(self) -> Any: def serialize_comments(self) -> Any:
return [ return [
comments.serialize_comment(comment, self.auth_user) comments.serialize_comment(comment, self.auth_user)
@ -783,11 +779,6 @@ def update_post_notes(post: model.Post, notes: Any) -> None:
) )
def update_post_desc(post: model.Post, desc: str) -> None:
assert post
post.desc = desc
def update_post_flags(post: model.Post, flags: List[str]) -> None: def update_post_flags(post: model.Post, flags: List[str]) -> None:
assert post assert post
target_flags = [] target_flags = []

View file

@ -83,12 +83,12 @@ def flip(source: Dict[Any, Any]) -> Dict[Any, Any]:
def is_valid_email(email: Optional[str]) -> bool: def is_valid_email(email: Optional[str]) -> bool:
"""Return whether given email address is valid or empty.""" """ Return whether given email address is valid or empty. """
return not email or re.match(r"^[^@]*@[^@]*\.[^@]*$", email) is not None return not email or re.match(r"^[^@]*@[^@]*\.[^@]*$", email) is not None
class dotdict(dict): class dotdict(dict):
"""dot.notation access to dictionary attributes.""" """ dot.notation access to dictionary attributes. """
def __getattr__(self, attr: str) -> Any: def __getattr__(self, attr: str) -> Any:
return self.get(attr) return self.get(attr)
@ -98,7 +98,7 @@ class dotdict(dict):
def parse_time_range(value: str) -> Tuple[datetime, datetime]: def parse_time_range(value: str) -> Tuple[datetime, datetime]:
"""Return tuple containing min/max time for given text representation.""" """ Return tuple containing min/max time for given text representation. """
one_day = timedelta(days=1) one_day = timedelta(days=1)
one_second = timedelta(seconds=1) one_second = timedelta(seconds=1)
almost_one_day = one_day - one_second almost_one_day = one_day - one_second

View file

@ -7,7 +7,7 @@ from szurubooru.rest.errors import HttpBadRequest
def _authenticate_basic_auth(username: str, password: str) -> model.User: def _authenticate_basic_auth(username: str, password: str) -> model.User:
"""Try to authenticate user. Throw AuthError for invalid users.""" """ Try to authenticate user. Throw AuthError for invalid users. """
user = users.get_user_by_name(username) user = users.get_user_by_name(username)
if not auth.is_valid_password(user, password): if not auth.is_valid_password(user, password):
raise errors.AuthError("Invalid password.") raise errors.AuthError("Invalid password.")
@ -17,7 +17,7 @@ def _authenticate_basic_auth(username: str, password: str) -> model.User:
def _authenticate_token( def _authenticate_token(
username: str, token: str username: str, token: str
) -> Tuple[model.User, model.UserToken]: ) -> Tuple[model.User, model.UserToken]:
"""Try to authenticate user. Throw AuthError for invalid users.""" """ Try to authenticate user. Throw AuthError for invalid users. """
user = users.get_user_by_name(username) user = users.get_user_by_name(username)
user_token = user_tokens.get_by_user_and_token(user, token) user_token = user_tokens.get_by_user_and_token(user, token)
if not auth.is_valid_token(user_token): if not auth.is_valid_token(user_token):
@ -72,7 +72,7 @@ def _get_user(ctx: rest.Context, bump_login: bool) -> Optional[model.User]:
def process_request(ctx: rest.Context) -> None: def process_request(ctx: rest.Context) -> None:
"""Bind the user to request. Update last login time if needed.""" """ Bind the user to request. Update last login time if needed. """
bump_login = ctx.get_param_as_bool("bump-login", default=False) bump_login = ctx.get_param_as_bool("bump-login", default=False)
auth_user = _get_user(ctx, bump_login) auth_user = _get_user(ctx, bump_login)
if auth_user: if auth_user:

View file

@ -29,7 +29,6 @@ def upgrade():
sa.Column("image_width", sa.Integer(), nullable=True), sa.Column("image_width", sa.Integer(), nullable=True),
sa.Column("image_height", sa.Integer(), nullable=True), sa.Column("image_height", sa.Integer(), nullable=True),
sa.Column("flags", sa.Integer(), nullable=False), sa.Column("flags", sa.Integer(), nullable=False),
sa.Column("description", sa.UnicodeText, nullable=True, default=""),
sa.Column("auto_fav_count", sa.Integer(), nullable=False), sa.Column("auto_fav_count", sa.Integer(), nullable=False),
sa.Column("auto_score", sa.Integer(), nullable=False), sa.Column("auto_score", sa.Integer(), nullable=False),
sa.Column("auto_feature_count", sa.Integer(), nullable=False), sa.Column("auto_feature_count", sa.Integer(), nullable=False),

View file

@ -110,6 +110,7 @@ class PostNote(Base):
post = sa.orm.relationship("Post") post = sa.orm.relationship("Post")
class PostRelation(Base): class PostRelation(Base):
__tablename__ = "post_relation" __tablename__ = "post_relation"
@ -221,7 +222,6 @@ class Post(Base):
canvas_width = sa.Column("image_width", sa.Integer) canvas_width = sa.Column("image_width", sa.Integer)
canvas_height = sa.Column("image_height", sa.Integer) canvas_height = sa.Column("image_height", sa.Integer)
mime_type = sa.Column("mime-type", sa.Unicode(32), nullable=False) mime_type = sa.Column("mime-type", sa.Unicode(32), nullable=False)
desc = sa.Column("description", sa.UnicodeText, nullable=True, default="")
# foreign tables # foreign tables
user = sa.orm.relationship("User") user = sa.orm.relationship("User")

View file

@ -11,7 +11,7 @@ from szurubooru.rest import context, errors, middleware, routes
def _json_serializer(obj: Any) -> str: def _json_serializer(obj: Any) -> str:
"""JSON serializer for objects not serializable by default JSON code""" """ JSON serializer for objects not serializable by default JSON code """
if isinstance(obj, datetime): if isinstance(obj, datetime):
serial = obj.isoformat("T") + "Z" serial = obj.isoformat("T") + "Z"
return serial return serial

View file

@ -122,34 +122,6 @@ def _pool_filter(
)(query, criterion, negated) )(query, criterion, negated)
def _category_filter(
query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool
) -> SaQuery:
assert criterion
# Step 1. find the id for the category
q1 = db.session.query(model.TagCategory.tag_category_id).filter(
model.TagCategory.name == criterion.value
)
# Step 2. find the tags with that category
q2 = db.session.query(model.Tag.tag_id).filter(
model.Tag.category_id.in_(q1)
)
# Step 3. find all posts that have at least one of those tags
q3 = db.session.query(model.PostTag.post_id).filter(
model.PostTag.tag_id.in_(q2)
)
# Step 4. profit
expr = model.Post.post_id.in_(q3)
if negated:
expr = ~expr
return query.filter(expr)
class PostSearchConfig(BaseSearchConfig): class PostSearchConfig(BaseSearchConfig):
def __init__(self) -> None: def __init__(self) -> None:
self.user = None # type: Optional[model.User] self.user = None # type: Optional[model.User]
@ -377,7 +349,6 @@ class PostSearchConfig(BaseSearchConfig):
), ),
), ),
(["pool"], _pool_filter), (["pool"], _pool_filter),
(["category"], _category_filter),
] ]
) )

View file

@ -145,9 +145,8 @@ def test_trying_to_update_without_privileges(
) )
@pytest.mark.parametrize("type", ["suggestions", "implications"])
def test_trying_to_create_tags_without_privileges( def test_trying_to_create_tags_without_privileges(
config_injector, context_factory, tag_factory, user_factory, type config_injector, context_factory, tag_factory, user_factory
): ):
tag = tag_factory(names=["tag"]) tag = tag_factory(names=["tag"])
db.session.add(tag) db.session.add(tag)
@ -166,7 +165,16 @@ def test_trying_to_create_tags_without_privileges(
with pytest.raises(errors.AuthError): with pytest.raises(errors.AuthError):
api.tag_api.update_tag( api.tag_api.update_tag(
context_factory( context_factory(
params={type: ["tag1", "tag2"], "version": 1}, params={"suggestions": ["tag1", "tag2"], "version": 1},
user=user_factory(rank=model.User.RANK_REGULAR),
),
{"tag_name": "tag"},
)
db.session.rollback()
with pytest.raises(errors.AuthError):
api.tag_api.update_tag(
context_factory(
params={"implications": ["tag1", "tag2"], "version": 1},
user=user_factory(rank=model.User.RANK_REGULAR), user=user_factory(rank=model.User.RANK_REGULAR),
), ),
{"tag_name": "tag"}, {"tag_name": "tag"},

View file

@ -43,26 +43,14 @@ def query_logger(pytestconfig):
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
@pytest.fixture(scope="function", autouse=True) @pytest.yield_fixture(scope="function", autouse=True)
def session(query_logger, transacted_postgresql_db): def session(query_logger, postgresql_db):
db.session = transacted_postgresql_db.session
transacted_postgresql_db.create_table(*model.Base.metadata.sorted_tables)
try:
yield transacted_postgresql_db.session
finally:
transacted_postgresql_db.reset_db()
@pytest.fixture(scope="function")
def nontransacted_session(query_logger, postgresql_db):
old_db_session = db.session
db.session = postgresql_db.session db.session = postgresql_db.session
postgresql_db.create_table(*model.Base.metadata.sorted_tables) postgresql_db.create_table(*model.Base.metadata.sorted_tables)
try: try:
yield postgresql_db.session yield postgresql_db.session
finally: finally:
postgresql_db.reset_db() postgresql_db.reset_db()
db.session = old_db_session
@pytest.fixture @pytest.fixture

View file

@ -7,7 +7,6 @@ from szurubooru.func import mime
"input_path,expected_mime_type", "input_path,expected_mime_type",
[ [
("mp4.mp4", "video/mp4"), ("mp4.mp4", "video/mp4"),
("mov.mov", "video/quicktime"),
("webm.webm", "video/webm"), ("webm.webm", "video/webm"),
("flash.swf", "application/x-shockwave-flash"), ("flash.swf", "application/x-shockwave-flash"),
("png.png", "image/png"), ("png.png", "image/png"),
@ -36,7 +35,6 @@ def test_get_mime_type_for_empty_file():
[ [
("video/mp4", "mp4"), ("video/mp4", "mp4"),
("video/webm", "webm"), ("video/webm", "webm"),
("video/quicktime", "mov"),
("application/x-shockwave-flash", "swf"), ("application/x-shockwave-flash", "swf"),
("image/png", "png"), ("image/png", "png"),
("image/jpeg", "jpg"), ("image/jpeg", "jpg"),
@ -72,8 +70,6 @@ def test_is_flash(input_mime_type, expected_state):
("VIDEO/WEBM", True), ("VIDEO/WEBM", True),
("video/mp4", True), ("video/mp4", True),
("VIDEO/MP4", True), ("VIDEO/MP4", True),
("video/quicktime", True),
("VIDEO/QUICKTIME", True),
("video/anything_else", False), ("video/anything_else", False),
("application/ogg", True), ("application/ogg", True),
("not a video", False), ("not a video", False),

View file

@ -1,5 +1,3 @@
import os
import pytest import pytest
from szurubooru import errors from szurubooru import errors
@ -18,9 +16,6 @@ def inject_config(tmpdir, config_injector):
) )
@pytest.mark.skipif(
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
)
def test_download(): def test_download():
url = "http://info.cern.ch/hypertext/WWW/TheProject.html" url = "http://info.cern.ch/hypertext/WWW/TheProject.html"
@ -67,9 +62,6 @@ def test_download():
assert actual_content == expected_content assert actual_content == expected_content
@pytest.mark.skipif(
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"url", "url",
[ [
@ -82,9 +74,6 @@ def test_too_large_download(url):
net.download(url, use_video_downloader=True) net.download(url, use_video_downloader=True)
@pytest.mark.skipif(
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"url,expected_sha1", "url,expected_sha1",
[ [
@ -107,9 +96,6 @@ def test_content_download(url, expected_sha1):
assert get_sha1(actual_content) == expected_sha1 assert get_sha1(actual_content) == expected_sha1
@pytest.mark.skipif(
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
)
def test_bad_content_downlaod(): def test_bad_content_downlaod():
url = "http://info.cern.ch/hypertext/WWW/TheProject.html" url = "http://info.cern.ch/hypertext/WWW/TheProject.html"
with pytest.raises(errors.ThirdPartyError): with pytest.raises(errors.ThirdPartyError):
@ -122,13 +108,11 @@ def test_no_webhooks(config_injector):
assert len(res) == 0 assert len(res) == 0
@pytest.mark.skipif(
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"webhook,status_code", "webhook,status_code",
[ [
("https://postman-echo.com/post", 200), ("https://postman-echo.com/post", 200),
("http://localhost/", 400),
("https://postman-echo.com/get", 400), ("https://postman-echo.com/get", 400),
], ],
) )
@ -137,9 +121,6 @@ def test_single_webhook(config_injector, webhook, status_code):
assert ret == status_code assert ret == status_code
@pytest.mark.skipif(
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
)
def test_multiple_webhooks(config_injector): def test_multiple_webhooks(config_injector):
config_injector( config_injector(
{ {

View file

@ -1,7 +1,7 @@
from datetime import datetime from datetime import datetime
from unittest.mock import patch from unittest.mock import patch
import pytest # noqa: F401 import pytest
from szurubooru import db, model from szurubooru import db, model
from szurubooru.func import snapshots, users from szurubooru.func import snapshots, users
@ -144,6 +144,46 @@ def test_create(tag_factory, user_factory):
assert results[0].data == "mocked" assert results[0].data == "mocked"
def test_modify_saves_non_empty_diffs(post_factory, user_factory):
if "sqlite" in db.session.get_bind().driver:
pytest.xfail(
"SQLite doesn't support transaction isolation, "
"which is required to retrieve original entity"
)
post = post_factory()
post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text="old")]
user = user_factory()
db.session.add_all([post, user])
db.session.commit()
post.source = "new source"
post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text="new")]
db.session.flush()
with patch("szurubooru.func.snapshots._post_to_webhooks"):
snapshots.modify(post, user)
db.session.flush()
results = db.session.query(model.Snapshot).all()
assert len(results) == 1
assert results[0].data == {
"type": "object change",
"value": {
"source": {
"type": "primitive change",
"old-value": None,
"new-value": "new source",
},
"notes": {
"type": "list change",
"removed": [
{"polygon": [[0, 0], [0, 1], [1, 1]], "text": "old"}
],
"added": [
{"polygon": [[0, 0], [0, 1], [1, 1]], "text": "new"}
],
},
},
}
def test_modify_doesnt_save_empty_diffs(tag_factory, user_factory): def test_modify_doesnt_save_empty_diffs(tag_factory, user_factory):
tag = tag_factory(names=["dummy"]) tag = tag_factory(names=["dummy"])
user = user_factory() user = user_factory()

View file

@ -1,59 +0,0 @@
from unittest.mock import patch
import pytest
from szurubooru import db, model
from szurubooru.func import snapshots
@pytest.fixture(autouse=True)
def session(query_logger, postgresql_db):
"""
Override db session for this specific test section only
"""
db.session = postgresql_db.session
postgresql_db.create_table(*model.Base.metadata.sorted_tables)
try:
yield postgresql_db.session
finally:
postgresql_db.reset_db()
def test_modify_saves_non_empty_diffs(post_factory, user_factory):
if "sqlite" in db.session.get_bind().driver:
pytest.xfail(
"SQLite doesn't support transaction isolation, "
"which is required to retrieve original entity"
)
post = post_factory()
post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text="old")]
user = user_factory()
db.session.add_all([post, user])
db.session.commit()
post.source = "new source"
post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text="new")]
db.session.flush()
with patch("szurubooru.func.snapshots._post_to_webhooks"):
snapshots.modify(post, user)
db.session.flush()
results = db.session.query(model.Snapshot).all()
assert len(results) == 1
assert results[0].data == {
"type": "object change",
"value": {
"source": {
"type": "primitive change",
"old-value": None,
"new-value": "new source",
},
"notes": {
"type": "list change",
"removed": [
{"polygon": [[0, 0], [0, 1], [1, 1]], "text": "old"}
],
"added": [
{"polygon": [[0, 0], [0, 1], [1, 1]], "text": "new"}
],
},
},
}

View file

@ -107,16 +107,17 @@ def test_update_category_name_reusing_other_name(
tag_categories.update_category_name(category, "NAME") tag_categories.update_category_name(category, "NAME")
@pytest.mark.parametrize("name", ["name", "NAME"])
def test_update_category_name_reusing_own_name( def test_update_category_name_reusing_own_name(
config_injector, tag_category_factory, name config_injector, tag_category_factory
): ):
config_injector({"tag_category_name_regex": ".*"}) config_injector({"tag_category_name_regex": ".*"})
category = tag_category_factory(name="name") for name in ["name", "NAME"]:
db.session.add(category) category = tag_category_factory(name="name")
db.session.flush() db.session.add(category)
tag_categories.update_category_name(category, name) db.session.flush()
assert category.name == name tag_categories.update_category_name(category, name)
assert category.name == name
db.session.rollback()
def test_update_category_color_with_empty_string(tag_category_factory): def test_update_category_color_with_empty_string(tag_category_factory):

View file

@ -513,14 +513,15 @@ def test_update_tag_names_trying_to_use_taken_name(
tags.update_tag_names(tag, ["A"]) tags.update_tag_names(tag, ["A"])
@pytest.mark.parametrize("name", list("aA")) def test_update_tag_names_reusing_own_name(config_injector, tag_factory):
def test_update_tag_names_reusing_own_name(config_injector, tag_factory, name):
config_injector({"tag_name_regex": "^[a-zA-Z]*$"}) config_injector({"tag_name_regex": "^[a-zA-Z]*$"})
tag = tag_factory(names=["a"]) for name in list("aA"):
db.session.add(tag) tag = tag_factory(names=["a"])
db.session.flush() db.session.add(tag)
tags.update_tag_names(tag, [name]) db.session.flush()
assert [tag_name.name for tag_name in tag.names] == [name] tags.update_tag_names(tag, [name])
assert [tag_name.name for tag_name in tag.names] == [name]
db.session.rollback()
def test_update_tag_names_changing_primary_name(config_injector, tag_factory): def test_update_tag_names_changing_primary_name(config_injector, tag_factory):
@ -532,6 +533,7 @@ def test_update_tag_names_changing_primary_name(config_injector, tag_factory):
db.session.flush() db.session.flush()
db.session.refresh(tag) db.session.refresh(tag)
assert [tag_name.name for tag_name in tag.names] == ["b", "a"] assert [tag_name.name for tag_name in tag.names] == ["b", "a"]
db.session.rollback()
@pytest.mark.parametrize("attempt", ["name", "NAME", "alias", "ALIAS"]) @pytest.mark.parametrize("attempt", ["name", "NAME", "alias", "ALIAS"])

View file

@ -136,6 +136,8 @@ def test_escaping(
) )
db.session.flush() db.session.flush()
if db_driver and db.session.get_bind().driver != db_driver:
pytest.xfail()
if expected_pool_names is None: if expected_pool_names is None:
with pytest.raises(errors.SearchError): with pytest.raises(errors.SearchError):
executor.execute(input, offset=0, limit=100) executor.execute(input, offset=0, limit=100)

View file

@ -863,55 +863,3 @@ def test_tumbleweed(
db.session.flush() db.session.flush()
verify_unpaged("special:tumbleweed", [4]) verify_unpaged("special:tumbleweed", [4])
verify_unpaged("-special:tumbleweed", [1, 2, 3]) verify_unpaged("-special:tumbleweed", [1, 2, 3])
@pytest.mark.parametrize(
"input,expected_post_ids",
[
("category:cat1", [1, 2, 3]),
("category:cat2", [3, 4]),
],
)
def test_search_by_tag_category(
verify_unpaged,
post_factory,
tag_factory,
tag_category_factory,
input,
expected_post_ids,
):
cat1 = tag_category_factory(name="cat1")
cat2 = tag_category_factory(name="cat2")
tag1 = tag_factory(names=["t1"], category=cat1)
tag2 = tag_factory(names=["t2"], category=cat1)
tag3 = tag_factory(names=["t3"], category=cat2)
post1 = post_factory(id=1)
post1.tags.append(tag1)
post2 = post_factory(id=2)
post2.tags.append(tag2)
post3 = post_factory(id=3)
post3.tags.append(tag1)
post3.tags.append(tag3)
post4 = post_factory(id=4)
post4.tags.append(tag3)
post5 = post_factory(id=5)
db.session.add_all(
[
tag1,
tag2,
tag3,
post1,
post2,
post3,
post4,
post5,
]
)
db.session.flush()
verify_unpaged(input, expected_post_ids)

View file

@ -134,6 +134,8 @@ def test_escaping(executor, tag_factory, input, expected_tag_names, db_driver):
) )
db.session.flush() db.session.flush()
if db_driver and db.session.get_bind().driver != db_driver:
pytest.xfail()
if expected_tag_names is None: if expected_tag_names is None:
with pytest.raises(errors.SearchError): with pytest.raises(errors.SearchError):
executor.execute(input, offset=0, limit=100) executor.execute(input, offset=0, limit=100)