Merge branch 'master' into style-changes

This commit is contained in:
Earlopain 2021-06-02 14:28:22 +02:00 committed by GitHub
commit eb1f432fc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
105 changed files with 2238 additions and 1705 deletions

View File

@ -1 +1 @@
2.7.0
2.7.3

View File

@ -16,40 +16,40 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (6.1.3.1)
actionpack (= 6.1.3.1)
activesupport (= 6.1.3.1)
actioncable (6.1.3.2)
actionpack (= 6.1.3.2)
activesupport (= 6.1.3.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.3.1)
actionpack (= 6.1.3.1)
activejob (= 6.1.3.1)
activerecord (= 6.1.3.1)
activestorage (= 6.1.3.1)
activesupport (= 6.1.3.1)
actionmailbox (6.1.3.2)
actionpack (= 6.1.3.2)
activejob (= 6.1.3.2)
activerecord (= 6.1.3.2)
activestorage (= 6.1.3.2)
activesupport (= 6.1.3.2)
mail (>= 2.7.1)
actionmailer (6.1.3.1)
actionpack (= 6.1.3.1)
actionview (= 6.1.3.1)
activejob (= 6.1.3.1)
activesupport (= 6.1.3.1)
actionmailer (6.1.3.2)
actionpack (= 6.1.3.2)
actionview (= 6.1.3.2)
activejob (= 6.1.3.2)
activesupport (= 6.1.3.2)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.3.1)
actionview (= 6.1.3.1)
activesupport (= 6.1.3.1)
actionpack (6.1.3.2)
actionview (= 6.1.3.2)
activesupport (= 6.1.3.2)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.3.1)
actionpack (= 6.1.3.1)
activerecord (= 6.1.3.1)
activestorage (= 6.1.3.1)
activesupport (= 6.1.3.1)
actiontext (6.1.3.2)
actionpack (= 6.1.3.2)
activerecord (= 6.1.3.2)
activestorage (= 6.1.3.2)
activesupport (= 6.1.3.2)
nokogiri (>= 1.8.5)
actionview (6.1.3.1)
activesupport (= 6.1.3.1)
actionview (6.1.3.2)
activesupport (= 6.1.3.2)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -59,26 +59,26 @@ GEM
activemodel (>= 4.1, < 6.2)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (6.1.3.1)
activesupport (= 6.1.3.1)
activejob (6.1.3.2)
activesupport (= 6.1.3.2)
globalid (>= 0.3.6)
activemodel (6.1.3.1)
activesupport (= 6.1.3.1)
activemodel (6.1.3.2)
activesupport (= 6.1.3.2)
activemodel-serializers-xml (1.0.2)
activemodel (> 5.x)
activesupport (> 5.x)
builder (~> 3.1)
activerecord (6.1.3.1)
activemodel (= 6.1.3.1)
activesupport (= 6.1.3.1)
activestorage (6.1.3.1)
actionpack (= 6.1.3.1)
activejob (= 6.1.3.1)
activerecord (= 6.1.3.1)
activesupport (= 6.1.3.1)
activerecord (6.1.3.2)
activemodel (= 6.1.3.2)
activesupport (= 6.1.3.2)
activestorage (6.1.3.2)
actionpack (= 6.1.3.2)
activejob (= 6.1.3.2)
activerecord (= 6.1.3.2)
activesupport (= 6.1.3.2)
marcel (~> 1.0.0)
mini_mime (~> 1.0.2)
activesupport (6.1.3.1)
activesupport (6.1.3.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -184,7 +184,7 @@ GEM
listen (3.5.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.9.0)
loofah (2.9.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -211,7 +211,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0225)
mini_mime (1.0.3)
mini_portile2 (2.5.0)
mini_portile2 (2.5.1)
minitest (5.14.4)
minitest-ci (3.4.0)
minitest (>= 5.0.6)
@ -234,10 +234,10 @@ GEM
netrc (0.11.0)
newrelic_rpm (6.15.0)
nio4r (2.5.7)
nokogiri (1.11.2)
nokogiri (1.11.3)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nokogiri (1.11.2-x64-mingw32)
nokogiri (1.11.3-x64-mingw32)
racc (~> 1.4)
nokogumbo (2.0.5)
nokogiri (~> 1.8, >= 1.8.4)
@ -266,29 +266,29 @@ GEM
rack-test (1.1.0)
rack (>= 1.0, < 3)
radix62 (1.0.1)
rails (6.1.3.1)
actioncable (= 6.1.3.1)
actionmailbox (= 6.1.3.1)
actionmailer (= 6.1.3.1)
actionpack (= 6.1.3.1)
actiontext (= 6.1.3.1)
actionview (= 6.1.3.1)
activejob (= 6.1.3.1)
activemodel (= 6.1.3.1)
activerecord (= 6.1.3.1)
activestorage (= 6.1.3.1)
activesupport (= 6.1.3.1)
rails (6.1.3.2)
actioncable (= 6.1.3.2)
actionmailbox (= 6.1.3.2)
actionmailer (= 6.1.3.2)
actionpack (= 6.1.3.2)
actiontext (= 6.1.3.2)
actionview (= 6.1.3.2)
activejob (= 6.1.3.2)
activemodel (= 6.1.3.2)
activerecord (= 6.1.3.2)
activestorage (= 6.1.3.2)
activesupport (= 6.1.3.2)
bundler (>= 1.15.0)
railties (= 6.1.3.1)
railties (= 6.1.3.2)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
railties (6.1.3.1)
actionpack (= 6.1.3.1)
activesupport (= 6.1.3.1)
railties (6.1.3.2)
actionpack (= 6.1.3.2)
activesupport (= 6.1.3.2)
method_source
rake (>= 0.8.7)
thor (~> 1.0)
@ -477,4 +477,4 @@ DEPENDENCIES
whenever
BUNDLED WITH
2.1.2
2.1.4

View File

@ -1,182 +0,0 @@
#!/bin/bash
# Run: curl -L -s https://raw.githubusercontent.com/zwagoth/e621ng/master/INSTALL.debian -o install.sh ; chmod +x install.sh ; ./install.sh
export RUBY_VERSION=2.5.3
export GITHUB_INSTALL_SCRIPTS=https://raw.githubusercontent.com/zwagoth/e621ng/master/script/install
export VIPS_VERSION=8.7.0
if [[ "$(whoami)" != "root" ]] ; then
echo "You must run this script as root"
exit 1
fi
verlte() {
[ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ]
}
verlt() {
[ "$1" = "$2" ] && return 1 || verlte $1 $2
}
echo "* DANBOORU INSTALLATION SCRIPT"
echo "*"
echo "* This script will install all the necessary packages to run Danbooru on a "
echo "* Debian server."
echo
echo -n "* Enter the hostname for this server (ex: danbooru.donmai.us): "
read HOSTNAME
if [[ -z "$HOSTNAME" ]] ; then
echo "* Must enter a hostname"
exit 1
fi
echo -n "* Enter the VLAN IP address for this server (ex: 172.16.0.1, enter nothing to skip): "
read VLAN_IP_ADDR
# Install packages
echo "* Installing packages..."
if [ -n "$(uname -a | grep Ubuntu)" ] ; then
LIBSSL_DEV_PKG=libssl-dev
else
LIBSSL_DEV_PKG=$( verlt `lsb_release -sr` 9.0 && echo libssl-dev || echo libssl1.0-dev )
fi
apt-get update
apt-get -y install $LIBSSL_DEV_PKG build-essential automake libxml2-dev libxslt-dev ncurses-dev sudo libreadline-dev flex bison ragel memcached libmemcached-dev git curl libcurl4-openssl-dev sendmail-bin sendmail nginx ssh coreutils ffmpeg mkvtoolnix
apt-get -y install libpq-dev postgresql-client
apt-get -y install liblcms2-dev libjpeg-turbo8-dev libexpat1-dev libgif-dev libpng-dev libexif-dev
# vrack specific stuff
if [ -n "$VLAN_IP_ADDR" ] ; then
apt-get -y install vlan
modprobe 8021q
echo "8021q" >> /etc/modules
vconfig add eno2 99
ip addr add $VLAN_IP_ADDR/24 dev eno2.99
ip link set up eno2.99
curl -L -s $GITHUB_INSTALL_SCRIPTS/vrack-cfg.yaml -o /etc/netplan/01-netcfg.yaml
fi
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
curl -sSL https://deb.nodesource.com/setup_10.x | sudo -E bash -
apt-get update
apt-get -y install nodejs yarn
apt-get remove cmdtest
if [ $? -ne 0 ]; then
echo "* Error installing packages; aborting"
exit 1
fi
# compile and install libvips (the version in apt is too old)
cd /tmp
wget -q https://github.com/libvips/libvips/releases/download/v$VIPS_VERSION/vips-$VIPS_VERSION.tar.gz
tar xzf vips-$VIPS_VERSION.tar.gz
cd vips-$VIPS_VERSION
./configure --prefix=/usr
make install
ldconfig
# Create user account
useradd -m danbooru
chsh -s /bin/bash danbooru
usermod -G danbooru,sudo danbooru
# Set up Postgres
export PG_VERSION=`pg_config --version | egrep -o '[0-9]{1,}\.[0-9]{1,}'`
if verlte 9.5 $PG_VERSION ; then
# only do this on postgres 9.5 and above
git clone https://github.com/r888888888/test_parser.git /tmp/test_parser
cd /tmp/test_parser
make install
fi
# Install rbenv
echo "* Installing rbenv..."
cd /tmp
sudo -i -u danbooru git clone git://github.com/sstephenson/rbenv.git ~danbooru/.rbenv
sudo -i -u danbooru touch ~danbooru/.bash_profile
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~danbooru/.bash_profile
echo 'eval "$(rbenv init -)"' >> ~danbooru/.bash_profile
sudo -i -u danbooru mkdir -p ~danbooru/.rbenv/plugins
sudo -i -u danbooru git clone git://github.com/sstephenson/ruby-build.git ~danbooru/.rbenv/plugins/ruby-build
sudo -i -u danbooru bash -l -c "rbenv install $RUBY_VERSION"
sudo -i -u danbooru bash -l -c "rbenv global $RUBY_VERSION"
# Generate secret token and secret key
echo "* Generating secret keys..."
sudo -i -u danbooru mkdir ~danbooru/.danbooru/
openssl rand -hex 32 > ~danbooru/.danbooru/secret_token
openssl rand -hex 32 > ~danbooru/.danbooru/session_secret_key
chown danbooru:danbooru ~danbooru/.danbooru/*
chmod 600 ~danbooru/.danbooru/*
# Install gems
echo "* Installing gems..."
sudo -i -u danbooru bash -l -c 'gem install --no-ri --no-rdoc bundler'
echo "* Install configuration scripts..."
# Update PostgreSQL
curl -L -s $GITHUB_INSTALL_SCRIPTS/postgresql_hba_conf -o /etc/postgresql/$PG_VERSION/main/pg_hba.conf
/etc/init.d/postgresql restart
sudo -u postgres createuser -s danbooru
sudo -i -u danbooru createdb danbooru2
# Setup nginx
curl -L -s $GITHUB_INSTALL_SCRIPTS/nginx.danbooru.conf -o /etc/nginx/sites-enabled/danbooru.conf
sed -i -e "s/__hostname__/$HOSTNAME/g" /etc/nginx/sites-enabled/danbooru.conf
/etc/init.d/nginx restart
# Setup logrotate
curl -L -s $GITHUB_INSTALL_SCRIPTS/danbooru_logrotate_conf -o /etc/logrotate.d/danbooru.conf
# Setup danbooru account
echo "* Enter a new password for the danbooru account"
passwd danbooru
echo "* Setting up SSH keys for the danbooru account"
sudo -i -u danbooru ssh-keygen
mkdir -p /var/www/danbooru2/shared/config
mkdir -p /var/www/danbooru2/shared/data
mkdir -p /var/www/danbooru2/shared/data/preview
mkdir -p /var/www/danbooru2/shared/data/sample
chown -R danbooru:danbooru /var/www/danbooru2
curl -L -s $GITHUB_INSTALL_SCRIPTS/database.yml.templ -o /var/www/danbooru2/shared/config/database.yml
curl -L -s $GITHUB_INSTALL_SCRIPTS/danbooru_local_config.rb.templ -o /var/www/danbooru2/shared/config/danbooru_local_config.rb
echo "* Almost done! You are now ready to deploy Danbooru onto this server."
echo "* Log into Github and fork https://github.com/zwagoth/e621ng into"
echo "* your own repository. Clone your fork onto your local development"
echo "* machine and modify the following files:"
echo "*"
echo "* config/deploy.rb (github repo url)"
echo "* config/deploy/production.rb (servers and users)"
echo "* config/unicorn/production.rb (users)"
echo "* config/application.rb (time zone)"
echo "*"
echo "* On the remote server you will want to modify this file:"
echo "*"
echo "* /var/www/danbooru2/shared/config/danbooru_local_config.rb"
echo "*"
read -p "Press [enter] to continue..."
echo "* Commit your changes and push them to your fork. You are now ready to"
echo "* deploy with the following command:"
echo "*"
echo "* bundle exec capistrano production deploy"
echo "*"
echo "* You can also run a server locally without having to deal with deploys"
echo "* by running the following command:"
echo "*"
echo "* bundle install"
echo "* bundle exec rake db:create db:migrate"
echo "* bundle exec rails server"
echo "*"
echo "* This will start a web process running on port 3000 that you can"
echo "* connect to. This is useful for development and testing purposes."
echo "* If something breaks post about it on the Danbooru Github. Good luck!"

View File

@ -1,114 +0,0 @@
#!/bin/bash
# Run: curl -L -s https://raw.githubusercontent.com/zwagoth/e621ng/master/INSTALL.debian -o install.sh ; chmod +x install.sh ; ./install.sh
export RUBY_VERSION=2.5.3
export GITHUB_INSTALL_SCRIPTS=https://raw.githubusercontent.com/zwagoth/e621ng/master/script/install
export VIPS_VERSION=8.7.0
verlte() {
[ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ]
}
verlt() {
[ "$1" = "$2" ] && return 1 || verlte $1 $2
}
# Install packages
echo "* Installing packages..."
if [ -n "$(uname -a | grep Ubuntu)" ] ; then
LIBSSL_DEV_PKG=libssl-dev
else
LIBSSL_DEV_PKG=$( verlt `lsb_release -sr` 9.0 && echo libssl-dev || echo libssl1.0-dev )
fi
sudo apt-get update
sudo apt-get -y install $LIBSSL_DEV_PKG build-essential automake libxml2-dev libxslt-dev ncurses-dev sudo libreadline-dev flex bison ragel memcached libmemcached-dev git curl libcurl4-openssl-dev sendmail-bin sendmail nginx ssh coreutils ffmpeg mkvtoolnix
sudo apt-get -y install libpq-dev postgresql-client postgresql postgresql-server-dev-10
sudo apt-get -y install liblcms2-dev libjpeg-turbo8-dev libexpat1-dev libgif-dev libpng-dev libexif-dev
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
curl -sSL https://deb.nodesource.com/setup_10.x | sudo -E bash -
sudo apt-get update
sudo apt-get -y install nodejs yarn
sudo apt-get remove cmdtest
if [ $? -ne 0 ]; then
echo "* Error installing packages; aborting"
exit 1
fi
# compile and install libvips (the version in apt is too old)
cd /tmp
wget -q https://github.com/libvips/libvips/releases/download/v$VIPS_VERSION/vips-$VIPS_VERSION.tar.gz
tar xzf vips-$VIPS_VERSION.tar.gz
cd vips-$VIPS_VERSION
./configure --prefix=/usr
sudo make install
sudo ldconfig
# Set up Postgres
export PG_VERSION=`pg_config --version | egrep -o '[0-9]{1,}\.[0-9]{1,}'`
if verlte 9.5 $PG_VERSION ; then
# only do this on postgres 9.5 and above
git clone https://github.com/r888888888/test_parser.git /tmp/test_parser
cd /tmp/test_parser
sudo make install
fi
# Install rbenv
echo "* Installing rbenv..."
cd /tmp
git clone git://github.com/sstephenson/rbenv.git ~/.rbenv
touch ~/.bash_profile
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
mkdir -p ~/.rbenv/plugins
git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
rbenv install $RUBY_VERSION
rbenv global $RUBY_VERSION
# Generate secret token and secret key
echo "* Generating secret keys..."
mkdir ~/.danbooru/
openssl rand -hex 32 > ~/.danbooru/secret_token
openssl rand -hex 32 > ~/.danbooru/session_secret_key
chmod 600 ~/.danbooru/*
# Install gems
echo "* Installing gems..."
gem install --no-ri --no-rdoc bundler
echo "* Install configuration scripts..."
# Update PostgreSQL
curl -L -s $GITHUB_INSTALL_SCRIPTS/postgresql_hba_conf -o /etc/postgresql/$PG_VERSION/main/pg_hba.conf
/etc/init.d/postgresql restart
sudo -u postgres createuser -s danbooru
sudo -u postgres createdb danbooru2
# Setup nginx
curl -L -s $GITHUB_INSTALL_SCRIPTS/nginx.danbooru.conf -o /etc/nginx/sites-enabled/danbooru.conf
sed -i -e "s/__hostname__/$HOSTNAME/g" /etc/nginx/sites-enabled/danbooru.conf
/etc/init.d/nginx restart
echo "* Clone the repo into your home folder."
echo "* Copy the database.yml and danbooru_local_config.rb templates from the "
echo "* scripts/install folder into the config folder and edit them to your needs."
echo "* Remember to add 'user: danbooru' to the database yml file for development roles."
echo "* You can then run a server locally without having to deal with deploys"
echo "* by running the following command:"
echo "*"
echo "* bundle install"
echo "* yarn install"
echo "* RAILS_ENV=development bundle exec rake db:migrate"
echo "* bundle exec rails server"
echo "*"
echo "* Run RAILS_ENV=development bundle exec rake db:seed after you have"
echo "* created a new user!"
echo "*"
echo "* This will start a web process running on port 3000 that you can"
echo "* connect to. This is useful for development and testing purposes."
echo "* If something breaks post about it on the Github. Good luck!"

View File

@ -7,18 +7,17 @@
1. Download and install the prerequisites
2. Open Command Prompt/Terminal and run the following commands:
```
vagrant plugin install vagrant-hostmanager
vagrant plugin install vagrant-bindfs
vagrant plugin install vagrant-hostmanager
vagrant plugin install vagrant-bindfs
vagrant plugin install vagrant-vbguest
```
3. Download and extract the repo
4. `cd` into the repo using Command Prompt/Terminal
5. Run the following command:
`vagrant up`
6. Once vagrant exists with an error, execute `vagrant up` again (Workaround to install the linux headers required by the guest additions)
7. This would be a good time to rewatch your favorite TV series installment, cook & have breakfast/lunch/dinner, walk the dog, clean your room, etc.<br>
6. This would be a good time to rewatch your favorite TV series installment, cook & have breakfast/lunch/dinner, walk the dog, clean your room, etc.<br>
By the time you get back the install will surely have completed.<sup>1</sup>
8. To confirm the installation worked, open the web browser of your choice and enter `http://e621.lc` into the address bar and see if the website loads correctly.
7. To confirm the installation worked, open the web browser of your choice and enter `http://e621.local` into the address bar and see if the website loads correctly.
<sub><sup>1</sup> If the install did not finish by the time an activity is complete please select another activity to avoid crippling boredom.</sub>
@ -26,28 +25,20 @@ By the time you get back the install will surely have completed.<sup>1</sup>
In case the VM doesn't start with the error `VT-x not available` and the error description `WHvCapabilityCodeHypervisorPresent is FALSE!` the the following solution should resolve the issue:
This error usually occours, when the `Hyper-V` or the `Windows Hypervisor Platform` features are activated. <br/>
This error usually occurs, when the `Hyper-V` or the `Windows Hypervisor Platform` features are activated. <br/>
To deactivate these features, press the windows key and enter `Turn Windows features on or off`, here you can deactivate both features by unchecking their respective checkboxes.
Don't forget to restart the computer after deactivating the features.
## Installation
It is recommended that you install Danbooru on a Debian-based system
since most of the required packages are available on APT. Danbooru
has been successfully installed on Fedora, CentOS, FreeBSD, and OS X.
The INSTALL.debian install script is straightforward and should be
simple to adapt for other platforms.
Installation follows the same steps as the vagrant setup script. Ubuntu 20.04 is the current installation target.
There is no script that performs these steps for you, as you need to split them up to match your infrastructure.
Running a single machine install in production is possible, but is likely to be somewhat sluggish due to contention in disk between postgresql and elasticsearch.
Minimum RAM is 4GB. You will need to adjust values in config files to match how much RAM is available.
If you are targeting more than a hundred thousand posts and reasonable user volumes, you probably want to procure yourself a database server. See tuning guides for postgresql and elasticsearch for help planning these requirements.
For best performance, you will need at least 256MB of RAM for
PostgreSQL and Rails. The memory requirement will grow as your
database gets bigger.
On production Danbooru uses PostgreSQL 9.4, but any 9.x release should
work.
Use your operating system's package management system whenever
possible. This will simplify the process of installing init scripts,
which will not always happen when compiling from source.
There are some forks that contain full docker setups. If you are looking for a Docker deployment and don't want to wait for this repo to slowly get there, look into those.
## Troubleshooting
@ -69,55 +60,9 @@ debug your Nginx configuration file.
4) Check all log files.
## Services
Danbooru employs numerous external services to delegate some
functionality.
For development purposes, you can just run mocked version of these
services. They're available in `scrtip/mock_services` and can be started
automatically using Foreman and the provided Procfile.
### Amazon Web Services
In order to enable the following features, you will need an AWS SQS
account:
* Pool versions
* Post versions
* IQDB
* Saved searches
* Related tags
### Google APIs
The following features requires a Google API account:
* Bulk revert
* Post versions report
### IQDB Service
IQDB integration is delegated to the [IQDBS service](https://github.com/r888888888/iqdbs).
### Archive Service
In order to access versioned data for pools and posts you will
need to install and configure the [Archives service](https://github.com/r888888888/archives).
### Reportbooru Service
The following features are delegated to the [Reportbooru service](https://github.com/r888888888/reportbooru):
* Related tags
* Missed searches report
* Popular searches report
* Favorite searches
* Upload trend graphs
### Recommender Service
Post recommendations require the [Recommender service](https://github.com/r888888888/recommender).
IQDB integration is delegated to the [IQDBS service](https://github.com/zwagoth/iqdbs).
### Cropped Thumbnails

3
Vagrantfile vendored
View File

@ -25,7 +25,7 @@ Vagrant.configure('2') do |config|
# end
VAGRANT_COMMAND = ARGV[0]
# config.ssh.username = 'danbooru' if VAGRANT_COMMAND == 'ssh'
config.ssh.username = 'danbooru' if VAGRANT_COMMAND == 'ssh'
config.vm.define 'default' do |node|
node.vm.hostname = 'e621.local'
@ -34,4 +34,3 @@ Vagrant.configure('2') do |config|
config.vm.provision 'shell', path: 'vagrant/install.sh'
end

View File

@ -0,0 +1,44 @@
module UserWarnable
extend ActiveSupport::Concern
WARNING_TYPES = {
'warning' => 1,
'record' => 2,
'ban' => 3,
'unmark' => nil
}
included do
scope :user_warned, -> { where('warning_type IS NOT NULL') }
validates :warning_type, inclusion: { :in => [1, 2, 3, nil] }
end
def user_warned!(type, user=CurrentUser.id)
unless WARNING_TYPES.has_key?(type)
errors.add(:warning_type, 'invalid warning type')
return
end
update({warning_type: WARNING_TYPES[type], warning_user_id: user})
end
def remove_user_warning!
update_columns({warning_type: nil, warning_user_id: nil})
end
def was_warned?
!warning_type.nil?
end
def warning_type_string
case warning_type
when 1
"User received a warning for the contents of this message."
when 2
"User received a record for the contents of this message."
when 3
"User was banned for the contents of this message."
else
"[This is a bug with the website. Woo!]"
end
end
end

View File

@ -0,0 +1,35 @@
module Admin
class StaffNotesController < ApplicationController
before_action :moderator_only
respond_to :html
def index
@user = User.where('id = ?', params[:user_id]).first
@notes = StaffNote.search(search_params.merge({user_id: params[:user_id]})).includes(:user, :creator).paginate(params[:page])
respond_with(@notes)
end
def new
@user = User.find(params[:user_id])
@staff_note = StaffNote.new(note_params)
respond_with(@note)
end
def create
@user = User.find(params[:user_id])
@staff_note = StaffNote.create(note_params.merge({creator: CurrentUser.user, user_id: @user.id}))
flash[:notice] = @staff_note.valid? ? "Staff Note added" : @staff_note.errors.full_messages.join("; ")
respond_with(@staff_note) do |format|
format.html do
redirect_back fallback_location: admin_staff_notes_path
end
end
end
private
def note_params
params.fetch(:staff_note, {}).permit([:body])
end
end
end

View File

@ -60,6 +60,8 @@ class ApplicationController < ActionController::Base
end
case exception
when ProcessingError
render_expected_error(400, exception)
when APIThrottled
render_expected_error(429, "Throttled: Too many requests")
when ActiveRecord::QueryCanceled
@ -128,13 +130,24 @@ class ApplicationController < ActionController::Base
DanbooruLogger.log(@exception, expected: @expected)
log = ExceptionLog.add(exception, CurrentUser.ip_addr, {
log_params = {
host: Socket.gethostname,
params: params,
params: request.filtered_parameters,
user_id: CurrentUser.id,
referrer: request.referrer,
user_agent: request.user_agent
}) if !@expected
}
# Required to unwrap exceptions that occur inside template rendering.
new_exception = exception
if exception.respond_to?(:cause) && exception.is_a?(ActionView::Template::Error)
new_exception = exception.cause
end
if new_exception&.is_a?(ActiveRecord::QueryCanceled)
log_params[:sql] = {}
log_params[:sql][:query] = new_exception&.sql || "[NOT FOUND?]"
log_params[:sql][:binds] = new_exception&.binds
end
log = ExceptionLog.add(exception, CurrentUser.ip_addr, log_params) if !@expected
@log_code = log&.code
render "static/error", layout: layout, status: status, formats: format
end

View File

@ -2,7 +2,7 @@ class BlipsController < ApplicationController
class BlipTooOld < Exception ; end
respond_to :html, :json
before_action :member_only, only: [:create, :new, :update, :edit, :hide]
before_action :moderator_only, only: [:unhide, :destroy]
before_action :moderator_only, only: [:unhide, :destroy, :warning]
rescue_from BlipTooOld, with: :blip_too_old
@ -84,6 +84,16 @@ class BlipsController < ApplicationController
end
end
def warning
@blip = Blip.find(params[:id])
if params[:record_type] == 'unmark'
@blip.remove_user_warning!
else
@blip.user_warned!(params[:record_type])
end
respond_with(@blip)
end
private
def search_params

View File

@ -1,7 +1,7 @@
class CommentsController < ApplicationController
respond_to :html, :json
before_action :member_only, :except => [:index, :search, :show]
before_action :moderator_only, only: [:unhide, :destroy]
before_action :moderator_only, only: [:unhide, :destroy, :warning]
skip_before_action :api_check
def index
@ -79,6 +79,16 @@ class CommentsController < ApplicationController
respond_with(@comment)
end
def warning
@comment = Comment.find(params[:id])
if params[:record_type] == 'unmark'
@comment.remove_user_warning!
else
@comment.user_warned!(params[:record_type])
end
respond_with(@comment)
end
private
def index_by_post
tags = params[:tags] || ""

View File

@ -11,10 +11,10 @@ class FavoritesController < ApplicationController
@user = User.find(user_id)
if @user.hide_favorites?
raise User::PrivilegeError.new
raise User::PrivilegeError.new "User has hidden their favorites"
end
@favorite_set = PostSets::Favorites.new(@user, params[:page])
@favorite_set = PostSets::Favorites.new(@user, params[:page], params[:limit])
respond_with(@favorite_set.posts) do |fmt|
fmt.json do
render json: @favorite_set.posts, root: 'posts'

View File

@ -1,8 +1,8 @@
class ForumPostsController < ApplicationController
respond_to :html, :json
before_action :member_only, :except => [:index, :show, :search]
before_action :moderator_only, only: [:destroy, :unhide]
before_action :load_post, :only => [:edit, :show, :update, :destroy, :hide, :unhide]
before_action :moderator_only, only: [:destroy, :unhide, :warning]
before_action :load_post, :only => [:edit, :show, :update, :destroy, :hide, :unhide, :warning]
before_action :check_min_level, :only => [:edit, :show, :update, :destroy, :hide, :unhide]
skip_before_action :api_check
@ -70,6 +70,15 @@ class ForumPostsController < ApplicationController
respond_with(@forum_post)
end
def warning
if params[:record_type] == 'unmark'
@forum_post.remove_user_warning!
else
@forum_post.user_warned!(params[:record_type])
end
respond_with(@forum_post)
end
private
def load_post
@forum_post = ForumPost.includes(topic: [:category]).find(params[:id])

View File

@ -1,10 +1,10 @@
module Maintenance
module User
class ApiKeysController < ApplicationController
before_action :check_privilege
before_action :member_only
before_action :authenticate!, :except => [:show]
rescue_from ::SessionLoader::AuthenticationFailure, :with => :authentication_failed
respond_to :html, :json
respond_to :html
def view
respond_with(CurrentUser.user, @api_key)
@ -22,10 +22,6 @@ module Maintenance
protected
def check_privilege
raise ::User::PrivilegeError unless params[:user_id].to_i == CurrentUser.id
end
def authenticate!
if ::User.authenticate(CurrentUser.user.name, params[:user][:password]) == CurrentUser.user
@api_key = CurrentUser.user.api_key || ApiKey.generate!(CurrentUser.user)

View File

@ -2,6 +2,7 @@ module Moderator
module Post
class PostsController < ApplicationController
before_action :approver_only
before_action :janitor_only, only: [:regenerate_thumbnails, :regenerate_videos]
before_action :admin_only, :only => [:expunge]
skip_before_action :api_check
@ -47,6 +48,20 @@ module Moderator
@post.expunge!
respond_with(@post)
end
def regenerate_thumbnails
@post = ::Post.find(params[:id])
raise ::User::PrivilegeError.new "Cannot regenerate thumbnails on deleted images" if @post.is_deleted?
@post.regenerate_image_samples!
respond_with(@post)
end
def regenerate_videos
@post = ::Post.find(params[:id])
raise ::User::PrivilegeError.new "Cannot regenerate thumbnails on deleted images" if @post.is_deleted?
@post.regenerate_video_samples!
respond_with(@post)
end
end
end
end

View File

@ -1,44 +1,68 @@
class PostReplacementsController < ApplicationController
respond_to :html, :json, :js
before_action :moderator_only, except: [:index]
respond_to :html
before_action :moderator_only, only: [:destroy]
before_action :janitor_only, only: [:create, :new, :approve, :reject, :promote]
content_security_policy only: [:new] do |p|
p.img_src :self, :data, "*"
end
def new
@post_replacement = Post.find(params[:post_id]).replacements.new
@post = Post.find(params[:post_id])
@post_replacement = @post.replacements.new
respond_with(@post_replacement)
end
def create
@post = Post.find(params[:post_id])
@post_replacement = @post.replace!(create_params)
flash[:notice] = "Post replaced"
@post_replacement = @post.replacements.create(create_params.merge(creator_id: CurrentUser.id, creator_ip_addr: CurrentUser.ip_addr))
if @post_replacement.errors.size == 0
flash[:notice] = "Post replacement submitted"
else
flash[:notice] = @post_replacement.errors.full_messages.join('; ')
end
respond_with(@post_replacement, location: @post)
end
def update
def approve
@post_replacement = PostReplacement.find(params[:id])
@post_replacement.update(update_params)
@post_replacement.approve!
respond_with(@post_replacement, location: post_path(@post_replacement.post))
end
def reject
@post_replacement = PostReplacement.find(params[:id])
@post_replacement.reject!
respond_with(@post_replacement)
end
def destroy
@post_replacement = PostReplacement.find(params[:id])
@post_replacement.destroy
respond_with(@post_replacement)
end
def promote
@post_replacement = PostReplacement.find(params[:id])
@post = @post_replacement.promote!
if @post.errors.any?
respond_with(@post)
else
respond_with(@post.post)
end
end
def index
params[:search][:post_id] = params.delete(:post_id) if params.has_key?(:post_id)
@post_replacements = PostReplacement.search(search_params).paginate(params[:page], limit: params[:limit])
@post_replacements = PostReplacement.visible(CurrentUser.user).search(search_params).paginate(params[:page], limit: params[:limit])
respond_with(@post_replacements)
end
private
def create_params
params.require(:post_replacement).permit(:replacement_url, :replacement_file, :final_source, :tags)
end
def update_params
params.require(:post_replacement).permit(
:old_file_ext, :old_file_size, :old_image_width, :old_image_height, :old_md5,
:file_ext, :file_size, :image_width, :image_height, :md5,
:original_url, :replacement_url
)
params.require(:post_replacement).permit(:replacement_url, :replacement_file, :reason, :source)
end
end

View File

@ -19,7 +19,11 @@ class TagsController < ApplicationController
expires_in params[:expiry].to_i.days if params[:expiry]
respond_with(@tags)
respond_with(@tags) do |fmt|
fmt.json do
render json: @tags.to_json
end
end
end
def preview

View File

@ -2,7 +2,7 @@ class UploadsController < ApplicationController
before_action :member_only
before_action :janitor_only, only: [:index, :show]
respond_to :html, :json
content_security_policy do |p|
content_security_policy only: [:new] do |p|
p.img_src :self, :data, "*"
end
@ -11,7 +11,6 @@ class UploadsController < ApplicationController
return access_denied("You can not upload during your first week.")
end
@source = Sources::Strategies.find(params[:url], params[:ref]) if params[:url].present?
@upload_notice_wiki = WikiPage.titled(Danbooru.config.upload_notice_wiki_page).first
@upload = Upload.new
respond_with(@upload)
end
@ -76,6 +75,6 @@ class UploadsController < ApplicationController
permitted_params << :locked_tags if CurrentUser.is_admin?
permitted_params << :locked_rating if CurrentUser.is_privileged?
params.require(:upload).permit(permitted_params)
params.require(:upload).permit(permitted_params).merge(uploader_id: CurrentUser.id, uploader_ip_addr: CurrentUser.ip_addr)
end
end

View File

@ -19,6 +19,7 @@ class WikiPagesController < ApplicationController
return
end
end
ensure_can_edit(@wiki_page, CurrentUser.user)
respond_with(@wiki_page)
end
@ -67,6 +68,7 @@ class WikiPagesController < ApplicationController
def update
@wiki_page = WikiPage.find(params[:id])
ensure_can_edit(@wiki_page, CurrentUser.user)
@wiki_page.update(wiki_page_params(:update))
respond_with(@wiki_page)
end
@ -79,6 +81,7 @@ class WikiPagesController < ApplicationController
def revert
@wiki_page = WikiPage.find(params[:id])
ensure_can_edit(@wiki_page, CurrentUser.user)
@version = @wiki_page.versions.find(params[:version_id])
@wiki_page.revert_to!(@version)
flash[:notice] = "Page was reverted"
@ -97,6 +100,11 @@ class WikiPagesController < ApplicationController
private
def ensure_can_edit(page, user)
return if user.is_janitor?
raise User::PrivilegeError.new("Wiki page is locked.") if page.is_locked
end
def normalize_search_params
if params[:title]
params[:search] ||= {}

View File

@ -88,7 +88,7 @@ class PostsDecorator < ApplicationDecorator
return "<em>none</em>".html_safe
end
if !options[:show_deleted] && post.is_deleted? && options[:tags] !~ /(?:status:(?:all|any|deleted))|(?:deletedby:)|(?:delreason:)/ && !options[:raw]
if !options[:show_deleted] && post.is_deleted? && options[:tags] !~ /(?:status:(?:all|any|deleted))|(?:deletedby:)|(?:delreason:)/i && !options[:raw]
return ""
end

View File

@ -0,0 +1,6 @@
module PostReplacementHelper
def replacement_thumbnail(replacement)
return tag.a(image_tag(replacement.replacement_thumb_url), href: replacement.replacement_file_url) if replacement.file_visible_to?(CurrentUser.user)
image_tag(replacement.replacement_thumb_url)
end
end

View File

@ -7,32 +7,25 @@ module TagsHelper
if tag.antecedent_alias
html << "<p class='hint'>This tag has been aliased to "
html << link_to(tag.antecedent_alias.consequent_name, show_or_new_wiki_pages_path(:title => tag.antecedent_alias.consequent_name))
html << " (#{link_to "learn more", wiki_pages_path(title: "help:tag_aliases")}).</p>"
html << " (#{link_to "learn more", wiki_pages_path(title: "e621:tag_aliases")}).</p>"
end
if tag.consequent_aliases.present?
html << "<p class='hint'>The following tags are aliased to this tag: "
html << raw(tag.consequent_aliases.map {|x| link_to(x.antecedent_name, show_or_new_wiki_pages_path(:title => x.antecedent_name))}.join(", "))
html << " (#{link_to "learn more", wiki_pages_path(title: "help:tag_aliases")}).</p>"
end
automatic_tags = TagImplication.automatic_tags_for([tag.name])
if automatic_tags.present?
html << "<p class='hint'>This tag automatically adds "
html << raw(automatic_tags.map {|x| link_to(x, show_or_new_wiki_pages_path(:title => x))}.join(", "))
html << " (#{link_to "learn more", wiki_pages_path(title: "help:autotags")}).</p>"
html << " (#{link_to "learn more", wiki_pages_path(title: "e621:tag_aliases")}).</p>"
end
if tag.antecedent_implications.present?
html << "<p class='hint'>This tag implicates "
html << raw(tag.antecedent_implications.map {|x| link_to(x.consequent_name, show_or_new_wiki_pages_path(:title => x.consequent_name))}.join(", "))
html << " (#{link_to "learn more", wiki_pages_path(title: "help:tag_implications")}).</p>"
html << " (#{link_to "learn more", wiki_pages_path(title: "e621:tag_implications")}).</p>"
end
if tag.consequent_implications.present?
html << "<p class='hint'>The following tags implicate this tag: "
html << raw(tag.consequent_implications.map {|x| link_to(x.antecedent_name, show_or_new_wiki_pages_path(:title => x.antecedent_name))}.join(", "))
html << " (#{link_to "learn more", wiki_pages_path(title: "help:tag_implications")}).</p>"
html << " (#{link_to "learn more", wiki_pages_path(title: "e621:tag_implications")}).</p>"
end
html.html_safe

View File

@ -138,27 +138,7 @@ Autocomplete.initialize_artist_autocomplete = function($fields) {
$(this).data("ui-autocomplete").menu.bindings = $();
},
source: function(req, resp) {
$.ajax({
url: "/artists.json",
data: {
"search[name_like]": req.term.trim().replace(/\s+/g, "_") + "*",
"search[is_active]": true,
"search[order]": "post_count",
"limit": 10,
"expiry": 7
},
method: "get",
success: function(data) {
resp($.map(data, function(artist) {
return {
type: "tag",
label: artist.name.replace(/_/g, " "),
value: artist.name,
category: Autocomplete.TAG_CATEGORIES.artist,
};
}));
}
});
Autocomplete.artist_source(req.term, resp);
}
});
};
@ -191,61 +171,11 @@ Autocomplete.initialize_wiki_autocomplete = function($fields) {
$(this).data("ui-autocomplete").menu.bindings = $();
},
source: function(req, resp) {
$.ajax({
url: "/wiki_pages.json",
data: {
"search[title]": req.term + "*",
"search[hide_deleted]": "Yes",
"search[order]": "post_count",
"limit": 10,
"expiry": 7
},
method: "get",
success: function(data) {
resp($.map(data, function(wiki_page) {
return {
type: "tag",
label: wiki_page.title.replace(/_/g, " "),
value: wiki_page.title,
category: wiki_page.category_name
};
}));
}
});
Autocomplete.wiki_source(req.term, resp);
}
});
};
Autocomplete.normal_source = function(term, resp) {
if (term.length < 3) {
return;
}
return $.ajax({
url: "/tags/autocomplete.json",
data: {
"search[name_matches]": term,
"expiry": 7
},
method: "get",
success: function(data) {
var d = $.map(data, function(tag) {
return {
type: "tag",
label: tag.name.replace(/_/g, " "),
antecedent: tag.antecedent_name,
value: tag.name,
category: tag.category,
source: tag.source,
weight: tag.weight,
post_count: tag.post_count
};
});
resp(d);
}
});
}
Autocomplete.parse_query = function(text, caret) {
var metatag = "";
var term = "";
@ -316,7 +246,7 @@ Autocomplete.on_tab = function(event) {
Autocomplete.render_item = function(list, item) {
var $link = $("<a/>");
$link.text(item.label);
$link.attr("href", "/posts?tags=" + encodeURIComponent(item.value));
$link.attr("href", Autocomplete.get_href(item));
$link.on("click.danbooru", function(e) {
e.preventDefault();
});
@ -357,7 +287,7 @@ Autocomplete.render_item = function(list, item) {
var $menu_item = $("<div/>").append($link);
var $list_item = $("<li/>").data("item.autocomplete", item).append($menu_item);
var data_attributes = ["type", "source", "antecedent", "value", "category", "post_count", "weight"];
var data_attributes = ["id", "type", "source", "antecedent", "value", "category", "post_count", "weight"];
data_attributes.forEach(attr => {
$list_item.attr(`data-autocomplete-${attr.replace(/_/g, "-")}`, item[attr]);
});
@ -365,6 +295,53 @@ Autocomplete.render_item = function(list, item) {
return $list_item.appendTo(list);
};
Autocomplete.get_href = function(item) {
switch(item.type) {
case "user":
return "/users/" + item.id;
case "pool":
return "/pools/" + item.id;
case "artist":
return "/artists/" + item.id;
case "wiki_page":
return "/wiki_pages/" + item.id;
case "tag":
default:
return "/posts?tags=" + encodeURIComponent(item.value);
}
}
Autocomplete.normal_source = function(term, resp) {
if (term.length < 3) {
return;
}
return $.ajax({
url: "/tags/autocomplete.json",
data: {
"search[name_matches]": term,
"expiry": 7
},
method: "get",
success: function(data) {
var d = $.map(data, function(tag) {
return {
id: tag.id,
type: "tag",
label: tag.name.replace(/_/g, " "),
antecedent: tag.antecedent_name,
value: tag.name,
category: tag.category,
source: tag.source,
weight: tag.weight,
post_count: tag.post_count
};
});
resp(d);
}
});
}
Autocomplete.static_metatags = {
order: Autocomplete.ORDER_METATAGS,
status: [
@ -414,6 +391,7 @@ Autocomplete.user_source = function(term, resp, prefix) {
success: function(data) {
resp($.map(data, function(user) {
return {
id: user.id,
type: "user",
label: user.name.replace(/_/, " "),
value: prefix + user.name,
@ -436,6 +414,7 @@ Autocomplete.pool_source = function(term, resp, metatag) {
success: function(data) {
resp($.map(data, function(pool) {
return {
id: pool.id,
type: "pool",
label: pool.name.replace(/_/g, " "),
value: (metatag ? (metatag + ":" + pool.name) : pool.name),
@ -447,9 +426,58 @@ Autocomplete.pool_source = function(term, resp, metatag) {
});
}
Autocomplete.artist_source = function(term, resp) {
return $.ajax({
url: "/artists.json",
data: {
"search[name_like]": term.trim().replace(/\s+/g, "_") + "*",
"search[is_active]": true,
"search[order]": "post_count",
"limit": 10,
"expiry": 7
},
method: "get",
success: function(data) {
resp($.map(data, function(artist) {
return {
id: artist.id,
type: "artist",
label: artist.name.replace(/_/g, " "),
value: artist.name,
category: Autocomplete.TAG_CATEGORIES.artist,
};
}));
}
});
}
Autocomplete.wiki_source = function(term, resp) {
return $.ajax({
url: "/wiki_pages.json",
data: {
"search[title]": term + "*",
"search[hide_deleted]": "Yes",
"search[order]": "post_count",
"limit": 10,
"expiry": 7
},
method: "get",
success: function(data) {
resp($.map(data, function(wiki_page) {
return {
id: wiki_page.id,
type: "wiki_page",
label: wiki_page.title.replace(/_/g, " "),
value: wiki_page.title,
category: wiki_page.category_name
};
}));
}
});
}
$(document).ready(function() {
Autocomplete.initialize_all();
});
export default Autocomplete;

View File

@ -419,7 +419,7 @@ let Note = {
$dialog.data("uiDialog")._title = function(title) {
title.html(this.options.title); // Allow unescaped html in dialog title
}
$dialog.dialog("option", "title", 'Edit note #' + id + ' (<a href="/wiki_pages/help:notes">view help</a>)');
$dialog.dialog("option", "title", 'Edit note #' + id + ' (<a href="/wiki_pages/e621:notes">view help</a>)');
$dialog.on("dialogclose.danbooru", function() {
Note.editing = false;
@ -828,4 +828,3 @@ $(function() {
});
export default Note

View File

@ -57,6 +57,8 @@ PostVersion.tag_script_selected = function() {
PostVersion.updated = 0;
let selected_rows = $(".post-version-select:checked").parents(".post-version");
const script = $("#update-tag-script").val();
if(!script)
return;
for (let row of selected_rows) {
let id = $(row).data("post-id");

View File

@ -361,6 +361,14 @@ Post.initialize_links = function() {
return;
Post.destroy($(e.target).data('pid'));
});
$("#regenerate-image-samples-link").on('click', e => {
e.preventDefault();
Post.regenerate_image_samples($(e.target).data('pid'));
});
$("#regenerate-video-samples-link").on('click', e => {
e.preventDefault();
Post.regenerate_video_samples($(e.target).data('pid'));
});
$(".disapprove-post-link").on('click', e => {
e.preventDefault();
const target = $(e.target);
@ -907,6 +915,24 @@ Post.destroy = function(post_id) {
});
};
Post.regenerate_image_samples = function(post_id) {
$.post(`/moderator/post/posts/${post_id}/regenerate_thumbnails.json`, {}
).fail(data => {
Utility.error("Error: " + data.responseJSON.reason);
}).done(data => {
Utility.notice("Image samples regenerated.");
});
};
Post.regenerate_video_samples = function(post_id) {
$.post(`/moderator/post/posts/${post_id}/regenerate_videos.json`, {}
).fail(data => {
Utility.error("Error: " + data.responseJSON.reason);
}).done(data => {
Utility.notice("Video samples will be regenerated in a few minutes.");
});
};
Post.approve = function(post_id, should_reload) {
Post.notice_update("inc");
SendQueue.add(function() {

View File

@ -0,0 +1,145 @@
const Replacer = {};
const thumbURLs = [
"/images/notfound-preview.png",
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
];
const thumbs = {
notfound: "/images/notfound-preview.png",
none: 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
};
Replacer.old_domain = "";
Replacer.update_preview_file = function (file) {
const reader = new FileReader();
reader.onload = function (e) {
Replacer.set_preview_url(e.target.result);
};
reader.readAsDataURL(file);
};
Replacer.update_preview_url = function () {
const url = $("#post_replacement_replacement_url").val();
if (!url) {
Replacer.upload_allow_clear();
return;
}
const sampleURL = Replacer.isSampleURL(url);
if (sampleURL !== false) {
$('#bad_upload_url_reason').text(sampleURL);
$('#bad_upload_url').show();
return;
} else {
$('#bad_upload_url').hide();
}
const domain = $("<a>").prop("href", url).prop("hostname");
if (domain && domain !== Replacer.old_domain) {
$.getJSON("/upload_whitelists/is_allowed.json", {url: url}, function(data) {
if(data.domain) {
Replacer.upload_allow_set(data.is_allowed, data.domain, data.reason);
if(!data.is_allowed)
Replacer.set_preview_url(thumbs.none);
}
});
} else if (!domain) {
Replacer.upload_allow_clear();
}
Replacer.old_domain = domain;
Replacer.set_preview_url(url);
};
Replacer.update_preview_dims = function () {
const img = $('#replacement_preview_img')[0];
if (thumbURLs.filter(function (x) {
return img.src.indexOf(x) !== -1;
}).length !== 0)
return;
Replacer.set_preview_dims(img.naturalHeight, img.naturalWidth);
};
Replacer.preview_error = function (e) {
const img = e.target;
Replacer.set_preview_dims(-1, -1);
if (thumbURLs.filter(function (x) {
return img.src.indexOf(x) !== -1;
}).length !== 0)
return;
Replacer.set_preview_url(thumbs.notfound);
};
Replacer.set_preview_dims = function (height, width) {
if (height <= 0 && width <= 0) {
$('#replacement_preview_dims').text('');
} else {
$('#replacement_preview_dims').text(`${width}x${height}`);
}
};
Replacer.set_preview_url = function (url) {
$('#replacement_preview_img').attr('src', url);
};
Replacer.update_preview = function () {
const $file = $("#post_replacement_replacement_file")[0];
if ($file && $file.files[0])
Replacer.update_preview_file($file.files[0]);
else
Replacer.update_preview_url();
}
Replacer.update_preview_paste = function () {
setTimeout(Replacer.update_preview, 150);
}
Replacer.isSampleURL = function (url) {
const patterns = [
{reason: 'Thumbnail URL', test: /[at]\.facdn\.net/gi},
{reason: 'Sample URL', test: /pximg\.net\/img-master/gi},
{reason: 'Sample URL', test: /d3gz42uwgl1r1y\.cloudfront\.net\/.*\/\d+x\d+\./gi},
{reason: 'Sample URL', test: /pbs\.twimg\.com\/media\/[\w\-_]+\.(jpg|png)(:large)?$/gi},
{reason: 'Sample URL', test: /pbs\.twimg\.com\/media\/[\w\-_]+\?format=(jpg|png)(?!&name=orig)/gi},
{reason: 'Sample URL', test: /derpicdn\.net\/.*\/large\./gi},
{reason: 'Sample URL', test: /metapix\.net\/files\/(preview|screen)\//gi},
{reason: 'Sample URL', test: /sofurryfiles\.com\/std\/preview/gi}];
for (let i = 0; i < patterns.length; ++i) {
const pattern = patterns[i];
if (pattern.test.test(url))
return pattern.reason;
}
return false;
}
Replacer.upload_allow_clear = function() {
$('#whitelist-warning').hide();
}
Replacer.upload_allow_set = function(allowed, domain, reason) {
const classes = ['whitelist-warning-disallowed', 'whitelist-warning-allowed'];
$('#whitelist-warning').removeClass().addClass(classes[allowed ? 1 : 0]);
$('#whitelist-warning-domain').text(domain);
if(allowed)
$('#whitelist-warning-not').hide();
else
$('#whitelist-warning-not').show();
$('#whitelist-warning').show();
}
Replacer.init_uploader = function () {
$('#replacement_preview_img').on('load', Replacer.update_preview_dims);
$('#replacement_preview_img').on('error', Replacer.preview_error);
$('#post_replacement_replacement_url').on('keyup', Replacer.update_preview);
$('#post_replacement_replacement_file').on('change', Replacer.update_preview);
$('#post_replacement_replacement_url').on('paste', Replacer.update_preview_paste);
};
$(function () {
if ($("#c-post-replacements > #a-new").length) {
Replacer.init_uploader();
}
});
export default Replacer;

View File

@ -0,0 +1,22 @@
import Utility from './utility.js';
$(() => {
$('.item-mark-user-warned').on('click', function(evt) {
const target = $(evt.target);
const type = target.data('item-type');
const id = target.data('item-id');
const record_type = target.data('record-type');
$.ajax({
type: "POST",
url: `/${type}s/${id}/warning.json`,
data: {
'record_type': record_type
},
}).done(function(data) {
location.reload();
}).fail(function(data) {
Utility.error("Failed to mark as warned.");
});
});
});

View File

@ -58,10 +58,12 @@
@import "specific/post_flags.scss";
@import "specific/post_mode_menu.scss";
@import "specific/posts.scss";
@import "specific/post_replacements.scss";
@import "specific/post_versions.scss";
@import "specific/related_tags.scss";
@import "specific/sessions.scss";
@import "specific/site_map.scss";
@import "specific/staff_notes.scss";
@import "specific/tags.scss";
@import "specific/takedowns.scss";
@import "specific/terms_of_service.scss";
@ -69,6 +71,7 @@
@import "specific/user_deletions.scss";
@import "specific/user_feedback.scss";
@import "specific/user_name_change_requests.scss";
@import "specific/user_warned.scss";
@import "specific/users.scss";
@import "specific/wiki_pages.scss";
@import "specific/wiki_page_versions.scss";

View File

@ -0,0 +1,7 @@
div#c-post-replacements {
.replacement-pending-row {
@include themable {
background-color: darken( themed("color-danger"), 10%);
}
}
}

View File

@ -0,0 +1,5 @@
.staff-note-list {
.staff-note {
margin-bottom: $padding-050;
}
}

View File

@ -0,0 +1,3 @@
.user-warning em {
@include themable { color: themed("color-rating-explicit"); }
}

View File

@ -88,8 +88,8 @@ module Danbooru
case key
when :limit
limit = @paginator_options.try(:[], :limit) || Danbooru.config.posts_per_page
if limit.to_i > 1_000
limit = 1_000
if limit.to_i > 320
limit = 320
end
limit

View File

@ -148,8 +148,8 @@ module Danbooru
case key
when :limit
limit = @paginator_options.try(:[], :limit) || Danbooru.config.posts_per_page
if limit.to_i > 1_000
limit = 1_000
if limit.to_i > 320
limit = 320
end
limit

View File

@ -5,9 +5,11 @@ module DanbooruImageResizer
SRGB_PROFILE = "#{Rails.root}/config/sRGB.icm"
# http://jcupitt.github.io/libvips/API/current/libvips-resample.html#vips-thumbnail
THUMBNAIL_OPTIONS = { size: :down, linear: false, no_rotate: true, export_profile: SRGB_PROFILE, import_profile: SRGB_PROFILE }
THUMBNAIL_OPTIONS_NO_ICC = { size: :down, linear: false, no_rotate: true, export_profile: SRGB_PROFILE }
# http://jcupitt.github.io/libvips/API/current/VipsForeignSave.html#vips-jpegsave
JPEG_OPTIONS = { background: 0, strip: true, interlace: true, optimize_coding: true }
CROP_OPTIONS = { linear: false, no_rotate: true, export_profile: SRGB_PROFILE, import_profile: SRGB_PROFILE, crop: :attention }
CROP_OPTIONS_NO_ICC = { linear: false, no_rotate: true, export_profile: SRGB_PROFILE, crop: :attention }
# XXX libvips-8.4 on Debian doesn't support the `Vips::Image.thumbnail` method.
# On 8.4 we have to shell out to vipsthumbnail instead. Remove when Debian supports 8.5.
@ -31,7 +33,12 @@ module DanbooruImageResizer
# http://jcupitt.github.io/libvips/API/current/Using-vipsthumbnail.md.html
def resize_ruby(file, width, height, resize_quality)
output_file = Tempfile.new
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **THUMBNAIL_OPTIONS)
begin
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **THUMBNAIL_OPTIONS)
rescue Vips::Error => e
raise e unless e.message =~ /icc_transform/i
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **THUMBNAIL_OPTIONS_NO_ICC)
end
resized_image.jpegsave(output_file.path, Q: resize_quality, **JPEG_OPTIONS)
output_file
@ -41,7 +48,12 @@ module DanbooruImageResizer
return nil unless Danbooru.config.enable_image_cropping
output_file = Tempfile.new
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **CROP_OPTIONS)
begin
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **CROP_OPTIONS)
rescue Vips::Error => e
raise e unless e.message =~ /icc_transform/i
resized_image = Vips::Image.thumbnail(file.path, width, height: height, **CROP_OPTIONS_NO_ICC)
end
resized_image.jpegsave(output_file.path, Q: resize_quality, **JPEG_OPTIONS)
output_file
@ -94,19 +106,11 @@ module DanbooruImageResizer
output_file
end
def validate_shell(file)
temp = Tempfile.new("validate")
output, status = Open3.capture2e("vips", "stats", file.path, "#{temp.path}.v")
# png | jpeg | gif
if output =~ /Read Error|Premature end of JPEG file|Failed to read from given file/m
return false
end
return true
ensure
temp.close
temp.unlink
def is_corrupt?(filename)
image = Vips::Image.new_from_file(filename, fail: true)
image.stats
false
rescue
true
end
end

View File

@ -324,7 +324,7 @@ class ElasticPostQueryBuilder
end
if q[:note_neg]
must_not.push({match: {notes: q[:note]}})
must_not.push({match: {notes: q[:note_neg]}})
end
if q[:delreason]

View File

@ -3,10 +3,10 @@ module PostSets
class Favorites < PostSets::Base
attr_reader :page, :limit
def initialize(user, page)
def initialize(user, page = 1, limit = CurrentUser.per_page)
@user = user
@page = page
@limit = CurrentUser.per_page
@limit = limit || CurrentUser.per_page
end
def public_tag_string

View File

@ -0,0 +1,61 @@
module PostThumbnailer
extend self
def generate_resizes(file, height, width, type)
if type == :video
video = FFMPEG::Movie.new(file.path)
crop_file = generate_video_crop_for(video, Danbooru.config.small_image_width)
preview_file = generate_video_preview_for(file.path, Danbooru.config.small_image_width)
sample_file = generate_video_sample_for(file.path)
elsif type == :image
preview_file = DanbooruImageResizer.resize(file, Danbooru.config.small_image_width, Danbooru.config.small_image_width, 87)
crop_file = DanbooruImageResizer.crop(file, Danbooru.config.small_image_width, Danbooru.config.small_image_width, 87)
if width > Danbooru.config.large_image_width
sample_file = DanbooruImageResizer.resize(file, Danbooru.config.large_image_width, height, 87)
end
end
[preview_file, crop_file, sample_file]
end
def generate_thumbnail(file, type)
if type == :video
preview_file = generate_video_preview_for(file.path, Danbooru.config.small_image_width)
elsif type == :image
preview_file = DanbooruImageResizer.resize(file, Danbooru.config.small_image_width, Danbooru.config.small_image_width, 87)
end
preview_file
end
def generate_video_crop_for(video, width)
vp = Tempfile.new(["video-preview", ".jpg"], binmode: true)
video.screenshot(vp.path, {:seek_time => 0, :resolution => "#{video.width}x#{video.height}"})
crop = DanbooruImageResizer.crop(vp, width, width, 87)
vp.close
return crop
end
def generate_video_preview_for(video, width)
output_file = Tempfile.new(["video-preview", ".jpg"], binmode: true)
stdout, stderr, status = Open3.capture3(Danbooru.config.ffmpeg_path, '-y', '-i', video, '-vf', "thumbnail,scale=#{width}:-1", '-frames:v', '1', output_file.path)
unless status == 0
Rails.logger.warn("[FFMPEG PREVIEW STDOUT] #{stdout.chomp!}")
Rails.logger.warn("[FFMPEG PREVIEW STDERR] #{stderr.chomp!}")
raise CorruptFileError.new("could not generate thumbnail")
end
output_file
end
def generate_video_sample_for(video)
output_file = Tempfile.new(["video-sample", ".jpg"], binmode: true)
stdout, stderr, status = Open3.capture3(Danbooru.config.ffmpeg_path, '-y', '-i', video, '-vf', 'thumbnail', '-frames:v', '1', output_file.path)
unless status == 0
Rails.logger.warn("[FFMPEG SAMPLE STDOUT] #{stdout.chomp!}")
Rails.logger.warn("[FFMPEG SAMPLE STDERR] #{stderr.chomp!}")
raise CorruptFileError.new("could not generate sample")
end
output_file
end
end

View File

@ -0,0 +1,3 @@
class ProcessingError < Exception
end

View File

@ -4,15 +4,17 @@ class StorageManager
DEFAULT_BASE_DIR = "#{Rails.root}/public/data"
IMAGE_TYPES = %i[preview large crop original]
attr_reader :base_url, :base_dir, :hierarchical, :large_image_prefix, :protected_prefix, :base_path
attr_reader :base_url, :base_dir, :hierarchical, :large_image_prefix, :protected_prefix, :base_path, :replacement_prefix
def initialize(base_url: default_base_url, base_path: default_base_path, base_dir: DEFAULT_BASE_DIR, hierarchical: false,
large_image_prefix: Danbooru.config.large_image_prefix,
protected_prefix: Danbooru.config.protected_path_prefix)
protected_prefix: Danbooru.config.protected_path_prefix,
replacement_prefix: Danbooru.config.replacement_path_prefix)
@base_url = base_url.chomp("/")
@base_dir = base_dir
@base_path = base_path
@protected_prefix = protected_prefix
@replacement_prefix = replacement_prefix
@hierarchical = hierarchical
@large_image_prefix = large_image_prefix
end
@ -48,9 +50,13 @@ class StorageManager
store(io, file_path(post.md5, post.file_ext, type))
end
def delete_file(post_id, md5, file_ext, type)
delete(file_path(md5, file_ext, type))
delete(file_path(md5, file_ext, type, true))
def store_replacement(io, replacement, image_size)
store(io, replacement_path(replacement.storage_id, replacement.file_ext, image_size))
end
def delete_file(post_id, md5, file_ext, type, scale_factor: nil)
delete(file_path(md5, file_ext, type, scale_factor: scale_factor))
delete(file_path(md5, file_ext, type, true, scale_factor: scale_factor))
end
def delete_post_files(post_or_md5, file_ext)
@ -69,6 +75,11 @@ class StorageManager
delete(file_path(md5, 'mp4', :original, true))
end
def delete_replacement(replacement)
delete(replacement_path(replacement.storage_id, replacement.file_ext, :original))
delete(replacement_path(replacement.storage_id, replacement.file_ext, :preview))
end
def open_file(post, type)
open(file_path(post.md5, post.file_ext, type))
end
@ -81,11 +92,15 @@ class StorageManager
raise NotImplementedError, "move_file_undelete not implemented"
end
def protected_params(url, post)
def move_file_replacement(post, replacement, direction)
raise NotImplementedError, "move_file_replacement not implemented"
end
def protected_params(url, post, secret: Danbooru.config.protected_file_secret)
user_id = CurrentUser.id
ip = CurrentUser.ip_addr
time = (Time.now + 15.minute).to_i
secret = Danbooru.config.protected_file_secret
secret = secret
hmac = Digest::MD5.base64digest("#{time} #{url} #{user_id} #{secret}").tr("+/","-_").gsub("==",'')
"?auth=#{hmac}&expires=#{time}&uid=#{user_id}"
end
@ -118,6 +133,14 @@ class StorageManager
file_url_ext(post, type, post.file_ext)
end
def replacement_url(replacement, image_size = :original)
subdir = subdir_for(replacement.storage_id)
file = "#{replacement.storage_id}#{'_thumb' if image_size == :preview}.#{replacement.file_ext}"
base = "#{base_path}/#{replacement_prefix}"
path = "#{base}/#{subdir}#{file}"
"#{base_url}#{path}#{protected_params(path, nil, secret: Danbooru.config.replacement_file_secret)}"
end
def root_url
origin = Addressable::URI.parse(base_url).origin
origin = "" if origin == "null" # base_url was relative
@ -161,6 +184,13 @@ class StorageManager
end
end
def replacement_path(replacement_or_storage_id, file_ext, image_size)
storage_id = replacement_or_storage_id.is_a?(String) ? replacement_or_storage_id : replacement_or_storage_id.storage_id
subdir = subdir_for(storage_id)
file = "#{storage_id}#{'_thumb' if image_size == :preview}.#{file_ext}"
"#{base_dir}/#{replacement_prefix}/#{subdir}#{file}"
end
def subdir_for(md5)
hierarchical ? "#{md5[0..1]}/#{md5[2..3]}/" : ""
end

View File

@ -64,6 +64,14 @@ class StorageManager::Local < StorageManager
move_file(path, new_path)
end
def move_file_replacement(post, replacement, direction)
if direction == :to_replacement
move_file(file_path(post, post.file_ext, :original), replacement_path(replacement, replacement.file_ext))
else
move_file(replacement_path(replacement, replacement.file_ext), file_path(post, post.file_ext, :original))
end
end
private
def move_file(old_path, new_path)

View File

@ -1,115 +0,0 @@
module TagAutocomplete
extend self
PREFIX_BOUNDARIES = "(_/:;-"
LIMIT = 10
class Result < Struct.new(:name, :post_count, :category, :antecedent_name, :source)
include ActiveModel::Serializers::JSON
include ActiveModel::Serializers::Xml
def attributes
(members + [:weight]).map { |x| [x.to_s, send(x)] }.to_h
end
def weight
case source
when :exact then 1.0
when :prefix then 0.8
when :alias then 0.2
when :correct then 0.1
end
end
end
def search(query)
query = Tag.normalize_name(query)
candidates = count_sort(
query,
search_exact(query, 8) +
search_prefix(query, 4) +
search_correct(query, 2) +
search_aliases(query, 3)
)
end
def count_sort(query, words)
words.uniq(&:name).sort_by do |x|
x.post_count * x.weight
end.reverse.slice(0, LIMIT)
end
def search_exact(query, n=4)
Tag
.where("name like ? escape e'\\\\'", query.to_escaped_for_sql_like + "%")
.where("post_count > 0")
.order("post_count desc")
.limit(n)
.pluck(:name, :post_count, :category)
.map {|row| Result.new(*row, nil, :exact)}
end
def search_correct(query, n=2)
if query.size <= 3
return []
end
Tag
.where("name % ?", query)
.where("abs(length(name) - ?) <= 3", query.size)
.where("name like ? escape E'\\\\'", query[0].to_escaped_for_sql_like + '%')
.where("post_count > 0")
.order(Arel.sql("similarity(name, #{Tag.connection.quote(query)}) DESC"))
.limit(n)
.pluck(:name, :post_count, :category)
.map {|row| Result.new(*row, nil, :correct)}
end
def search_prefix(query, n=3)
if query.size >= 5
return []
end
if query.size <= 1
return []
end
if query =~ /[-_()]/
return []
end
if query.size >= 3
min_post_count = 0
else
min_post_count = 5_000
n += 2
end
regexp = "([a-z0-9])[a-z0-9']*($|[^a-z0-9']+)"
Tag
.where('regexp_replace(name, ?, ?, ?) like ?', regexp, '\1', 'g', query.to_escaped_for_sql_like + '%')
.where("post_count > ?", min_post_count)
.where("post_count > 0")
.order("post_count desc")
.limit(n)
.pluck(:name, :post_count, :category)
.map {|row| Result.new(*row, nil, :prefix)}
end
def search_aliases(query, n=10)
wildcard_name = query + "*"
TagAlias
.select("tags.name, tags.post_count, tags.category, tag_aliases.antecedent_name")
.joins("INNER JOIN tags ON tags.name = tag_aliases.consequent_name")
.where("tag_aliases.antecedent_name LIKE ? ESCAPE E'\\\\'", wildcard_name.to_escaped_for_sql_like)
.active
.where("tags.name NOT LIKE ? ESCAPE E'\\\\'", wildcard_name.to_escaped_for_sql_like)
.where("tag_aliases.post_count > 0")
.order("tag_aliases.post_count desc")
.limit(n)
.pluck(:name, :post_count, :category, :antecedent_name)
.map {|row| Result.new(*row, :alias)}
end
end

View File

@ -1,8 +1,6 @@
class UploadService
class Replacer
extend Memoist
class Error < Exception;
end
attr_reader :post, :replacement
@ -11,90 +9,54 @@ class UploadService
@replacement = replacement
end
def comment_replacement_message(post, replacement)
%("#{replacement.creator.name}":[/users/#{replacement.creator.id}] replaced this post with a new image:\n\n#{replacement_message(post, replacement)})
end
def replacement_message(post, replacement)
linked_source = linked_source(replacement.replacement_url)
linked_source_was = linked_source(post.source_was)
<<-EOS.strip_heredoc
[table]
[tbody]
[tr]
[th]Old[/th]
[td]#{linked_source_was}[/td]
[td]#{post.md5_was}[/td]
[td]#{post.file_ext_was}[/td]
[td]#{post.image_width_was} x #{post.image_height_was}[/td]
[td]#{post.file_size_was.to_s(:human_size, precision: 4)}[/td]
[/tr]
[tr]
[th]New[/th]
[td]#{linked_source}[/td]
[td]#{post.md5}[/td]
[td]#{post.file_ext}[/td]
[td]#{post.image_width} x #{post.image_height}[/td]
[td]#{post.file_size.to_s(:human_size, precision: 4)}[/td]
[/tr]
[/tbody]
[/table]
EOS
end
def linked_source(source)
return nil if source.nil?
# truncate long sources in the middle: "www.pixiv.net...lust_id=23264933"
truncated_source = source.gsub(%r{\Ahttps?://}, "").truncate(64, omission: "...#{source.last(32)}")
if source =~ %r{\Ahttps?://}i
%("#{truncated_source}":[#{source}])
else
truncated_source
end
end
def undo!
undo_replacement = post.replacements.create(replacement_url: replacement.original_url)
undoer = Replacer.new(post: post, replacement: undo_replacement)
undoer.process!
end
def source_strategy(upload)
return Sources::Strategies.find(upload.source, upload.referer_url)
end
def find_replacement_url(repl, upload)
if repl.replacement_file.present?
return "file://#{repl.replacement_file.original_filename}"
return "file://#{repl.file_name}"
end
if !upload.source.present?
raise "No source found in upload for replacement"
end
if source_strategy(upload).canonical_url.present?
return source_strategy(upload).canonical_url
raise ProcessingError "No source found in upload for replacement"
end
return upload.source
end
def create_backup_replacement
begin
repl = post.replacements.new(creator_id: post.uploader_id, creator_ip_addr: post.uploader_ip_addr, status: 'original',
image_width: post.image_width, image_height: post.image_height, file_ext: post.file_ext,
file_size: post.file_size, md5: post.md5, file_name: "#{post.md5}.#{post.file_ext}",
source: post.source, reason: 'Backup of original file', is_backup: true)
repl.replacement_file = Danbooru.config.storage_manager.open(Danbooru.config.storage_manager.file_path(post, post.file_ext, :original))
repl.save
rescue Exception => e
raise ProcessingError, "Failed to create post file backup: #{e.message}"
end
raise ProcessingError, "Could not create post file backup?" if !repl.valid?
end
def process!
# Prevent trying to replace deleted posts
raise ProcessingError, "Cannot replace post: post is deleted." if post.is_deleted?
create_backup_replacement
replacement.replacement_file = Danbooru.config.storage_manager.open(Danbooru.config.storage_manager.replacement_path(replacement, replacement.file_ext, :original))
upload = Upload.create(
uploader_id: CurrentUser.id,
uploader_ip_addr: CurrentUser.ip_addr,
rating: post.rating,
tag_string: replacement.tags,
source: replacement.replacement_url,
tag_string: post.tag_string,
source: replacement.source,
file: replacement.replacement_file,
replaced_post: post,
original_post_id: post.id
original_post_id: post.id,
replacement_id: replacement.id
)
begin
if upload.invalid? || upload.is_errored?
raise Error, upload.status
raise ProcessingError, upload.status
end
upload.update(status: "processing")
@ -105,41 +67,30 @@ class UploadService
upload.save!
rescue Exception => x
upload.update(status: "error: #{x.class} - #{x.message}", backtrace: x.backtrace.join("\n"))
raise Error, upload.status
raise ProcessingError, upload.status
end
md5_changed = upload.md5 != post.md5
replacement.replacement_url = find_replacement_url(replacement, upload)
if md5_changed
post.queue_delete_files(PostReplacement::DELETION_GRACE_PERIOD)
post.delete_files
post.generated_samples = nil
end
replacement.file_ext = upload.file_ext
replacement.file_size = upload.file_size
replacement.image_height = upload.image_height
replacement.image_width = upload.image_width
replacement.md5 = upload.md5
post.md5 = upload.md5
post.file_ext = upload.file_ext
post.image_width = upload.image_width
post.image_height = upload.image_height
post.file_size = upload.file_size
post.source = replacement.final_source.presence || replacement.replacement_url
post.source = "#{replacement.source}\n" + post.source
post.tag_string = upload.tag_string
# Reset ownership information on post.
post.uploader_id = replacement.creator_id
post.uploader_ip_addr = replacement.creator_ip_addr
rescale_notes(post)
update_ugoira_frame_data(post, upload)
if md5_changed
CurrentUser.as(User.system) do
post.comments.create!(body: comment_replacement_message(post, replacement), do_not_bump_post: true)
end
end
replacement.save!
replacement.update({status: 'approved', approver_id: CurrentUser.id})
post.save!
if post.is_video?

View File

@ -3,6 +3,8 @@ class UploadService
extend self
class CorruptFileError < RuntimeError; end
IMAGE_TYPES = %i[original large preview crop]
def file_header_to_file_ext(file)
case File.read(file.path, 16)
when /^\xff\xd8/n
@ -36,7 +38,7 @@ class UploadService
Upload.find(upload_id).update(status: "preprocessed + deleted")
end
end
Danbooru.config.storage_manager.delete_post_files(md5, file_ext)
end
@ -86,58 +88,14 @@ class UploadService
end
def generate_resizes(file, upload)
if upload.is_video?
video = FFMPEG::Movie.new(file.path)
crop_file = generate_video_crop_for(video, Danbooru.config.small_image_width)
preview_file = generate_video_preview_for(file.path, Danbooru.config.small_image_width)
sample_file = generate_video_sample_for(file.path)
elsif upload.is_ugoira?
if upload.is_ugoira?
preview_file = PixivUgoiraConverter.generate_preview(file)
crop_file = PixivUgoiraConverter.generate_crop(file)
sample_file = PixivUgoiraConverter.generate_webm(file, upload.context["ugoira"]["frame_data"])
elsif upload.is_image?
preview_file = DanbooruImageResizer.resize(file, Danbooru.config.small_image_width, Danbooru.config.small_image_width, 85)
crop_file = DanbooruImageResizer.crop(file, Danbooru.config.small_image_width, Danbooru.config.small_image_width, 85)
if upload.image_width > Danbooru.config.large_image_width
sample_file = DanbooruImageResizer.resize(file, Danbooru.config.large_image_width, upload.image_height, 90)
end
[preview_file, crop_file, sample_file]
else
PostThumbnailer.generate_resizes(file, upload.image_height, upload.image_width, upload.is_video? ? :video : :image)
end
[preview_file, crop_file, sample_file]
end
def generate_video_crop_for(video, width)
vp = Tempfile.new(["video-preview", ".jpg"], binmode: true)
video.screenshot(vp.path, {:seek_time => 0, :resolution => "#{video.width}x#{video.height}"})
crop = DanbooruImageResizer.crop(vp, width, width, 85)
vp.close
return crop
end
def generate_video_preview_for(video, width)
output_file = Tempfile.new(["video-preview", ".jpg"], binmode: true)
stdout, stderr, status = Open3.capture3(Danbooru.config.ffmpeg_path, '-y', '-i', video, '-vf', "thumbnail,scale=#{width}:-1", '-frames:v', '1', output_file.path)
unless status == 0
Rails.logger.warn("[FFMPEG PREVIEW STDOUT] #{stdout.chomp!}")
Rails.logger.warn("[FFMPEG PREVIEW STDERR] #{stderr.chomp!}")
raise CorruptFileError.new("could not generate thumbnail")
end
output_file
end
def generate_video_sample_for(video)
output_file = Tempfile.new(["video-sample", ".jpg"], binmode: true)
stdout, stderr, status = Open3.capture3(Danbooru.config.ffmpeg_path, '-y', '-i', video, '-vf', 'thumbnail', '-frames:v', '1', output_file.path)
unless status == 0
Rails.logger.warn("[FFMPEG SAMPLE STDOUT] #{stdout.chomp!}")
Rails.logger.warn("[FFMPEG SAMPLE STDERR] #{stderr.chomp!}")
raise CorruptFileError.new("could not generate sample")
end
output_file
end
def process_file(upload, file, original_post_id: nil)
@ -214,24 +172,8 @@ class UploadService
return file if file.present?
raise RuntimeError, "No file or source URL provided" if upload.direct_url_parsed.blank?
attempts = 0
begin
download = Downloads::File.new(upload.direct_url_parsed, upload.referer_url)
file, strategy = download.download!
if !DanbooruImageResizer.validate_shell(file)
raise CorruptFileError.new("File is corrupted")
end
rescue
if attempts == 3
raise
end
attempts += 1
retry
end
download = Downloads::File.new(upload.direct_url_parsed, upload.referer_url)
file, strategy = download.download!
if download.data[:ugoira_frame_data].present?
upload.context = {

View File

@ -1,4 +1,5 @@
class Blip < ApplicationRecord
include UserWarnable
simple_versioning
belongs_to_creator
user_status_counter :blip_count
@ -48,6 +49,7 @@ class Blip < ApplicationRecord
end
def can_edit?(user)
return false if was_warned? && !user.is_moderator?
(creator_id == user.id && created_at > 5.minutes.ago) || user.is_moderator?
end

View File

@ -1,5 +1,5 @@
class Comment < ApplicationRecord
include UserWarnable
simple_versioning
belongs_to_creator
belongs_to_updater
@ -168,6 +168,7 @@ class Comment < ApplicationRecord
end
def editable_by?(user)
return false if was_warned? && !user.is_moderator?
creator_id == user.id || user.is_moderator?
end

View File

@ -1,5 +1,5 @@
class ForumPost < ApplicationRecord
include UserWarnable
simple_versioning
attr_readonly :topic_id
belongs_to_creator
@ -183,6 +183,7 @@ class ForumPost < ApplicationRecord
end
def editable_by?(user)
return false if was_warned? && !user.is_moderator?
(creator_id == user.id || user.is_moderator?) && visible?(user)
end

View File

@ -95,6 +95,9 @@ class ModAction < ApplicationRecord
:post_undelete,
:post_unapprove,
:post_rating_lock,
:post_replacement_accept,
:post_replacement_reject,
:post_replacement_delete,
:report_reason_create,
:report_reason_delete,
:report_reason_update,

View File

@ -65,6 +65,8 @@ class Post < ApplicationRecord
has_many :versions, -> {order("post_versions.id ASC")}, :class_name => "PostArchive", :dependent => :destroy
IMAGE_TYPES = %i[original large preview crop]
module FileMethods
extend ActiveSupport::Concern
@ -279,6 +281,23 @@ class Post < ApplicationRecord
PostVideoConversionJob.perform_async(self.id)
end
end
def regenerate_video_samples!
# force code to assume no samples exist
update_column(:generated_samples, nil)
generate_video_samples(later: true)
end
def regenerate_image_samples!
file = self.file()
preview_file, crop_file, sample_file = ::PostThumbnailer.generate_resizes(file, image_height, image_width, is_video? ? :video : :image)
storage_manager.store_file(sample_file, self, :large) if sample_file.present?
storage_manager.store_file(preview_file, self, :preview) if preview_file.present?
storage_manager.store_file(crop_file, self, :crop) if crop_file.present?
update({has_cropped: crop_file.present?})
ensure
file.close
end
end
module ImageMethods
@ -841,7 +860,6 @@ class Post < ApplicationRecord
normalized_tags = apply_locked_tags(normalized_tags, @locked_to_add, @locked_to_remove)
normalized_tags = %w(tagme) if normalized_tags.empty?
normalized_tags = add_automatic_tags(normalized_tags)
# normalized_tags = normalized_tags + Tag.create_for_list(TagImplication.automatic_tags_for(normalized_tags))
normalized_tags = TagImplication.with_descendants(normalized_tags)
enforce_dnp_tags(normalized_tags)
normalized_tags -= @locked_to_remove if @locked_to_remove # Prevent adding locked tags through implications or aliases.
@ -1580,6 +1598,11 @@ class Post < ApplicationRecord
UserStatus.for_user(uploader_id).update_all("post_deleted_count = post_deleted_count + 1")
give_favorites_to_parent(options) if options[:move_favorites]
give_post_sets_to_parent if options[:move_favorites]
reject_pending_replacements
end
def reject_pending_replacements
replacements.where(status: 'pending').update_all(status: 'rejected')
end
def undelete!(options = {})
@ -1607,15 +1630,6 @@ class Post < ApplicationRecord
move_files_on_undelete
UserStatus.for_user(uploader_id).update_all("post_deleted_count = post_deleted_count - 1")
end
def replace!(params)
transaction do
replacement = replacements.create(params)
processor = UploadService::Replacer.new(post: self, replacement: replacement)
processor.process!
replacement
end
end
end
module VersionMethods

View File

@ -1,38 +1,224 @@
class PostReplacement < ApplicationRecord
DELETION_GRACE_PERIOD = 24.hours
self.table_name = 'post_replacements2'
belongs_to :post
belongs_to :creator, class_name: "User"
before_validation :initialize_fields, on: :create
attr_accessor :replacement_file, :final_source, :tags
belongs_to :approver, class_name: "User", optional: true
attr_accessor :replacement_file, :replacement_url, :final_source, :tags, :is_backup
def initialize_fields
self.creator = CurrentUser.user
self.original_url = post.source
self.tags = post.tag_string + " " + self.tags.to_s
validate :user_is_not_limited, on: :create
validate :post_is_valid, on: :create
validate :set_file_name, on: :create
validate :fetch_source_file, on: :create
validate :update_file_attributes, on: :create
validate :no_pending_duplicates, on: :create
validate :write_storage_file, on: :create
self.old_file_ext = post.file_ext
self.old_file_size = post.file_size
self.old_image_width = post.image_width
self.old_image_height = post.image_height
self.old_md5 = post.md5
before_destroy :remove_files
def replacement_url_parsed
return nil unless replacement_url =~ %r!\Ahttps?://!i
Addressable::URI.heuristic_parse(replacement_url) rescue nil
end
module PostMethods
def post_is_valid
if post.is_deleted?
self.errors.add(:post, "is deleted")
return false
end
end
end
module FileMethods
def is_image?
%w(jpg jpeg gif png).include?(file_ext)
end
def is_flash?
%w(swf).include?(file_ext)
end
def is_video?
%w(webm).include?(file_ext)
end
def is_ugoira?
%w(zip).include?(file_ext)
end
end
def no_pending_duplicates
return true if is_backup
post = Post.where(md5: md5).first
if post
self.errors.add(:md5, "duplicate of existing post ##{post.id}")
return false
end
replacements = PostReplacement.where(status: 'pending', md5: md5)
replacements.each do |replacement|
self.errors.add(:md5, "duplicate of pending replacement on post ##{replacement.post_id}")
end
replacements.size == 0
end
def user_is_not_limited
return true if status == 'original'
replaceable = creator.can_replace_post_with_reason
if replaceable != true
self.errors.add(:creator, User.throttle_reason(replaceable))
return false
end
uploadable = creator.can_upload_with_reason
if uploadable != true
self.errors.add(:creator, User.upload_reason_string(uploadable))
return false
end
# Janitor bypass replacement limits
return true if creator.is_janitor?
if post.replacements.where(creator_id: creator.id).where('created_at > ?', 1.day.ago).count >= Danbooru.config.post_replacement_per_day_limit
self.errors.add(:creator, 'has already suggested too many replacements for this post today')
return false
end
if post.replacements.where(creator_id: creator.id).count >= Danbooru.config.post_replacement_per_post_limit
self.errors.add(:creator, 'has already suggested too many total replacements for this post')
return false
end
true
end
def source_list
source.split("\n").uniq.reject(&:blank?)
end
module StorageMethods
def remove_files
ModAction.log(:post_replacement_delete, {id: id, post_id: post_id, md5: md5, storage_id: storage_id})
Danbooru.config.storage_manager.delete_replacement(self)
end
def fetch_source_file
return if replacement_file.present?
download = Downloads::File.new(replacement_url_parsed, "")
file, strategy = download.download!
self.replacement_file = file
self.source = replacement_url + "\n#{self.source}"
rescue Downloads::File::Error
self.errors.add(:replacement_url, "failed to fetch file")
throw :abort
end
def update_file_attributes
self.file_ext = UploadService::Utils.file_header_to_file_ext(replacement_file)
if file_ext == "bin"
self.errors.add(:base, "Unknown or invalid file format")
throw :abort
end
self.file_size = replacement_file.size
self.md5 = Digest::MD5.file(replacement_file.path).hexdigest
UploadService::Utils.calculate_dimensions(self, replacement_file) do |width, height|
self.image_width = width
self.image_height = height
end
end
def set_file_name
if replacement_file.present?
self.file_name = replacement_file.try(:original_filename) || File.basename(replacement_file.path)
else
if replacement_url_parsed.blank?
self.errors.add(:base, "No file or source URL provided")
throw :abort
end
self.file_name = replacement_url_parsed.basename
end
end
def write_storage_file
self.storage_id = SecureRandom.hex(16)
Danbooru.config.storage_manager.store_replacement(replacement_file, self, :original)
thumbnail_file = PostThumbnailer.generate_thumbnail(replacement_file, is_video? ? :video : :image)
Danbooru.config.storage_manager.store_replacement(thumbnail_file, self, :preview)
ensure
thumbnail_file.try(:close!)
end
def replacement_file_path
Danbooru.config.storage_manager.replacement_path(self, file_ext, :original)
end
def replacement_thumb_path
Danbooru.config.storage_manager.replacement_path(self, file_ext, :preview)
end
def replacement_file_url
Danbooru.config.storage_manager.replacement_url(self)
end
def replacement_thumb_url
Danbooru.config.storage_manager.replacement_url(self, :preview)
end
end
module ApiMethods
def hidden_attributes
super + [:storage_id]
end
end
module ProcessingMethods
def approve!
transaction do
ModAction.log(:post_replacement_accept, {post_id: post.id, replacement_id: self.id, old_md5: post.md5, new_md5: self.md5})
processor = UploadService::Replacer.new(post: post, replacement: self)
processor.process!
end
end
def promote!
transaction do
processor = UploadService.new(new_upload_params)
new_post = processor.start!
update_attribute(:status, 'promoted')
new_post
end
end
def reject!
ModAction.log(:post_replacement_reject, {post_id: post.id, replacement_id: self.id})
update_attribute(:status, 'rejected')
end
end
module PromotionMethods
def new_upload_params
{
uploader_id: creator_id,
uploader_ip_addr: creator_ip_addr,
file: Danbooru.config.storage_manager.open(Danbooru.config.storage_manager.replacement_path(self, file_ext, :original)),
tag_string: post.tag_string,
rating: post.rating,
source: "#{self.source}\n" + post.source,
parent_id: post.id,
description: post.description,
locked_tags: post.locked_tags,
replacement_id: self.id
}
end
end
concerning :Search do
class_methods do
def post_tags_match(query)
where(post_id: PostQueryBuilder.new(query).build.reorder(""))
end
def search(params = {})
q = super
q = q.attribute_matches(:replacement_url, params[:replacement_url])
q = q.attribute_matches(:original_url, params[:original_url])
q = q.attribute_matches(:old_file_ext, params[:old_file_ext])
q = q.attribute_matches(:file_ext, params[:file_ext])
q = q.attribute_matches(:old_md5, params[:old_md5])
q = q.attribute_matches(:md5, params[:md5])
q = q.attribute_exact_matches(:file_ext, params[:file_ext])
q = q.attribute_exact_matches(:md5, params[:md5])
q = q.attribute_exact_matches(:status, params[:status])
if params[:creator_id].present?
q = q.where(creator_id: params[:creator_id].split(",").map(&:to_i))
@ -46,19 +232,44 @@ class PostReplacement < ApplicationRecord
q = q.where(post_id: params[:post_id].split(",").map(&:to_i))
end
if params[:post_tags_match].present?
q = q.post_tags_match(params[:post_tags_match])
end
q.apply_default_order(params)
q.order(Arel.sql("CASE status WHEN 'pending' THEN 0 ELSE 1 END ASC, id DESC"))
end
def pending
where(status: 'pending')
end
def rejected
where(status: 'rejected')
end
def approved
where(status: 'approved')
end
def for_user(id)
where(creator_id: id.to_i)
end
def visible(user)
return where('status != ?', 'rejected') if user.is_anonymous?
return all if user.is_janitor?
where('creator_id = ? or status != ?', user.id, 'rejected')
end
end
end
def suggested_tags_for_removal
tags = post.tag_array.select { |tag| Danbooru.config.remove_tag_after_replacement?(tag) }
tags = tags.map { |tag| "-#{tag}" }
tags.join(" ")
def file_visible_to?(user)
return true if user.is_janitor?
false
end
include ApiMethods
include StorageMethods
include FileMethods
include ProcessingMethods
include PromotionMethods
include PostMethods
end

View File

@ -0,0 +1,7 @@
class StaffAuditLog < ApplicationRecord
belongs_to :user, :class_name => "User"
def self.log(category, user, details)
create(user: user, action: category.to_s, values: details)
end
end

66
app/models/staff_note.rb Normal file
View File

@ -0,0 +1,66 @@
class StaffNote < ApplicationRecord
belongs_to :creator, :class_name => "User"
belongs_to :user
after_create :add_audit_entry
module SearchMethods
def for_creator(user_id)
user_id.present? ? where("creator_id = ?", user_id) : none
end
def for_creator_name(user_name)
for_creator(User.name_to_id(user_name))
end
def for_user(user_id)
user_id.present? ? where('creator_id = ?', user_id) : none
end
def for_user_name(user_name)
for_user(User.name_to_id(user_name))
end
def search(params)
q = super
if params[:resolved]
q = q.attribute_matches(:resolved, params[:resolved])
end
if params[:user_id].present?
q = q.where('user_id = ?', params[:user_id])
end
if params[:user_name].present?
q = q.for_user_name(params[:user_name])
end
if params[:creator_name].present?
q = q.for_creator_name(params[:creator_name])
end
q.apply_default_order(params)
end
def default_order
order("resolved asc, id desc")
end
end
extend SearchMethods
def add_audit_entry
StaffAuditLog.log(:staff_note_add, creator, {user_id: user_id})
end
def resolve!
self.resolved = true
save
end
def unresolve!
self.resolved = false
save
end
end

View File

@ -1139,10 +1139,10 @@ class Tag < ApplicationRecord
name = normalize_name(name)
wildcard_name = name + '*'
query1 = Tag.select("tags.name, tags.post_count, tags.category, null AS antecedent_name")
query1 = Tag.select("tags.id, tags.name, tags.post_count, tags.category, null AS antecedent_name")
.search(:name_matches => wildcard_name, :order => "count").limit(10)
query2 = TagAlias.select("tags.name, tags.post_count, tags.category, tag_aliases.antecedent_name")
query2 = TagAlias.select("tags.id, tags.name, tags.post_count, tags.category, tag_aliases.antecedent_name")
.joins("INNER JOIN tags ON tags.name = tag_aliases.consequent_name")
.where("tag_aliases.antecedent_name LIKE ? ESCAPE E'\\\\'", wildcard_name.to_escaped_for_sql_like)
.active
@ -1155,7 +1155,7 @@ class Tag < ApplicationRecord
tags = Tag.select("DISTINCT ON (name, post_count) *").from(sql_query).order("post_count desc").limit(10).to_a
if tags.size == 0
tags = Tag.select("tags.name, tags.post_count, tags.category, null AS antecedent_name").fuzzy_name_matches(name).order_similarity(name).nonempty.limit(10)
tags = Tag.select("tags.id, tags.name, tags.post_count, tags.category, null AS antecedent_name").fuzzy_name_matches(name).order_similarity(name).nonempty.limit(10)
end
tags

View File

@ -28,12 +28,6 @@ class TagImplication < TagRelationship
def descendants_with_originals(names)
active.where(antecedent_name: names).map { |x| [x.antecedent_name, x.descendant_names] }.uniq
end
def automatic_tags_for(names)
# TODO: Remove this?
tags = []
tags.uniq
end
end
def descendants

View File

@ -150,9 +150,9 @@ class TagRelationship < ApplicationRecord
params[:order] ||= "status"
case params[:order].downcase
when "created_at"
q = q.order("created_at desc")
q = q.order("created_at desc nulls last, id desc")
when "updated_at"
q = q.order("updated_at desc")
q = q.order("updated_at desc nulls last, id desc")
when "name"
q = q.order("antecedent_name asc, consequent_name asc")
when "tag_count"

View File

@ -88,6 +88,7 @@ class Takedown < ApplicationRecord
def add_posts_by_ids!(ids)
added_ids = []
with_lock do
ids = ids.gsub(/(https?:\/\/)?(e621|e926)\.net\/posts\/(\d+)/i, '\3')
self.post_ids = (post_array + ids.scan(/\d+/).map(&:to_i)).uniq.join(' ')
added_ids = self.post_array - self.post_array_was
save!

View File

@ -8,11 +8,18 @@ class Upload < ApplicationRecord
validate_file_ext(record)
validate_md5_uniqueness(record)
validate_file_size(record)
validate_file_integrity(record)
validate_video_container_format(record)
validate_video_duration(record)
validate_resolution(record)
end
def validate_file_integrity(record)
if record.file_ext.in?(["jpg", "jpeg", "gif", "png"]) && DanbooruImageResizer.is_corrupt?(record.file.path)
record.errors[:file] << "File is corrupt"
end
end
def validate_file_ext(record)
if record.file_ext == "bin"
record.errors.add(:file_ext, "is invalid (only JPEG, PNG, GIF, and WebM files are allowed")
@ -20,6 +27,9 @@ class Upload < ApplicationRecord
end
def validate_file_size(record)
if record.file_size <= 16
record.errors[:file_size] << "is too small"
end
max_size = Danbooru.config.max_file_sizes.fetch(record.file_ext, 0)
if record.file_size > max_size
record.errors.add(:file_size, "is too large. Maximum allowed for this file type is #{max_size / (1024*1024)} MiB")
@ -34,6 +44,16 @@ class Upload < ApplicationRecord
return
end
replacements = PostReplacement.pending.where(md5: record.md5)
replacements = replacements.where('id != ?', record.replacement_id) if record.replacement_id
if !record.replaced_post && replacements.size > 0
replacements.each do |rep|
record.errors.add(:md5) << "duplicate of pending replacement on post ##{rep.post_id}"
end
return
end
md5_post = Post.find_by_md5(record.md5)
if md5_post.nil?
@ -81,7 +101,7 @@ class Upload < ApplicationRecord
end
attr_accessor :as_pending, :replaced_post, :file, :direct_url, :is_apng, :original_post_id, :locked_tags, :locked_rating
attr_accessor :as_pending, :replaced_post, :file, :direct_url, :is_apng, :original_post_id, :locked_tags, :locked_rating, :replacement_id
belongs_to :uploader, :class_name => "User"
belongs_to :post, optional: true
@ -97,8 +117,6 @@ class Upload < ApplicationRecord
serialize :context, JSON
def initialize_attributes
self.uploader_id = CurrentUser.id
self.uploader_ip_addr = CurrentUser.ip_addr
self.server = Danbooru.config.server_host
end

View File

@ -100,7 +100,6 @@ class User < ApplicationRecord
before_create :encrypt_password_on_create
before_update :encrypt_password_on_update
after_save :update_cache
before_create :promote_to_admin_if_first_user
#after_create :notify_sock_puppets
after_create :create_user_status
has_many :feedback, :class_name => "UserFeedback", :dependent => :destroy
@ -304,18 +303,6 @@ class User < ApplicationRecord
UserPromotion.new(self, CurrentUser.user, new_level, options).promote!
end
def promote_to_admin_if_first_user
return if Rails.env.test?
if User.admins.count == 0
self.level = Levels::ADMIN
self.can_approve_posts = true
self.can_upload_free = true
else
self.level = Levels::MEMBER
end
end
def role
level_string.downcase.to_sym
end
@ -535,6 +522,8 @@ class User < ApplicationRecord
:is_janitor?, 7.days)
create_user_throttle(:forum_vote, -> { Danbooru.config.forum_vote_limit - ForumPostVote.by(id).where("created_at > ?", 1.hour.ago).count },
:is_janitor?, 3.days)
create_user_throttle(:replace_post, ->{ Danbooru.config.replace_post_limit - PostReplacement.for_user(id).where("created_at > ?", 1.hour.ago).count },
:can_approve_posts?, 7.days)
def can_remove_from_pools?
is_member? && older_than(7.days)
@ -557,15 +546,15 @@ class User < ApplicationRecord
end
def can_upload_with_reason
if hourly_upload_limit <= 0
if hourly_upload_limit <= 0 && !Danbooru.config.disable_throttles
:REJ_UPLOAD_HOURLY
elsif can_upload_free? || is_admin?
true
elsif younger_than(7.days) && !Danbooru.config.disable_throttles
elsif younger_than(7.days)
:REJ_UPLOAD_NEWBIE
elsif !is_privileged? && post_edit_limit <= 0
elsif !is_privileged? && post_edit_limit <= 0 && !Danbooru.config.disable_throttles
:REJ_UPLOAD_EDIT
elsif upload_limit <= 0
elsif upload_limit <= 0 && !Danbooru.config.disable_throttles
:REJ_UPLOAD_LIMIT
else
true
@ -590,10 +579,12 @@ class User < ApplicationRecord
def upload_limit_pieces
deleted_count = Post.deleted.for_user(id).count
rejected_replacement_count = PostReplacement.rejected.for_user(id).count
unapproved_count = Post.pending_or_flagged.for_user(id).count
unapproved_replacements_count = PostReplacement.pending.for_user(id).count
approved_count = Post.for_user(id).where('is_flagged = false AND is_deleted = false AND is_pending = false').count
return {deleted: deleted_count, approved: approved_count, pending: unapproved_count}
return {deleted: deleted_count, approved: approved_count, pending: unapproved_count + unapproved_replacements_count}
end
memoize :upload_limit_pieces

View File

@ -165,7 +165,6 @@ class WikiPage < ApplicationRecord
self.title = version.title
self.body = version.body
self.is_locked = version.is_locked
self.other_names = version.other_names
end

View File

@ -7,7 +7,7 @@ class PostPresenter < Presenter
return "<em>none</em>".html_safe
end
if !options[:show_deleted] && post.is_deleted? && options[:tags] !~ /(?:status:(?:all|any|deleted))|(?:deletedby:)|(?:delreason:)/ && !options[:raw]
if !options[:show_deleted] && post.is_deleted? && options[:tags] !~ /(?:status:(?:all|any|deleted))|(?:deletedby:)|(?:delreason:)/i && !options[:raw]
return ""
end

View File

@ -183,4 +183,16 @@ class UserPresenter
return false if user.enable_privacy_mode? && !CurrentUser.is_admin?
true
end
def show_staff_notes?
CurrentUser.is_moderator?
end
def staff_notes
StaffNote.where(user_id: user.id).order(id: :desc).limit(15)
end
def new_staff_note
StaffNote.new(user_id: user.id)
end
end

View File

@ -12,5 +12,7 @@
<strong>Extra Params:</strong>
<pre class="box-section"><%= JSON.pretty_generate(@exception_log.extra_params) %></pre>
<strong>Stacktrace:</strong>
<pre class="box-section"><%= Rails.backtrace_cleaner.clean(@exception_log.trace.split("\n")).join("\n") %></pre>
<strong>Raw Stacktrace:</strong>
<pre class="box-section"><%= @exception_log.trace %></pre>
</div>

View File

@ -0,0 +1,13 @@
<div id='searchform_hide'>
<%= search_show_link %>
</div>
<div class='section' id='searchform' style='width:400px;<% unless params[:show] %>display:none;
<% end %>'>
<%= simple_form_for(:search, :method => :get, url: admin_staff_notes_path, defaults: {required: false}, html: {class: "inline-form"}) do |f| %>
<%= f.input :creator_name, label: "Creator Name", input_html: {data: {autocomplete: "user"}} %>
<%= f.input :user_name, label: "User Name", input_html: {data: {autocomplete: "user"}} %>
<%= f.input :body_matches, label: "Body" %>
<%= f.submit "Search" %>
<% end %>
<%= search_hide_link %>
</div>

View File

@ -0,0 +1,19 @@
<div id="c-staff-notes">
<div id="a-index">
<% if @user %>
<h1>Staff Notes for <%= link_to_user @user %></h1>
<% else %>
<h1>Staff Notes</h1>
<% end %>
<%= render "search" %>
<%= render "admin/staff_notes/partials/list_of_notes", staff_notes: @notes %>
<%= numbered_paginator(@notes) %>
</div>
</div>
<%= render "users/secondary_links" %>
<% content_for(:page_title) do %>
<%= @user ? "#{@user.name} - Staff Notes" : "Staff Notes" %>
<% end %>

View File

@ -0,0 +1,13 @@
<div id="c-staff-notes">
<div id="a-new">
<h1>New Staff Note for <%= link_to_user(@user) %></h1>
<%= render "admin/staff_notes/partials/new", staff_note: @staff_note, user: @user %>
</div>
</div>
<%= render "users/secondary_links" %>
<% content_for(:page_title) do %>
New Staff Note
<% end %>

View File

@ -0,0 +1,3 @@
<div id="p-staff-notes-list" class="staff-note-list">
<%= render :partial => "admin/staff_notes/partials/staff_note", :collection => staff_notes %>
</div>

View File

@ -0,0 +1,7 @@
<%= error_messages_for :staff_note %>
<%= simple_form_for(staff_note, url: user_staff_notes_path(user_id: user.id), method: :post) do |f| %>
<%= dtext_field "staff_note", "body", name: "", :value => staff_note.body, :input_id => "staff_note_body_for_#{staff_note.id}", :preview_id => "dtext-preview-for-#{staff_note.id}" %>
<%= f.button :submit, "Submit" %>
<%= dtext_preview_button "staff_note", "body", :input_id => "staff_note_body_for_#{staff_note.id}", :preview_id => "dtext-preview-for-#{staff_note.id}" %>
<% end %>

View File

@ -0,0 +1,19 @@
<article class="staff-note comment-post-grid" id="staff-note-<%= staff_note.id %>">
<div class="author-info">
<div class="name-rank">
<h4 class="author-name"><%= link_to_user staff_note.creator %></h4>
<%= staff_note.creator.level_string %>
</div>
<div class="post-time">
<%= link_to time_ago_in_words_tagged(staff_note.created_at), user_staff_notes_path(user_id: staff_note.user_id, anchor: "staff-note-#{staff_note.id}") %>
</div>
</div>
<div class="content">
<% unless @user %>
<h4>On <%= link_to_user staff_note.user %></h4>
<% end %>
<div class="body styled-dtext">
<%= format_text(staff_note.body, allow_color: true) %>
</div>
</div>
</article>

View File

@ -23,13 +23,21 @@
<div class="body styled-dtext">
<%= format_text(blip.body, allow_color: blip.creator.is_privileged?) %>
</div>
<% if blip.was_warned? %>
<div class="user-warning">
<hr>
<em><%= blip.warning_type_string %></em>
</div>
<% end %>
<div class="content-menu">
<menu>
<li><%= tag.a "Reply", href: '#', class: 'blip-reply-link', 'data-bid': blip.id %></li>
<% if blip.can_edit?(CurrentUser.user) %>
<li><%= link_to "Edit", edit_blip_path(blip) %></li>
<% if CurrentUser.is_member? %>
<li><%= tag.a "Reply", href: '#', class: 'blip-reply-link', 'data-bid': blip.id %></li>
<% if blip.can_edit?(CurrentUser.user) %>
<li><%= link_to "Edit", edit_blip_path(blip) %></li>
<% end %>
<li><%= tag.a "@", href: '#', class: 'blip-atme-link', 'data-bid': blip.id %></li>
<% end %>
<li><%= tag.a "@", href: '#', class: 'blip-atme-link', 'data-bid': blip.id %></li>
<% if !blip.is_hidden && blip.can_hide?(CurrentUser.user) %>
<li><%= link_to "Hide", hide_blip_path(blip), data: {confirm: "Are you sure you want to hide this blip?"}, method: :post %></li>
@ -56,6 +64,15 @@
<strong>IP</strong>
<span><%= link_to_ip blip.creator_ip_addr %></span>
</li>
<li>|</li>
<% if !blip.was_warned? %>
<li>Mark for:</li>
<li><%= tag.a "Warning", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'blip', 'item-id': blip.id, 'record-type': 'warning' } %></li>
<li><%= tag.a "Record", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'blip', 'item-id': blip.id, 'record-type': 'record' } %></li>
<li><%= tag.a "Ban", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'blip', 'item-id': blip.id, 'record-type': 'ban' } %></li>
<% else %>
<li><%= tag.a "Unmark", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'blip', 'item-id': blip.id, 'record-type': 'unmark' } %></li>
<% end %>
<% end %>
</menu>
</div>

View File

@ -24,6 +24,12 @@
<%= render "application/update_notice", record: comment %>
</div>
<% if comment.was_warned? %>
<div class="user-warning">
<hr>
<em><%= comment.warning_type_string %></em>
</div>
<% end %>
<div class="content-menu">
<menu>
<% if @post || @posts || comment.post.present? %>
@ -58,6 +64,15 @@
<strong>IP</strong>
<span><%= link_to_ip comment.creator_ip_addr %></span>
</li>
<li>|</li>
<% if !comment.was_warned? %>
<li>Mark for:</li>
<li><%= tag.a "Warning", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'comment', 'item-id': comment.id, 'record-type': 'warning' } %></li>
<li><%= tag.a "Record", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'comment', 'item-id': comment.id, 'record-type': 'record' } %></li>
<li><%= tag.a "Ban", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'comment', 'item-id': comment.id, 'record-type': 'ban' } %></li>
<% else %>
<li><%= tag.a "Unmark", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'comment', 'item-id': comment.id, 'record-type': 'unmark' } %></li>
<% end %>
<% end %>
<% end %>
</menu>

View File

@ -23,6 +23,12 @@
<%= format_text(parse_embedded_tag_request_text(forum_post.body), allow_color: forum_post.creator.is_privileged?) %>
</div>
<%= render "application/update_notice", record: forum_post %>
<% if forum_post.was_warned? %>
<div class="user-warning">
<hr>
<em><%= forum_post.warning_type_string %></em>
</div>
<% end %>
<div class="content-menu">
<menu>
<% if CurrentUser.is_member? && @forum_topic && params[:controller] != "forum_posts" %>
@ -65,6 +71,15 @@
<span><%= link_to_ip forum_post.creator_ip_addr %></span>
</li>
<li>|</li>
<% if !forum_post.was_warned? %>
<li>Mark for:</li>
<li><%= tag.a "Warning", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'forum_post', 'item-id': forum_post.id, 'record-type': 'warning' } %></li>
<li><%= tag.a "Record", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'forum_post', 'item-id': forum_post.id, 'record-type': 'record' } %></li>
<li><%= tag.a "Ban", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'forum_post', 'item-id': forum_post.id, 'record-type': 'ban' } %></li>
<% else %>
<li><%= tag.a "Unmark", href: '#', class: 'item-mark-user-warned', data: { 'item-type': 'forum_post', 'item-id': forum_post.id, 'record-type': 'unmark' } %></li>
<% end %>
<li>|</li>
<% end %>
<% if params[:controller] == "forum_posts" %>
<li><%= link_to "Parent", forum_topic_path(forum_post.topic, :page => forum_post.forum_topic_page, :anchor => "forum_post_#{forum_post.id}") %></li>

View File

@ -6,15 +6,19 @@
<thead>
<tr>
<th>Creator</th>
<th>Date</th>
<th>Message</th>
<th></th>
<% if CurrentUser.is_moderator? %>
<th></th>
<% end %>
</tr>
</thead>
<tbody>
<% @news_updates.each do |news_update| %>
<tr id="news-update-<%= news_update.id %>">
<td><%= link_to_user news_update.creator %></td>
<td><%= news_update.message %></td>
<td><%= compact_time news_update.updated_at %></td>
<td><div class="styled-dtext"><%= format_text news_update.message %></div></td>
<% if CurrentUser.is_moderator? %>
<td><%= link_to "Edit", edit_news_update_path(news_update) %> | <%= link_to "Delete", news_update_path(news_update), :method => :delete %></td>
<% end %>

View File

@ -4,10 +4,30 @@
</div>
<%= simple_form_for(post_replacement, url: post_replacements_path(post_id: post_replacement.post_id), method: :post) do |f| %>
<div>
<% if post.visible? %>
<%= PostPresenter.preview(post, tags: "status:any", no_blacklist: true) %>
<% end %>
<div><%= "#{post.image_width}x#{post.image_height} (#{post.file_size.to_s(:human_size, precision: 5)})" %></div>
</div>
<%= f.input :replacement_file, label: "File", as: :file %>
<%= f.input :replacement_url, label: "Replacement URL", hint: "The source URL to download the replacement from.", as: :string, input_html: { value: post_replacement.post.normalized_source } %>
<%= f.input :final_source, label: "Final Source", hint: "If present, the source field will be changed to this after replacement.", as: :string, input_html: { value: post_replacement.post.source } %>
<%= f.input :tags, label: "Tags", as: :string, input_html: { value: post_replacement.suggested_tags_for_removal, data: { autocomplete: "tag-edit" } } %>
<%= f.input :replacement_url, label: "Replacement URL", hint: "The source URL to download the replacement from.", as: :string, input_html: {size: 40} %>
<div class="box-section sect_red" id="bad_upload_url" style="display: none;">
The direct URL entered has the following problem: <span id="bad_upload_url_reason"></span><br>
You should review <a href="/wiki/show/howto:sites_and_sources">the sourcing guide</a>.
</div>
<div id="whitelist-warning" style="display: none;"
:class="{'whitelist-warning-allowed': whitelist.allowed, 'whitelist-warning-disallowed': !whitelist.allowed}">
<span>Uploads from <b id="whitelist-warning-domain"></b> are <span id="whitelist-warning-not">not </span>permitted.</span>
</div>
<%= f.input :source, label: "Additional Source", hint: "(Optional) The submission page the replacement file came from.", input_html: {size: 40} %>
<%= f.input :reason, label: "Reason", hint: "Tell us why this file should replace the original.", as: :string, input_html: {size: 40} %>
<%= f.submit "Submit" %>
<% end %>
<div id="replacement-upload-preview">
<div id="replacement_preview_dims"></div>
<img id="replacement_preview_img" src="" style="max-width: 100%;"/>
</div>
</div>

View File

@ -0,0 +1,7 @@
<%= simple_form_for(:search, url: post_replacements_path, method: :get, defaults: { required: false }, html: { class: "inline-form" }) do |f| %>
<%= f.input :md5, label: "MD5", input_html: { value: params.dig(:search, :md5) } %>
<%= f.input :creator_name, label: "Creator", input_html: { value: params.dig(:search, :creator_name) } %>
<%= f.input :post_id, label: "Post ID", input_html: { value: params.dig(:search, :post_id) } %>
<%= f.input :status, label: "status", collection: ["pending", "rejected", "approved", "promoted"], include_blank: true, selected: params.dig(:search, :status) %>
<%= f.submit "Search" %>
<% end %>

View File

@ -2,78 +2,96 @@
<div id="a-index">
<h1>Post Replacements</h1>
<%= render "search" %>
<%= render "posts/partials/common/inline_blacklist" %>
<%= simple_form_for(:search, url: post_replacements_path, method: :get, defaults: { required: false }, html: { class: "inline-form" }) do |f| %>
<%= f.input :creator_name, label: "Replacer", input_html: { value: params.dig(:search, :creator_name), data: { autocomplete: "user" } } %>
<%= f.input :post_tags_match, label: "Tags", input_html: { value: params.dig(:search, :post_tags_match), data: { autocomplete: "tag-query" } } %>
<%= f.submit "Search" %>
<% end %>
<table width="100%" class="striped autofit">
<table width="100%" class="striped autofit" style="max-width:100%">
<thead>
<tr>
<th width="1%">Post</th>
<th>Source</th>
<th>MD5</th>
<th>Size</th>
<th>Replacer</th>
<th width="1%">Replacement</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<% @post_replacements.each do |post_replacement| %>
<tr>
<tr class="<%= 'replacement-pending-row' if post_replacement.status == 'pending' %>">
<td><%= PostPresenter.preview(post_replacement.post, show_deleted: true) %></td>
<td><%= replacement_thumbnail(post_replacement) %></td>
<td>
<dl>
<dt>Original Source</dt>
<dd><%= external_link_to post_replacement.original_url, truncate: 64 %></dd>
<dt>Replacement Source</dt>
<dd>
<% if post_replacement.replacement_url.present? %>
<%= external_link_to post_replacement.replacement_url, truncate: 64 %>
<% if post_replacement.source.present? %>
<ul>
<% post_replacement.source_list.each do |source| %>
<li><%= external_link_to source, truncate: 64 %></li>
<% end %>
</ul>
<% else %>
<em>file</em>
<% end %>
</dd>
<dt>Filename</dt>
<dd><%= post_replacement.file_name %></dd>
<% if post_replacement.md5.present? %>
<dt>Replacement MD5</dt>
<dd><%= post_replacement.md5 %></dd>
<% end %>
</dl>
<div style="display:grid;grid-template-columns: 1fr 1fr;grid-auto-rows: auto;">
<dl>
<% if post_replacement.post %>
<dt>Current Size</dt>
<dd>
<%= post_replacement.post.image_width %>x<%= post_replacement.post.image_height %>
(<%= post_replacement.post.file_size.to_s(:human_size, precision: 5) %>, <%= post_replacement.post.file_ext %>)
</dd>
<% end %>
</dl>
<dl>
<% if %i[image_width image_height file_size file_ext].all? { |k| post_replacement[k].present? } %>
<dt>Replacement Size</dt>
<dd>
<%= post_replacement.image_width %>x<%= post_replacement.image_height %>
(<%= post_replacement.file_size.to_s(:human_size, precision: 5) %>, <%= post_replacement.file_ext %>)
</dd>
<% end %>
</dl>
</div>
</td>
<td>
<dl>
<dt>Status</dt>
<dd><%= post_replacement.status %></dd>
<dt>Reason</dt>
<dd><%= post_replacement.reason %></dd>
<% if post_replacement.status == 'approved' %>
<dt>Approver</dt>
<dd><%= link_to_user post_replacement.approver %></dd>
<% end %>
<dt>Replacer</dt>
<dd>
<%= compact_time post_replacement.created_at %>
<br> by <%= link_to_user post_replacement.creator %>
<%= link_to "»", post_replacements_path(search: params[:search].merge(creator_name: post_replacement.creator.name)) %>
</dd>
</dl>
</td>
<td>
<% if post_replacement.old_md5.present? && post_replacement.md5.present? %>
<dl>
<dt>Original MD5</dt>
<dd><%= post_replacement.old_md5 %></dd>
<dt>Replacement MD5</dt>
<dd><%= post_replacement.md5 %></dd>
</dl>
<% if CurrentUser.is_janitor? %>
<%= link_to "Approve", approve_post_replacement_path(post_replacement), method: :PUT %><br>
<%= link_to "Reject", reject_post_replacement_path(post_replacement), method: :PUT %><br>
<%= link_to "As New Post", promote_post_replacement_path(post_replacement), method: :PUT %><br>
<% end %>
</td>
<td>
<% if %i[old_image_width old_image_height old_file_size old_file_ext image_width image_height file_size file_ext].all? { |k| post_replacement[k].present? } %>
<dl>
<dt>Original Size</dt>
<dd>
<%= post_replacement.old_image_width %>x<%= post_replacement.old_image_height %>
(<%= post_replacement.old_file_size.to_s(:human_size, precision: 4) %>, <%= post_replacement.old_file_ext %>)
</dd>
<dt>Replacement Size</dt>
<dd>
<%= post_replacement.image_width %>x<%= post_replacement.image_height %>
(<%= post_replacement.file_size.to_s(:human_size, precision: 4) %>, <%= post_replacement.file_ext %>)
</dd>
</dl>
<% if CurrentUser.is_moderator? %>
<%= link_to "Destroy", post_replacement_path(post_replacement), method: :DELETE, 'data-confirm': 'Are you sure you want to destroy this replacement?' %>
<% end %>
</td>
<td>
<%= compact_time post_replacement.created_at %>
<br> by <%= link_to_user post_replacement.creator %>
<%= link_to "»", post_replacements_path(search: params[:search].merge(creator_name: post_replacement.creator.name)) %>
</td>
</tr>
<% end %>
</tbody>

View File

@ -1,6 +1,6 @@
<div id="c-post-replacements">
<div id="a-new">
<%= render "new", post_replacement: @post_replacement %>
<%= render "new", post_replacement: @post_replacement, post: @post %>
</div>
</div>

View File

@ -7,7 +7,9 @@
<% if params[:action] == "index" %>
| <%= subnav_link_to "Undo selected", "" %>
<%= subnav_link_to "Select All", "" %>
<% if CurrentUser.is_admin? %>
| <%= subnav_link_to "Apply Tag Script To Selected", "" %>
<% end %>
<% end %>
</menu>
<% end %>

View File

@ -47,6 +47,12 @@
</div>
<% end %>
<% if CurrentUser.is_janitor? && post.replacements.pending.any? %>
<div class="notice notice-flagged">
<p>This post has <%= fast_link_to "pending replacements.", post_replacements_path(:search => {:post_id => @post.id}) %></p>
</div>
<% end %>
<% if CurrentUser.is_janitor? && (post.is_flagged? || post.is_deleted?) && post.appeals.any? %>
<div class="notice notice-appealed">
<p>This post was appealed:</p>

View File

@ -63,6 +63,12 @@
</div>
<% end %>
<% if !CurrentUser.is_janitor? && post.replacements.pending.any? %>
<div class="notice notice-flagged">
<p>This post has <%= fast_link_to "pending replacements.", post_replacements_path(:search => {:post_id => @post.id}) %></p>
</div>
<% end %>
<% if !CurrentUser.is_janitor? && (post.is_flagged? || post.is_deleted?) && post.appeals.any? %>
<div class="notice notice-appealed">
<p>This post was appealed:</p>

View File

@ -51,9 +51,12 @@
<li><%= link_to "Update IQDB", update_iqdb_post_path(@post) %></li>
<li><%= tag.a "Destroy", href: '#', id: 'destroy-post-link', 'data-pid': post.id %></li>
<% end %>
<% if CurrentUser.is_moderator? %>
<li><%= link_to "Replace image", new_post_replacement_path(post_id: post.id), id: "replace-image" %></li>
<% end %>
<% if CurrentUser.is_janitor? %>
<li><%= link_to "Replace image", new_post_replacement_path(post_id: post.id), id: "replace-image" %></li>
<li><%= tag.a "Regenerate Thumbnails", href: '#', id: 'regenerate-image-samples-link', 'data-pid': post.id %></li>
<% if post.is_video? %>
<li><%= tag.a "Regenerate Video Samples", href: '#', id: 'regenerate-video-samples-link', 'data-pid': post.id %></li>
<% end %>
<% end %>
<% end %>

View File

@ -86,7 +86,7 @@
<ul>
<li><h1>Blips</h1></li>
<li><%= link_to("Listing", blips_path) %></li>
<li><%= link_to("Help", wiki_pages_path(title: "help:blips")) %></li>
<li><%= link_to("Help", wiki_pages_path(title: "e621:blips")) %></li>
</ul>
</section>
<section>

View File

@ -3,14 +3,31 @@
<div class="profile-container">
<%= render "statistics", :presenter => @presenter, :user => @user %>
<% if @presenter.show_staff_notes? %>
<div class="styled-dtext">
<div class="expandable">
<div class="expandable-header">
<span class="section-arrow"></span>
<span>Staff Notes (<%= @presenter.staff_notes.count %>)</span>
</div>
<div class="expandable-content">
<h4><%= link_to "Staff Notes", user_staff_notes_path(user_id: @user.id) %></h4>
<%= render "admin/staff_notes/partials/list_of_notes", staff_notes: @presenter.staff_notes %>
<h4>New Staff Note</h4>
<%= render "admin/staff_notes/partials/new", staff_note: @presenter.new_staff_note, user: @user %>
</div>
</div>
</div>
<% end %>
<div class="bottom-section">
<div class="posts-section">
<% if @presenter.can_view_favorites? %>
<div class="blacklist">
<%= render "posts/partials/common/inline_blacklist" %>
<%= render "posts/partials/common/inline_blacklist" %>
</div>
<div class="posts">
<%= render "post_summary", :presenter => @presenter, :user => @user %>
<%= render "post_summary", :presenter => @presenter, :user => @user %>
</div>
<% end %>
</div>

View File

@ -18,7 +18,7 @@
<div id="wiki-page-body" class="styled-dtext">
<% if @wiki_page.visible? %>
<%= format_text(@wiki_page.body, allow_color: true) %>
<%= format_text(@wiki_page.body, allow_color: true, max_thumbs: 75) %>
<% if @wiki_page.artist %>
<p><%= link_to "View artist", @wiki_page.artist %></p>

View File

@ -58,17 +58,13 @@ module Danbooru
#
# Run `rake db:seed` to create this account if it doesn't already exist in your install.
def system_user
"E621_Bot"
"auto_moderator"
end
def upload_feedback_topic
ForumTopic.where(title: "Upload Feedback Thread").first
end
def upgrade_account_email
contact_email
end
def source_code_url
"https://github.com/zwagoth/e621ng"
end
@ -95,11 +91,6 @@ module Danbooru
"Anonymous"
end
# This is a salt used to make dictionary attacks on account passwords harder.
def password_salt
"choujin-steiner"
end
def levels
{
"Anonymous" => 0,
@ -162,6 +153,14 @@ fart'
"abc123"
end
def replacement_path_prefix
"replacements"
end
def replacement_file_secret
"abc123"
end
def deleted_preview_url
"/images/deleted-preview.png"
end
@ -271,6 +270,10 @@ fart'
30
end
def replace_post_limit
10
end
def ticket_limit
30
end
@ -280,6 +283,14 @@ fart'
30
end
def post_replacement_per_day_limit
2
end
def post_replacement_per_post_limit
5
end
def remember_key
"abc123"
end
@ -649,10 +660,6 @@ fart'
nil
end
def upload_notice_wiki_page
"help:upload_notice"
end
def flag_notice_wiki_page
"help:flag_notice"
end
@ -715,14 +722,6 @@ fart'
nil
end
def tinami_login
nil
end
def tinami_password
nil
end
def nico_seiga_login
nil
end
@ -731,14 +730,6 @@ fart'
nil
end
def pixa_login
nil
end
def pixa_password
nil
end
def nijie_login
nil
end
@ -794,12 +785,6 @@ fart'
"/var/www/danbooru2/shared"
end
def stripe_secret_key
end
def stripe_publishable_key
end
def twitter_api_key
end
@ -841,11 +826,6 @@ fart'
'noreply@localhost'
end
# impose additional requirements to create tag aliases and implications
def strict_tag_requirements
true
end
# For downloads, if the host matches any of these IPs, block it
def banned_ip_for_download?(ip_addr)
raise ArgumentError unless ip_addr.is_a?(IPAddr)
@ -865,19 +845,6 @@ fart'
def twitter_site
end
def addthis_key
end
# enable s3-nginx proxy caching
def use_s3_proxy?(post)
false
end
# include essential tags in image urls (requires nginx/apache rewrites)
def enable_seo_post_urls
false
end
# enable some (donmai-specific) optimizations for post counts
def estimate_post_counts
false
@ -888,19 +855,10 @@ fart'
true
end
# Enables recording of popular searches, missed searches, and post view
# counts. Requires Reportbooru to be configured and running - see below.
def enable_post_search_counts
false
end
# reportbooru options - see https://github.com/r888888888/reportbooru
def reportbooru_server
end
def reportbooru_key
end
def iqdb_enabled?
false
end

View File

@ -57,4 +57,5 @@ Rails.application.configure do
# config.logger = Logger.new(STDOUT)
# config.log_level = :info
end

View File

@ -5,4 +5,6 @@
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code
# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'".
Rails.backtrace_cleaner.add_filter { |line| line.gsub(Rails.root.to_s, '') }
Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"]

View File

@ -3,7 +3,6 @@ Rails.application.routes.draw do
require 'sidekiq/web'
require 'sidekiq_unique_jobs/web'
Sidekiq::Web.set :session_secret, Rails.application.credentials[:secret_key_base]
mount Sidekiq::Web => '/sidekiq', constraints: AdminRouteConstraint.new
namespace :admin do
@ -22,6 +21,7 @@ Rails.application.routes.draw do
resource :dashboard, :only => [:show]
resources :exceptions, only: [:index, :show]
resource :reowner, controller: 'reowner', only: [:new, :create]
resources :staff_notes, only: [:index]
end
resources :edit_histories
namespace :moderator do
@ -52,6 +52,8 @@ Rails.application.routes.draw do
get :confirm_ban
post :ban
post :unban
post :regenerate_thumbnails
post :regenerate_videos
end
end
end
@ -136,6 +138,7 @@ Rails.application.routes.draw do
member do
post :hide
post :unhide
post :warning
end
end
resources :comment_votes, only: [:index, :delete, :lock] do
@ -167,6 +170,7 @@ Rails.application.routes.draw do
member do
post :hide
post :unhide
post :warning
end
collection do
get :search
@ -246,7 +250,13 @@ Rails.application.routes.draw do
get :diff
end
end
resources :post_replacements, :only => [:index, :new, :create, :update]
resources :post_replacements, :only => [:index, :new, :create, :destroy] do
member do
put :approve
put :reject
put :promote
end
end
resources :deleted_posts, only: [:index]
resources :posts, :only => [:index, :show, :update] do
resources :events, :only => [:index], :controller => "post_events"
@ -327,6 +337,7 @@ Rails.application.routes.draw do
resource :api_key, :only => [:show, :view, :update, :destroy], :controller => "maintenance/user/api_keys" do
post :view
end
resources :staff_notes, only: [:index, :new, :create], controller: "admin/staff_notes"
collection do
get :home
@ -365,6 +376,7 @@ Rails.application.routes.draw do
member do
post :hide
post :unhide
post :warning
end
end
resources :post_report_reasons

View File

@ -0,0 +1,25 @@
class CreateNewReplacementsTable < ActiveRecord::Migration[6.0]
def change
create_table :post_replacements2 do |t|
t.timestamps
t.integer :post_id, null: false
t.integer :creator_id, null: false
t.inet :creator_ip_addr, null: false
t.integer :approver_id
t.string :file_ext, length: 8, null: false
t.integer :file_size, null: false
t.integer :image_height, null: false
t.integer :image_width, null: false
t.string :md5, null: false
t.string :source
t.string :file_name, length: 512
t.string :storage_id, null: false
t.string :status, null: false, default: 'pending'
t.string :reason, null: false, length: 500
t.boolean :protected, null: false, default: false
end
add_index :post_replacements2, :creator_id
add_index :post_replacements2, :post_id
end
end

View File

@ -0,0 +1,10 @@
class AddUserWarnFields < ActiveRecord::Migration[6.1]
def change
add_column :blips, :warning_type, :integer
add_column :blips, :warning_user_id, :integer
add_column :forum_posts, :warning_type, :integer
add_column :forum_posts, :warning_user_id, :integer
add_column :comments, :warning_type, :integer
add_column :comments, :warning_user_id, :integer
end
end

View File

@ -0,0 +1,9 @@
class CreateMissingUserIdIndexes < ActiveRecord::Migration[6.1]
def change
add_index :favorites, :user_id
add_index :favorites, :post_id
add_index :post_votes, :user_id
add_index :post_votes, :post_id
add_index :comments, :creator_id
end
end

View File

@ -0,0 +1,11 @@
class AddStaffNotesTable < ActiveRecord::Migration[6.1]
def change
create_table :staff_notes do |t|
t.timestamps
t.references :user, null: false, foreign_key: true, index: true
t.integer :creator_id, null: false, index: true
t.string :body
t.boolean :resolved, null: false, default: false
end
end
end

View File

@ -0,0 +1,9 @@
class SetUserLevelDefault < ActiveRecord::Migration[6.1]
def up
change_column :users, :level, :integer, default: 20
end
def down
change_column :users, :level, :integer, default: 0
end
end

View File

@ -0,0 +1,10 @@
class AddStaffAuditLogsTable < ActiveRecord::Migration[6.1]
def change
create_table :staff_audit_logs do |t|
t.timestamps
t.references :user, null: false, foreign_key: true, index: true
t.string :action, null: false, default: 'unknown_action'
t.json :values
end
end
end

View File

@ -22,6 +22,7 @@ admin = User.find_or_create_by!(name: "admin") do |user|
user.password_hash = ""
user.email = "admin@e621.net"
user.can_upload_free = true
user.can_approve_posts = true
user.level = User::Levels::ADMIN
end
@ -31,7 +32,8 @@ User.find_or_create_by!(name: Danbooru.config.system_user) do |user|
user.password_hash = ""
user.email = "system@e621.net"
user.can_upload_free = true
user.level = User::Levels::ADMIN
user.can_approve_posts = true
user.level = User::Levels::JANITOR
end
ForumCategory.find_or_create_by!(id: Danbooru.config.alias_implication_forum_category) do |category|
@ -44,18 +46,32 @@ unless Rails.env.test?
CurrentUser.ip_addr = "127.0.0.1"
resources = YAML.load_file Rails.root.join("db", "seeds.yml")
resources["images"].each do |image|
puts image["url"]
url = "https://e621.net/posts.json?limit=100&tags=id:" + resources["post_ids"].join(",")
response = HTTParty.get(url, {
headers: {"User-Agent" => "e621ng/seeding"}
})
json = JSON.parse(response.body)
data = Net::HTTP.get(URI(image["url"]))
json["posts"].each do |post|
puts post["file"]["url"]
data = Net::HTTP.get(URI(post["file"]["url"]))
file = Tempfile.new.binmode
file.write data
post["tags"].each do |category, tags|
Tag.find_or_create_by_name_list(tags.map {|tag| category + ":" + tag})
end
md5 = Digest::MD5.hexdigest(data)
service = UploadService.new({
uploader_id: CurrentUser.id,
uploader_ip_addr: CurrentUser.ip_addr,
file: file,
tag_string: image["tags"],
rating: "s",
tag_string: post["tags"].values.flatten.join(" "),
source: post["sources"].join("\n"),
description: post["description"],
rating: post["rating"],
md5: md5,
md5_confirmation: md5
})

View File

@ -1,446 +1,81 @@
images:
- tags: zoodystopia 2016 anthro bandage black_nose brown_fur canine clothed clothing
collar disney fox fur gloves_(marking) green_eyes looking_at_viewer male mammal
markings multicolored_fur nick_wilde open_shirt orange_fur pantssad shirt shock_collar
simple_background solo standing turningtides_(artist) white_background zootopia
url: https://static1.e621.net/data/87/8f/878f64f8df553ce09218bc9b1083b4e3.png
- tags: 5:4 2016 3_toes anthro barefoot canine claws clothed clothing cuddling disney
duo fabienne_growley female flat_chested fox green_eyes ipoke judy_hopps lagomorph
male mammal nick_wilde peter_moosebridge phone purple_eyes rabbit sitting smile
sofa soles television toes zootopia
url: https://static1.e621.net/data/62/f1/62f197461e755a7d4bd5884dc8ff20d6.jpg
- tags: begging begging_pose blue_fur blue_tail canine chest_tuft dipstick_tail facial_markings
fan_character feral feralized fluffy fur looking_away male mammal markings mask_(marking)
multicolored_fur multicolored_tail nintendo pokémon pokémon_(species) portrait red_eyes
riolu simple_background smile solo tuft two_tone_fur two_tone_tail umpherio_(umpherio)
video_games white_background white_fur white_tail xnirox
url: https://static1.e621.net/data/97/38/973887346d669d97c7a75b7324855773.png
- tags: felid pantherine 2017 armor big_breasts black_lips blue_eyes blue_hair bodysuit
breasts clothed clothing f-r95 female fingerless_gloves fluffy fluffy_tail gloves
hair inner_ear_fluff inside knee_pads long_tail looking_away looking_outside mammal
pink_noseshoulder_pads sitting skinsuit snow_leopard solo space spots tight_clothing
toeless_footwear yuna_snowleopard
url: https://static1.e621.net/data/34/7f/347fec14942b176b074c1cad64c31ad2.png
- tags: back_muscles 2015 anthro ass_up bed bedroom black_fur black_stripes book bookshelf
butt crawling feline fireplace fur green_eyes hax_(artist) inside kenket lofi
lying male mammal morning multicolored_fur nude on_floor on_front one_eye_closed
orange_fur painting pawpads platerug shaded solo striped_body stripes table tiger
vase white_fur window
url: https://static1.e621.net/data/sample/6f/f4/6ff406e64fe870f43512d863a8fd119b.jpg
- tags: pink_eyes 2018 anthro canine clothed clothing cookie digital_media_(artwork)
dog female food fur hair looking_at_viewer mammal nana_(fadey) open_mouthsmile
solo standing tofu93
url: https://static1.e621.net/data/sample/0f/01/0f01bc5401835aa1a082b19e83a0e956.jpg
- tags: 2013 anthro beverage blue_fur blue_hair borny_(character) canine clothing
coffee digital_media_(artwork) food fox fur green_eyes hair hoodie male mammal
open_mouth phonesolo wolfy-nail
url: https://static1.e621.net/data/sample/ac/9f/ac9f231b988d4b6f43b66e89e1d5578e.jpg
- tags: clothed detailed_background fully_clothed holding_object menacing outside
standing yellow_sclera 2015 anthro cheetah clothing feline female flashlight gun
handgun mammal muscular muscular_female pistol police ranged_weaponsolo street_lamp
sunrise tirrel uniform weapon
url: https://static1.e621.net/data/sample/d3/ae/d3aee9d4de485f2be935e84ebbd971b7.jpg
- tags: 2015 ambiguous_gender anthro blue_eyes clothing cosplay costume day detailed_background
dragon dreamworks falvie fudge_the_otter how_to_train_your_dragon kigurumi mammal
membranous_wings mustelid night_fury open_mouth otter outsidesitting sky smile
solo star starry_sky sunset toothless wings
url: https://static1.e621.net/data/sample/0f/d1/0fd1d92190633265fb32a800d65c82d7.jpg
- tags: holding_object food ambiguous_gender beverage black_fur blue_fur blush canine
chigiri clothing coffee cup first_person_view fur happy long_ears lucario mammal
nintendo one_eye_closed pokémon red_eyes sitting smile solo steam video_games wink
url: https://static1.e621.net/data/5a/c6/5ac6787ce0216184bb1737ffc97972e7.png
- tags: android animatronic anthro canine clothed clothing exposed_endoskeleton eye_patch
eyewear five_nights_at_freddy's fox foxy_(fnaf) fur hook hook_hand machine male
mammal naoren piratered_fur robot simple_background solo topless video_games white_background
yellow_eyes
url: https://static1.e621.net/data/17/0a/170ad0093d771d4b99c59f9626712578.png
- tags: 2015 finger_claws 5_fingers <3 anthro blush boss_monster breasts caprine claws
clothed clothing eyebrows eyelashes female fur gastropod goat hi-biki hi_res horn
long_ears looking_at_viewer mammal poserobe simple_background smile snail solo
toriel undertale video_games white_fur wide_hips
url: https://static1.e621.net/data/sample/31/16/31162bd0709f5c79726eb55a05997237.jpg
- tags: 2017 4_toes anthro arnou barefoot black_fur black_nose blue_eyes canine clothed
clothing cuddling day detailed_background digitigrade duo fur hoodie koul male
male/male mammal outside red_eyes smile toes tree wolf wulfcole
url: https://static1.e621.net/data/sample/e2/4e/e24e60d0e2604a1e18de6ec698462e75.jpg
- tags: 2014 anthro black_nose blue_eyes blush brown_fur clothed clothing collar countershade_face
countershading female flower flower_in_hair fur headshot_portrait kiki looking_away
mammal markings multicolored_fur mustelid open_mouth otter plant portrait rarakiesimple_background
solo spots teeth tongue two_tone_fur whiskers white_fur
url: https://static1.e621.net/data/8d/8e/8d8ec834c0b6bd02181510e3bc3f7e4d.png
- tags: pretending anthro antlers brown_fur cellphone cervine cloud deer duo feline
fur green_eyes hi_res horn humor imminent_death lion male mammal nbowa outside
phone predator/preyselfie sky tirrel tirrel_(character) tongue tongue_out tree
url: https://static1.e621.net/data/sample/d9/0e/d90ea5d58bbb37f6c08d054946a478f9.jpg
- tags: "... ambiguous_gender anthro black_fur blue_fur blush canine close-up crossed_arms
fur icon jia looking_at_viewer lucario mammal nintendo pokémon pokémon_(species) red_eyes
simple_background solo tan_fur video_games white_background"
url: https://static1.e621.net/data/sample/db/b6/dbb6db28e0514bb981f417edf6479c9a.jpg
- tags: winged_arms wings ... absurd_res anthro avian beak bird blush breath_of_the_wild
clothed clothing dialogue english_text feathers hi_res kass_(zelda) male muscular
muscular_male nintendo outsiderito sketch solo tacklebox text the_legend_of_zelda
video_games
url: https://static1.e621.net/data/sample/c6/a3/c6a32022972d9807cfaae5000f45b2c4.jpg
- tags: too_early_for_this 2014 5_fingers annoyed anthro beverage big_eyebrows black_eyebrows
black_eyes black_fur black_hair black_nose black_shirt blue_fur bust_portrait
canine clothed clothing coffee coffee_mug countertop crossed_arms cup dialogue
digital_media_(artwork) ear_piercing english_text eyebrows facial_hair facial_piercing
food fur gauged_ear goatee grumpy hair holding_cup holding_object hot_drink lip_piercing
lol_comments looking_at_viewer male mammal multicolored_fur multicolored_hair
nose_piercing nose_ring open_mouth piercing portrait profanityreaction_image restricted_palette
shirt signature simple_background sitting sky3 skye_blue solo speech_bubble steam
striped_ears table teeth text thick_eyebrows two_tone_fur two_tone_hair white_background
white_fur white_hair wolf
url: https://static1.e621.net/data/sample/16/61/1661025f6995846607e709c6da99e123.jpg
- tags: 2013 ambiguous_gender black_eyes cloud cloudscape colorful detailed_background
feral fish happy lol_comments marine open_mouth outside rainbow rainbow_archreaction_image
shark sky solo sparkles spunky_(artist) super_gay teeth
url: https://static1.e621.net/data/e5/03/e503e7d3465337448b715cfd6457edd0.jpg
- tags: 2017 anthro belly big_breasts breasts brown_hair canine chinese_clothing chinese_dress
cleavage cleavage_cutout clothed clothing dress female food fox gradient_background
hair hairclip hi_res kemono long_hair looking_at_viewer mammal purple_eyessetouchi_kurage
simple_background slightly_chubby solo voluptuous waiter wide_hips
url: https://static1.e621.net/data/sample/b1/0d/b10d8740126b4fb20d4b21920d131c8f.jpg
- tags: brown_hair orange_hair sitting striped_body striped_fur striped_tail 2018
<3 anthro black_topwear blue_eyes blue_topwear breasts clothed clothing collaboration
countershade_legs countershade_torso countershading day deymos digital_media_(artwork)
dipstick_tail domestic_cat felid feline felis female fingerless_(marking) flower
fully_clothed fur hair inside iskra legwear looking_aside mammal multicolored_body
multicolored_fur multicolored_tail orange_body orange_fur panties pink_nose plantshaded
shirt solo stripes sunflower tan_body tan_fur thigh_highs underwear url vera_(iskra)
white_legwear window
url: https://static1.e621.net/data/sample/80/8d/808d424fd5c3ccbaddac9fc5b92f028a.jpg
- tags: 2016 anthro blush buckteeth canine disney duo embarrassed english_text female
fireworks fox freedomthai fur green_eyes grey_fur half-closed_eyes hand_on_head
hi_res holding_object judy_hopps lagomorph larger_male long_ears male male/female
mammal nick_wilde night open_mouth orange_fur outside pen predator/prey purple_eyes
rabbitromantic_ambiance size_difference smaller_female smile smirk tears teeth
text zootopia
url: https://static1.e621.net/data/sample/c6/32/c6321f137f68f99f2bee51dba3bf1de5.jpg
- tags: ambiguous_gender capcom cub dragon horn kneeling kosian looking_at_viewer
membranous_wings monster_hunter red_eyes rukodiora scalie simple_background solo
video_games white_background wings young
url: https://static1.e621.net/data/sample/da/92/da922cd07342972262e98de824595cd1.jpg
- tags: 2014 anthro beach bikini bikini_top blue_eyes bottomless clothed clothing
cloud detailed_background digital_media_(artwork) ear_piercing falvie fangs female
grin hair looking_at_viewer mammal markings mustelid neck_tuft otter outside piercingsand
seaside short_hair sky smile solo swimsuit tuft water white_hair
url: https://static1.e621.net/data/sample/aa/3f/aa3f6074e6c14ff78b40ff51f109869b.jpg
- tags: casual_nudity day eyebrows eyelashes grey_hair half-closed_eyes looking_aside
multicolored_fur multicolored_hair partially_submerged skinny_dipping slim snout
sunlight swimming tan_fur thick_tail 2015 anthro back_muscles blonde_hair brown_eyes
brown_fur brown_hair brown_nose butt digital_media_(artwork) eyewear fangs female
feral fish fur goggles hair hi_res high-angle_view mammal marine mikhaila mouth_hold
mustelid nude otter patreonshort_hair solo teeth tsampikos water wet wide_hips
url: https://static1.e621.net/data/sample/82/ab/82abacf96b5e3bde9d92ef6e5e0197eb.jpg
- tags: 5_fingers anthro_on_anthro black_nose blush brown_fur brown_nose brown_stripes
brown_tail canid cheek_tuft chest_tuft embrace eyebrows felid fluffy fluffy_tail
gloves_(marking) glowing_ears glowing_fur glowing_spots green_fur green_spots
holding_object holding_plush humanoid_hands interspecies markings orange_tail
pantherine romantic_couple snout spots spotted_fur striped_fur striped_tail tan_belly
tan_fur tuft white_belly white_tail anthro bed bed_sheet bedding bioluminescence
brown_hair canine cuddling digital_media_(artwork) duo eyes_closed fox fur gabe_(mytigertail)
glowing hair hug inside lan lying male male/male mammal multicolored_fur mytigertail
nude on_bed on_side open_mouth orange_fur orange_stripes pillow plushie rating:q
sleeping smile spooning stripes tiger two_tone_fur white_fur zeta-haru
url: https://static1.e621.net/data/sample/b8/89/b889bbc7002cc1152ed7f1c8e3a14670.jpg
- tags: 2014 3:2 afternoon alien big_ears black_scales blue_eyes blue_fur blue_nose
claws clothed clothing cosplay costume crossover daww disney dragon dreamworks
duo experiment_(species) feral fish food fur grasp green_sclera hat head_tuft
headgear holding_food holding_object how_to_train_your_dragon inside lilo_and_stitch
long_ears looking_down looking_up lying male marine membranous_wings night_fury
no_sclera offering_(disambiguation) offering_food offering_to_another on_front
on_ground outstretched_arms raised_arm scales scalie scarf sharp_teeth size_difference
smile standing stitch sunlight teeth toe_claws toothless tsaoshin wings
url: https://static1.e621.net/data/50/cc/50cc1f4f731dca8cb66fdb9fd42bc57d.png
- tags: 2017 ambiguous_form ambiguous_gender big_ears brown_eyes canid canine derp_eyes
dipstick_ears ears_down english_text fangs felisrandomis fennec fox frown fur
headshot_portrait humor inner_ear_fluff lol_comments mammal multiple_poses open_mouth
open_smile portrait pose screaming simple_background smile solo tan_fur teeth
text the_truth tongue weasyl
url: https://static1.e621.net/data/79/d7/79d74e8154dcea5339ff51f77fd3c98b.png
- tags: 2018 anthro black_hair blue_eyes blush canid canine canis clothing domestic_dog
faceless_male female hair human male mammal on_lap sally_(povinne10) talilly tongue
tongue_out
url: https://static1.e621.net/data/08/1e/081e279f730cccb5948853e8d1ea6a9f.jpg
- tags: 2018 <3 ambiguous_gender blush coal_(rakkuguy) eyes_closed happy human kobold
mammal motion_lines one_eye_closed open_mouth petting rakkuguy scalie simple_background
smile tailwag tongue tongue_out white_background
url: https://static1.e621.net/data/48/5d/485d385d6bb518ee12c78747054e827c.jpg
- tags: 2018 <3 3_toes ambiguous/ambiguous ambiguous_gender anthro anthro_on_anthro
blue_fur butt canid canine cewljoke chest_tuft cute_fangs duo evolutionary_family
eyes_closed fur hindpaw hi_res hug kneeling level_difference lucario mammal nintendo
nude pawpads paws pink_pawpads pokémon pokémon_(species) red_eyes riolu signature
simple_background smile spikes toes tuft video_games white_background wholesome
url: https://static1.e621.net/data/69/14/6914390405ab241003e48b4cfeeea5f3.png
- tags: 2017 absurd_res ambiguous_gender anatomy black_background blep canid canine
cross-eyed diagram english_text feral fox fur hi_res humor mammal meme red_fox
riot_the_red_fox simple_background sitting solo text tongue tongue_out whiskers
zillion_ross
url: https://static1.e621.net/data/8b/a7/8ba7610b0a2b91d91aebf4a55e131aa7.png
- tags: 2018 4_toes anatomy black_background black_fur black_hair black_nose blue_fur
blue_hair blue_tongue canid canine claws diagram ear_piercing english_text feral
fox fur fur_markings gloves_(marking) hair hi_res humor industrial_piercing male
mammal markings meme mostly_nude multicolored_fur multicolored_hair open_mouth
original_character_do_not_steal owo owo_whats_this paws piercing quadruped restricted_palette
scarf simple_background socks_(marking) solo teeth text the_truth toe_claws toes
tongue two_tone_hair white_fur zillion_ross
url: https://static1.e621.net/data/63/3b/633bbadd77a49e662768428ffc026491.png
- tags: 2017 <3 alcohol avian beak beer beverage bird black_feathers bottle corvid
crow deep_throat digital_media_(artwork) english_text feathers feral hi_res humor
lol_comments oral oral_penetration penetration solo swish talons text wings
url: https://static1.e621.net/data/7c/d9/7cd90352fa57ddaeb3f422de355e727c.jpg
- tags: 2017 5_fingers alternate_form anthro canid canine clothed clothing cup cups_on_ears
cute_fangs dagger detailed_background dialogue digital_media_(artwork) dutch_angle
english_text fox fur gregg_(nitw) happy inside jacket knife koul looking_at_viewer
male mammal melee_weapon night_in_the_woods open_mouth orange_fur smile solo speech_bubble
text video_games weapon
url: https://static1.e621.net/data/4f/be/4fbe36ed49bf8d501edb8509a554837d.jpg
- tags: ambiguous_gender canid canine conditional_dnp daww derp_eyes digital_media_(artwork)
digital_painting_(artwork) ears_back eyes_closed feral flexible fluffy fox full-length_portrait
fur humor lol_comments mammal markings oops orange_fur paws playing portrait red_fox
side_view simple_background snow socks_(marking) solo trunchbull upside_down whiskers
white_background white_fur
url: https://static1.e621.net/data/94/d1/94d108d2e4ed476a113f93e7768accce.jpg
- tags: anthro canid canine clothed clothing english_text fangs female fur hair image_macro
impact_(font) jurassic_park mammal meme nasusbot open_mouth parody profanity reaction_image
skrillex smile solo teeth text
url: https://static1.e621.net/data/cb/a4/cba428d3227dd7358dbf63787c759e8d.png
- tags: "<3 3_toes ambiguous_gender anthro barefoot biped black_fur black_markings
blue_fur blue_tail blush canid canine cel_shading cheek_tuft collar digital_media_(artwork)
digitigrade facial_markings feet fur fur_tuft gloves_(marking) gradient_background
kneeling leash leash_in_mouth lucario mammal markings mouth_hold multicolored_fur
neck_tuft nintendo nude object_in_mouth paws pink_background pokémon pokémon_(species)
pseudo_clothing rakkuguy red_eyes shaded side_view simple_background smile snout
solo spikes submissive toes tuft video_games white_background yellow_fur"
url: https://static1.e621.net/data/a1/1e/a11e49bd7c556049770b3e905e9528ef.png
- tags: 2013 4_fingers anthro barefoot beverage black_fur black_nose canid canine
clothed clothing coffee coffee_mug cup dipstick_ears dipstick_tail english_text
eyes_closed fennec fingerless_(marking) fluffy fluffy_tail food fox fully_clothed
fur gloves_(marking) hair hi_res inner_ear_fluff lying male mammal markings multicolored_tail
orange_fur orange_hair pants reaching shirt side_view simple_background sleeping
socks_(marking) solo sound_effects text thanshuhai tired toeless_(marking) tongue
tongue_out white_background white_fur zzz
url: https://static1.e621.net/data/8d/eb/8deb2ae9fe81f9f6383a24c591bdd3be.png
- tags: 2016 alolan_vulpix altered_reflection ambiguous_gender black_nose brown_eyes
brown_hair canid canine contrast digital_media_(artwork) duality feral fur grey_nose
hair hindpaw hi_res inner_ear_fluff looking_at_viewer mammal multi_tail nintendo
paws pokémon pokémon_(species) realistic reflection regional_variant solo split_screen
standing symmetry tamberella tan_fur video_games vulpix white_fur
url: https://static1.e621.net/data/7e/11/7e11e929a5c5b58078d600d3aa23cf21.jpg
- tags: anthro canid canine clothed clothing disney edit english_text fox fully_clothed
gradient_background lyrics male mammal michael_jackson music nick_wilde simple_background
smile smooth_criminal solo song sound text zootopia 羽默空_(artist)
url: https://static1.e621.net/data/fc/09/fc090f0332c2ae9e91bb6953179bceaf.jpg
- tags: anthro antlers brown_fur cellphone cervid cloud duo felid fur green_eyes hi_res
horn humor imminent_death lion male mammal nbowa outside pantherine phone predator/prey
pretending selfie sky tirrel tirrel_(character) tongue tongue_out tree
url: https://static1.e621.net/data/d9/0e/d90ea5d58bbb37f6c08d054946a478f9.jpg
- tags: 2016 anthro black_fur black_hair black_nose blizzard_entertainment bound breasts
canid canine cheek_tuft conditional_dnp crying digital_media_(artwork) domestic_cat
dream_eater duo e621 english_text eyelashes fan_character felid feline felis female
feral floof flora_fauna fluffy food food_creature fruit fur green_eyes hair hi_res
horn humor impact_(font) kingdom_hearts lol_comments looking_up mammal melon meloncat
melonyan meme meow_wow meta nude paralee_(character) plant poof profanity rainbow
ratte sad square_enix tardar_sauce tears technicolor_yawn text tuft video_games
vomit wall_of_text warcraft watermelon were werecanid werecanine werewolf worgen
url: https://static1.e621.net/data/42/2a/422aaf2c210d6e0e48480ce419b06896.png
- tags: 2016 <3 4_fingers abstract_background anthro asriel_dreemurr big_eyes black_background
blush boss_monster bovid braeburned brown_background caprine cheek_tuft clothing
cute_fangs digital_media_(artwork) fangs feels fur gesture goat gradient_background
green_clothing green_topwear hair half-length_portrait hand_heart head_tuft hi_res
long_ears looking_at_viewer love male mammal multicolored_clothing open_mouth
pink_tongue portrait sharp_teeth signature simple_background smile solo sweater
teeth tongue tuft undertale video_games white_fur yellow_bottomwear yellow_clothing
young
url: https://static1.e621.net/data/14/e7/14e7eff45c61ba29e02f4f59846dc6bc.png
- tags: "... <3 absurd_res ambiguous_gender beady_eyes blep dialogue english_text
feral forked_tongue hi_res iron_and_whine python reptile scalie simple_background
snake solo text tongue tongue_out traditional_media_(artwork) white_background"
url: https://static1.e621.net/data/7f/6a/7f6af5deef179238510cfa1d606ef81f.jpg
- tags: 2016 alantka anthro canid canine cuddling detailed_background disney duo embrace
eyes_closed female forest fox fur gloves_(marking) green_eyes grey_fur judy_hopps
lagomorph long_ears looking_down male mammal markings nick_wilde orange_fur outside
predator/prey rabbit romantic_couple size_difference smile style_parody tree zootopia
url: https://static1.e621.net/data/e1/28/e128617350d9e02bf816f0d4d529c210.jpg
- tags: 2016 absurd_res anthro barefoot bed bedroom bracelet buckteeth canid canine
choker dancing dennyvixen dipstick_tail disney duo earbuds eyes_closed female
fox fur gloves_(marking) good_morning grey_fur happy headphones hi_res inner_ear_fluff
inside ipod ipod_nano jewelry judy_hopps lagomorph long_ears male mammal markings
multicolored_tail nick_wilde on_bed open_mouth orange_fur pawpads portable_music_player
predator/prey rabbit smile teeth two_tone_tail upscale zootopia
url: https://static1.e621.net/data/2e/d4/2ed4a63c7105f9e6ca099033403b242a.png
- tags: ">.< :< :> :| •≠• :3 ambiguous_gender black_eyes :d d: expressions food_creature
frown group humor looking_at_viewer low_res mint not_furry :o o3o open_mouth plastic
real smile tic_tac tongue tongue_out w4nw4n x_x"
url: https://static1.e621.net/data/e5/1e/e51edfb8ec608817cca55d9bf7579c47.jpg
- tags: absurd_res anthro bed bouquet canid canine canis clothing duo eyes_closed
female flower_bouquet hair hi_res human larger_anthro larger_female male mammal
size_difference sleeping smaller_human smaller_male spooning vanchamarl were werecanid
werecanine werewolf wholesome wolf zephra
url: https://static1.e621.net/data/14/b5/14b5352ee09a0dee5a565f5c0e6bfb3c.png
- tags: 2016 anthro blue_eyes boss_monster bovid caprine cellphone crossover disney
english_text female fur goat holding_object horn image_macro long_ears mammal
meme monsters_inc open_mouth parody phone pixar reaction_image solo sulley taken_(movie)
text toriel undertale video_games white_fur wolfjedisamuel
url: https://static1.e621.net/data/fb/52/fb52b8b36ecb85eaf155855a23e70f24.png
- tags: animated anthro asriel_dreemurr blush boss_monster bovid caprine clothing
computer edit empty_eyes english_text floppy_ears goat happy hyperactive lol_comments
male mammal o_o simple_background smile solo text undertale undertale-tales video_games
url: https://static1.e621.net/data/bf/3c/bf3ce93ea4f3e7e3719c59b6557343b0.gif
- tags: 2016 angry anthro antlers bell cervid collar computer furry_problems green_eyes
horn laptop lol_comments male mammal reaction_image solo tirrel tirrel_(character)
url: https://static1.e621.net/data/9c/39/9c39de16aa06ea3afbc0ca4cd3137960.jpg
- tags: anthro big_breasts black_fur black_nose blue_eyes breasts claws clothed clothing
english_text female fur giant_panda gillpanda gillpanda_(character) looking_at_viewer
mammal morbidly_obese motivational_poster multicolored_fur obese overweight overweight_female
pawpads pen sharp_teeth simple_background smile solo teeth text thumbs_up two_tone_fur
ursid white_background white_fur
url: https://static1.e621.net/data/fa/67/fa67485134b0ee19fdaa49a68161f4f5.jpg
- tags: 2015 anthro asriel_dreemurr big_eyes boss_monster bovid caprine clothed clothing
fangs fatz_geronimo goat hoodie horn long_ears male mammal purple_background reaction_image
simple_background solo undertale video_games wide_eyed
url: https://static1.e621.net/data/f1/8d/f18db1184cf4c62d7afbdf8fd28f39f7.png
- tags: 2015 anatomy anthro asriel_dreemurr boss_monster bovid bukoya-star caprine
chart child clothed clothing cub english_text flower fur goat green_eyes grey_background
hi_res long_ears looking_at_viewer male mammal open_mouth plant simple_background
solo text undertale video_games white_fur young
url: https://static1.e621.net/data/b6/33/b633e381a700c619f7ea22666cf6b93d.jpg
- tags: 2015 <3 anthro asriel_dreemurr_(god_form) blue_background boss_monster bovid
caprine clothed clothing digital_media_(artwork) eating fluffy_hair fur gem goat
hair horn looking_at_viewer male mammal pocky red_eyes robe saturnspace simple_background
solo undertale video_games white_fur white_hair
url: https://static1.e621.net/data/8b/80/8b8000f1845841e50515eb0aa224abfb.png
- tags: 2015 ambiguous_gender baseball_cap beady_eyes black_eyes digital_media_(artwork)
feral forked_tongue grey_tongue half-length_portrait hat lol_comments meme mostly_nude
portrait purple_headwear python reptile scalie simple_background smile snake solo
tongue tongue_out unknown_artist white_background
url: https://static1.e621.net/data/bf/f6/bff630817262325b514eee7ff197802e.png
- tags: 2015 ambiguous_gender avian beak bird bread cere_(feature) columbid common_pigeon
crumbs cryptid-creations derp_eyes doing_it_wrong feathers feral flour folded_wings
food humor in_bread lol_comments meme pigeon red_eyes simple_background solo watermark
what white_background
url: https://static1.e621.net/data/f9/1f/f91f92ac563c3bbe4e569d68a46dfafe.png
- tags: 2015 ambiguous_gender anthro blue_eyes clothing cosplay costume day detailed_background
dragon dreamworks falvie fudge_the_otter how_to_train_your_dragon kigurumi mammal
membranous_wings mustelid night_fury open_mouth otter outside sitting sky smile
solo star starry_sky sunset toothless wings
url: https://static1.e621.net/data/0f/d1/0fd1d92190633265fb32a800d65c82d7.png
- tags: 2015 5_fingers ambiguous_gender anthro black_lips black_nose canid canine
canis controller domestic_cat duo eye_contact felid feline felis fluffy fluffy_tail
fur grey_fur handpaw hax_(artist) inside jackal kenket lamp lofi long_mouth looking_at_another
looking_down looking_up lying mammal no_sclera nude on_back orange_eyes pawpads
paws pink_nose reaching remote_control romantic_couple shaded side_view slice_of_life
smile snout sofa table tv_remote whiskers white_fur yellow_eyes
url: https://static1.e621.net/data/0c/09/0c09080148c52ed89f16fe8fbc731dcf.jpg
- tags: 2012 anthro beverage black_fur building cafe camera_view canid canine car
chair cheek_tuft clothed clothing coffee coffee_mug coffee_shop cup detailed_background
digital_media_(artwork) eyebrows fog food fox fur green_eyes hair half-length_portrait
high-angle_view inside looking_at_viewer male mammal multicolored_fur orange_fur
orange_hair portrait red_fox selfie sitting slice_of_life smile solo table thanshuhai
tuft vehicle white_fur
url: https://static1.e621.net/data/a0/47/a0476a240b0b780b926d831a3e6426ea.png
- tags: angry blue_body blue_eyes blue_hair blush dialogue english_text equine eyelashes
eyes_closed female friendship_is_magic hair horn horse humor long_hair mammal
my_little_pony open_mouth orange_background pink_tongue princess_luna_(mlp) profanity
reaction_image simple_background solo sweat teeth text the_truth tongue unknown_artist
winged_unicorn wings
url: https://static1.e621.net/data/ef/e5/efe507d244c5841d8496ce52f4f706a6.png
- tags: "... ambiguous_gender bed bedding black_fur blanket checkered_background domestic_cat
english_text felid feline felis female fur garfield_(series) lying mae_(nitw)
mammal night_in_the_woods on_bed on_front pattern_background reaction_image simple_background
solo speech_bubble text under_covers unknown_artist video_games"
url: https://static1.e621.net/data/19/43/194344f13372ea3e80ef007dd56463c2.png
- tags: 2015 anthro cheetah clothed clothing detailed_background felid feline female
flashlight fully_clothed gun handgun holding_object mammal menacing muscular muscular_female
outside pistol police ranged_weapon solo standing street_lamp sunrise tirrel uniform
weapon yellow_sclera
url: https://static1.e621.net/data/d3/ae/d3aee9d4de485f2be935e84ebbd971b7.jpg
- tags: 2015 anthro antlers bell blue_eyes brown_hair canid canine car cervid cheetah
clothed clothing collar comic dialogue driver driving duo english_text felid feline
female green_eyes hair hat headlight hi_res horn humor inside_car male mammal
outside police profanity running text tirrel tirrel_(character) uniform vehicle
url: https://static1.e621.net/data/ed/e7/ede74f19c7adc6115fb751d729eceaf8.jpg
- tags: 2013 ambiguous_gender angry brown_fur canid canine chibi dipstick_tail english_text
feral fox fur humor mammal markings multicolored_fur multicolored_tail orange_fur
profanity running serious simple_background socks_(marking) solo technicolorpie
technicolor_pie text two_tone_tail white_background white_fur yellow_fur
url: https://static1.e621.net/data/44/53/4453fcdd6d4ce1cf6d6da88c9a35bafc.png
- tags: 2013 abstract_background ambiguous_gender anthro arm_support blonde_hair bow_tie
bust_portrait clothed clothing coat cosplay digital_media_(artwork) felid fur
gene_wilder hair half-closed_eyes hat humor inner_ear_fluff leaning_on_elbow lol_comments
long_hair low_res mammal meme pantherine parody portrait reaction_image smile
solo tassy tiger top_hat willy_wonka willy_wonka_and_the_chocolate_factory yellow_eyes
yellow_fur
url: https://static1.e621.net/data/d1/bb/d1bbf569513c5acf13b1be36d1d64bff.png
- tags: begging begging_pose blue_fur blue_tail canid canine chest_tuft conditional_dnp
dipstick_tail facial_markings fan_character feral feralized fluffy fur looking_away
male mammal markings mask_(marking) multicolored_fur multicolored_tail nintendo
pokémon pokémon_(species) portrait red_eyes riolu simple_background smile solo
tuft two_tone_fur two_tone_tail umpherio_(umpherio) video_games white_background
white_fur white_tail xnirox
url: https://static1.e621.net/data/97/38/973887346d669d97c7a75b7324855773.png
- tags: 2014 <3 anthro avian beak bird black_eyes black_nose blue_body blue_feathers
blue_jay blush brown_fur cartoon_network chest_tuft comic corvid dialogue duo
english_text eyes_closed fangs feathers fur grey_beak hi_res kissing kissing_cheek
love male male/male mammal markings mordecai_(regular_show) open_beak open_mouth
procyonid raccoon regular_show rigby_(regular_show) romantic_couple simple_background
stripes surprise tan_fur teeth text tongue tuft white_background white_body white_feathers
wholesome xiamtheferret
url: https://static1.e621.net/data/ca/c9/cac98850f5b5b48440c100ad800caac1.png
- tags: anthro blonde_hair blue_eyes bone_necklace breasts carnivore clothed clothing
digital_media_(artwork) domestic_cat eating eyebrows felid feline felis female
food front_view hair half-length_portrait holding_food holding_object inuki inuki_(character)
jewelry mammal meat melloque necklace pink_nose ponytail portrait shirt shirt_logo
signature simple_background smile solo steak teeth
url: https://static1.e621.net/data/63/43/63433706609c428f2027986940547834.png
- tags: 2015 applejack_(mlp) blonde_hair cutie_mark earth_pony equine face_paint feathers
female feral friendship_is_magic green_eyes hair headdress hi_res horse jewelry
looking_at_viewer mammal my_little_pony native_american necklace pony portrait
santagiera solo teeth
url: https://static1.e621.net/data/ac/85/ac85e5ea07ae712946bb897bff734d59.jpg
- tags: 2015 animatronic anthro avian beak beverage bird blue_eyes blush bonnie_(fnaf)
canid canine cellphone chica_(fnaf) chicken eye_patch eyewear five_nights_at_freddy's
food fox foxy_(fnaf) freddy_(fnaf) group happy hat humor kissing lagomorph laugh
looking_away machine male male/male mammal phone purple_eyes rabbit robot smile
spin_the_bottle ursid video_games xiamtheferret yellow_eyes
url: https://static1.e621.net/data/be/55/be5581393bcf2451ca90c16ba48c694c.png
- tags: 2014 anthro antiander blue_eyes canid canine clothed clothing dress feathered_wings
feathers female fox gloves hair heterochromia hi_res holly_(plant) long_hair mammal
plant solo source_request white_hair wings
url: https://static1.e621.net/data/7c/8a/7c8a07df68f6b8c93d425158396df811.png
- tags: 2012 ambiguous_gender black_fur black_nose eeveelution fadingsky feral fire
fur markings nintendo pink_background pokémon pokémon_(species) red_eyes simple_background
smoke solo umbreon video_games yellow_markings
url: https://static1.e621.net/data/d8/2f/d82fc9c3bfa78c17db4d117950063124.png
- tags: 2014 ass_up avian beak blue_eyes blue_fur braeburned digital_media_(artwork)
earth_pony equine feathered_wings feathers female feral friendship_is_magic fur
gilda_(mlp) group gryphon hair horse laser laser_pointer mammal multicolored_hair
my_little_pony pegasus pink_fur pink_hair pinkie_pie_(mlp) pony purple_eyes rainbow_dash_(mlp)
rainbow_hair tongue tongue_out wings yellow_eyes
url: https://static1.e621.net/data/8c/08/8c08e77a944dd6df29eb3e908f5bd93f.png
- tags: ambiguous_gender anthro bust_portrait canid canine canis close-up darkwingo
dingo eye_reflection fluffy green_background green_eyes head_tilt hi_res hybrid
jewelry mammal markings necklace portrait scar simple_background smile solo thanshuhai
wolf
url: https://static1.e621.net/data/d7/10/d710ff227b96e59a655d152ac2fcc9f7.jpg
- tags: 2014 <3 anthro beverage beverage_can black_hair blue_hair blue_highlights
clothing collar digital_media_(artwork) domestic_cat fangs felid feline felis
female food gradient_hair green_eyes hair highlights jacket looking_at_viewer
mammal smile solo tentacletongue wolfy-nail young
url: https://static1.e621.net/data/86/94/86942ce7f5be8ba952fef914c5593d2e.jpg
- tags: 2012 ambiguous_gender bed bedding blanket blue_eyes eeveelution feral hi_res
looking_at_viewer lying nintendo on_bed on_front open_mouth pawpads pokémon pokémon_(species)
shadow solo tartii under_covers vaporeon video_games watermark
url: https://static1.e621.net/data/c0/d4/c0d4964ebc0fb0eeddb61bc03cbc6a2b.jpg
- tags: 2014 5:4 anthro atryl avian beak black_feathers brown_feathers cloud digital_media_(artwork)
duo eye_contact feathered_wings feathers female friendship_is_magic genevieve_(mlp)
grass greta_(mlp) grey_eyes gryphon hug my_little_pony one_eye_closed outside
sky smile white_feathers wings wink
url: https://static1.e621.net/data/67/c9/67c952de260ede67d0fe507bb99d3a6e.png
- tags: 2014 3_toes ambiguous_gender animated anthro ass_up black_scales butt digitigrade
dragon felid feral group hindpaw humor inside_train lamp loop low_res male mammal
membranous_wings metro multicolored_scales paws pushing rubble scales scalie size_difference
solo_focus subway tirrel toes train tunnel two_tone_scales vehicle white_scales
wings
url: https://static1.e621.net/data/7d/0d/7d0de8443bad63b6bd56a24ca0d0fb98.gif
post_ids:
- 864982
- 831434
- 604569
- 1384351
- 713232
- 2058923
- 348854
- 677272
- 721020
- 783617
- 524011
- 738084
- 1324650
- 447317
- 902353
- 608312
- 1196171
- 539888
- 426556
- 1158932
- 1787376
- 844336
- 133831
- 543720
- 675477
- 468397
- 480698
- 1201721
- 2104542
- 1910123
- 1162821
- 1559520
- 1559522
- 1337331
- 1289163
- 1124466
- 922038
- 1051272
- 1014268
- 972377
- 891419
- 902353
- 896454
- 882158
- 864033
- 854142
- 853653
- 818700
- 830459
- 818006
- 818294
- 802009
- 810790
- 799347
- 1401572
- 766674
- 735523
- 722531
- 721020
- 2038195
- 701921
- 692610
- 687969
- 677272
- 672692
- 668603
- 664515
- 604569
- 605113
- 589740
- 586160
- 584859
- 577089
- 559155
- 494915
- 492433
- 464910
- 444992
- 429880
- 437103

View File

@ -412,7 +412,9 @@ CREATE TABLE public.blips (
is_hidden boolean DEFAULT false,
body_index tsvector NOT NULL,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
updated_at timestamp without time zone NOT NULL,
warning_type integer,
warning_user_id integer
);
@ -527,7 +529,9 @@ CREATE TABLE public.comments (
updater_ip_addr inet,
do_not_bump_post boolean DEFAULT false NOT NULL,
is_hidden boolean DEFAULT false NOT NULL,
is_sticky boolean DEFAULT false NOT NULL
is_sticky boolean DEFAULT false NOT NULL,
warning_type integer,
warning_user_id integer
);
@ -919,7 +923,9 @@ CREATE TABLE public.forum_posts (
is_hidden boolean DEFAULT false NOT NULL,
created_at timestamp without time zone,
updated_at timestamp without time zone,
creator_ip_addr inet
creator_ip_addr inet,
warning_type integer,
warning_user_id integer
);
@ -2016,6 +2022,73 @@ CREATE TABLE public.schema_migrations (
);
--
-- Name: staff_audit_logs; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.staff_audit_logs (
id bigint NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL,
user_id bigint NOT NULL,
action character varying DEFAULT 'unknown_action'::character varying NOT NULL,
"values" json
);
--
-- Name: staff_audit_logs_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.staff_audit_logs_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: staff_audit_logs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.staff_audit_logs_id_seq OWNED BY public.staff_audit_logs.id;
--
-- Name: staff_notes; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.staff_notes (
id bigint NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL,
user_id bigint NOT NULL,
creator_id integer NOT NULL,
body character varying,
resolved boolean DEFAULT false NOT NULL
);
--
-- Name: staff_notes_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.staff_notes_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: staff_notes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.staff_notes_id_seq OWNED BY public.staff_notes.id;
--
-- Name: tag_aliases; Type: TABLE; Schema: public; Owner: -
--
@ -2552,7 +2625,7 @@ CREATE TABLE public.users (
password_hash character varying NOT NULL,
email character varying,
email_verification_key character varying,
level integer DEFAULT 0 NOT NULL,
level integer DEFAULT 20 NOT NULL,
base_upload_limit integer DEFAULT 10 NOT NULL,
last_logged_in_at timestamp without time zone,
last_forum_read_at timestamp without time zone,
@ -3019,6 +3092,20 @@ ALTER TABLE ONLY public.posts ALTER COLUMN change_seq SET DEFAULT nextval('publi
ALTER TABLE ONLY public.saved_searches ALTER COLUMN id SET DEFAULT nextval('public.saved_searches_id_seq'::regclass);
--
-- Name: staff_audit_logs id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.staff_audit_logs ALTER COLUMN id SET DEFAULT nextval('public.staff_audit_logs_id_seq'::regclass);
--
-- Name: staff_notes id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.staff_notes ALTER COLUMN id SET DEFAULT nextval('public.staff_notes_id_seq'::regclass);
--
-- Name: tag_aliases id; Type: DEFAULT; Schema: public; Owner: -
--
@ -3541,6 +3628,22 @@ ALTER TABLE ONLY public.schema_migrations
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
--
-- Name: staff_audit_logs staff_audit_logs_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.staff_audit_logs
ADD CONSTRAINT staff_audit_logs_pkey PRIMARY KEY (id);
--
-- Name: staff_notes staff_notes_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.staff_notes
ADD CONSTRAINT staff_notes_pkey PRIMARY KEY (id);
--
-- Name: tag_aliases tag_aliases_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -3879,6 +3982,13 @@ CREATE INDEX index_comment_votes_on_user_id ON public.comment_votes USING btree
CREATE INDEX index_comments_on_body_index ON public.comments USING gin (body_index);
--
-- Name: index_comments_on_creator_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_comments_on_creator_id ON public.comments USING btree (creator_id);
--
-- Name: index_comments_on_creator_id_and_post_id; Type: INDEX; Schema: public; Owner: -
--
@ -3970,6 +4080,20 @@ CREATE INDEX index_favorite_groups_on_creator_id ON public.favorite_groups USING
CREATE INDEX index_favorite_groups_on_lower_name ON public.favorite_groups USING btree (lower(name));
--
-- Name: index_favorites_on_post_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_favorites_on_post_id ON public.favorites USING btree (post_id);
--
-- Name: index_favorites_on_user_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_favorites_on_user_id ON public.favorites USING btree (user_id);
--
-- Name: index_favorites_on_user_id_and_post_id; Type: INDEX; Schema: public; Owner: -
--
@ -4383,6 +4507,20 @@ CREATE INDEX index_post_versions_on_updater_id ON public.post_versions USING btr
CREATE INDEX index_post_versions_on_updater_ip_addr ON public.post_versions USING btree (updater_ip_addr);
--
-- Name: index_post_votes_on_post_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_post_votes_on_post_id ON public.post_votes USING btree (post_id);
--
-- Name: index_post_votes_on_user_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_post_votes_on_user_id ON public.post_votes USING btree (user_id);
--
-- Name: index_post_votes_on_user_id_and_post_id; Type: INDEX; Schema: public; Owner: -
--
@ -4481,6 +4619,27 @@ CREATE INDEX index_saved_searches_on_query ON public.saved_searches USING btree
CREATE INDEX index_saved_searches_on_user_id ON public.saved_searches USING btree (user_id);
--
-- Name: index_staff_audit_logs_on_user_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_staff_audit_logs_on_user_id ON public.staff_audit_logs USING btree (user_id);
--
-- Name: index_staff_notes_on_creator_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_staff_notes_on_creator_id ON public.staff_notes USING btree (creator_id);
--
-- Name: index_staff_notes_on_user_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_staff_notes_on_user_id ON public.staff_notes USING btree (user_id);
--
-- Name: index_tag_aliases_on_antecedent_name; Type: INDEX; Schema: public; Owner: -
--
@ -4824,6 +4983,14 @@ CREATE TRIGGER trigger_posts_on_tag_index_update BEFORE INSERT OR UPDATE ON publ
CREATE TRIGGER trigger_wiki_pages_on_update BEFORE INSERT OR UPDATE ON public.wiki_pages FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('body_index', 'public.danbooru', 'body', 'title');
--
-- Name: staff_audit_logs fk_rails_02329e5ef9; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.staff_audit_logs
ADD CONSTRAINT fk_rails_02329e5ef9 FOREIGN KEY (user_id) REFERENCES public.users(id);
--
-- Name: post_image_hashes fk_rails_2b7afcc2f0; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -4840,6 +5007,14 @@ ALTER TABLE ONLY public.favorites
ADD CONSTRAINT fk_rails_a7668ef613 FOREIGN KEY (user_id) REFERENCES public.users(id);
--
-- Name: staff_notes fk_rails_bab7e2d92a; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.staff_notes
ADD CONSTRAINT fk_rails_bab7e2d92a FOREIGN KEY (user_id) REFERENCES public.users(id);
--
-- Name: favorites fk_rails_d20e53bb68; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -5084,6 +5259,11 @@ INSERT INTO "schema_migrations" (version) VALUES
('20201113073842'),
('20201220172926'),
('20201220190335'),
('20210117173030');
('20210117173030'),
('20210405040522'),
('20210425020131'),
('20210426025625'),
('20210430201028'),
('20210506235640');

View File

@ -1,6 +1,63 @@
FactoryBot.define do
factory(:post_replacement) do
original_url { FFaker::Internet.http_url }
creator_ip_addr { "127.0.0.1" }
replacement_url { FFaker::Internet.http_url }
reason { FFaker::Lorem.words.join(" ") }
factory(:webm_replacement) do
replacement_file do
f = Tempfile.new
IO.copy_stream("#{Rails.root}/test/files/test-512x512.webm", f.path)
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "video.webm")
end
end
factory(:mp4_replacement) do
replacement_file do
f = Tempfile.new
IO.copy_stream("#{Rails.root}/test/files/test-300x300.mp4", f.path)
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "video.mp4")
end
end
factory(:jpg_replacement) do
replacement_file do
f = Tempfile.new
IO.copy_stream("#{Rails.root}/test/files/test.jpg", f.path)
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "test.jpg")
end
end
factory(:jpg_invalid_replacement) do
replacement_file do
f = Tempfile.new
IO.copy_stream("#{Rails.root}/test/files/test-corrupt.jpg", f.path)
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "test.jpg")
end
end
factory(:gif_replacement) do
replacement_file do
f = Tempfile.new
IO.copy_stream("#{Rails.root}/test/files/test.gif", f.path)
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "test.gif")
end
end
factory(:empty_replacement) do
replacement_file do
f = Tempfile.new
IO.copy_stream("#{Rails.root}/test/files/empty.jpg", f.path)
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "test.jpg")
end
end
factory(:png_replacement) do
replacement_file do
f = Tempfile.new
IO.copy_stream("#{Rails.root}/test/files/test.png", f.path)
ActionDispatch::Http::UploadedFile.new(tempfile: f, filename: "test.png")
end
end
end
end

0
test/files/empty.jpg Normal file
View File

View File

@ -14,19 +14,20 @@ class PostReplacementsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = create(:moderator_user, can_approve_posts: true, created_at: 1.month.ago)
@user.as_current do
UploadWhitelist.create!(pattern: "*raikou1.donmai.us/*", reason: "test")
@post = create(:post, source: "https://google.com")
@post_replacement = create(:post_replacement, post: @post)
@upload = UploadService.new(FactoryBot.attributes_for(:jpg_upload).merge({uploader: @user})).start!
@post = @upload.post
@replacement = create(:png_replacement, creator: @user, post: @post)
end
end
context "create action" do
should "render" do
should "accept new non duplicate replacement" do
file = Rack::Test::UploadedFile.new("#{Rails.root}/test/files/alpha.png", "image/png")
params = {
format: :json,
post_id: @post.id,
post_replacement: {
replacement_url: "https://raikou1.donmai.us/d3/4e/d34e4cf0a437a5d65f8e82b7bcd02606.jpg",
replacement_file: file,
reason: 'test replacement'
}
}
@ -35,38 +36,54 @@ class PostReplacementsControllerTest < ActionDispatch::IntegrationTest
@post.reload
end
# travel_to(Time.now + PostReplacement::DELETION_GRACE_PERIOD + 1.day) do
# Delayed::Worker.new.work_off
# end
assert_response :success
assert_equal("https://raikou1.donmai.us/d3/4e/d34e4cf0a437a5d65f8e82b7bcd02606.jpg", @post.source)
assert_equal("d34e4cf0a437a5d65f8e82b7bcd02606", @post.md5)
assert_equal("d34e4cf0a437a5d65f8e82b7bcd02606", Digest::MD5.file(@post.file(:original)).hexdigest)
assert_redirected_to post_path(@post)
end
end
context "update action" do
should "update the replacement" do
params = {
format: :json,
id: @post_replacement.id,
post_replacement: {
file_size_was: 23,
file_size: 42,
}
}
context "reject action" do
should "reject replacement" do
put_auth reject_post_replacement_path(@replacement), @user
assert_redirected_to post_replacement_path(@replacement)
@replacement.reload
@post.reload
assert_equal @replacement.status, "rejected"
assert_not_equal @post.md5, @replacement.md5
end
end
put_auth post_replacement_path(@post_replacement), @user, params: params
@post_replacement.reload
assert_equal(23, @post_replacement.file_size_was)
assert_equal(42, @post_replacement.file_size)
context "approve action" do
should "replace post" do
put_auth approve_post_replacement_path(@replacement), @user
assert_redirected_to post_path(@post)
@replacement.reload
@post.reload
assert_equal @replacement.md5, @post.md5
assert_equal @replacement.status, "approved"
end
end
context "promote action" do
should "create post" do
put_auth promote_post_replacement_path(@replacement), @user
last_post = Post.last
assert_redirected_to post_path(last_post)
@replacement.reload
@post.reload
assert_equal @replacement.md5, last_post.md5
assert_equal @replacement.status, "promoted"
end
end
context "index action" do
should "render" do
get post_replacements_path, params: {format: "json"}
get post_replacements_path
assert_response :success
end
end
context "new action" do
should "render" do
get_auth new_post_replacement_path, @user, params: {post_id: @post.id}
assert_response :success
end
end

View File

@ -1,115 +0,0 @@
require 'test_helper'
class TagAutocompleteTest < ActiveSupport::TestCase
subject { TagAutocomplete }
context "#search" do
should "be case insensitive" do
create(:tag, name: "abcdef", post_count: 1)
assert_equal(["abcdef"], subject.search("A").map(&:name))
end
should "not return duplicates" do
create(:tag, name: "red_eyes", post_count: 5001)
assert_equal(%w[red_eyes], subject.search("re").map(&:name))
end
end
context "#search_exact" do
setup do
@tags = [
create(:tag, name: "abcdef", post_count: 1),
create(:tag, name: "abczzz", post_count: 2),
create(:tag, name: "abcyyy", post_count: 0),
create(:tag, name: "bbbbbb")
]
end
should "find the tags" do
expected = [
@tags[1],
@tags[0]
].map(&:name)
assert_equal(expected, subject.search_exact("abc", 3).map(&:name))
end
end
context "#search_correct" do
setup do
CurrentUser.stubs(:id).returns(1)
@tags = [
create(:tag, name: "abcde", post_count: 1),
create(:tag, name: "abcdz", post_count: 2),
# one char mismatch
create(:tag, name: "abcez", post_count: 2),
# too long
create(:tag, name: "abcdefghijk", post_count: 2),
# wrong prefix
create(:tag, name: "bbcdef", post_count: 2),
# zero post count
create(:tag, name: "abcdy", post_count: 0),
# completely different
create(:tag, name: "bbbbb")
]
end
should "find the tags" do
expected = [
@tags[0],
@tags[1],
@tags[2]
].map(&:name)
assert_equal(expected, subject.search_correct("abcd", 3).map(&:name))
end
end
context "#search_prefix" do
setup do
@tags = [
create(:tag, name: "abcdef", post_count: 1),
create(:tag, name: "alpha_beta_cat", post_count: 2),
create(:tag, name: "alpha_beta_dat", post_count: 0),
create(:tag, name: "alpha_beta_(cane)", post_count: 2),
create(:tag, name: "alpha_beta/cane", post_count: 2)
]
end
should "find the tags" do
expected = [
@tags[1],
@tags[3],
@tags[4]
].map(&:name)
assert_equal(expected, subject.search_prefix("abc", 3).map(&:name))
end
end
context "#search_aliases" do
setup do
@user = create(:user)
@tags = [
create(:tag, name: "/abc", post_count: 0),
create(:tag, name: "abcdef", post_count: 1),
create(:tag, name: "zzzzzz", post_count: 1),
]
as_user do
@aliases = [
create(:tag_alias, antecedent_name: "/abc", consequent_name: "abcdef", status: "active", post_count: 1)
]
end
end
should "find the tags" do
results = subject.search_aliases("/abc", 3)
assert_equal(1, results.size)
assert_equal("abcdef", results[0].name)
assert_equal("/abc", results[0].antecedent_name)
end
end
end

Some files were not shown because too many files have changed in this diff Show More