accidentally leaked secrets so had to nuke shit fuck
Some checks failed
CI / test (push) Has been cancelled
6
.env.example
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
DATABASE_CONTAINER=Neocities-Database
|
||||||
|
DATABASE=neocities
|
||||||
|
WEB_INTERFACE_CONTAINER=Neocities-Interface
|
||||||
|
REDIS_CONTAINER=Neocities-Caching
|
||||||
|
DATABASE_USER=neocities
|
||||||
|
DATABASE_PASSWORD=changeme
|
||||||
3
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
*.sh text eol=lf
|
||||||
|
docker/docker-entrypoint.sh text eol=lf
|
||||||
|
docker/Dockerfile text eol=lf
|
||||||
46
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: CI
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: ci_test
|
||||||
|
POSTGRES_PASSWORD: citestpassword
|
||||||
|
# Set health checks to wait until postgres has started
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
# Maps tcp port 5432 on service container to the host
|
||||||
|
- 5432:5432
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
# Set health checks to wait until redis has started
|
||||||
|
options: >-
|
||||||
|
--health-cmd "redis-cli ping"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
# Maps port 6379 on service container to the host
|
||||||
|
- 6379:6379
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- run: sudo apt-get update && sudo apt-get -y install libimlib2-dev chromium-browser
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: '3.3'
|
||||||
|
bundler-cache: true
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bundle install
|
||||||
|
- name: Run tests with Coveralls
|
||||||
|
env:
|
||||||
|
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||||
|
run: bundle exec rake
|
||||||
36
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/docker/config.yml.docker
|
||||||
|
.config
|
||||||
|
tests/coverage
|
||||||
|
config.yml
|
||||||
|
.DS_Store
|
||||||
|
public/css/neo.css
|
||||||
|
public/css/neo.css.map
|
||||||
|
public/site_thumbnails
|
||||||
|
public/sites
|
||||||
|
public/site_screenshots
|
||||||
|
public/site_screenshots_test
|
||||||
|
public/site_thumbnails_test
|
||||||
|
*.swp
|
||||||
|
files/map.txt
|
||||||
|
files/supporter-map.txt
|
||||||
|
files/maps
|
||||||
|
.sass-cache
|
||||||
|
.sass-cache/*
|
||||||
|
files/sslsites.zip
|
||||||
|
.tm_properties
|
||||||
|
.vagrant
|
||||||
|
public/banned_sites
|
||||||
|
public/deleted_sites
|
||||||
|
files/disposable_email_whitelist.conf
|
||||||
|
files/disposable_email_blacklist.conf
|
||||||
|
files/banned_email_blacklist.conf
|
||||||
|
files/letsencrypt.key
|
||||||
|
files/tor.txt
|
||||||
|
.bundle
|
||||||
|
ext/black_box.rb
|
||||||
|
files/trumpplan.txt
|
||||||
|
public/sitemap
|
||||||
|
.env
|
||||||
|
docker/config.yml.docker
|
||||||
|
docker/config.yaml.docker
|
||||||
|
docker/*
|
||||||
95
Gemfile
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
gem 'connection_pool', '~> 2.4'
|
||||||
|
gem 'uri'
|
||||||
|
gem 'logger', '1.6.5' # Fixes a load dep error where 1.7.0 breaks init, probably change in the future
|
||||||
|
gem 'sinatra'
|
||||||
|
gem 'redis'
|
||||||
|
gem 'sequel'
|
||||||
|
gem 'redis-namespace'
|
||||||
|
gem 'bcrypt'
|
||||||
|
gem 'sinatra-flash', require: 'sinatra/flash'
|
||||||
|
gem 'sinatra-xsendfile', require: 'sinatra/xsendfile'
|
||||||
|
gem 'puma', require: nil
|
||||||
|
gem 'sidekiq', '~> 7'
|
||||||
|
gem 'mail'
|
||||||
|
gem 'tilt'
|
||||||
|
gem 'erubi'
|
||||||
|
gem 'stripe' #, source: 'https://code.stripe.com/'
|
||||||
|
gem 'terrapin'
|
||||||
|
gem 'sass', require: nil
|
||||||
|
gem 'filesize'
|
||||||
|
gem 'thread'
|
||||||
|
gem 'rest-client', require: 'rest_client'
|
||||||
|
gem 'addressable', '>= 2.8.0', require: 'addressable/uri'
|
||||||
|
gem 'ostruct' # stripe-ruby-mock depends on this default gem, which Ruby 4 no longer loads by default
|
||||||
|
gem 'geoip'
|
||||||
|
gem 'io-extra', require: 'io/extra'
|
||||||
|
#gem 'rye'
|
||||||
|
gem 'coveralls_reborn', require: false
|
||||||
|
gem 'sanitize'
|
||||||
|
gem 'will_paginate'
|
||||||
|
gem 'simpleidn'
|
||||||
|
gem 'gandi'
|
||||||
|
gem 'hoe', require: nil
|
||||||
|
gem 'msgpack'
|
||||||
|
gem 'acme-client', '~> 2.0.0'
|
||||||
|
gem 'http'
|
||||||
|
gem 'htmlentities'
|
||||||
|
gem 'rinku'
|
||||||
|
gem 'image_optim'
|
||||||
|
gem 'image_optim_pack'
|
||||||
|
gem 'ipaddress'
|
||||||
|
gem 'feedjira', '2.1.4'
|
||||||
|
gem 'monetize'
|
||||||
|
gem 'xmlrpc'
|
||||||
|
gem 'magic'
|
||||||
|
gem 'pg'
|
||||||
|
gem 'sequel_pg', require: nil
|
||||||
|
gem 'hiredis'
|
||||||
|
gem 'activesupport'
|
||||||
|
gem 'facter', require: nil
|
||||||
|
gem 'maxmind-db'
|
||||||
|
gem 'json', '>= 2.3.0'
|
||||||
|
gem 'nokogiri'
|
||||||
|
gem 'webp-ffi'
|
||||||
|
gem 'rszr'
|
||||||
|
gem 'zip_tricks'
|
||||||
|
gem 'twilio-ruby'
|
||||||
|
gem 'phonelib'
|
||||||
|
gem 'dnsbl-client'
|
||||||
|
gem 'minfraud'
|
||||||
|
gem 'image_optimizer' # apt install optipng jpegoptim pngquant
|
||||||
|
gem 'rubyzip', require: 'zip'
|
||||||
|
gem 'airbrake'
|
||||||
|
gem 'csv'
|
||||||
|
|
||||||
|
group :development, :test do
|
||||||
|
gem 'pry'
|
||||||
|
end
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem 'shotgun', require: nil
|
||||||
|
gem 'certified'
|
||||||
|
end
|
||||||
|
|
||||||
|
group :test do
|
||||||
|
gem 'faker'
|
||||||
|
gem 'fabrication', require: 'fabrication'
|
||||||
|
gem 'minitest'
|
||||||
|
gem 'minitest-reporters', require: 'minitest/reporters'
|
||||||
|
gem 'rack-test', require: 'rack/test'
|
||||||
|
gem 'mocha', require: nil
|
||||||
|
gem 'rake', '>= 12.3.3', require: nil
|
||||||
|
gem 'capybara', require: nil #, '2.10.1', require: nil
|
||||||
|
gem 'selenium-webdriver'
|
||||||
|
gem 'rack_session_access', require: nil
|
||||||
|
gem 'webmock', require: nil
|
||||||
|
gem 'stripe-ruby-mock', '~> 5.0.0', require: 'stripe_mock'
|
||||||
|
gem 'timecop'
|
||||||
|
gem 'mock_redis'
|
||||||
|
gem 'simplecov', require: nil
|
||||||
|
gem 'm'
|
||||||
|
end
|
||||||
|
|
||||||
|
gem "rackup", "~> 2.3"
|
||||||
722
Gemfile.lock
Normal file
|
|
@ -0,0 +1,722 @@
|
||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
acme-client (2.0.34)
|
||||||
|
base64 (~> 0.2)
|
||||||
|
faraday (>= 1.0, < 3.0.0)
|
||||||
|
faraday-retry (>= 1.0, < 3.0.0)
|
||||||
|
activesupport (8.1.3.1)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
drb
|
||||||
|
i18n (>= 1.6, < 2)
|
||||||
|
json
|
||||||
|
logger (>= 1.4.2)
|
||||||
|
minitest (>= 5.1)
|
||||||
|
securerandom (>= 0.3)
|
||||||
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
|
uri (>= 0.13.1)
|
||||||
|
addressable (2.9.0)
|
||||||
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
|
airbrake (13.0.5)
|
||||||
|
airbrake-ruby (~> 6.0)
|
||||||
|
airbrake-ruby (6.2.2)
|
||||||
|
rbtree3 (~> 0.6)
|
||||||
|
ansi (1.6.0)
|
||||||
|
base64 (0.3.0)
|
||||||
|
bcrypt (3.1.22)
|
||||||
|
bigdecimal (4.1.2)
|
||||||
|
builder (3.3.0)
|
||||||
|
capybara (3.40.0)
|
||||||
|
addressable
|
||||||
|
matrix
|
||||||
|
mini_mime (>= 0.1.3)
|
||||||
|
nokogiri (~> 1.11)
|
||||||
|
rack (>= 1.6.0)
|
||||||
|
rack-test (>= 0.6.3)
|
||||||
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
|
xpath (~> 3.2)
|
||||||
|
certified (1.0.0)
|
||||||
|
climate_control (1.2.0)
|
||||||
|
coderay (1.1.3)
|
||||||
|
concurrent-ruby (1.3.8)
|
||||||
|
connection_pool (2.5.5)
|
||||||
|
coveralls_reborn (1.0.0)
|
||||||
|
simplecov (>= 1.0)
|
||||||
|
term-ansicolor (>= 1.11)
|
||||||
|
thor (>= 1.5)
|
||||||
|
tins (>= 1.54)
|
||||||
|
crack (1.0.1)
|
||||||
|
bigdecimal
|
||||||
|
rexml
|
||||||
|
crass (1.0.7)
|
||||||
|
csv (3.3.6)
|
||||||
|
dante (0.2.0)
|
||||||
|
date (3.5.1)
|
||||||
|
dnsbl-client (1.1.1)
|
||||||
|
domain_name (0.6.20240107)
|
||||||
|
drb (2.2.3)
|
||||||
|
erb (6.0.7)
|
||||||
|
erubi (1.13.1)
|
||||||
|
exifr (1.5.1)
|
||||||
|
fabrication (3.0.0)
|
||||||
|
facter (4.6.1)
|
||||||
|
hocon (~> 1.3)
|
||||||
|
thor (>= 1.0.1, < 2.0)
|
||||||
|
faker (3.8.0)
|
||||||
|
i18n (>= 1.8.11, < 2)
|
||||||
|
faraday (1.10.6)
|
||||||
|
faraday-em_http (~> 1.0)
|
||||||
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-httpclient (~> 1.0)
|
||||||
|
faraday-multipart (~> 1.0)
|
||||||
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.0)
|
||||||
|
faraday-patron (~> 1.0)
|
||||||
|
faraday-rack (~> 1.0)
|
||||||
|
faraday-retry (~> 1.0)
|
||||||
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-em_http (1.0.0)
|
||||||
|
faraday-em_synchrony (1.0.1)
|
||||||
|
faraday-excon (1.1.0)
|
||||||
|
faraday-httpclient (1.0.1)
|
||||||
|
faraday-multipart (1.2.0)
|
||||||
|
multipart-post (~> 2.0)
|
||||||
|
faraday-net_http (1.0.2)
|
||||||
|
faraday-net_http_persistent (1.2.0)
|
||||||
|
faraday-patron (1.0.0)
|
||||||
|
faraday-rack (1.0.0)
|
||||||
|
faraday-retry (1.0.4)
|
||||||
|
faraday_middleware (1.2.1)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
feedjira (2.1.4)
|
||||||
|
faraday (>= 0.9)
|
||||||
|
faraday_middleware (>= 0.9)
|
||||||
|
loofah (>= 2.0)
|
||||||
|
sax-machine (>= 1.0)
|
||||||
|
ffi (1.17.4-aarch64-linux-gnu)
|
||||||
|
ffi (1.17.4-aarch64-linux-musl)
|
||||||
|
ffi (1.17.4-arm-linux-gnu)
|
||||||
|
ffi (1.17.4-arm-linux-musl)
|
||||||
|
ffi (1.17.4-arm64-darwin)
|
||||||
|
ffi (1.17.4-x86_64-darwin)
|
||||||
|
ffi (1.17.4-x86_64-linux-gnu)
|
||||||
|
ffi (1.17.4-x86_64-linux-musl)
|
||||||
|
ffi-compiler (1.4.2)
|
||||||
|
ffi (>= 1.15.5)
|
||||||
|
rake
|
||||||
|
filesize (0.2.0)
|
||||||
|
fspath (3.1.2)
|
||||||
|
gandi (3.3.28)
|
||||||
|
hashie
|
||||||
|
xmlrpc
|
||||||
|
geoip (1.6.4)
|
||||||
|
hashdiff (1.2.1)
|
||||||
|
hashie (5.1.0)
|
||||||
|
logger
|
||||||
|
hiredis (0.6.3)
|
||||||
|
hocon (1.4.0)
|
||||||
|
hoe (4.7.1)
|
||||||
|
rake (~> 13.0)
|
||||||
|
htmlentities (4.4.2)
|
||||||
|
http (6.0.4)
|
||||||
|
http-cookie (~> 1.0)
|
||||||
|
llhttp (~> 0.6.1)
|
||||||
|
http-accept (1.7.0)
|
||||||
|
http-cookie (1.1.6)
|
||||||
|
domain_name (~> 0.5)
|
||||||
|
i18n (1.15.2)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
image_optim (0.32.0)
|
||||||
|
exifr (~> 1.2, >= 1.2.2)
|
||||||
|
fspath (~> 3.0)
|
||||||
|
image_size (>= 1.5, < 4)
|
||||||
|
in_threads (~> 1.3)
|
||||||
|
progress (~> 3.0, >= 3.0.1)
|
||||||
|
image_optim_pack (0.13.1.20260816)
|
||||||
|
fspath (>= 2.1, < 4)
|
||||||
|
image_optim (~> 0.19)
|
||||||
|
image_optim_pack (0.13.1.20260816-aarch64-linux)
|
||||||
|
fspath (>= 2.1, < 4)
|
||||||
|
image_optim (~> 0.19)
|
||||||
|
image_optim_pack (0.13.1.20260816-aarch64-linux-gnu)
|
||||||
|
fspath (>= 2.1, < 4)
|
||||||
|
image_optim (~> 0.19)
|
||||||
|
image_optim_pack (0.13.1.20260816-aarch64-linux-musl)
|
||||||
|
fspath (>= 2.1, < 4)
|
||||||
|
image_optim (~> 0.19)
|
||||||
|
image_optim_pack (0.13.1.20260816-arm64-darwin)
|
||||||
|
fspath (>= 2.1, < 4)
|
||||||
|
image_optim (~> 0.19)
|
||||||
|
image_optim_pack (0.13.1.20260816-x86_64-darwin)
|
||||||
|
fspath (>= 2.1, < 4)
|
||||||
|
image_optim (~> 0.19)
|
||||||
|
image_optim_pack (0.13.1.20260816-x86_64-linux-gnu)
|
||||||
|
fspath (>= 2.1, < 4)
|
||||||
|
image_optim (~> 0.19)
|
||||||
|
image_optim_pack (0.13.1.20260816-x86_64-linux-musl)
|
||||||
|
fspath (>= 2.1, < 4)
|
||||||
|
image_optim (~> 0.19)
|
||||||
|
image_optimizer (1.9.0)
|
||||||
|
image_size (3.6.0)
|
||||||
|
in_threads (1.6.0)
|
||||||
|
io-console (0.9.2)
|
||||||
|
io-extra (1.5.0)
|
||||||
|
ipaddress (0.8.3)
|
||||||
|
irb (1.18.0)
|
||||||
|
pp (>= 0.6.0)
|
||||||
|
prism (>= 1.3.0)
|
||||||
|
rdoc (>= 4.0.0)
|
||||||
|
reline (>= 0.4.2)
|
||||||
|
json (2.21.2)
|
||||||
|
jwt (3.2.0)
|
||||||
|
base64
|
||||||
|
llhttp (0.6.2)
|
||||||
|
logger (1.6.5)
|
||||||
|
loofah (2.25.2)
|
||||||
|
crass (~> 1.0.2)
|
||||||
|
nokogiri (>= 1.12.0)
|
||||||
|
m (1.7.0)
|
||||||
|
rake
|
||||||
|
magic (0.2.9)
|
||||||
|
ffi (>= 0.6.3)
|
||||||
|
mail (2.9.1)
|
||||||
|
logger
|
||||||
|
mini_mime (>= 0.1.1)
|
||||||
|
net-imap
|
||||||
|
net-pop
|
||||||
|
net-smtp
|
||||||
|
matrix (0.4.3)
|
||||||
|
maxmind-db (1.4.0)
|
||||||
|
maxmind-geoip2 (1.6.0)
|
||||||
|
connection_pool (>= 2.2, < 4.0)
|
||||||
|
http (>= 4.3, < 7.0)
|
||||||
|
maxmind-db (~> 1.4)
|
||||||
|
method_source (1.1.0)
|
||||||
|
mime-types (3.7.0)
|
||||||
|
logger
|
||||||
|
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
||||||
|
mime-types-data (3.2026.0701)
|
||||||
|
minfraud (2.10.0)
|
||||||
|
connection_pool (>= 2.2, < 4.0)
|
||||||
|
http (>= 4.3, < 7.0)
|
||||||
|
maxmind-geoip2 (~> 1.6)
|
||||||
|
simpleidn (~> 0.1, >= 0.1.1)
|
||||||
|
mini_mime (1.1.5)
|
||||||
|
minitest (6.0.6)
|
||||||
|
drb (~> 2.0)
|
||||||
|
prism (~> 1.5)
|
||||||
|
minitest-reporters (1.8.0)
|
||||||
|
ansi
|
||||||
|
builder
|
||||||
|
minitest (>= 5.0, < 7)
|
||||||
|
ruby-progressbar
|
||||||
|
mize (0.6.1)
|
||||||
|
mocha (3.1.0)
|
||||||
|
ruby2_keywords (>= 0.0.5)
|
||||||
|
mock_redis (0.55.0)
|
||||||
|
redis (~> 5)
|
||||||
|
monetize (2.0.0)
|
||||||
|
money (~> 7.0)
|
||||||
|
money (7.1.1)
|
||||||
|
bigdecimal
|
||||||
|
i18n (~> 1.9)
|
||||||
|
msgpack (1.8.4)
|
||||||
|
multi_json (1.21.1)
|
||||||
|
multipart-post (2.4.1)
|
||||||
|
mustermann (3.1.1)
|
||||||
|
net-imap (0.6.6)
|
||||||
|
date
|
||||||
|
net-protocol
|
||||||
|
net-pop (0.1.2)
|
||||||
|
net-protocol
|
||||||
|
net-protocol (0.2.2)
|
||||||
|
timeout
|
||||||
|
net-smtp (0.5.1)
|
||||||
|
net-protocol
|
||||||
|
netrc (0.11.0)
|
||||||
|
nio4r (2.7.5)
|
||||||
|
nokogiri (1.19.4-aarch64-linux-gnu)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.4-aarch64-linux-musl)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.4-arm-linux-gnu)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.4-arm-linux-musl)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.4-arm64-darwin)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.4-x86_64-darwin)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.4-x86_64-linux-gnu)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.4-x86_64-linux-musl)
|
||||||
|
racc (~> 1.4)
|
||||||
|
ostruct (0.6.3)
|
||||||
|
pg (1.6.3)
|
||||||
|
pg (1.6.3-aarch64-linux)
|
||||||
|
pg (1.6.3-aarch64-linux-musl)
|
||||||
|
pg (1.6.3-arm64-darwin)
|
||||||
|
pg (1.6.3-x86_64-darwin)
|
||||||
|
pg (1.6.3-x86_64-linux)
|
||||||
|
pg (1.6.3-x86_64-linux-musl)
|
||||||
|
phonelib (0.10.24)
|
||||||
|
pp (0.6.4)
|
||||||
|
prettyprint
|
||||||
|
prettyprint (0.2.0)
|
||||||
|
prism (1.9.0)
|
||||||
|
progress (3.6.0)
|
||||||
|
pry (0.16.0)
|
||||||
|
coderay (~> 1.1)
|
||||||
|
method_source (~> 1.0)
|
||||||
|
reline (>= 0.6.0)
|
||||||
|
public_suffix (7.0.5)
|
||||||
|
puma (8.0.2)
|
||||||
|
nio4r (~> 2.0)
|
||||||
|
racc (1.8.1)
|
||||||
|
rack (3.2.7)
|
||||||
|
rack-protection (4.2.1)
|
||||||
|
base64 (>= 0.1.0)
|
||||||
|
logger (>= 1.6.0)
|
||||||
|
rack (>= 3.0.0, < 4)
|
||||||
|
rack-session (2.1.2)
|
||||||
|
base64 (>= 0.1.0)
|
||||||
|
rack (>= 3.0.0)
|
||||||
|
rack-test (2.2.0)
|
||||||
|
rack (>= 1.3)
|
||||||
|
rack_session_access (0.2.0)
|
||||||
|
builder (>= 2.0.0)
|
||||||
|
rack (>= 1.0.0)
|
||||||
|
rackup (2.3.1)
|
||||||
|
rack (>= 3)
|
||||||
|
rake (13.4.2)
|
||||||
|
rb-fsevent (0.11.2)
|
||||||
|
rb-inotify (0.11.1)
|
||||||
|
ffi (~> 1.0)
|
||||||
|
rbs (4.1.3)
|
||||||
|
logger
|
||||||
|
prism (>= 1.6.0)
|
||||||
|
tsort
|
||||||
|
rbtree3 (0.7.1)
|
||||||
|
rdoc (8.0.0)
|
||||||
|
erb
|
||||||
|
prism (>= 1.6.0)
|
||||||
|
rbs (>= 4.0.0)
|
||||||
|
tsort
|
||||||
|
readline (0.0.4)
|
||||||
|
reline
|
||||||
|
redis (5.4.1)
|
||||||
|
redis-client (>= 0.22.0)
|
||||||
|
redis-client (0.30.1)
|
||||||
|
connection_pool
|
||||||
|
redis-namespace (1.11.0)
|
||||||
|
redis (>= 4)
|
||||||
|
regexp_parser (2.12.0)
|
||||||
|
reline (0.7.0)
|
||||||
|
io-console (~> 0.5)
|
||||||
|
rest-client (2.1.0)
|
||||||
|
http-accept (>= 1.7.0, < 2.0)
|
||||||
|
http-cookie (>= 1.0.2, < 2.0)
|
||||||
|
mime-types (>= 1.16, < 4.0)
|
||||||
|
netrc (~> 0.8)
|
||||||
|
rexml (3.4.4)
|
||||||
|
rinku (2.0.6)
|
||||||
|
rszr (1.5.0)
|
||||||
|
ruby-progressbar (1.13.0)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
|
rubyzip (3.4.1)
|
||||||
|
sanitize (7.0.0)
|
||||||
|
crass (~> 1.0.2)
|
||||||
|
nokogiri (>= 1.16.8)
|
||||||
|
sass (3.7.4)
|
||||||
|
sass-listen (~> 4.0.0)
|
||||||
|
sass-listen (4.0.0)
|
||||||
|
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||||
|
rb-inotify (~> 0.9, >= 0.9.7)
|
||||||
|
sax-machine (1.3.2)
|
||||||
|
securerandom (0.4.1)
|
||||||
|
selenium-webdriver (4.44.0)
|
||||||
|
base64 (~> 0.2)
|
||||||
|
logger (~> 1.4)
|
||||||
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
|
rubyzip (>= 1.2.2, < 4.0)
|
||||||
|
websocket (~> 1.0)
|
||||||
|
sequel (5.107.0)
|
||||||
|
bigdecimal
|
||||||
|
sequel_pg (1.19.0)
|
||||||
|
pg (>= 0.18.0, != 1.2.0)
|
||||||
|
sequel (>= 4.38.0)
|
||||||
|
shotgun (0.9.2)
|
||||||
|
rack (>= 1.0)
|
||||||
|
sidekiq (7.3.9)
|
||||||
|
base64
|
||||||
|
connection_pool (>= 2.3.0)
|
||||||
|
logger
|
||||||
|
rack (>= 2.2.4)
|
||||||
|
redis-client (>= 0.22.2)
|
||||||
|
simplecov (1.1.1)
|
||||||
|
simpleidn (0.2.3)
|
||||||
|
sinatra (4.2.1)
|
||||||
|
logger (>= 1.6.0)
|
||||||
|
mustermann (~> 3.0)
|
||||||
|
rack (>= 3.0.0, < 4)
|
||||||
|
rack-protection (= 4.2.1)
|
||||||
|
rack-session (>= 2.0.0, < 3)
|
||||||
|
tilt (~> 2.0)
|
||||||
|
sinatra-flash (0.3.0)
|
||||||
|
sinatra (>= 1.0.0)
|
||||||
|
sinatra-xsendfile (0.4.2)
|
||||||
|
sinatra (>= 0.9.1)
|
||||||
|
stripe (13.5.1)
|
||||||
|
stripe-ruby-mock (5.0.0)
|
||||||
|
dante (>= 0.2.0)
|
||||||
|
drb (>= 2.0.4, < 3)
|
||||||
|
multi_json (~> 1.0)
|
||||||
|
stripe (> 5, < 14)
|
||||||
|
sync (0.5.0)
|
||||||
|
term-ansicolor (1.11.3)
|
||||||
|
tins (~> 1)
|
||||||
|
terrapin (1.1.1)
|
||||||
|
climate_control
|
||||||
|
thor (1.5.0)
|
||||||
|
thread (0.2.2)
|
||||||
|
tilt (2.8.0)
|
||||||
|
timecop (0.9.11)
|
||||||
|
timeout (0.6.1)
|
||||||
|
tins (1.56.0)
|
||||||
|
bigdecimal
|
||||||
|
irb
|
||||||
|
mize (~> 0.6)
|
||||||
|
readline
|
||||||
|
sync
|
||||||
|
tsort (0.2.0)
|
||||||
|
twilio-ruby (7.10.5)
|
||||||
|
faraday (>= 0.9, < 3.0)
|
||||||
|
jwt (>= 1.5, < 4.0)
|
||||||
|
nokogiri (>= 1.6, < 2.0)
|
||||||
|
tzinfo (2.0.6)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
uri (1.1.1)
|
||||||
|
webmock (3.26.2)
|
||||||
|
addressable (>= 2.8.0)
|
||||||
|
crack (>= 0.3.2)
|
||||||
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
|
webp-ffi (0.4.0)
|
||||||
|
ffi (>= 1.9.0)
|
||||||
|
ffi-compiler (>= 0.1.2)
|
||||||
|
webrick (1.9.2)
|
||||||
|
websocket (1.2.11)
|
||||||
|
will_paginate (4.0.1)
|
||||||
|
xmlrpc (0.3.4)
|
||||||
|
rexml
|
||||||
|
webrick
|
||||||
|
xpath (3.2.0)
|
||||||
|
nokogiri (~> 1.8)
|
||||||
|
zip_tricks (5.6.0)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
aarch64-linux
|
||||||
|
aarch64-linux-gnu
|
||||||
|
aarch64-linux-musl
|
||||||
|
arm-linux-gnu
|
||||||
|
arm-linux-musl
|
||||||
|
arm64-darwin
|
||||||
|
x86_64-darwin
|
||||||
|
x86_64-linux-gnu
|
||||||
|
x86_64-linux-musl
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
acme-client (~> 2.0.0)
|
||||||
|
activesupport
|
||||||
|
addressable (>= 2.8.0)
|
||||||
|
airbrake
|
||||||
|
bcrypt
|
||||||
|
capybara
|
||||||
|
certified
|
||||||
|
connection_pool (~> 2.4)
|
||||||
|
coveralls_reborn
|
||||||
|
csv
|
||||||
|
dnsbl-client
|
||||||
|
erubi
|
||||||
|
fabrication
|
||||||
|
facter
|
||||||
|
faker
|
||||||
|
feedjira (= 2.1.4)
|
||||||
|
filesize
|
||||||
|
gandi
|
||||||
|
geoip
|
||||||
|
hiredis
|
||||||
|
hoe
|
||||||
|
htmlentities
|
||||||
|
http
|
||||||
|
image_optim
|
||||||
|
image_optim_pack
|
||||||
|
image_optimizer
|
||||||
|
io-extra
|
||||||
|
ipaddress
|
||||||
|
json (>= 2.3.0)
|
||||||
|
logger (= 1.6.5)
|
||||||
|
m
|
||||||
|
magic
|
||||||
|
mail
|
||||||
|
maxmind-db
|
||||||
|
minfraud
|
||||||
|
minitest
|
||||||
|
minitest-reporters
|
||||||
|
mocha
|
||||||
|
mock_redis
|
||||||
|
monetize
|
||||||
|
msgpack
|
||||||
|
nokogiri
|
||||||
|
ostruct
|
||||||
|
pg
|
||||||
|
phonelib
|
||||||
|
pry
|
||||||
|
puma
|
||||||
|
rack-test
|
||||||
|
rack_session_access
|
||||||
|
rackup (~> 2.3)
|
||||||
|
rake (>= 12.3.3)
|
||||||
|
redis
|
||||||
|
redis-namespace
|
||||||
|
rest-client
|
||||||
|
rinku
|
||||||
|
rszr
|
||||||
|
rubyzip
|
||||||
|
sanitize
|
||||||
|
sass
|
||||||
|
selenium-webdriver
|
||||||
|
sequel
|
||||||
|
sequel_pg
|
||||||
|
shotgun
|
||||||
|
sidekiq (~> 7)
|
||||||
|
simplecov
|
||||||
|
simpleidn
|
||||||
|
sinatra
|
||||||
|
sinatra-flash
|
||||||
|
sinatra-xsendfile
|
||||||
|
stripe
|
||||||
|
stripe-ruby-mock (~> 5.0.0)
|
||||||
|
terrapin
|
||||||
|
thread
|
||||||
|
tilt
|
||||||
|
timecop
|
||||||
|
twilio-ruby
|
||||||
|
uri
|
||||||
|
webmock
|
||||||
|
webp-ffi
|
||||||
|
will_paginate
|
||||||
|
xmlrpc
|
||||||
|
zip_tricks
|
||||||
|
|
||||||
|
CHECKSUMS
|
||||||
|
acme-client (2.0.34) sha256=3204a94addbdf82c06fe7a5d87a43c77ee135f0cbd05cb0ada18fffdf1935395
|
||||||
|
activesupport (8.1.3.1) sha256=85458765f25ea48b9019c46b6bb3fa5683197bf4280d9f06710a6e8d7a831376
|
||||||
|
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
|
||||||
|
airbrake (13.0.5) sha256=901f5074c25d5ef77ed87f5bde7a28400a7324f5d7013a8a12d07e0099cc31b6
|
||||||
|
airbrake-ruby (6.2.2) sha256=293e34fb36e763e1b6d67ab584cce7c5b6fe9eea1a70c26d8c13c0f5d7de2fbc
|
||||||
|
ansi (1.6.0) sha256=ac9ea0c0ea8d32fb4e271348e609963ac78882f34b73836c2a02b3622e666658
|
||||||
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
||||||
|
bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032
|
||||||
|
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
|
||||||
|
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
|
||||||
|
capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef
|
||||||
|
certified (1.0.0) sha256=aa4cdf0e90e7ee96f6e0ce3daae39eaa8f0486124e0d92daf64d2105aeb9069c
|
||||||
|
climate_control (1.2.0) sha256=36b21896193fa8c8536fa1cd843a07cf8ddbd03aaba43665e26c53ec1bd70aa5
|
||||||
|
coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b
|
||||||
|
concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
|
||||||
|
connection_pool (2.5.5) sha256=e54ff92855753df1fd7c59fa04a398833355f27dd14c074f8c83a05f72a716ad
|
||||||
|
coveralls_reborn (1.0.0) sha256=0c1f548f84fe8b51ca423b3d614866d48b6bf9a78a4c7a0692604755b7bb8684
|
||||||
|
crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
|
||||||
|
crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295
|
||||||
|
csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456
|
||||||
|
dante (0.2.0) sha256=939776f04b4d253ffbbcf53341631aa2ee6e6cf314dedade2e60ac43b40a6fe6
|
||||||
|
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
|
||||||
|
dnsbl-client (1.1.1) sha256=ff94b0500c4f6663481367a7a6654d16f6fa4cb78ff188f7976d9d65577e8d56
|
||||||
|
domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933
|
||||||
|
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
||||||
|
erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
|
||||||
|
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
|
||||||
|
exifr (1.5.1) sha256=ad87a5dbc92946fbf1d8ccd178d557d95d9168e8b3c5c5f381ea2bffcc312521
|
||||||
|
fabrication (3.0.0) sha256=a6a0bfad9071348ad3cb1701df788524b888c0cdd044b988893e7509f7463be3
|
||||||
|
facter (4.6.1) sha256=479971e82de8b084f8dd42ad7552fbefc81846507aa80c67ca0d10c75ca3b75f
|
||||||
|
faker (3.8.0) sha256=c147b308df73a90f27a4fc84f18d4c22ef0ad9c2a64b2b61c86fd0ca71753efc
|
||||||
|
faraday (1.10.6) sha256=7ff4802a6b312876a2241b3e641ce0d5045e168dd871b422c35b505e5261ad4d
|
||||||
|
faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689
|
||||||
|
faraday-em_synchrony (1.0.1) sha256=bf3ce45dcf543088d319ab051f80985ea6d294930635b7a0b966563179f81750
|
||||||
|
faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940
|
||||||
|
faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b
|
||||||
|
faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757
|
||||||
|
faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682
|
||||||
|
faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335
|
||||||
|
faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7
|
||||||
|
faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0
|
||||||
|
faraday-retry (1.0.4) sha256=dc659233777fabf96c69c2ffe56c0a5d2c102af90321a42cc6c90157bcd716aa
|
||||||
|
faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9
|
||||||
|
feedjira (2.1.4) sha256=4b3dbc74d618ef6a92c5f711160804e9141446c2ed0f87b0e611824efa649a2f
|
||||||
|
ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
|
||||||
|
ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
|
||||||
|
ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564
|
||||||
|
ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95
|
||||||
|
ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b
|
||||||
|
ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496
|
||||||
|
ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
|
||||||
|
ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
|
||||||
|
ffi-compiler (1.4.2) sha256=a9d69d5d9ced6f64776ddafa535867ad90e5740ad37bed7afb4de6e450da126e
|
||||||
|
filesize (0.2.0) sha256=0f5db8629d628b60857dd8e7b8d52f427e6b7b9dc49265bfda71c0d383cbe79e
|
||||||
|
fspath (3.1.2) sha256=b5ac9bafb97e2c8f8f9e303cd98ebd484be76fe9aa588bc4d01c6d99e78c9d75
|
||||||
|
gandi (3.3.28) sha256=6ced52ab889c76634a39824d266b586122ceba184ef0e30b05805725efa7ab38
|
||||||
|
geoip (1.6.4) sha256=4c9a4636e0ff66633271e91ac568bdda95520788e7610618d45350798935c1c5
|
||||||
|
hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
|
||||||
|
hashie (5.1.0) sha256=c266471896f323c446ea8207f8ffac985d2718df0a0ba98651a3057096ca3870
|
||||||
|
hiredis (0.6.3) sha256=7f052e320f7d24b5c2a9fdf67c6ff6facdf6e256394a703511bce34ecf445212
|
||||||
|
hocon (1.4.0) sha256=e71023ed7c56ae780ec34c0ce7789a233bcead08c045d50bc7b3af40f5afcd80
|
||||||
|
hoe (4.7.1) sha256=10c2edac888a16cffdd7d7d523ac685fa7ecc4afc529acbf20ba9ef5bf47738d
|
||||||
|
htmlentities (4.4.2) sha256=bbafbdf69f2eca9262be4efef7e43e6a1de54c95eb600f26984f71d2fe96c5c3
|
||||||
|
http (6.0.4) sha256=3768f902489ea8309424f1881c668af8d3ca73675648a755dfe2cb0030d42d26
|
||||||
|
http-accept (1.7.0) sha256=c626860682bfbb3b46462f8c39cd470fd7b0584f61b3cc9df5b2e9eb9972a126
|
||||||
|
http-cookie (1.1.6) sha256=ba4b82be64de61dc281243dac70e3c382c45142f20268ed9276a3670c93feaa9
|
||||||
|
i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
|
||||||
|
image_optim (0.32.0) sha256=457d5ea28ee93244c2aabc7134e06455587fb670f210e88e1be40aea882c2ba3
|
||||||
|
image_optim_pack (0.13.1.20260816) sha256=5e37513014fb90900b032467f83abc36b5c8b87bd715cf1d6c85e3c8642d7438
|
||||||
|
image_optim_pack (0.13.1.20260816-aarch64-linux) sha256=fd0525ecf79a7d4db58e328b892c1604796451d8aa273f1a816b8d85c1b15a0f
|
||||||
|
image_optim_pack (0.13.1.20260816-aarch64-linux-gnu) sha256=a4ad086a83f040dff9bab568d178cce858c49cef7599e2dd0ca98f9488063d9a
|
||||||
|
image_optim_pack (0.13.1.20260816-aarch64-linux-musl) sha256=70359dd3ecfa6d3870c6b0760adcb0d13ff7479ff8c2394089d23f29add37d4b
|
||||||
|
image_optim_pack (0.13.1.20260816-arm64-darwin) sha256=14429a57b302e3574290efb10405efe84ba264782755eb73d8781d878ba114e4
|
||||||
|
image_optim_pack (0.13.1.20260816-x86_64-darwin) sha256=5591c9bac5d0a062a6e2c3964071f853ad1f5ee61aeac3f00bd877159c9b39f6
|
||||||
|
image_optim_pack (0.13.1.20260816-x86_64-linux-gnu) sha256=ff3f4813f76229f6b7c6641cbefa2d289e61c9227e0277789f80e97f98ce63f0
|
||||||
|
image_optim_pack (0.13.1.20260816-x86_64-linux-musl) sha256=50aa4d521bb9a6d880d9040c1ef3f19d4ab12dbdbaa1b2ec77d584e37ac5f163
|
||||||
|
image_optimizer (1.9.0) sha256=2181d3a6b8f36c1e39add1a2b626ecf83d269e766a26d48dca18dc025d5ee531
|
||||||
|
image_size (3.6.0) sha256=1b9c4196cb658bf503a887920543a3991e927a4b767a5c770fb9665bce0dcb28
|
||||||
|
in_threads (1.6.0) sha256=91a7e6138d279dc632f59b8a9a409e47148948e297c0f69c92f9a2479a182149
|
||||||
|
io-console (0.9.2) sha256=efa74f891dd03c0939a931dfc6e74c2813d904763d456ea9762b0525e748db08
|
||||||
|
io-extra (1.5.0) sha256=c5d9e36a72261a2151bbf18d561f0b91320454290cfad41b3e3d73eabf08d294
|
||||||
|
ipaddress (0.8.3) sha256=85640c4f9194c26937afc8c78e3074a8e7c97d5d1210358d1440f01034d006f5
|
||||||
|
irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
|
||||||
|
json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
|
||||||
|
jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7
|
||||||
|
llhttp (0.6.2) sha256=3c3c59aafb1e1594ab2f45293478f697458f5a91eb0f0db4b27f61a064024982
|
||||||
|
logger (1.6.5) sha256=c3cfe56d01656490ddd103d38b8993d73d86296adebc5f58cefc9ec03741e56b
|
||||||
|
loofah (2.25.2) sha256=2007f746959ac65552456e04b433e83deb22759ab38c838b4445c70e43425918
|
||||||
|
m (1.7.0) sha256=058f793da8150c51353cc59366ffae7774683e868bede3c78f81d920fb9d633a
|
||||||
|
magic (0.2.9) sha256=f91a61dfd1f894666347178acd9b803372913a5c6e889427789d2372046e73a3
|
||||||
|
mail (2.9.1) sha256=06574eca475253d6c18145dd70af80d0eb970182d55053497c5f4d797ea160e8
|
||||||
|
matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b
|
||||||
|
maxmind-db (1.4.0) sha256=0765c79329847fe92687a7f215a6bae843121a09ac7c8d8d9930856bcb849052
|
||||||
|
maxmind-geoip2 (1.6.0) sha256=886f726e2419293e3d8dc3395568c790574327da5d8d26bd1d0a01c6b36d2f70
|
||||||
|
method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
|
||||||
|
mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56
|
||||||
|
mime-types-data (3.2026.0701) sha256=cd8811e1fb89d836499ba0582368a10ee74cef929ba956d1d5ddca045e6a730f
|
||||||
|
minfraud (2.10.0) sha256=88ffbabc700c77770985f49a85cd21e458ee5272fd9deeedfdccdbed9c14cec6
|
||||||
|
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
|
||||||
|
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
|
||||||
|
minitest-reporters (1.8.0) sha256=8ce5280fb73ad3178ae525454df169b6f28c1b38b1d088ea91815d3a370ba384
|
||||||
|
mize (0.6.1) sha256=4031558979ff5426fda24c75a149b4e4c0faf4cacf2fae8938f83866ab94b780
|
||||||
|
mocha (3.1.0) sha256=75f42d69ebfb1f10b32489dff8f8431d37a418120ecdfc07afe3bc183d4e1d56
|
||||||
|
mock_redis (0.55.0) sha256=1562199cb94b40775ff73b09f33346074138a6f35e1ac0b614b0404777cd41f0
|
||||||
|
monetize (2.0.0) sha256=152980cadff9fd11a95817d56e3d7d69b07998c4506f15d971edf05851f82aba
|
||||||
|
money (7.1.1) sha256=8ed5cd99dfbc45e82688ea711cb59ea3c396c2608cd88876502b164c90269d4e
|
||||||
|
msgpack (1.8.4) sha256=4411c22d350dd1c20250f7eada3cca2695438c2f769cf0782f0cd065d90a3e7b
|
||||||
|
multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080
|
||||||
|
multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8
|
||||||
|
mustermann (3.1.1) sha256=4c6170c7234d5499c345562ba7c7dfe73e1754286dcc1abb053064d66a127198
|
||||||
|
net-imap (0.6.6) sha256=96aa4ee50df3060203e649efc341f53480b791d49e150f2fdebf68beb141a8df
|
||||||
|
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
|
||||||
|
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
|
||||||
|
net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
|
||||||
|
netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
|
||||||
|
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
|
||||||
|
nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f
|
||||||
|
nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af
|
||||||
|
nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca
|
||||||
|
nokogiri (1.19.4-arm-linux-musl) sha256=588923c101bcfa78869734d247d25b598674323e7f22474fc468f6e5647311eb
|
||||||
|
nokogiri (1.19.4-arm64-darwin) sha256=a46db9853286e6597b36ebc6953817d15acf3a299583eb3f89fdc6f91dd63527
|
||||||
|
nokogiri (1.19.4-x86_64-darwin) sha256=7fd17057d3e1f00e9954a74b3cd76595d3d4a5ef233b7ed9599047c204f70551
|
||||||
|
nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a
|
||||||
|
nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b
|
||||||
|
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
|
||||||
|
pg (1.6.3) sha256=1388d0563e13d2758c1089e35e973a3249e955c659592d10e5b77c468f628a99
|
||||||
|
pg (1.6.3-aarch64-linux) sha256=0698ad563e02383c27510b76bf7d4cd2de19cd1d16a5013f375dd473e4be72ea
|
||||||
|
pg (1.6.3-aarch64-linux-musl) sha256=06a75f4ea04b05140146f2a10550b8e0d9f006a79cdaf8b5b130cde40e3ecc2c
|
||||||
|
pg (1.6.3-arm64-darwin) sha256=7240330b572e6355d7c75a7de535edb5dfcbd6295d9c7777df4d9dddfb8c0e5f
|
||||||
|
pg (1.6.3-x86_64-darwin) sha256=ee2e04a17c0627225054ffeb43e31a95be9d7e93abda2737ea3ce4a62f2729d6
|
||||||
|
pg (1.6.3-x86_64-linux) sha256=5d9e188c8f7a0295d162b7b88a768d8452a899977d44f3274d1946d67920ae8d
|
||||||
|
pg (1.6.3-x86_64-linux-musl) sha256=9c9c90d98c72f78eb04c0f55e9618fe55d1512128e411035fe229ff427864009
|
||||||
|
phonelib (0.10.24) sha256=5e8a623040fa496346fa71c48b3184c95caeddf58a36c041f03c0d201d4681ae
|
||||||
|
pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
|
||||||
|
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
|
||||||
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
||||||
|
progress (3.6.0) sha256=360ed306dfa43d6174e847d563c70736dca249e2333cfec4b0387306c86cd573
|
||||||
|
pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e
|
||||||
|
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
|
||||||
|
puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb
|
||||||
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
||||||
|
rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7
|
||||||
|
rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac
|
||||||
|
rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8
|
||||||
|
rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463
|
||||||
|
rack_session_access (0.2.0) sha256=03eb98f2027429ccbbeb18556006dfb6d928b0557ad3770783b8e2f368198d6b
|
||||||
|
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
||||||
|
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
||||||
|
rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe
|
||||||
|
rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
|
||||||
|
rbs (4.1.3) sha256=0c4474a9751cdc14364bfad0b3e53678323bbdc2c31683b0445932867dbab8c4
|
||||||
|
rbtree3 (0.7.1) sha256=ab60ead728a5491b70df4f4065e180b18dbab5319f817ce1dbf5dd906f26d8ba
|
||||||
|
rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
|
||||||
|
readline (0.0.4) sha256=6138eef17be2b98298b672c3ea63bf9cb5158d401324f26e1e84f235879c1d6a
|
||||||
|
redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
|
||||||
|
redis-client (0.30.1) sha256=5151bc5c7bbfe48623732cdae3b900d8a22dc691cc7cdfacfb351ac55116522d
|
||||||
|
redis-namespace (1.11.0) sha256=e91a1aa2b2d888b6dea1d4ab8d39e1ae6fac3426161feb9d91dd5cca598a2239
|
||||||
|
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
||||||
|
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
|
||||||
|
rest-client (2.1.0) sha256=35a6400bdb14fae28596618e312776c158f7ebbb0ccad752ff4fa142bf2747e3
|
||||||
|
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
||||||
|
rinku (2.0.6) sha256=8b60670e3143f3db2b37efa262971ce3619ec23092045498ef9f077d82828d7d
|
||||||
|
rszr (1.5.0) sha256=504e2fa83fedbacdcbd16af86ab645f751df38491b2688d274eb8e1238d18298
|
||||||
|
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
||||||
|
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
|
||||||
|
rubyzip (3.4.1) sha256=0a79e745b5c25872ebd148457df5665da8530ed8626c993b01457128f173ca02
|
||||||
|
sanitize (7.0.0) sha256=269d1b9d7326e69307723af5643ec032ff86ad616e72a3b36d301ac75a273984
|
||||||
|
sass (3.7.4) sha256=808b0d39053aa69068df939e24671fe84fd5a9d3314486e1a1457d0934a4255d
|
||||||
|
sass-listen (4.0.0) sha256=ae9dcb76dd3e234329e5ba6e213f48e532c5a3e7b0b4d8a87f13aaca0cc18377
|
||||||
|
sax-machine (1.3.2) sha256=a1112678039eea4c402827ca0d377744e0f882fd6386f29f7b0023d938750d3a
|
||||||
|
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
|
||||||
|
selenium-webdriver (4.44.0) sha256=6f1df072529af369589c46f0e01132952aabb250cfd683c274d74dc1eb5d8477
|
||||||
|
sequel (5.107.0) sha256=019629313205295b007f6df2c5eab19ddc40710d549fd445ac300f2853a370db
|
||||||
|
sequel_pg (1.19.0) sha256=cf384d09c6ea2c0ab31d1f95dcf12b0801b34829fe496ac102e43394498a82f3
|
||||||
|
shotgun (0.9.2) sha256=4ef19e63f98c620272666c98fa63e1e733c880b2ac112ac31578918fa2670411
|
||||||
|
sidekiq (7.3.9) sha256=1108712e1def89002b28e3545d5ae15d4a57ffd4d2c25d97bb1360988826b5a7
|
||||||
|
simplecov (1.1.1) sha256=25825ef13f0b2e74694d769817dad6ab8e90131dabdaa666e522fea105521e78
|
||||||
|
simpleidn (0.2.3) sha256=08ce96f03fa1605286be22651ba0fc9c0b2d6272c9b27a260bc88be05b0d2c29
|
||||||
|
sinatra (4.2.1) sha256=b7aeb9b11d046b552972ade834f1f9be98b185fa8444480688e3627625377080
|
||||||
|
sinatra-flash (0.3.0) sha256=fef044da4341840dcddc6f1cc9582c2babc6932aefbf1b00cda66f3bf6f717ee
|
||||||
|
sinatra-xsendfile (0.4.2) sha256=f60849bb60922f27191973b8af61c4ac1908fd5e29707bb4ce68bf6fe9e8f2cb
|
||||||
|
stripe (13.5.1) sha256=748e22cc6d84c944135298b865763524ff1192cee8bc7fd7baa987889a018eb3
|
||||||
|
stripe-ruby-mock (5.0.0) sha256=20566a0ad10ff026c7678c1aaf467c6f55d2f97316424846154e189f6c422316
|
||||||
|
sync (0.5.0) sha256=668356cc07c59ac7ed9ecf34fec3929831f179c07adb1f3e1c3b7a1609a638fd
|
||||||
|
term-ansicolor (1.11.3) sha256=8c01ec3abd095eb94dca8aed5a127d36e594239eb525eb306259c9e930534603
|
||||||
|
terrapin (1.1.1) sha256=9cae91045cffe5764b2c02ed10ddd847c580346ddd193870558e99891fd75329
|
||||||
|
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
|
||||||
|
thread (0.2.2) sha256=0ef704d20878d4dc9090fd2e8e99282fdc7bb047b2ee53fe15916bdfd97776f5
|
||||||
|
tilt (2.8.0) sha256=ba472eb2716fe1e04112d6d219a9dae938ec09a6a1e2ad3ecc7922e79bde3721
|
||||||
|
timecop (0.9.11) sha256=41284dc6e5041f2184f781ace766f942108c842f8d8c1386a26e6343decc7542
|
||||||
|
timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
|
||||||
|
tins (1.56.0) sha256=5ae6cf81cab8fd4b63abd2ade4eaf75543e6973c664276d949ec7027e029b46e
|
||||||
|
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
||||||
|
twilio-ruby (7.10.5) sha256=af0611751e0880be233aff5c4756c8ea2b6c25f5a62f5ed2eea881c88db6b52a
|
||||||
|
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
|
||||||
|
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
|
||||||
|
webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90
|
||||||
|
webp-ffi (0.4.0) sha256=438c32c614b68a7249fc2ee2920c217061c05978dde2cfb9ee2d28af695beb0a
|
||||||
|
webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
|
||||||
|
websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737
|
||||||
|
will_paginate (4.0.1) sha256=107b226ebe1d393d274575956a7c472e1eefdd97d8828e01b72d425d15a875b9
|
||||||
|
xmlrpc (0.3.4) sha256=53a2e08dcff82e58f652c88e1d1ad49467ae341b8a2020339c00315112de5442
|
||||||
|
xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e
|
||||||
|
zip_tricks (5.6.0) sha256=ea644925d97bceee6f86ce0e1ec5a3f0cb84eabf5e56f740ff5d4b518a5aea19
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
4.0.3
|
||||||
23
LICENSE.txt
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
Copyright (c) 2013, Kyle Drake
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY
|
||||||
|
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||||
|
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
DAMAGE.
|
||||||
43
README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# HITHIS IS NEOCITIES FOR FEMSLASH AND YURI
|
||||||
|
|
||||||
|
thansk to bald-cap for initial docker containerization :)
|
||||||
|
|
||||||
|
<a href="https://femslash.city">Come on down to femslash city we got SIGHTS</a>
|
||||||
|
|
||||||
|
### NOTE: THIS IS NOT FOR NEOCITIES SUPPORT! Any issues filed not related to the source code itself will be closed. For support please contact: https://neocities.org/contact
|
||||||
|
|
||||||
|
# Neocities.org
|
||||||
|
|
||||||
|
[](https://github.com/neocities/neocities/actions?query=workflow%3ACI)
|
||||||
|
[](https://coveralls.io/github/neocities/neocities?branch=master)
|
||||||
|
|
||||||
|
The web site for Neocities! It's open source. Want a feature on the site? Send a pull request!
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Neocities can be quickly launched in development mode with [Vagrant](https://www.vagrantup.com). Vagrant builds a virtual machine that automatically installs everything you need to run Neocities as a developer. Install Vagrant, then from the command line:
|
||||||
|
|
||||||
|
```
|
||||||
|
vagrant up --provision
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Make a copy of `config.yml.template` in the root directory, and rename it to `config.yml`. Then:
|
||||||
|
|
||||||
|
```
|
||||||
|
vagrant ssh
|
||||||
|
bundle exec rackup -o 0.0.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can access the running site from your browser: http://127.0.0.1:9292
|
||||||
|
|
||||||
|
## Want to contribute?
|
||||||
|
|
||||||
|
If you'd like to fix a bug, or make an improvement, or add a new feature, it's easy! Just send us a Pull Request.
|
||||||
|
|
||||||
|
1. Fork it (https://github.com/neocities/neocities/fork)
|
||||||
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||||
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||||
|
4. Push to the branch (`git push origin my-new-feature`)
|
||||||
|
5. Create new Pull Request
|
||||||
263
Rakefile
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
require 'rake/testtask'
|
||||||
|
|
||||||
|
task :environment do
|
||||||
|
require './environment.rb'
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Run all tests"
|
||||||
|
Rake::TestTask.new do |t|
|
||||||
|
t.libs << "spec"
|
||||||
|
t.test_files = FileList['tests/**/*_tests.rb']
|
||||||
|
t.verbose = false
|
||||||
|
t.warning = false
|
||||||
|
end
|
||||||
|
|
||||||
|
task :default => :test
|
||||||
|
|
||||||
|
desc "prune logs"
|
||||||
|
task :prune_logs => [:environment] do
|
||||||
|
Stat.prune!
|
||||||
|
StatLocation.prune!
|
||||||
|
StatReferrer.prune!
|
||||||
|
StatPath.prune!
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "parse logs"
|
||||||
|
task :parse_logs => [:environment] do
|
||||||
|
Stat.parse_logfiles $config['logs_path']
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Update disposable email blacklist'
|
||||||
|
task :update_disposable_email_blacklist => [:environment] do
|
||||||
|
# Formerly: https://raw.githubusercontent.com/martenson/disposable-email-domains/master/disposable_email_blocklist.conf
|
||||||
|
uri = URI.parse('https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt')
|
||||||
|
File.write(Site::DISPOSABLE_EMAIL_BLACKLIST_PATH, HTTP.get(uri))
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Update banned IPs list'
|
||||||
|
task :update_blocked_ips => [:environment] do
|
||||||
|
|
||||||
|
filename = 'listed_ip_365_ipv46'
|
||||||
|
zip_path = "/tmp/#{filename}.zip"
|
||||||
|
|
||||||
|
File.open(zip_path, 'wb') do |file|
|
||||||
|
response = HTTP.get "https://www.stopforumspam.com/downloads/#{filename}.zip"
|
||||||
|
response.body.each do |chunk|
|
||||||
|
file.write chunk
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Zip::File.open(zip_path) do |zip_file|
|
||||||
|
zip_file.each do |entry|
|
||||||
|
if entry.name == "#{filename}.txt"
|
||||||
|
ips = entry.get_input_stream.read
|
||||||
|
insert_hashes = []
|
||||||
|
ips.each_line { |ip| insert_hashes << { ip: ip.strip, created_at: Time.now } }
|
||||||
|
ips = nil
|
||||||
|
|
||||||
|
# Database transaction
|
||||||
|
DB.transaction do
|
||||||
|
DB[:blocked_ips].delete
|
||||||
|
DB[:blocked_ips].multi_insert insert_hashes
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
FileUtils.rm zip_path
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'rebuild_thumbnails'
|
||||||
|
task :rebuild_thumbnails => [:environment] do
|
||||||
|
dirs = Dir[Site::SITE_FILES_ROOT+'/**/*'].collect {|s| s.sub(Site::SITE_FILES_ROOT, '')}.collect {|s| s.sub('/', '')}
|
||||||
|
dirs.each do |d|
|
||||||
|
next if File.directory?(d)
|
||||||
|
|
||||||
|
full_path = d.split('/')
|
||||||
|
|
||||||
|
username = full_path.first
|
||||||
|
path = '/'+full_path[1..full_path.length].join('/')
|
||||||
|
|
||||||
|
if Pathname(path).extname.gsub('.', '').match Site::IMAGE_REGEX
|
||||||
|
begin
|
||||||
|
ThumbnailWorker.new.perform username, path
|
||||||
|
rescue Magick::ImageMagickError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'compute_scores'
|
||||||
|
task :compute_scores => [:environment] do
|
||||||
|
Site.compute_scores
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'renew_ssl_certs'
|
||||||
|
task :renew_ssl_certs => [:environment] do
|
||||||
|
delay = 0
|
||||||
|
DB[%{select id from sites where (domain is not null or domain != '') and is_banned != 't' and is_deleted != 't' and (cert_updated_at is null or cert_updated_at < ?)}, 60.days.ago].all.each do |site|
|
||||||
|
LetsEncryptWorker.perform_in delay.seconds, site[:id]
|
||||||
|
delay += 10
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'purge_tmp_turds'
|
||||||
|
task :purge_tmp_turds => [:environment] do
|
||||||
|
['neocities_screenshot*', 'RackMultipart*', 'neocities_saving_file*', 'newinstall-*', '*.dmp', 'davfile*', 'magick*', '*.scan', '*.jpg'].each do |target|
|
||||||
|
Dir.glob("/tmp/#{target}").select {|filename| File::Stat.new(filename).ctime < (Time.now - 3600)}.each {|filename| FileUtils.rm(filename)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'compute_follow_count_scores'
|
||||||
|
task :compute_follow_count_scores => [:environment] do
|
||||||
|
|
||||||
|
Site.select(:id,:username,:follow_count).all.each do |site|
|
||||||
|
count = site.scorable_follow_count
|
||||||
|
|
||||||
|
if count != 0
|
||||||
|
puts "#{site.username} #{site.follow_count} => #{count}"
|
||||||
|
end
|
||||||
|
DB['update sites set follow_count=? where id=?', count, site.id].first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'ml_screenshots_list_dump'
|
||||||
|
task :ml_screenshots_list_dump => [:environment] do
|
||||||
|
['phishing', 'spam', 'ham', nil].each do |classifier|
|
||||||
|
File.open("./files/screenshot-urls#{classifier.nil? ? '' : '-'+classifier.to_s}.txt", 'w') do |fp|
|
||||||
|
SiteFile.where(classifier: classifier).where(path: 'index.html').each do |site_file|
|
||||||
|
begin
|
||||||
|
fp.write "#{site_file.site.screenshot_url('index.html', Site::SCREENSHOT_RESOLUTIONS.first)}\n"
|
||||||
|
rescue NoMethodError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'generate_sitemap'
|
||||||
|
task :generate_sitemap => [:environment] do
|
||||||
|
=begin
|
||||||
|
sorted_sites = {}
|
||||||
|
|
||||||
|
# We pop off array, so highest scores go last.
|
||||||
|
sites = Site.
|
||||||
|
select(:id, :username, :updated_at, :profile_enabled).
|
||||||
|
where(site_changed: true).
|
||||||
|
exclude(updated_at: nil).
|
||||||
|
exclude(is_deleted: true).
|
||||||
|
order(:score).
|
||||||
|
all
|
||||||
|
|
||||||
|
site_files = []
|
||||||
|
|
||||||
|
sites.each do |site|
|
||||||
|
site.site_files_dataset.exclude(path: 'not_found.html').where(path: /\.html?$/).all.each do |site_file|
|
||||||
|
|
||||||
|
if site.uri(site_file.path) == site.uri
|
||||||
|
priority = 0.5
|
||||||
|
else
|
||||||
|
priority = 0.4
|
||||||
|
end
|
||||||
|
|
||||||
|
site_files << [site.uri(site_file.path), site_file.updated_at.utc.iso8601, priority]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sites = nil
|
||||||
|
GC.start
|
||||||
|
=end
|
||||||
|
|
||||||
|
sitemap_root = File.join Site::PUBLIC_ROOT, 'sitemap'
|
||||||
|
FileUtils.mkdir_p sitemap_root
|
||||||
|
|
||||||
|
=begin
|
||||||
|
index = 0
|
||||||
|
until site_files.empty?
|
||||||
|
sfs = site_files.pop 50000
|
||||||
|
|
||||||
|
file = File.open File.join(sitemap_root, "sites-#{index}.xml.gz"), 'w'
|
||||||
|
|
||||||
|
Zlib::GzipWriter.open File.join(sitemap_root, "sites-#{index}.xml.gz") do |gz|
|
||||||
|
gz.write %{<?xml version="1.0" encoding="UTF-8"?>\n}
|
||||||
|
gz.write %{<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n}
|
||||||
|
|
||||||
|
sfs.each do |sf|
|
||||||
|
gz.write %{<url><loc>#{sf[0].encode(xml: :text)}</loc><lastmod>#{sf[1].encode(xml: :text)}</lastmod><priority>#{sf[2].to_s.encode(xml: :text)}</priority></url>\n}
|
||||||
|
end
|
||||||
|
|
||||||
|
gz.write %{</urlset>}
|
||||||
|
end
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
end
|
||||||
|
=end
|
||||||
|
|
||||||
|
# Set basic neocities.org root paths
|
||||||
|
builder = Nokogiri::XML::Builder.new { |xml|
|
||||||
|
xml.urlset(xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9') {
|
||||||
|
File.read(File.join(DIR_ROOT, 'files', 'root_site_uris.txt')).each_line { |uri|
|
||||||
|
priority, changefreq, uri = uri.strip.split(',')
|
||||||
|
xml.url {
|
||||||
|
xml.loc uri
|
||||||
|
xml.changefreq changefreq
|
||||||
|
xml.priority priority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Zlib::GzipWriter.open File.join(sitemap_root, 'root.xml.gz') do |gz|
|
||||||
|
gz.write builder.to_xml(encoding: 'UTF-8')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tagged sites sitemap
|
||||||
|
builder = Nokogiri::XML::Builder.new { |xml|
|
||||||
|
xml.urlset(xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9') {
|
||||||
|
Tag.popular_names(Site.count).each { |tag|
|
||||||
|
xml.url {
|
||||||
|
xml.loc "https://neocities.org/browse?sort_by=views&tag=#{tag[:name]}"
|
||||||
|
xml.changefreq 'daily'
|
||||||
|
xml.lastmod Time.now.utc.iso8601
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Zlib::GzipWriter.open File.join(sitemap_root, 'tags.xml.gz') do |gz|
|
||||||
|
gz.write builder.to_xml(encoding: 'UTF-8')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Final index.xml.gz entrypoint
|
||||||
|
Zlib::GzipWriter.open File.join(sitemap_root, 'index.xml.gz') do |gz|
|
||||||
|
gz.write %{<?xml version="1.0" encoding="UTF-8"?>\n}
|
||||||
|
gz.write %{<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n}
|
||||||
|
gz.write %{<sitemap><loc>https://neocities.org/sitemap/root.xml.gz</loc><lastmod>#{Time.now.utc.iso8601}</lastmod></sitemap>\n}
|
||||||
|
gz.write %{<sitemap><loc>https://neocities.org/sitemap/tags.xml.gz</loc><lastmod>#{Time.now.utc.iso8601}</lastmod></sitemap>\n}
|
||||||
|
|
||||||
|
=begin
|
||||||
|
0.upto(index-1).each do |i|
|
||||||
|
gz.write %{<sitemap><loc>https://neocities.org/sitemap/sites-#{i}.xml.gz</loc><lastmod>#{Time.now.utc.iso8601}</lastmod></sitemap>\n}
|
||||||
|
end
|
||||||
|
=end
|
||||||
|
gz.write %{</sitemapindex>}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'dedupe tags'
|
||||||
|
task :dedupetags => [:environment] do
|
||||||
|
Tag.all.each do |tag|
|
||||||
|
begin
|
||||||
|
tag.reload
|
||||||
|
rescue Sequel::Error => e
|
||||||
|
next if e.message =~ /Record not found/
|
||||||
|
end
|
||||||
|
|
||||||
|
matching_tags = Tag.exclude(id: tag.id).where(name: tag.name).all
|
||||||
|
|
||||||
|
matching_tags.each do |matching_tag|
|
||||||
|
DB[:sites_tags].where(tag_id: matching_tag.id).update(tag_id: tag.id)
|
||||||
|
matching_tag.delete
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
68
SETUP.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Neocities Local Development Setup
|
||||||
|
|
||||||
|
## On Windows (host machine)
|
||||||
|
|
||||||
|
1. Install Ruby
|
||||||
|
```
|
||||||
|
winget install RubyInstallerTeam.Ruby.4.0
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add Ruby to your PATH (run in PowerShell)
|
||||||
|
```powershell
|
||||||
|
$rubyPath = "C:\Ruby40-x64\bin"
|
||||||
|
$oldPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||||
|
if ($oldPath -notlike "*$rubyPath*") {
|
||||||
|
[Environment]::SetEnvironmentVariable("Path", $oldPath + ";$rubyPath", "User")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Then restart your terminal for the change to take effect.
|
||||||
|
|
||||||
|
3. Clone the repository
|
||||||
|
```
|
||||||
|
git clone https://github.com/bald-cap/neocities.git
|
||||||
|
cd neocities
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Fix line endings on Vagrant scripts
|
||||||
|
```
|
||||||
|
dos2unix vagrant/common.sh vagrant/database.sh vagrant/development.sh vagrant/fs-primary.sh vagrant/redis.sh vagrant/ruby.sh vagrant/webapp.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Install Vagrant
|
||||||
|
```
|
||||||
|
winget install --id Hashicorp.Vagrant
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Copy config file
|
||||||
|
```
|
||||||
|
cp config.yml.template config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Start and provision the VM
|
||||||
|
```
|
||||||
|
vagrant up --provision
|
||||||
|
```
|
||||||
|
|
||||||
|
7. SSH into the VM
|
||||||
|
```
|
||||||
|
vagrant ssh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inside the VM
|
||||||
|
|
||||||
|
8. Add the rackup gem
|
||||||
|
```
|
||||||
|
sudo bundle add rackup
|
||||||
|
```
|
||||||
|
|
||||||
|
9. Install gems
|
||||||
|
```
|
||||||
|
sudo bundle install
|
||||||
|
```
|
||||||
|
|
||||||
|
10. Start the server
|
||||||
|
```
|
||||||
|
bundle exec rackup -o 0.0.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The site will be available at http://127.0.0.1:9292
|
||||||
12
Vagrantfile
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
VAGRANTFILE_API_VERSION = '2'
|
||||||
|
|
||||||
|
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||||
|
config.vm.box = 'ubuntu/jammy64'
|
||||||
|
config.vm.provision :shell, path: './vagrant/development.sh'
|
||||||
|
config.vm.network :forwarded_port, guest: 9292, host: 9292
|
||||||
|
|
||||||
|
config.vm.provider :virtualbox do |vb|
|
||||||
|
vb.customize ['modifyvm', :id, '--memory', '8192']
|
||||||
|
vb.name = 'neocities'
|
||||||
|
end
|
||||||
|
end
|
||||||
151
app.rb
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
require './environment.rb'
|
||||||
|
require './app_helpers.rb'
|
||||||
|
|
||||||
|
use Rack::Session::Cookie, key: 'neocities',
|
||||||
|
path: '/',
|
||||||
|
expire_after: 31556926, # one year in seconds
|
||||||
|
secret: Base64.strict_decode64($config['session_secret']),
|
||||||
|
httponly: true,
|
||||||
|
same_site: :lax,
|
||||||
|
secure: ENV['RACK_ENV'] == 'production'
|
||||||
|
|
||||||
|
use Rack::TempfileReaper
|
||||||
|
|
||||||
|
helpers do
|
||||||
|
def site_change_file_display_class(filename)
|
||||||
|
return 'html' if filename.match(Site::HTML_REGEX)
|
||||||
|
return 'image' if filename.match(Site::IMAGE_REGEX)
|
||||||
|
'misc'
|
||||||
|
end
|
||||||
|
|
||||||
|
def site_change_file_display_name(filename)
|
||||||
|
path = filename.to_s.sub(%r{\A/+}, '')
|
||||||
|
path = path.sub(%r{/index\.html?\z}, '')
|
||||||
|
path = path.sub(/\.html?\z/, '')
|
||||||
|
path.empty? ? 'index' : path
|
||||||
|
end
|
||||||
|
|
||||||
|
def csrf_token_input_html
|
||||||
|
%{<input name="csrf_token" type="hidden" value="#{csrf_token}">}
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_admin
|
||||||
|
redirect '/' unless signed_in? && current_site.is_admin
|
||||||
|
end
|
||||||
|
|
||||||
|
def hcaptcha_input
|
||||||
|
return '' if ENV['RACK_ENV'] == 'test'
|
||||||
|
%{
|
||||||
|
<script src="https://hcaptcha.com/1/api.js" async defer></script>
|
||||||
|
<div id="captcha_input" class="h-captcha" data-sitekey="#{$config['hcaptcha_site_key']}"></div>
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
set :protection, :frame_options => "DENY"
|
||||||
|
|
||||||
|
GEOCITIES_NEIGHBORHOODS = %w{
|
||||||
|
area51
|
||||||
|
athens
|
||||||
|
augusta
|
||||||
|
baja
|
||||||
|
bourbonstreet
|
||||||
|
capecanaveral
|
||||||
|
capitolhill
|
||||||
|
collegepark
|
||||||
|
colosseum
|
||||||
|
enchantedforest
|
||||||
|
hollywood
|
||||||
|
motorcity
|
||||||
|
napavalley
|
||||||
|
nashville
|
||||||
|
petsburgh
|
||||||
|
pipeline
|
||||||
|
rainforest
|
||||||
|
researchtriangle
|
||||||
|
siliconvalley
|
||||||
|
soho
|
||||||
|
sunsetstrip
|
||||||
|
timessquare
|
||||||
|
televisioncity
|
||||||
|
tokyo
|
||||||
|
vienna
|
||||||
|
westhollywood
|
||||||
|
yosemite
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def redirect_to_internet_archive_for_geocities_sites
|
||||||
|
match = request.path.match /^\/(\w+)\/.+$/i
|
||||||
|
if match && GEOCITIES_NEIGHBORHOODS.include?(match.captures.first.downcase)
|
||||||
|
redirect "https://wayback.archive.org/http://geocities.com/#{request.path}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
WHITELISTED_POST_PATHS = ['/create_validate_all', '/create_validate', '/create', '/search', '/search/'].freeze
|
||||||
|
|
||||||
|
before do
|
||||||
|
if request.path.match /^\/api\//i
|
||||||
|
@api = true
|
||||||
|
content_type :json
|
||||||
|
elsif request.path.match /^\/webhooks\//
|
||||||
|
# Skips the CSRF/validation check for stripe web hooks
|
||||||
|
elsif request.path.match /^\/admin/
|
||||||
|
require_admin
|
||||||
|
redirect '/' if request.post? && !WHITELISTED_POST_PATHS.include?(request.path_info) && !csrf_safe?
|
||||||
|
elsif current_site && current_site.email_not_validated? && !(request.path =~ /^\/site\/.+\/confirm_email|^\/settings\/change_email|^\/welcome|^\/supporter|^\/signout|^\/contact|^\/aup|^\/terms/)
|
||||||
|
redirect "/site/#{current_site.username}/confirm_email"
|
||||||
|
elsif current_site && current_site.phone_verification_needed? && !(request.path =~ /^\/site\/.+\/confirm_email|^\/settings\/change_email|^\/site\/.+\/confirm_phone|^\/welcome|^\/supporter|^\/signout|^\/aup|^\/terms/)
|
||||||
|
redirect "/site/#{current_site.username}/confirm_phone"
|
||||||
|
elsif current_site && current_site.tutorial_required && !(request.path =~ /^\/site\/.+\/confirm_email|^\/settings\/change_email|^\/site\/.+\/confirm_phone|^\/welcome|^\/supporter|^\/tutorial\/.+|^\/aup|^\/terms/)
|
||||||
|
redirect '/tutorial/html/1'
|
||||||
|
else
|
||||||
|
content_type :html, 'charset' => 'utf-8'
|
||||||
|
redirect '/' if request.post? && !WHITELISTED_POST_PATHS.include?(request.path_info) && !csrf_safe?
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:page]
|
||||||
|
params[:page] = params[:page].to_s
|
||||||
|
unless params[:page] =~ /^\d+$/ && params[:page].to_i > 0
|
||||||
|
params[:page] = '1'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:tag]
|
||||||
|
begin
|
||||||
|
params.delete 'tag' if params[:tag].nil? || !params[:tag].is_a?(String) || params[:tag].strip.empty? || params[:tag].match?(Tag::INVALID_TAG_REGEX)
|
||||||
|
rescue Encoding::CompatibilityError, ArgumentError
|
||||||
|
params.delete 'tag'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
if @api || (!signed_in? && request.path == '/')
|
||||||
|
request.session_options[:skip] = true
|
||||||
|
else
|
||||||
|
# Set issue timestamp on session cookie if it doesn't exist yet
|
||||||
|
session['i'] = Time.now.to_i if session && !session['i'] && session['id']
|
||||||
|
end
|
||||||
|
|
||||||
|
unless self.class.development?
|
||||||
|
response.headers['Content-Security-Policy'] = %{default-src 'self' data: blob: 'unsafe-inline'; script-src 'self' blob: 'unsafe-inline' 'unsafe-eval' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com; style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; connect-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://api.stripe.com; frame-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
not_found do
|
||||||
|
api_not_found if @api
|
||||||
|
redirect_to_internet_archive_for_geocities_sites
|
||||||
|
@title = 'Not Found'
|
||||||
|
erb :'not_found'
|
||||||
|
end
|
||||||
|
|
||||||
|
error do
|
||||||
|
if @api
|
||||||
|
api_error 500, 'server_error', 'there has been an unknown server error, please try again later'
|
||||||
|
end
|
||||||
|
|
||||||
|
erb :'error'
|
||||||
|
end
|
||||||
|
|
||||||
|
Dir['./app/**/*.rb'].each {|f| require f}
|
||||||
102
app/activity.rb
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
get '/activity' do
|
||||||
|
@page = params[:page].to_i
|
||||||
|
@page = 1 if @page < 1
|
||||||
|
@description = 'See recent activity from Neocities websites.'
|
||||||
|
|
||||||
|
halt 404 if params[:event_id]
|
||||||
|
|
||||||
|
tag = nil
|
||||||
|
if params[:tag]
|
||||||
|
begin
|
||||||
|
tag = Tag.clean_name(params[:tag].to_s)
|
||||||
|
if tag.empty? || tag.length > Tag::NAME_LENGTH_MAX || tag.match?(Tag::INVALID_TAG_REGEX) || tag.match?(/\s/)
|
||||||
|
params.delete 'tag'
|
||||||
|
tag = nil
|
||||||
|
else
|
||||||
|
params[:tag] = tag
|
||||||
|
end
|
||||||
|
rescue Encoding::CompatibilityError, ArgumentError
|
||||||
|
params.delete 'tag'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tag_record = nil
|
||||||
|
if params[:tag]
|
||||||
|
tag_record = Tag.select(:id).where(name: params[:tag]).first
|
||||||
|
@description = "See recent activity from Neocities websites tagged #{tag}."
|
||||||
|
|
||||||
|
if tag_record
|
||||||
|
query1 = Event
|
||||||
|
.join(:sites, id: :site_id)
|
||||||
|
.join(:sites_tags, site_id: :id)
|
||||||
|
.where(sites_tags__tag_id: tag_record.id)
|
||||||
|
.where(events__is_deleted: false, sites__is_deleted: false)
|
||||||
|
.where{sites__score > Event::ACTIVITY_TAG_SCORE_LIMIT}
|
||||||
|
.where(sites__is_nsfw: false)
|
||||||
|
.where(follow_id: nil)
|
||||||
|
.select_all(:events)
|
||||||
|
|
||||||
|
query2 = Event
|
||||||
|
.join(:sites, id: :actioning_site_id)
|
||||||
|
.join(:sites_tags, site_id: :id)
|
||||||
|
.where(sites_tags__tag_id: tag_record.id)
|
||||||
|
.where(events__is_deleted: false, sites__is_deleted: false)
|
||||||
|
.where{sites__score > Event::ACTIVITY_TAG_SCORE_LIMIT}
|
||||||
|
.where(sites__is_nsfw: false)
|
||||||
|
.where(follow_id: nil)
|
||||||
|
.select_all(:events)
|
||||||
|
|
||||||
|
if current_site
|
||||||
|
blocking_site_ids = current_site.blocking_site_ids
|
||||||
|
query1 = query1.where(Sequel.|({events__site_id: nil}, ~{events__site_id: blocking_site_ids})).where(Sequel.|({events__actioning_site_id: nil}, ~{events__actioning_site_id: blocking_site_ids}))
|
||||||
|
query2 = query2.where(Sequel.|({events__site_id: nil}, ~{events__site_id: blocking_site_ids})).where(Sequel.|({events__actioning_site_id: nil}, ~{events__actioning_site_id: blocking_site_ids}))
|
||||||
|
end
|
||||||
|
|
||||||
|
ds = query1.union(query2, all: false).order(Sequel.desc(:created_at))
|
||||||
|
else
|
||||||
|
ds = Event.where(id: nil).order(Sequel.desc(:created_at))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
ds = Event.news_feed_default_dataset.exclude(sites__is_nsfw: true)
|
||||||
|
|
||||||
|
if current_site
|
||||||
|
blocking_site_ids = current_site.blocking_site_ids
|
||||||
|
ds = ds.where(Sequel.|({events__site_id: nil}, ~{events__site_id: blocking_site_ids})).where(Sequel.|({events__actioning_site_id: nil}, ~{events__actioning_site_id: blocking_site_ids}))
|
||||||
|
end
|
||||||
|
|
||||||
|
ds = ds.where(
|
||||||
|
Sequel.expr(Sequel[:sites][:score] > Event::GLOBAL_SCORE_LIMIT) |
|
||||||
|
Sequel.expr(Sequel[:actioning_sites][:score] > Event::GLOBAL_SCORE_LIMIT)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
record_count = nil
|
||||||
|
if current_site.nil?
|
||||||
|
if params[:tag] && tag_record.nil?
|
||||||
|
record_count = 0
|
||||||
|
else
|
||||||
|
cache_key = if params[:tag]
|
||||||
|
['activity_public_tag_record_count', params[:tag]]
|
||||||
|
else
|
||||||
|
:activity_public_record_count
|
||||||
|
end
|
||||||
|
|
||||||
|
if SimpleCache.expired?(cache_key)
|
||||||
|
record_count = SimpleCache.store(cache_key, ds.count, 10.minutes)
|
||||||
|
else
|
||||||
|
record_count = SimpleCache.get(cache_key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
max_record_count = Event::GLOBAL_PAGINATION_LENGTH * 100
|
||||||
|
record_count = [record_count, max_record_count].min
|
||||||
|
max_page = (record_count / Event::GLOBAL_PAGINATION_LENGTH.to_f).ceil
|
||||||
|
max_page = 1 if max_page < 1
|
||||||
|
halt 404 if @page > max_page
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagination_dataset = ds.paginate(@page, Event::GLOBAL_PAGINATION_LENGTH, record_count)
|
||||||
|
@events = @pagination_dataset.all
|
||||||
|
|
||||||
|
erb :'activity'
|
||||||
|
end
|
||||||
496
app/admin.rb
Normal file
|
|
@ -0,0 +1,496 @@
|
||||||
|
get '/admin' do
|
||||||
|
require_admin
|
||||||
|
@banned_sites = Site.select(:username).filter(is_banned: true).order(:username).all
|
||||||
|
@nsfw_sites = Site.select(:username).filter(is_nsfw: true).order(:username).all
|
||||||
|
erb :'admin'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/reports' do
|
||||||
|
require_admin
|
||||||
|
@page = params[:page] ? params[:page].to_i : 1
|
||||||
|
@page = 1 if @page < 1
|
||||||
|
@per_page = 51
|
||||||
|
|
||||||
|
@moderation_blur_categories = $config['moderation_blur_categories']
|
||||||
|
|
||||||
|
@reports = Report.join(:sites, id: :site_id).where(sites__is_deleted: false, sites__is_banned: false).order(:reports__created_at.desc).select_all(:reports).paginate(@page, @per_page)
|
||||||
|
@pagination_dataset = @reports
|
||||||
|
|
||||||
|
erb :'admin/reports'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/reports/:report_id/dismiss' do
|
||||||
|
require_admin
|
||||||
|
content_type :json
|
||||||
|
|
||||||
|
report = Report[params[:report_id]]
|
||||||
|
return {success: false, error: 'Report not found'}.to_json if report.nil?
|
||||||
|
|
||||||
|
report.destroy
|
||||||
|
|
||||||
|
# Dismiss any other reports for the site
|
||||||
|
DB[:reports].where(site_id: report.site_id).delete
|
||||||
|
|
||||||
|
{success: true, message: 'Report dismissed'}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/site_files/train' do
|
||||||
|
require_admin
|
||||||
|
site = Site[params[:site_id]]
|
||||||
|
site_file = site.site_files_dataset.where(path: params[:path]).first
|
||||||
|
not_found if site_file.nil?
|
||||||
|
site.untrain site_file.path
|
||||||
|
site.train site_file.path, params[:classifier]
|
||||||
|
'ok'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/usage' do
|
||||||
|
require_admin
|
||||||
|
today = Date.today
|
||||||
|
current_month = Date.new today.year, today.month, 1
|
||||||
|
start_month = Date.new(2016, 2, 1)
|
||||||
|
|
||||||
|
# Build date range for batch query
|
||||||
|
months = []
|
||||||
|
month = current_month
|
||||||
|
until month < start_month
|
||||||
|
months << month
|
||||||
|
month = month.prev_month
|
||||||
|
end
|
||||||
|
|
||||||
|
# Batch query for all monthly stats at once using proper date comparisons
|
||||||
|
monthly_stats_query = <<-SQL
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('month', created_at) as month,
|
||||||
|
SUM(views) as views,
|
||||||
|
SUM(hits) as hits,
|
||||||
|
SUM(bandwidth) as bandwidth
|
||||||
|
FROM daily_site_stats
|
||||||
|
WHERE created_at >= ?
|
||||||
|
AND created_at <= ?
|
||||||
|
GROUP BY DATE_TRUNC('month', created_at)
|
||||||
|
ORDER BY month DESC
|
||||||
|
SQL
|
||||||
|
|
||||||
|
monthly_data = DB[monthly_stats_query, start_month, current_month.next_month].all
|
||||||
|
|
||||||
|
# Convert to hash for easy lookup
|
||||||
|
stats_by_month = {}
|
||||||
|
monthly_data.each do |row|
|
||||||
|
month_key = row[:month].to_date
|
||||||
|
stats_by_month[month_key] = {
|
||||||
|
views: row[:views] || 0,
|
||||||
|
hits: row[:hits] || 0,
|
||||||
|
bandwidth: row[:bandwidth] || 0
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Batch query for all popular sites data at once
|
||||||
|
popular_sites_query = <<-SQL
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('month', stats.created_at) as month,
|
||||||
|
sites.username,
|
||||||
|
SUM(stats.bandwidth) as bandwidth
|
||||||
|
FROM stats
|
||||||
|
LEFT JOIN sites ON sites.id = stats.site_id
|
||||||
|
WHERE stats.created_at >= ?
|
||||||
|
AND stats.created_at <= ?
|
||||||
|
AND sites.username IS NOT NULL
|
||||||
|
GROUP BY DATE_TRUNC('month', stats.created_at), sites.username
|
||||||
|
SQL
|
||||||
|
|
||||||
|
all_popular_sites = DB[popular_sites_query, start_month, current_month.next_month].all
|
||||||
|
|
||||||
|
# Group popular sites by month and get top 50 for each
|
||||||
|
popular_by_month = {}
|
||||||
|
all_popular_sites.group_by { |row| row[:month].to_date }.each do |month_key, sites|
|
||||||
|
popular_by_month[month_key] = sites
|
||||||
|
.sort_by { |s| -(s[:bandwidth] || 0) }
|
||||||
|
.take(50)
|
||||||
|
.map { |s| { username: s[:username], bandwidth: s[:bandwidth] } }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Combine the results
|
||||||
|
@monthly_stats = []
|
||||||
|
months.each do |month|
|
||||||
|
stats = stats_by_month[month]
|
||||||
|
next unless stats && stats[:views] != 0 && stats[:hits] != 0 && stats[:bandwidth] != 0
|
||||||
|
|
||||||
|
@monthly_stats.push({
|
||||||
|
views: stats[:views],
|
||||||
|
hits: stats[:hits],
|
||||||
|
bandwidth: stats[:bandwidth],
|
||||||
|
date: month,
|
||||||
|
popular_sites: popular_by_month[month] || []
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
erb :'admin/usage'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/email' do
|
||||||
|
require_admin
|
||||||
|
erb :'admin/email'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/stats' do
|
||||||
|
require_admin
|
||||||
|
@stats = {
|
||||||
|
total_hosted_site_hits: DB['SELECT SUM(hits) FROM sites'].first[:sum],
|
||||||
|
total_hosted_site_views: DB['SELECT SUM(views) FROM sites'].first[:sum],
|
||||||
|
total_site_changes: DB['select max(changed_count) from sites'].first[:max],
|
||||||
|
total_sites: Site.count
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start with the date of the first created site
|
||||||
|
start = Site.select(:created_at).
|
||||||
|
exclude(created_at: nil).
|
||||||
|
order(:created_at).
|
||||||
|
first[:created_at].to_date
|
||||||
|
|
||||||
|
runner = start
|
||||||
|
monthly_stats = []
|
||||||
|
now = Date.today
|
||||||
|
|
||||||
|
while Date.new(runner.year, runner.month, 1) <= Date.new(now.year, now.month, 1)
|
||||||
|
# Get sites created in this time range
|
||||||
|
sites_in_range = Site.where(created_at: runner..runner.next_month)
|
||||||
|
sites_from_start = Site.where(created_at: start..runner.next_month)
|
||||||
|
|
||||||
|
supporters_count = Site.where(created_at: start..runner.next_month, parent_site_id: nil)
|
||||||
|
.where(
|
||||||
|
Sequel.|(
|
||||||
|
~{stripe_customer_id: nil} & ~{stripe_subscription_id: nil} & {plan_ended: [false, nil]} & ~{plan_type: ['free', 'special']},
|
||||||
|
{paypal_active: true}
|
||||||
|
)
|
||||||
|
).count
|
||||||
|
|
||||||
|
monthly_stats.push(
|
||||||
|
date: runner,
|
||||||
|
sites_created: sites_in_range.count,
|
||||||
|
total_from_start: sites_from_start.count,
|
||||||
|
supporters: supporters_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
runner = runner.next_month
|
||||||
|
end
|
||||||
|
|
||||||
|
@stats[:monthly_stats] = monthly_stats
|
||||||
|
|
||||||
|
@stats[:current_supporters] = Site.where(parent_site_id: nil)
|
||||||
|
.where(
|
||||||
|
Sequel.|(
|
||||||
|
~{stripe_customer_id: nil} & ~{stripe_subscription_id: nil} & {plan_ended: [false, nil]} & ~{plan_type: ['free', 'special']},
|
||||||
|
{paypal_active: true}
|
||||||
|
)
|
||||||
|
).count
|
||||||
|
@stats[:supporter_percentage] = (@stats[:current_supporters].to_f / @stats[:total_sites] * 100).round(2)
|
||||||
|
|
||||||
|
erb :'admin/stats'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/ban_history' do
|
||||||
|
require_admin
|
||||||
|
|
||||||
|
@title = 'Ban History'
|
||||||
|
@page = params[:page] ? params[:page].to_i : 1
|
||||||
|
@page = 1 if @page < 1
|
||||||
|
@per_page = 51
|
||||||
|
|
||||||
|
@sites = Site.where(is_banned: true).order(:banned_at.desc).exclude(banned_at: nil). paginate(@page, @per_page)
|
||||||
|
@pagination_dataset = @sites
|
||||||
|
|
||||||
|
erb :'admin/ban_history'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/email' do
|
||||||
|
require_admin
|
||||||
|
%i{subject body}.each do |k|
|
||||||
|
if params[k].nil? || params[k].empty?
|
||||||
|
flash[:error] = "#{k.capitalize} is missing."
|
||||||
|
redirect '/admin/email'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sites = Site.newsletter_sites
|
||||||
|
|
||||||
|
day = 0
|
||||||
|
|
||||||
|
until sites.empty?
|
||||||
|
seconds = 0.0
|
||||||
|
queued_sites = []
|
||||||
|
Site::EMAIL_BLAST_MAXIMUM_PER_DAY.times {
|
||||||
|
break if sites.empty?
|
||||||
|
queued_sites << sites.pop
|
||||||
|
}
|
||||||
|
|
||||||
|
queued_sites.each do |site|
|
||||||
|
EmailWorker.perform_at((day.days.from_now + seconds), {
|
||||||
|
from: Site::FROM_EMAIL,
|
||||||
|
to: site.email,
|
||||||
|
subject: params[:subject],
|
||||||
|
body: params[:body]
|
||||||
|
})
|
||||||
|
seconds += 0.5
|
||||||
|
end
|
||||||
|
|
||||||
|
day += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[:success] = "#{sites.length} emails have been queued, #{Site::EMAIL_BLAST_MAXIMUM_PER_DAY} per day."
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/ban' do
|
||||||
|
require_admin
|
||||||
|
if params[:usernames].empty?
|
||||||
|
flash[:error] = 'no usernames provided'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
usernames = params[:usernames].split("\n").collect {|u| u.strip}
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
ip_deleted_count = 0
|
||||||
|
|
||||||
|
usernames.each do |username|
|
||||||
|
next if username == ''
|
||||||
|
site = Site[username: username]
|
||||||
|
next if site.nil? || site.is_banned
|
||||||
|
|
||||||
|
if !params[:classifier].empty?
|
||||||
|
site.untrain 'index.html'
|
||||||
|
site.train 'index.html', params[:classifier]
|
||||||
|
end
|
||||||
|
|
||||||
|
site.ban!
|
||||||
|
deleted_count += 1
|
||||||
|
|
||||||
|
if !params[:ban_using_ips].empty? && IPAddress.valid?(site.ip)
|
||||||
|
sites = Site.filter(ip: site.ip, is_banned: false).all
|
||||||
|
sites.each do |s|
|
||||||
|
next if usernames.include?(s.username)
|
||||||
|
s.ban!
|
||||||
|
end
|
||||||
|
ip_deleted_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:classifier] == 'spam' || params[:classifier] == 'phishing'
|
||||||
|
next unless IPAddress.valid?(site.ip)
|
||||||
|
StopForumSpamWorker.perform_async(
|
||||||
|
username: site.username,
|
||||||
|
email: site.email,
|
||||||
|
ip: site.ip,
|
||||||
|
classifier: params[:classifier]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[:success] = "#{ip_deleted_count + deleted_count} sites have been banned, including #{ip_deleted_count} matching IPs."
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/unban' do
|
||||||
|
require_admin
|
||||||
|
site = Site[username: params[:username]]
|
||||||
|
|
||||||
|
if site.nil?
|
||||||
|
flash[:error] = 'User not found'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
if !site.is_banned
|
||||||
|
flash[:error] = 'Site is not banned'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
site.unban!
|
||||||
|
|
||||||
|
flash[:success] = "Site #{site.username} was unbanned."
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/mark_nsfw' do
|
||||||
|
require_admin
|
||||||
|
site = Site[username: params[:username]]
|
||||||
|
|
||||||
|
if site.nil?
|
||||||
|
flash[:error] = 'User not found'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
site.is_nsfw = true
|
||||||
|
site.admin_nsfw = true
|
||||||
|
site.save_changes validate: false
|
||||||
|
|
||||||
|
flash[:success] = 'MISSION ACCOMPLISHED'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/unmark_nsfw' do
|
||||||
|
require_admin
|
||||||
|
site = Site[username: params[:username]]
|
||||||
|
|
||||||
|
if site.nil?
|
||||||
|
flash[:error] = 'User not found'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
site.is_nsfw = false
|
||||||
|
site.admin_nsfw = false
|
||||||
|
site.save_changes validate: false
|
||||||
|
|
||||||
|
flash[:success] = 'Site unmarked as NSFW'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/mark_moderated' do
|
||||||
|
require_admin
|
||||||
|
|
||||||
|
not_found if params[:site_id].blank?
|
||||||
|
site = Site[params[:site_id]]
|
||||||
|
not_found if site.nil?
|
||||||
|
|
||||||
|
site_ids = Site.moderation_dataset.select(:id).where{created_at <= site.created_at}.all.collect {|s| s.id}
|
||||||
|
|
||||||
|
DB[:sites].where(id: site_ids).update(needs_moderation: false)
|
||||||
|
|
||||||
|
remaining_ds = Site.moderation_dataset.order(:created_at.desc)
|
||||||
|
remaining_ds = remaining_ds.paginate(1, Site::BROWSE_PAGINATION_LENGTH)
|
||||||
|
last_page = remaining_ds.page_count
|
||||||
|
last_page = 1 if last_page < 1
|
||||||
|
|
||||||
|
uri = Addressable::URI.parse request.referer
|
||||||
|
query = Rack::Utils.parse_query uri.query
|
||||||
|
query['page'] = last_page.to_s
|
||||||
|
uri.query = Rack::Utils.build_query(query)
|
||||||
|
redirect uri.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/feature' do
|
||||||
|
require_admin
|
||||||
|
site = Site[username: params[:username]]
|
||||||
|
|
||||||
|
if site.nil?
|
||||||
|
flash[:error] = 'User not found'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
site.featured_at = Time.now
|
||||||
|
site.save_changes(validate: false)
|
||||||
|
flash[:success] = 'Site has been featured.'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/verify_email' do
|
||||||
|
require_admin
|
||||||
|
site = Site[username: params[:username]]
|
||||||
|
|
||||||
|
if site.nil?
|
||||||
|
flash[:error] = 'User not found'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
if site.email_confirmed
|
||||||
|
flash[:error] = 'Email is already confirmed'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
site.email_confirmed = true
|
||||||
|
site.save_changes(validate: false)
|
||||||
|
flash[:success] = "Email for #{site.username} has been manually verified."
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/admin/site/change_password' do
|
||||||
|
require_admin
|
||||||
|
site = Site[username: params[:username]]
|
||||||
|
not_found if site.nil?
|
||||||
|
|
||||||
|
unless site.parent?
|
||||||
|
flash[:error] = 'Password can only be changed on parent sites.'
|
||||||
|
redirect "/admin/site/#{site.username}"
|
||||||
|
end
|
||||||
|
|
||||||
|
new_password = params[:new_password].to_s
|
||||||
|
site.password = new_password
|
||||||
|
site.valid?
|
||||||
|
|
||||||
|
if new_password != params[:new_password_confirm].to_s
|
||||||
|
site.errors.add :password, 'New passwords do not match.'
|
||||||
|
end
|
||||||
|
|
||||||
|
if site.errors.empty?
|
||||||
|
site.password_reset_token = nil
|
||||||
|
site.password_reset_confirmed = false
|
||||||
|
site.save_changes
|
||||||
|
|
||||||
|
site.send_email(
|
||||||
|
subject: '[Neocities] Your password has been changed',
|
||||||
|
body: Tilt.new('./views/templates/email/password_changed.erb', pretty: true).render(self)
|
||||||
|
)
|
||||||
|
|
||||||
|
flash[:success] = 'Password changed.'
|
||||||
|
else
|
||||||
|
flash[:error] = site.errors.first.last.first
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect "/admin/site/#{site.username}"
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/admin/masquerade/:username' do
|
||||||
|
require_admin
|
||||||
|
site = Site[username: params[:username]]
|
||||||
|
not_found if site.nil?
|
||||||
|
session[:id] = site.id
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
get %r{/admin/site/(.+)} do |username_or_email_or_domain|
|
||||||
|
require_admin
|
||||||
|
ident = request.path_info.sub(%r{\A/admin/site/}, '')
|
||||||
|
|
||||||
|
if ident.blank?
|
||||||
|
flash[:error] = 'username or email or domain required'
|
||||||
|
redirect '/admin'
|
||||||
|
end
|
||||||
|
|
||||||
|
ident = ident.to_s.strip
|
||||||
|
ident = ident.sub(%r{\A(https?):/+}, '\1://')
|
||||||
|
|
||||||
|
begin
|
||||||
|
parsed_ident = Addressable::URI.parse(ident)
|
||||||
|
ident = parsed_ident.host if parsed_ident.host
|
||||||
|
rescue Addressable::URI::InvalidURIError
|
||||||
|
end
|
||||||
|
|
||||||
|
ident = ident.split('/').first.to_s.downcase
|
||||||
|
|
||||||
|
if ident =~ /@/
|
||||||
|
@site = Site[email: ident]
|
||||||
|
elsif ident.end_with?('.neocities.org')
|
||||||
|
@site = Site[username: ident.sub(/\.neocities\.org\z/, '')]
|
||||||
|
elsif ident =~ /.+\..+$/
|
||||||
|
@site = Site.where(domain: ident).first
|
||||||
|
else
|
||||||
|
@site = Site[username: ident]
|
||||||
|
end
|
||||||
|
|
||||||
|
if @site.nil?
|
||||||
|
flash[:error] = "site not found"
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
@title = "Site Info - #{@site.username}"
|
||||||
|
@moderation_blur_categories = Array($config['moderation_blur_categories'])
|
||||||
|
@moderation_blur_paths = Report
|
||||||
|
.where(site_id: @site.id, type: @moderation_blur_categories)
|
||||||
|
.all
|
||||||
|
.map do |report|
|
||||||
|
path = report.site_file_path.to_s.sub(%r{\A/+}, '')
|
||||||
|
path.empty? ? 'index.html' : path
|
||||||
|
end
|
||||||
|
.uniq
|
||||||
|
|
||||||
|
erb :'admin/site'
|
||||||
|
end
|
||||||
391
app/api.rb
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
require 'base64'
|
||||||
|
require 'rack/mime'
|
||||||
|
|
||||||
|
get '/api' do
|
||||||
|
@title = 'Developers API'
|
||||||
|
@description = 'Use the Neocities API to upload files and manage your site programmatically.'
|
||||||
|
erb :'api'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/api/upload_hash' do
|
||||||
|
require_api_credentials
|
||||||
|
res = {}
|
||||||
|
files = []
|
||||||
|
|
||||||
|
params.each do |path, sha1_hash|
|
||||||
|
unless sha1_hash.is_a?(String)
|
||||||
|
api_error 400, 'nested_parameters_not_allowed', 'nested parameters are not allowed; each path must directly map to a SHA-1 hash string'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
params.each do |k,v|
|
||||||
|
res[k] = current_site.sha1_hash_match? k, v
|
||||||
|
end
|
||||||
|
api_success files: res
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/api/list' do
|
||||||
|
require_api_credentials
|
||||||
|
|
||||||
|
files = []
|
||||||
|
|
||||||
|
if params[:path].nil? || params[:path].empty? || params[:path] == '/'
|
||||||
|
file_list = current_site.site_files
|
||||||
|
else
|
||||||
|
file_list = current_site.file_list params[:path]
|
||||||
|
end
|
||||||
|
|
||||||
|
file_list.each do |file|
|
||||||
|
new_file = {}
|
||||||
|
new_file[:path] = file[:path]
|
||||||
|
new_file[:is_directory] = file[:is_directory]
|
||||||
|
new_file[:size] = file[:size] unless file[:is_directory]
|
||||||
|
new_file[:created_at] = file[:created_at].rfc2822
|
||||||
|
new_file[:updated_at] = file[:updated_at].rfc2822
|
||||||
|
new_file[:sha1_hash] = file[:sha1_hash] unless file[:is_directory]
|
||||||
|
files << new_file
|
||||||
|
end
|
||||||
|
|
||||||
|
files.each {|f| f[:path].sub!(/^\//, '')}
|
||||||
|
|
||||||
|
api_success files: files
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/api/download' do
|
||||||
|
require_api_credentials
|
||||||
|
|
||||||
|
api_error 400, 'missing_path', 'you must provide path' if params[:path].blank?
|
||||||
|
api_error 400, 'bad_path', 'path must be a string' unless params[:path].is_a?(String)
|
||||||
|
api_error 400, 'bad_path', 'path is not valid' if params[:path].include?('..') || current_site.invalid_path?(params[:path])
|
||||||
|
|
||||||
|
begin
|
||||||
|
path = current_site.scrubbed_path params[:path]
|
||||||
|
rescue ArgumentError
|
||||||
|
api_error 400, 'bad_path', 'path is not valid'
|
||||||
|
end
|
||||||
|
|
||||||
|
site_file = current_site.site_files_dataset.where(path: path).first
|
||||||
|
api_error 400, 'missing_file', "could not find #{path}" if site_file.nil?
|
||||||
|
api_error 400, 'cannot_download_directory', 'cannot download a directory' if site_file.is_directory
|
||||||
|
|
||||||
|
file_path = current_site.files_path path
|
||||||
|
api_error 400, 'missing_file', "could not find #{path}" unless File.file?(file_path)
|
||||||
|
|
||||||
|
last_modified = site_file.updated_at || site_file.created_at || File.mtime(file_path)
|
||||||
|
headers 'Last-Modified' => last_modified.httpdate,
|
||||||
|
'ETag' => %("#{site_file.sha1_hash}"),
|
||||||
|
'Cache-Control' => 'private, no-store'
|
||||||
|
|
||||||
|
send_file file_path, type: Rack::Mime.mime_type(File.extname(path), 'application/octet-stream')
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_files(params, files = [])
|
||||||
|
# Check if the entire input is directly an array of files
|
||||||
|
if params.is_a?(Array)
|
||||||
|
params.each do |item|
|
||||||
|
# Call extract_files on each item if it's an Array or Hash to handle nested structures
|
||||||
|
if item.is_a?(Array) || item.is_a?(Hash)
|
||||||
|
extract_files(item, files)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elsif params.is_a?(Hash)
|
||||||
|
params.each do |key, value|
|
||||||
|
# If the value is a Hash and contains a :tempfile key, it's considered an uploaded file.
|
||||||
|
if value.is_a?(Hash) && value.has_key?(:tempfile) && !value[:tempfile].nil?
|
||||||
|
files << {filename: value[:name], tempfile: value[:tempfile]}
|
||||||
|
elsif value.is_a?(Array)
|
||||||
|
value.each do |val|
|
||||||
|
if val.is_a?(Hash) && val.has_key?(:tempfile) && !val[:tempfile].nil?
|
||||||
|
# Directly add the file info if it's an uploaded file within an array
|
||||||
|
files << {filename: val[:name], tempfile: val[:tempfile]}
|
||||||
|
elsif val.is_a?(Hash) || val.is_a?(Array)
|
||||||
|
# Recursively search for more files if the element is a Hash or Array
|
||||||
|
extract_files(val, files)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elsif value.is_a?(Hash)
|
||||||
|
# Recursively search for more files if the value is a Hash
|
||||||
|
extract_files(value, files)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
files
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/api/upload' do
|
||||||
|
require_api_credentials
|
||||||
|
files = extract_files params
|
||||||
|
|
||||||
|
if !params[:username].blank?
|
||||||
|
site = Site[username: params[:username]]
|
||||||
|
|
||||||
|
if site.nil? || site.is_deleted
|
||||||
|
api_error 400, 'site_not_found', "could not find site"
|
||||||
|
end
|
||||||
|
|
||||||
|
if site.owned_by?(current_site)
|
||||||
|
@_site = site
|
||||||
|
else
|
||||||
|
api_error 400, 'site_not_allowed', "not allowed to change this site with your current logged in site"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
results = current_site.store_files files
|
||||||
|
|
||||||
|
if results.is_a?(Hash) && results[:error]
|
||||||
|
api_error 400, results[:error_type], results[:message]
|
||||||
|
end
|
||||||
|
|
||||||
|
if results.is_a?(Array)
|
||||||
|
success_count = results.count(true)
|
||||||
|
unchanged_count = results.count(:unchanged)
|
||||||
|
failure_count = results.count(false)
|
||||||
|
total = results.length
|
||||||
|
change_attempts = total - unchanged_count
|
||||||
|
|
||||||
|
if failure_count == total
|
||||||
|
api_error 500, 'upload_failed', 'all files failed to upload'
|
||||||
|
elsif failure_count > 0
|
||||||
|
message_parts = []
|
||||||
|
if success_count > 0
|
||||||
|
denominator = change_attempts.zero? ? total : change_attempts
|
||||||
|
message_parts << "#{success_count} of #{denominator} file(s) uploaded"
|
||||||
|
end
|
||||||
|
message_parts << "#{unchanged_count} file(s) already up to date" if unchanged_count > 0
|
||||||
|
message_parts << "#{failure_count} file(s) failed" if failure_count > 0
|
||||||
|
api_success message_parts.join(', ')
|
||||||
|
elsif success_count > 0
|
||||||
|
if unchanged_count > 0
|
||||||
|
api_success "#{success_count} file(s) uploaded, #{unchanged_count} already up to date"
|
||||||
|
else
|
||||||
|
api_success 'your file(s) have been successfully uploaded'
|
||||||
|
end
|
||||||
|
else
|
||||||
|
api_success 'no changes, files already up to date'
|
||||||
|
end
|
||||||
|
else
|
||||||
|
api_success 'your file(s) have been successfully uploaded'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/api/rename' do
|
||||||
|
require_api_credentials
|
||||||
|
|
||||||
|
api_error 400, 'missing_arguments', 'you must provide path and new_path' if params[:path].blank? || params[:new_path].blank?
|
||||||
|
|
||||||
|
path = current_site.scrubbed_path params[:path]
|
||||||
|
new_path = current_site.scrubbed_path params[:new_path]
|
||||||
|
|
||||||
|
unless path.is_a?(String)
|
||||||
|
api_error 400, 'bad_path', "#{path} is not a valid path, cancelled renaming"
|
||||||
|
end
|
||||||
|
|
||||||
|
unless new_path.is_a?(String)
|
||||||
|
api_error 400, 'bad_new_path', "#{new_path} is not a valid new_path, cancelled renaming"
|
||||||
|
end
|
||||||
|
|
||||||
|
site_file = current_site.site_files.select {|sf| sf.path == path}.first
|
||||||
|
|
||||||
|
if site_file.nil?
|
||||||
|
api_error 400, 'missing_file', "could not find #{path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
res = site_file.rename new_path
|
||||||
|
|
||||||
|
if res.first == true
|
||||||
|
api_success "#{path} has been renamed to #{new_path}"
|
||||||
|
else
|
||||||
|
api_error 400, 'rename_error', res.last
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/api/delete' do
|
||||||
|
require_api_credentials
|
||||||
|
|
||||||
|
api_error 400, 'missing_filenames', 'you must provide files to delete' if params[:filenames].nil? || params[:filenames].empty?
|
||||||
|
api_error 400, 'bad_filename', 'filenames must be an array, canceled deleting' unless params[:filenames].is_a?(Array)
|
||||||
|
|
||||||
|
paths = []
|
||||||
|
params[:filenames].each do |path|
|
||||||
|
unless path.is_a?(String)
|
||||||
|
api_error 400, 'bad_filename', "#{path} is not a valid filename, canceled deleting"
|
||||||
|
end
|
||||||
|
|
||||||
|
submitted_path = path
|
||||||
|
|
||||||
|
begin
|
||||||
|
path = current_site.scrubbed_path submitted_path
|
||||||
|
rescue ArgumentError
|
||||||
|
api_error 400, 'bad_filename', "#{submitted_path} is not a valid filename, canceled deleting"
|
||||||
|
end
|
||||||
|
|
||||||
|
if path.empty?
|
||||||
|
api_error 400, 'cannot_delete_site_directory', 'cannot delete the root directory of the site'
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_site.invalid_path?(submitted_path) || current_site.invalid_path?(path)
|
||||||
|
api_error 400, 'bad_filename', "#{submitted_path} is not a valid filename, canceled deleting"
|
||||||
|
end
|
||||||
|
|
||||||
|
if path == 'index.html'
|
||||||
|
api_error 400, 'cannot_delete_index', 'you cannot delete your index.html file, canceled deleting'
|
||||||
|
end
|
||||||
|
|
||||||
|
if !current_site.file_exists?(path)
|
||||||
|
api_error 400, 'missing_files', "#{path} was not found on your site, canceled deleting"
|
||||||
|
end
|
||||||
|
|
||||||
|
paths << path
|
||||||
|
end
|
||||||
|
|
||||||
|
paths.each do |path|
|
||||||
|
current_site.delete_file(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
api_success 'file(s) have been deleted'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/api/create_directory' do
|
||||||
|
dashboard_request = params[:from_dashboard] == 'true' && current_site && csrf_safe?
|
||||||
|
|
||||||
|
require_api_credentials
|
||||||
|
|
||||||
|
if params[:path].blank? && !dashboard_request
|
||||||
|
api_error 400, 'missing_path', 'you must provide path'
|
||||||
|
end
|
||||||
|
|
||||||
|
path = if dashboard_request
|
||||||
|
"#{params[:dir] || ''}/#{params[:name]}"
|
||||||
|
else
|
||||||
|
params[:path]
|
||||||
|
end
|
||||||
|
result = current_site.create_directory path
|
||||||
|
|
||||||
|
if dashboard_request
|
||||||
|
dashboard_dir = current_site.scrubbed_path(params[:dir])
|
||||||
|
|
||||||
|
@api = false
|
||||||
|
flash[:error] = result unless result == true
|
||||||
|
redirect "/dashboard?dir=#{Rack::Utils.escape dashboard_dir}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if result == true
|
||||||
|
api_success 'directory has been created'
|
||||||
|
else
|
||||||
|
api_error 400, 'create_directory_error', result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/api/info' do
|
||||||
|
if params[:sitename]
|
||||||
|
site = Site[username: params[:sitename]]
|
||||||
|
api_error 400, 'site_not_found', "could not find site #{params[:sitename]}" if site.nil? || site.is_banned
|
||||||
|
api_success api_info_for(site)
|
||||||
|
else
|
||||||
|
init_api_credentials
|
||||||
|
api_success api_info_for(current_site)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/api/key' do
|
||||||
|
require_api_credentials
|
||||||
|
current_site.generate_api_key! if current_site.api_key.blank?
|
||||||
|
api_success api_key: current_site.api_key
|
||||||
|
end
|
||||||
|
|
||||||
|
def api_info_for(site)
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
sitename: site.username,
|
||||||
|
views: site.views,
|
||||||
|
hits: site.hits,
|
||||||
|
created_at: site.created_at.rfc2822,
|
||||||
|
last_updated: site.site_updated_at ? site.site_updated_at.rfc2822 : nil,
|
||||||
|
domain: site.domain,
|
||||||
|
supporter: site.supporter?,
|
||||||
|
tags: site.tags.collect {|t| t.name}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Catch-all for missing api calls
|
||||||
|
|
||||||
|
get '/api/:name' do
|
||||||
|
api_not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/api/:name' do
|
||||||
|
api_not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_api_credentials
|
||||||
|
if current_site && csrf_safe?
|
||||||
|
api_error_invalid_auth unless current_site.required_validations_met?
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if !request.env['HTTP_AUTHORIZATION'].nil?
|
||||||
|
init_api_credentials
|
||||||
|
else
|
||||||
|
api_error_invalid_auth
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def init_api_credentials
|
||||||
|
auth = request.env['HTTP_AUTHORIZATION']
|
||||||
|
|
||||||
|
begin
|
||||||
|
if bearer_match = auth.match(/^Bearer (.+)/)
|
||||||
|
api_key = bearer_match.captures.first
|
||||||
|
api_error_invalid_auth if api_key.nil? || api_key.empty?
|
||||||
|
else
|
||||||
|
user, pass = Base64.decode64(auth.match(/Basic (.+)/)[1]).split(':')
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
api_error_invalid_auth
|
||||||
|
end
|
||||||
|
|
||||||
|
if defined?(api_key) && !api_key.blank?
|
||||||
|
site = Site[api_key: api_key]
|
||||||
|
elsif defined?(user) && defined?(pass)
|
||||||
|
site = Site.get_site_from_login user, pass
|
||||||
|
else
|
||||||
|
api_error_invalid_auth
|
||||||
|
end
|
||||||
|
|
||||||
|
if site.nil? || site.is_banned || site.is_deleted || !(site.required_validations_met?)
|
||||||
|
api_error_invalid_auth
|
||||||
|
end
|
||||||
|
|
||||||
|
DB['update sites set api_calls=api_calls+1 where id=?', site.id].first
|
||||||
|
|
||||||
|
session[:id] = site.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def api_success(message_or_obj)
|
||||||
|
output = {result: 'success'}
|
||||||
|
|
||||||
|
if message_or_obj.is_a?(String)
|
||||||
|
output[:message] = message_or_obj
|
||||||
|
else
|
||||||
|
output.merge! message_or_obj
|
||||||
|
end
|
||||||
|
|
||||||
|
api_response(200, output)
|
||||||
|
end
|
||||||
|
|
||||||
|
def api_response(status, output)
|
||||||
|
halt status, JSON.pretty_generate(output)+"\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def api_error(status, error_type, message)
|
||||||
|
api_response(status, result: 'error', error_type: error_type, message: message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def api_error_invalid_auth
|
||||||
|
api_error 403, 'invalid_auth', 'invalid credentials - please check your username and password (or your api key)'
|
||||||
|
end
|
||||||
|
|
||||||
|
def api_not_found
|
||||||
|
api_error 404, 'not_found', 'the requested api call does not exist'
|
||||||
|
end
|
||||||
7
app/blog.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
get '/blog/?' do
|
||||||
|
redirect 'https://blog.neocities.org', 301
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/blog/:article' do |article|
|
||||||
|
redirect "https://blog.neocities.org/#{article}.html", 301
|
||||||
|
end
|
||||||
131
app/browse.rb
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
get '/browse/?' do
|
||||||
|
@page = params[:page]
|
||||||
|
@page = 1 if @page.not_an_integer?
|
||||||
|
@description = 'Browse Neocities websites by tags, popularity, and recent updates.'
|
||||||
|
|
||||||
|
if params[:tag]
|
||||||
|
params[:tag] = params[:tag].gsub(Tag::INVALID_TAG_REGEX, '').gsub(/\s+/, '').slice(0, Tag::NAME_LENGTH_MAX)
|
||||||
|
@title = "Sites tagged #{params[:tag]}"
|
||||||
|
@description = "Browse Neocities websites tagged #{params[:tag]}."
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_education?
|
||||||
|
ds = education_sites_dataset
|
||||||
|
else
|
||||||
|
ds = browse_sites_dataset
|
||||||
|
end
|
||||||
|
|
||||||
|
ds = ds.paginate @page.to_i, Site::BROWSE_PAGINATION_LENGTH
|
||||||
|
@pagination_dataset = ds
|
||||||
|
@sites = ds.all
|
||||||
|
|
||||||
|
site_ids = @sites.collect {|s| s[:id]}
|
||||||
|
tags = DB['select site_id,name from tags join sites_tags on tags.id=sites_tags.tag_id where site_id IN ?', site_ids].all
|
||||||
|
|
||||||
|
@site_tags = {}
|
||||||
|
site_ids.each do |site_id|
|
||||||
|
@site_tags[site_id] = tags.select {|t| t[:site_id] == site_id}.collect {|t| t[:name]}
|
||||||
|
end
|
||||||
|
|
||||||
|
erb :browse
|
||||||
|
end
|
||||||
|
|
||||||
|
def education_sites_dataset
|
||||||
|
ds = Site.filter is_deleted: false
|
||||||
|
ds = ds.association_join(:tags).select_all(:sites)
|
||||||
|
params[:tag] = current_site.tags.first.name
|
||||||
|
ds = ds.where ['tags.name = ?', params[:tag]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def browse_sites_dataset
|
||||||
|
ds = Site.browse_dataset
|
||||||
|
ds = ds.where is_education: false
|
||||||
|
|
||||||
|
if current_site
|
||||||
|
ds = ds.or sites__id: current_site.id
|
||||||
|
|
||||||
|
if !current_site.blocking_site_ids.empty?
|
||||||
|
ds = ds.where(Sequel.~(Sequel.qualify(:sites, :id) => current_site.blocking_site_ids))
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_site.blocks_dataset.count
|
||||||
|
ds = ds.where(
|
||||||
|
Sequel.~(Sequel.qualify(:sites, :id) => current_site.blocks_dataset.select(:actioning_site_id).all.collect {|s| s.actioning_site_id})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_site && current_site.is_admin && params[:sites]
|
||||||
|
ds = ds.where sites__username: params[:sites].split(',')
|
||||||
|
return ds
|
||||||
|
end
|
||||||
|
|
||||||
|
params[:sort_by] ||= 'special_sauce'
|
||||||
|
|
||||||
|
case params[:sort_by]
|
||||||
|
when 'special_sauce'
|
||||||
|
ds = ds.where{score > 1} unless params[:tag]
|
||||||
|
ds = ds.order :score.desc, :follow_count.desc, :views.desc, :site_updated_at.desc
|
||||||
|
when 'random'
|
||||||
|
ds = ds.where{score > 3} unless params[:tag]
|
||||||
|
ds = ds.order(Sequel.lit('RANDOM()'))
|
||||||
|
when 'most_followed'
|
||||||
|
ds = ds.where{views > Site::BROWSE_MINIMUM_FOLLOWER_VIEWS}
|
||||||
|
ds = ds.where{follow_count > Site::BROWSE_FOLLOWER_MINIMUM_FOLLOWS}
|
||||||
|
ds = ds.where{updated_at > Site::BROWSE_FOLLOWER_UPDATED_AT_CUTOFF.ago} unless params[:tag]
|
||||||
|
ds = ds.order :follow_count.desc, :score.desc, :updated_at.desc
|
||||||
|
when 'last_updated'
|
||||||
|
ds = ds.where{score > 3} unless params[:tag]
|
||||||
|
ds = ds.exclude site_updated_at: nil
|
||||||
|
ds = ds.order :site_updated_at.desc
|
||||||
|
when 'newest'
|
||||||
|
ds = ds.where{views > Site::BROWSE_MINIMUM_VIEWS} unless current_site && current_site.is_admin
|
||||||
|
ds = ds.exclude site_updated_at: nil
|
||||||
|
ds = ds.order :created_at.desc, :views.desc
|
||||||
|
when 'moderation'
|
||||||
|
not_found unless current_site && current_site.is_admin
|
||||||
|
ds = Site.moderation_dataset
|
||||||
|
ds = ds.order :created_at.desc
|
||||||
|
when 'oldest'
|
||||||
|
ds = ds.where{score > 0.4} unless params[:tag]
|
||||||
|
ds = ds.exclude site_updated_at: nil
|
||||||
|
ds = ds.order(:created_at, :views.desc)
|
||||||
|
when 'hits'
|
||||||
|
ds = ds.where{score > 5}
|
||||||
|
ds = ds.order(:hits.desc, :site_updated_at.desc)
|
||||||
|
when 'views'
|
||||||
|
ds = ds.where{score > 5}
|
||||||
|
ds = ds.order(:views.desc, :site_updated_at.desc)
|
||||||
|
when 'featured'
|
||||||
|
ds = ds.exclude featured_at: nil
|
||||||
|
ds = ds.order :featured_at.desc
|
||||||
|
when 'tipping_enabled'
|
||||||
|
ds = ds.where tipping_enabled: true
|
||||||
|
ds = ds.where("(tipping_paypal is not null and tipping_paypal != '') or (tipping_bitcoin is not null and tipping_bitcoin != '')")
|
||||||
|
ds = ds.where{score > 1} unless params[:tag]
|
||||||
|
ds = ds.group :sites__id
|
||||||
|
ds = ds.order :follow_count.desc, :views.desc, :updated_at.desc
|
||||||
|
when 'blocks'
|
||||||
|
require_admin
|
||||||
|
ds = ds.select{[sites.*, Sequel[count(site_id)].as(:total)]}
|
||||||
|
ds = ds.inner_join :blocks, :site_id => :id
|
||||||
|
ds = ds.group :sites__id
|
||||||
|
ds = ds.order :total.desc
|
||||||
|
else
|
||||||
|
not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
unless params[:sort_by] == 'moderation'
|
||||||
|
ds = ds.where ['sites.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)]
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:tag]
|
||||||
|
ds = ds.select_all :sites
|
||||||
|
ds = ds.inner_join :sites_tags, :site_id => :id
|
||||||
|
ds = ds.inner_join :tags, :id => :sites_tags__tag_id
|
||||||
|
ds = ds.where ['tags.name = ?', params[:tag]]
|
||||||
|
ds = ds.where ['tags.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)]
|
||||||
|
end
|
||||||
|
|
||||||
|
ds
|
||||||
|
end
|
||||||
21
app/comment.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
post '/comment/:comment_id/toggle_like' do |comment_id|
|
||||||
|
require_login
|
||||||
|
content_type :json
|
||||||
|
comment = Comment[id: comment_id]
|
||||||
|
return 403 if comment.event.site.is_blocking?(current_site) || current_site.is_blocking?(comment.event.site)
|
||||||
|
liked_response = comment.toggle_site_like(current_site) ? 'liked' : 'unliked'
|
||||||
|
{result: liked_response, comment_like_count: comment.comment_likes_dataset.count, liking_site_names: comment.liking_site_usernames}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/comment/:comment_id/delete' do |comment_id|
|
||||||
|
require_login
|
||||||
|
content_type :json
|
||||||
|
comment = Comment[id: comment_id]
|
||||||
|
|
||||||
|
if comment.event.site == current_site || comment.actioning_site == current_site
|
||||||
|
comment.delete
|
||||||
|
return {result: 'success'}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
return {result: 'error'}.to_json
|
||||||
|
end
|
||||||
57
app/contact.rb
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
get '/contact' do
|
||||||
|
@show_contact_form = params[:show_contact_form] == 'yes'
|
||||||
|
@description = 'Contact Neocities support and get help with your account or site.'
|
||||||
|
erb :'contact'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/contact' do
|
||||||
|
@errors = []
|
||||||
|
if params[:email].empty? || params[:subject].empty? || params[:body].empty?
|
||||||
|
@errors << 'Please fill out all fields'
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:email] != params[:confirm_email]
|
||||||
|
@errors << 'Email addresses do not match'
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:faq_check] == 'no'
|
||||||
|
@errors << 'Please check Frequently Asked Questions before sending a contact message'
|
||||||
|
end
|
||||||
|
|
||||||
|
unless hcaptcha_valid?
|
||||||
|
@errors << 'Captcha was not filled out (or was filled out incorrectly)'
|
||||||
|
end
|
||||||
|
|
||||||
|
if !@errors.empty?
|
||||||
|
erb :'contact'
|
||||||
|
else
|
||||||
|
body = params[:body]
|
||||||
|
|
||||||
|
if current_site
|
||||||
|
body = "current username: #{current_site.username}\n\n" + body
|
||||||
|
if parent_site != current_site
|
||||||
|
body = "parent username: #{parent_site.username}\n\n" + body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_site && current_site.supporter?
|
||||||
|
subject = "#{params[:subject]} [Supporter]"
|
||||||
|
else
|
||||||
|
subject = params[:subject]
|
||||||
|
end
|
||||||
|
|
||||||
|
subject += " [#{SecureRandom.hex[0..3]}]"
|
||||||
|
|
||||||
|
EmailWorker.perform_async({
|
||||||
|
from: Site::FROM_EMAIL,
|
||||||
|
reply_to: params[:email],
|
||||||
|
to: $config['support_email'],
|
||||||
|
subject: subject,
|
||||||
|
body: body,
|
||||||
|
no_footer: true
|
||||||
|
})
|
||||||
|
|
||||||
|
flash[:success] = 'Your contact message has been sent.'
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
end
|
||||||
154
app/create.rb
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
CREATE_MATCH_REGEX = /^username$|^password$|^email$|^new_tags_string$|^is_education$/
|
||||||
|
|
||||||
|
def education_whitelist_required?
|
||||||
|
return true if params[:is_education] == 'true' && $config['education_tag_whitelist']
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def education_whitelisted?
|
||||||
|
return true if education_whitelist_required? && !$config['education_tag_whitelist'].select {|t| params[:new_tags_string].match(Regexp.quote(t))}.empty?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/create_validate_all' do
|
||||||
|
content_type :json
|
||||||
|
fields = params.select {|p| p.match CREATE_MATCH_REGEX}
|
||||||
|
|
||||||
|
begin
|
||||||
|
site = Site.new fields
|
||||||
|
rescue ArgumentError => e
|
||||||
|
if e.message == 'input string invalid'
|
||||||
|
return {error: 'invalid input'}.to_json
|
||||||
|
else
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if site.valid?
|
||||||
|
return [].to_json if education_whitelisted?
|
||||||
|
end
|
||||||
|
|
||||||
|
resp = site.errors.collect {|e| [e.first, e.last.first]}
|
||||||
|
resp << ['captcha', 'Please complete the captcha.'] if params[:'h-captcha-response'].empty? && !self.class.test?
|
||||||
|
resp.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/create_validate' do
|
||||||
|
content_type :json
|
||||||
|
|
||||||
|
if !params[:field].match CREATE_MATCH_REGEX
|
||||||
|
return {error: 'not a valid field'}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
site = Site.new(params[:field] => params[:value])
|
||||||
|
rescue ArgumentError => e
|
||||||
|
if e.message == 'input string invalid'
|
||||||
|
return {error: 'invalid input'}.to_json
|
||||||
|
else
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
site.is_education = params[:is_education]
|
||||||
|
site.valid?
|
||||||
|
|
||||||
|
field_sym = params[:field].to_sym
|
||||||
|
|
||||||
|
if site.errors[field_sym]
|
||||||
|
return {error: site.errors[field_sym].first}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
{result: 'ok'}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/create' do
|
||||||
|
content_type :json
|
||||||
|
dashboard_if_signed_in
|
||||||
|
|
||||||
|
@site = Site.new(
|
||||||
|
username: params[:username],
|
||||||
|
password: params[:password],
|
||||||
|
email: params[:email],
|
||||||
|
new_tags_string: params[:new_tags_string],
|
||||||
|
is_education: params[:is_education] == 'true' ? true : false,
|
||||||
|
ip: request.ip,
|
||||||
|
ga_adgroupid: session[:ga_adgroupid]
|
||||||
|
)
|
||||||
|
|
||||||
|
if education_whitelist_required?
|
||||||
|
if education_whitelisted?
|
||||||
|
@site.email_confirmed = true
|
||||||
|
else
|
||||||
|
flash[:error] = 'The class tag is invalid.'
|
||||||
|
return {result: 'error'}.to_json
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if !hcaptcha_valid?
|
||||||
|
flash[:error] = 'The captcha was not valid, please try again.'
|
||||||
|
return {result: 'error'}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
if Site.ip_create_limit?(request.ip)
|
||||||
|
flash[:error] = 'Your IP address has created too many sites, please try again later or contact support.'
|
||||||
|
return {result: 'error'}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
if Site.disposable_mx_record?(@site.email)
|
||||||
|
flash[:error] = 'Cannot use a disposable email address.'
|
||||||
|
return {result: 'error'}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
if defined?(BlackBox.create_disabled?) && BlackBox.create_disabled?(@site, request)
|
||||||
|
flash[:error] = 'Site creation is not currently available from your location, please try again later.'
|
||||||
|
return {result: 'error'}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
if defined?(BlackBox.tutorial_required?) && BlackBox.tutorial_required?(@site, request)
|
||||||
|
@site.tutorial_required = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if !@site.valid?
|
||||||
|
flash[:error] = @site.errors.first.last.first
|
||||||
|
return {result: 'error'}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@site.email_confirmed = true if self.class.development?
|
||||||
|
@site.phone_verified = true if self.class.development?
|
||||||
|
|
||||||
|
begin
|
||||||
|
@site.phone_verification_required = true if self.class.production? && BlackBox.phone_verification_required?(@site)
|
||||||
|
rescue => e
|
||||||
|
EmailWorker.perform_async({
|
||||||
|
from: Site::FROM_EMAIL,
|
||||||
|
to: 'errors@neocities.org',
|
||||||
|
subject: "[Neocities Error] Phone verification exception",
|
||||||
|
body: "#{e.inspect}\n#{e.backtrace}",
|
||||||
|
no_footer: true
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
@site.save
|
||||||
|
rescue Sequel::UniqueConstraintViolation => e
|
||||||
|
if e.message =~ /username.+already exists/
|
||||||
|
flash[:error] = 'Username already exists.'
|
||||||
|
return {result: 'error'}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
|
||||||
|
unless education_whitelisted?
|
||||||
|
send_confirmation_email @site
|
||||||
|
|
||||||
|
@site.send_email(
|
||||||
|
subject: "[Neocities] Welcome to Neocities!",
|
||||||
|
body: Tilt.new('./views/templates/email/welcome.erb', pretty: true).render(self)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
session[:id] = @site.id
|
||||||
|
{result: 'ok'}.to_json
|
||||||
|
end
|
||||||
40
app/dashboard.rb
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
get '/dashboard' do
|
||||||
|
require_login
|
||||||
|
dashboard_init
|
||||||
|
dont_browser_cache
|
||||||
|
|
||||||
|
unless current_site.dashboard_accessed
|
||||||
|
current_site.dashboard_accessed = true
|
||||||
|
current_site.save_changes validate: false
|
||||||
|
end
|
||||||
|
|
||||||
|
erb :'dashboard/index'
|
||||||
|
end
|
||||||
|
|
||||||
|
def dashboard_init
|
||||||
|
if params[:dir] && params[:dir][0] != '/'
|
||||||
|
params[:dir] = '/'+params[:dir]
|
||||||
|
end
|
||||||
|
|
||||||
|
if !File.directory?(current_site.files_path(params[:dir]))
|
||||||
|
if !File.directory?(current_site.files_path)
|
||||||
|
flash[:error] = 'Could not find your web site, please contact support.'
|
||||||
|
signout
|
||||||
|
redirect '/'
|
||||||
|
else
|
||||||
|
flash[:error] = 'Could not find the requested directory.'
|
||||||
|
redirect '/dashboard'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@dir = params[:dir]
|
||||||
|
@file_list = current_site.file_list @dir
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/dashboard/files' do
|
||||||
|
require_login
|
||||||
|
dashboard_init
|
||||||
|
dont_browser_cache
|
||||||
|
|
||||||
|
erb :'dashboard/files', layout: false
|
||||||
|
end
|
||||||
34
app/dmca.rb
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
get '/dmca' do
|
||||||
|
@title = 'DMCA'
|
||||||
|
@description = 'Submit a DMCA notice and review copyright policy information for Neocities.'
|
||||||
|
erb :'dmca'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/dmca/contact' do
|
||||||
|
@title = 'DMCA'
|
||||||
|
@errors = []
|
||||||
|
|
||||||
|
if params[:email].empty? || params[:subject].empty? || params[:urls].empty? || params[:body].empty?
|
||||||
|
@errors << 'Please fill out all fields'
|
||||||
|
end
|
||||||
|
|
||||||
|
if !hcaptcha_valid?
|
||||||
|
@errors << 'Captcha was not filled out (or was filled out incorrectly)'
|
||||||
|
end
|
||||||
|
|
||||||
|
if !@errors.empty?
|
||||||
|
erb :'dmca'
|
||||||
|
else
|
||||||
|
EmailWorker.perform_async({
|
||||||
|
from: Site::FROM_EMAIL,
|
||||||
|
reply_to: params[:email],
|
||||||
|
to: 'dmca@neocities.org',
|
||||||
|
subject: "[Neocities DMCA Notice]: #{params[:subject]}",
|
||||||
|
body: "#{params[:urls].to_s}\n#{params[:body].to_s}",
|
||||||
|
no_footer: true
|
||||||
|
})
|
||||||
|
|
||||||
|
flash[:success] = 'Your DMCA notification has been sent.'
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
end
|
||||||
29
app/domain.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
get '/domain/new' do
|
||||||
|
require_login
|
||||||
|
@title = 'Register a Domain'
|
||||||
|
|
||||||
|
erb :'domain/new'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/domain/check_availability.json' do
|
||||||
|
require_login
|
||||||
|
content_type :json
|
||||||
|
|
||||||
|
timer = Time.now.to_i
|
||||||
|
|
||||||
|
while true
|
||||||
|
if (Time.now.to_i - timer) > 60
|
||||||
|
api_error 200, :contact_fail, 'Error contacting domain server, please try again.'
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
res = $gandi.domain.available([params[:domain]])[params[:domain]]
|
||||||
|
rescue => Gandi::DataError
|
||||||
|
api_error 200, :invalid_domain, 'Domain name was invalid, please try another.'
|
||||||
|
end
|
||||||
|
|
||||||
|
api_success res unless res == 'pending'
|
||||||
|
sleep 0.2
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
69
app/event.rb
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
post '/event/:event_id/toggle_like' do |event_id|
|
||||||
|
require_login
|
||||||
|
content_type :json
|
||||||
|
event = Event[id: event_id]
|
||||||
|
return 403 if event.site && event.site.is_blocking?(current_site)
|
||||||
|
return 403 if event.actioning_site && event.actioning_site.is_blocking?(current_site)
|
||||||
|
liked_response = event.toggle_site_like(current_site) ? 'liked' : 'unliked'
|
||||||
|
{result: liked_response, event_like_count: event.likes_dataset.count, liking_site_names: event.liking_site_usernames}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/event/:event_id/comment' do |event_id|
|
||||||
|
require_login
|
||||||
|
content_type :json
|
||||||
|
event = Event[id: event_id]
|
||||||
|
|
||||||
|
if event.site && event.site.is_blocking?(current_site)
|
||||||
|
flash[:error] = comment_unavailable_message(event.site)
|
||||||
|
return {result: 'error', message: flash[:error]}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
if event.actioning_site && event.actioning_site.is_blocking?(current_site)
|
||||||
|
flash[:error] = comment_unavailable_message(event.actioning_site)
|
||||||
|
return {result: 'error', message: flash[:error]}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
site = event.site
|
||||||
|
message = normalize_comment_message(params[:message])
|
||||||
|
message_error = comment_message_error(message)
|
||||||
|
unavailable_message = comment_unavailable_message(site)
|
||||||
|
|
||||||
|
if(site.is_blocking?(current_site) ||
|
||||||
|
site.profile_comments_enabled == false ||
|
||||||
|
current_site.commenting_allowed? == false ||
|
||||||
|
(current_site.is_a_jerk? && event.site_id != current_site.id && !site.is_following?(current_site)) ||
|
||||||
|
message_error)
|
||||||
|
flash[:error] = message_error || unavailable_message
|
||||||
|
return {result: 'error', message: flash[:error]}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
event.add_site_comment current_site, message
|
||||||
|
{result: 'success'}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/event/:event_id/update_profile_comment' do |event_id|
|
||||||
|
require_login
|
||||||
|
content_type :json
|
||||||
|
event = Event[id: event_id]
|
||||||
|
message = normalize_comment_message(params[:message])
|
||||||
|
message_error = comment_message_error(message)
|
||||||
|
return {result: 'error'}.to_json unless (current_site.id == event.profile_comment.actioning_site_id &&
|
||||||
|
message_error.nil?)
|
||||||
|
|
||||||
|
event.profile_comment.update message: message
|
||||||
|
return {result: 'success'}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/event/:event_id/delete' do |event_id|
|
||||||
|
require_login
|
||||||
|
content_type :json
|
||||||
|
|
||||||
|
event = Event[id: event_id]
|
||||||
|
|
||||||
|
if event.site_id == current_site.id || event.actioning_site_id == current_site.id
|
||||||
|
event.delete
|
||||||
|
return {result: 'success'}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
return {result: 'error'}.to_json
|
||||||
|
end
|
||||||
240
app/index.rb
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
get '/?' do
|
||||||
|
hosted_site = Site.find_by_host(request.host)
|
||||||
|
|
||||||
|
if hosted_site
|
||||||
|
path = request.path_info.sub(%r{\A/}, '')
|
||||||
|
path = 'index.html' if path.empty?
|
||||||
|
|
||||||
|
path = hosted_site.scrubbed_path(path)
|
||||||
|
|
||||||
|
unless hosted_site.file_exists?(path)
|
||||||
|
not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
file_path = hosted_site.files_path(path)
|
||||||
|
|
||||||
|
send_file file_path,
|
||||||
|
type: Rack::Mime.mime_type(
|
||||||
|
File.extname(path),
|
||||||
|
'application/octet-stream'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_site
|
||||||
|
|
||||||
|
require_login
|
||||||
|
|
||||||
|
redirect '/dashboard' if current_site.is_education
|
||||||
|
|
||||||
|
@page = params[:page]
|
||||||
|
@page = 1 if @page.not_an_integer?
|
||||||
|
|
||||||
|
if params[:activity] == 'mine'
|
||||||
|
events_dataset = current_site.latest_events(@page)
|
||||||
|
elsif params[:event_id]
|
||||||
|
event = Event.select(:id).where(id: params[:event_id]).first
|
||||||
|
not_found if event.nil?
|
||||||
|
not_found if event.is_deleted
|
||||||
|
events_dataset = Event.where(id: params[:event_id]).paginate(1, 1)
|
||||||
|
else
|
||||||
|
events_dataset = current_site.news_feed(@page)
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagination_dataset = events_dataset
|
||||||
|
@events = events_dataset.all
|
||||||
|
|
||||||
|
current_site.events_dataset.update notification_seen: true
|
||||||
|
|
||||||
|
halt erb :'home', locals: {site: current_site}
|
||||||
|
end
|
||||||
|
|
||||||
|
com_preview = (params[:index_com] == '1' || params[:index_com] == 'true') &&
|
||||||
|
ENV['RACK_ENV'] != 'production'
|
||||||
|
|
||||||
|
if request.host == 'neocities.com' || request.host == 'www.neocities.com' || com_preview
|
||||||
|
@canonical_host = 'https://neocities.com'
|
||||||
|
@canonical_path = request.path_info
|
||||||
|
if SimpleCache.expired?(:index_com_html)
|
||||||
|
index_com_html = SimpleCache.store :index_com_html, erb(:index_com, layout: :index_layout), 1.hour
|
||||||
|
else
|
||||||
|
index_com_html = SimpleCache.get :index_com_html
|
||||||
|
end
|
||||||
|
|
||||||
|
halt index_com_html
|
||||||
|
end
|
||||||
|
|
||||||
|
if SimpleCache.expired?(:sites_count)
|
||||||
|
@sites_count = SimpleCache.store :sites_count, Site.count.roundup(100), 4.hours
|
||||||
|
else
|
||||||
|
@sites_count = SimpleCache.get :sites_count
|
||||||
|
end
|
||||||
|
|
||||||
|
if SimpleCache.expired?(:total_hits_count)
|
||||||
|
@total_hits_count = SimpleCache.store :total_hits_count, DB['SELECT SUM(hits) AS hits FROM SITES'].first[:hits], 4.hours
|
||||||
|
else
|
||||||
|
@total_hits_count = SimpleCache.get :total_hits_count
|
||||||
|
end
|
||||||
|
|
||||||
|
@total_hits_count ||= 0
|
||||||
|
|
||||||
|
if SimpleCache.expired?(:total_views_count)
|
||||||
|
@total_views_count = SimpleCache.store :total_views_count, DB['SELECT SUM(views) AS views FROM SITES'].first[:views], 4.hours
|
||||||
|
else
|
||||||
|
@total_views_count = SimpleCache.get :total_views_count
|
||||||
|
end
|
||||||
|
|
||||||
|
@total_views_count ||= 0
|
||||||
|
|
||||||
|
if SimpleCache.expired?(:changed_count)
|
||||||
|
@changed_count = SimpleCache.store :changed_count, DB['SELECT SUM(changed_count) AS changed_count FROM SITES'].first[:changed_count], 4.hours
|
||||||
|
else
|
||||||
|
@changed_count = SimpleCache.get :changed_count
|
||||||
|
end
|
||||||
|
|
||||||
|
@changed_count ||= 0
|
||||||
|
|
||||||
|
=begin
|
||||||
|
if SimpleCache.expired?(:blog_feed_html)
|
||||||
|
@blog_feed_html = ''
|
||||||
|
|
||||||
|
begin
|
||||||
|
xml = HTTP.timeout(global: 2).get('https://blog.neocities.org/feed.xml').to_s
|
||||||
|
feed = Feedjira::Feed.parse xml
|
||||||
|
feed.entries[0..2].each do |entry|
|
||||||
|
@blog_feed_html += %{<a href="#{entry.url}">#{entry.title.split('.').first}</a> <span style="float: right">#{entry.published.strftime('%b %-d, %Y')}</span><br>}
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
@blog_feed_html = 'The latest news on Neocities can be found on our blog.'
|
||||||
|
end
|
||||||
|
|
||||||
|
@blog_feed_html = SimpleCache.store :blog_feed_html, @blog_feed_html, 8.hours
|
||||||
|
else
|
||||||
|
@blog_feed_html = SimpleCache.get :blog_feed_html
|
||||||
|
end
|
||||||
|
=end
|
||||||
|
|
||||||
|
@blog_feed_html = 'The latest news on Neocities can be found on our blog.'
|
||||||
|
|
||||||
|
if SimpleCache.expired?(:featured_sites)
|
||||||
|
@featured_sites = Site.order(:score.desc).exclude(is_nsfw: true).exclude(is_deleted: true).limit(1000).all.sample(12).collect {|s| {screenshot_url: s.screenshot_url('index.html', '540x405'), uri: s.uri, title: s.title}}
|
||||||
|
SimpleCache.store :featured_sites, @featured_sites, 1.hour
|
||||||
|
else
|
||||||
|
@featured_sites = SimpleCache.get :featured_sites
|
||||||
|
end
|
||||||
|
|
||||||
|
@create_disabled = false
|
||||||
|
@description = 'Create and surf awesome websites for free.'
|
||||||
|
|
||||||
|
erb :index, layout: :index_layout
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/sitemap.xml' do
|
||||||
|
content_type 'application/xml', charset: 'UTF-8'
|
||||||
|
|
||||||
|
if request.host == 'neocities.com' || request.host == 'www.neocities.com'
|
||||||
|
erb :sitemap_com, layout: false
|
||||||
|
else
|
||||||
|
erb :sitemap_org, layout: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/welcome' do
|
||||||
|
require_login
|
||||||
|
redirect '/' if current_site.supporter?
|
||||||
|
@title = 'Welcome!'
|
||||||
|
erb :'welcome', locals: {site: current_site}
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/education' do
|
||||||
|
redirect '/' if signed_in?
|
||||||
|
@description = 'Learn how educators and students use Neocities to build websites in class.'
|
||||||
|
erb :education, layout: :index_layout
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/donate' do
|
||||||
|
@description = 'Support Neocities and help keep independent web publishing open to everyone.'
|
||||||
|
erb :'donate'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/about' do
|
||||||
|
@description = 'Read about the Neocities mission and the team building a better web.'
|
||||||
|
erb :'about'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/terms' do
|
||||||
|
@description = 'Review the Neocities Terms of Service.'
|
||||||
|
erb :'terms'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/privacy' do
|
||||||
|
@description = 'Review the Neocities Privacy Policy.'
|
||||||
|
erb :'privacy'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/press' do
|
||||||
|
@description = 'Press resources, facts, and media information about Neocities.'
|
||||||
|
erb :'press'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/legal/?' do
|
||||||
|
@title = 'Legal Guide to Neocities'
|
||||||
|
@description = 'Browse legal information and policy guidance for Neocities.'
|
||||||
|
erb :'legal'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/thankyou' do
|
||||||
|
@title = 'Thank you!'
|
||||||
|
@description = 'Thank you for supporting Neocities.'
|
||||||
|
erb :'thankyou'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/cli' do
|
||||||
|
@title = 'Command Line Interface'
|
||||||
|
@description = 'Use the Neocities command line tool to deploy and manage your site from terminal.'
|
||||||
|
erb :'cli'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/forgot_username' do
|
||||||
|
@title = 'Forgot Username'
|
||||||
|
@description = 'Recover the username for your Neocities account.'
|
||||||
|
erb :'forgot_username'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/forgot_username' do
|
||||||
|
if params[:email].blank?
|
||||||
|
flash[:error] = 'Cannot use an empty email address!'
|
||||||
|
redirect '/forgot_username'
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
sites = Site.get_recovery_sites_with_email params[:email]
|
||||||
|
rescue ArgumentError
|
||||||
|
redirect '/forgot_username'
|
||||||
|
end
|
||||||
|
|
||||||
|
sites.each do |site|
|
||||||
|
body = <<-EOT
|
||||||
|
Hello! This is the Neocities cat, and I have received a username lookup request using this email address.
|
||||||
|
|
||||||
|
Your username is #{site.username}
|
||||||
|
|
||||||
|
If you didn't request this, you can ignore it. Or hide under a bed. Or take a nap. Your call.
|
||||||
|
|
||||||
|
Meow,
|
||||||
|
the Neocities Cat
|
||||||
|
EOT
|
||||||
|
|
||||||
|
body.strip!
|
||||||
|
|
||||||
|
EmailWorker.perform_async({
|
||||||
|
from: Site::FROM_EMAIL,
|
||||||
|
to: params[:email],
|
||||||
|
subject: '[Neocities] Username lookup',
|
||||||
|
body: body
|
||||||
|
})
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[:success] = 'If your email was valid, the Neocities Cat will send an e-mail with your username in it.'
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
37
app/mockup.rb
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
=begin
|
||||||
|
get '/mockup/home' do
|
||||||
|
erb :'mockup/home'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/mockup/edit' do
|
||||||
|
erb :'mockup/edit'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/mockup/profile' do
|
||||||
|
require_login
|
||||||
|
erb :'mockup/profile', locals: {site: current_site}
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/mockup/browse' do
|
||||||
|
erb :'mockup/browse'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/mockup/tips' do
|
||||||
|
erb :'mockup/tips'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/mockup/welcome' do
|
||||||
|
require_login
|
||||||
|
erb :'mockup/welcome', locals: {site: current_site}
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/mockup/stats' do
|
||||||
|
require_login
|
||||||
|
erb :'mockup/stats', locals: {site: current_site}
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/mockup/tutorial-c1p2' do
|
||||||
|
require_login
|
||||||
|
erb :'mockup/tutorial-c1p2', locals: {site: current_site}
|
||||||
|
end
|
||||||
|
=end
|
||||||
99
app/password_reset.rb
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
get '/password_reset' do
|
||||||
|
@title = 'Password Reset'
|
||||||
|
@description = 'Reset your Neocities account password.'
|
||||||
|
redirect '/' if signed_in?
|
||||||
|
erb :'password_reset'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/send_password_reset' do
|
||||||
|
if params[:email].blank?
|
||||||
|
flash[:error] = 'You must enter a valid email address.'
|
||||||
|
redirect '/password_reset'
|
||||||
|
end
|
||||||
|
|
||||||
|
sites = Site.get_recovery_sites_with_email params[:email]
|
||||||
|
|
||||||
|
sites.each do |site|
|
||||||
|
if site.is_banned
|
||||||
|
flash[:error] = 'Sorry, we cannot restore this account.'
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if sites.length > 0
|
||||||
|
token = SecureRandom.uuid.gsub('-', '')+'-'+Time.now.to_i.to_s
|
||||||
|
sites.each do |site|
|
||||||
|
next unless site.parent?
|
||||||
|
site.password_reset_token = token
|
||||||
|
site.save_changes validate: false
|
||||||
|
|
||||||
|
body = <<-EOT
|
||||||
|
Hello! This is the Penelope the Neocities cat, and I have received a password reset request for your e-mail address.
|
||||||
|
|
||||||
|
Go to this URL to reset your password: https://neocities.org/password_reset_confirm?username=#{Rack::Utils.escape(site.username)}&token=#{Rack::Utils.escape(token)}
|
||||||
|
|
||||||
|
This link will expire in 24 hours.
|
||||||
|
|
||||||
|
If you didn't request this password reset, you can ignore it. Or hide under a bed. Or take a nap. Your call.
|
||||||
|
|
||||||
|
Meow,
|
||||||
|
Penelope
|
||||||
|
EOT
|
||||||
|
|
||||||
|
body.strip!
|
||||||
|
|
||||||
|
EmailWorker.perform_async({
|
||||||
|
from: 'web@neocities.org',
|
||||||
|
to: params[:email],
|
||||||
|
subject: '[Neocities] Password Reset',
|
||||||
|
body: body
|
||||||
|
})
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[:success] = "We sent an e-mail with password reset instructions. Check your spam folder if you don't see it in your inbox."
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/password_reset_confirm' do
|
||||||
|
@title = 'Password Reset Confirm'
|
||||||
|
|
||||||
|
if params[:token].nil? || params[:token].strip.empty?
|
||||||
|
flash[:error] = 'Token cannot be empty.'
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
reset_site = Site.where(username: params[:username], password_reset_token: params[:token]).first
|
||||||
|
|
||||||
|
if reset_site.nil?
|
||||||
|
flash[:error] = 'Could not find a site with this username and token.'
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
timestamp = Time.at(reset_site.password_reset_token.split('-').last.to_i)
|
||||||
|
|
||||||
|
if Time.now.to_i - timestamp.to_i > Site::PASSWORD_RESET_EXPIRATION_TIME
|
||||||
|
flash[:error] = 'Token has expired.'
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
if reset_site.is_banned
|
||||||
|
flash[:error] = 'Sorry, we cannot restore this account.'
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
if reset_site.is_deleted
|
||||||
|
unless reset_site.undelete!
|
||||||
|
flash[:error] = 'Sorry, we cannot restore this account.'
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
reset_site.password_reset_confirmed = true
|
||||||
|
reset_site.save_changes
|
||||||
|
|
||||||
|
session[:id] = reset_site.id
|
||||||
|
|
||||||
|
redirect '/settings#password'
|
||||||
|
end
|
||||||
3
app/plan.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
get '/plan/?' do
|
||||||
|
redirect '/supporter'
|
||||||
|
end
|
||||||
82
app/search.rb
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
SEARCH_URL = $config['search_url']
|
||||||
|
SEARCH_CONNECT_TIMEOUT = $config['search_connect_timeout']
|
||||||
|
SEARCH_WRITE_TIMEOUT = $config['search_write_timeout']
|
||||||
|
SEARCH_READ_TIMEOUT = $config['search_read_timeout']
|
||||||
|
|
||||||
|
post '/search/?' do
|
||||||
|
query = params[:q].to_s.strip
|
||||||
|
redirect query.blank? ? '/search' : "/search?#{Rack::Utils.build_query(q: query)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/search/?' do
|
||||||
|
@query = params[:q].to_s.strip
|
||||||
|
@title = @query.blank? ? 'Neocities Search' : 'Site Search'
|
||||||
|
@description = 'Search websites hosted on Neocities.'
|
||||||
|
|
||||||
|
if !@query.blank?
|
||||||
|
@start = params[:start].to_i
|
||||||
|
@start = 0 if @start < 0
|
||||||
|
|
||||||
|
@items = []
|
||||||
|
@total_results = 0
|
||||||
|
|
||||||
|
begin
|
||||||
|
@resp = JSON.parse HTTP.timeout(connect: SEARCH_CONNECT_TIMEOUT, write: SEARCH_WRITE_TIMEOUT, read: SEARCH_READ_TIMEOUT).get(SEARCH_URL, params: {
|
||||||
|
num: 100,
|
||||||
|
start: @start,
|
||||||
|
q: Rack::Utils.escape(@query) + ' -filetype:pdf -filetype:txt site:*.neocities.org'
|
||||||
|
}).to_s
|
||||||
|
rescue HTTP::Error, JSON::ParserError
|
||||||
|
@resp = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
if @resp.is_a?(Hash) && @resp['error'].nil? && @resp.dig('searchInformation', 'totalResults').to_i != 0
|
||||||
|
@total_results = @resp['searchInformation']['totalResults'].to_i
|
||||||
|
@resp['items'].each do |item|
|
||||||
|
link = Addressable::URI.parse(item['link'])
|
||||||
|
path = link.path || '/'
|
||||||
|
unencoded_path = begin
|
||||||
|
Rack::Utils.unescape(Rack::Utils.unescape(path)) # Yes, it needs to be decoded twice
|
||||||
|
rescue ArgumentError
|
||||||
|
path # Fall back when the path includes invalid %-encoding
|
||||||
|
end
|
||||||
|
item['unencoded_link'] = unencoded_path == '/' ? link.host : link.host+unencoded_path
|
||||||
|
item['link'] = link
|
||||||
|
|
||||||
|
next if link.host == 'neocities.org'
|
||||||
|
|
||||||
|
username = link.host.split('.').first
|
||||||
|
site = Site[username: username]
|
||||||
|
next if site.nil? || site.is_deleted || site.is_nsfw
|
||||||
|
item['username'] = username if site.profile_enabled
|
||||||
|
|
||||||
|
screenshot_path = unencoded_path
|
||||||
|
|
||||||
|
screenshot_path << 'index' if screenshot_path[-1] == '/'
|
||||||
|
|
||||||
|
if ENV['RACK_ENV'] == 'development'
|
||||||
|
screenshot_path += '.html'
|
||||||
|
screenshot_found = true
|
||||||
|
else
|
||||||
|
['.html', '.htm'].each do |ext|
|
||||||
|
if site.screenshot_exists?(screenshot_path + ext, '540x405')
|
||||||
|
screenshot_path += ext
|
||||||
|
screenshot_found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
item['screenshot_url'] = site.screenshot_url(screenshot_path, '540x405')
|
||||||
|
@items << item if screenshot_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@items = nil
|
||||||
|
@total_results = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
erb :'search'
|
||||||
|
end
|
||||||
446
app/settings.rb
Normal file
|
|
@ -0,0 +1,446 @@
|
||||||
|
require 'socket'
|
||||||
|
require 'ipaddr'
|
||||||
|
|
||||||
|
get '/settings/?' do
|
||||||
|
require_login
|
||||||
|
@site = parent_site
|
||||||
|
erb :'settings/account'
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_ownership_for_settings
|
||||||
|
@site = Site[username: params[:username]]
|
||||||
|
|
||||||
|
not_found if @site.nil?
|
||||||
|
|
||||||
|
unless @site.owned_by? parent_site
|
||||||
|
flash[:error] = 'Cannot edit this site, you do not have permission.'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/settings/invoices/?' do
|
||||||
|
require_login
|
||||||
|
@title = 'Invoices'
|
||||||
|
@invoices = parent_site.stripe_customer_id ? Stripe::Invoice.list(customer: parent_site.stripe_customer_id) : []
|
||||||
|
erb :'settings/invoices'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/settings/:username/?' do |username|
|
||||||
|
# This is for the email_unsubscribe below
|
||||||
|
pass if Site.select(:id).where(username: username).first.nil?
|
||||||
|
require_login
|
||||||
|
require_ownership_for_settings
|
||||||
|
|
||||||
|
@title = "Site settings for #{username}"
|
||||||
|
erb :'settings/site'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/:username/delete' do
|
||||||
|
require_login
|
||||||
|
require_ownership_for_settings
|
||||||
|
|
||||||
|
if params[:confirm_username] != @site.username
|
||||||
|
flash[:error] = 'Site user name and entered user name did not match.'
|
||||||
|
redirect "/settings/#{@site.username}#delete"
|
||||||
|
end
|
||||||
|
|
||||||
|
@site.deleted_reason = params[:deleted_reason]
|
||||||
|
@site.save validate: false
|
||||||
|
@site.destroy
|
||||||
|
|
||||||
|
flash[:success] = 'Site deleted.'
|
||||||
|
|
||||||
|
if @site.username == current_site.username
|
||||||
|
signout
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect '/settings#sites'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/:username/profile' do
|
||||||
|
require_login
|
||||||
|
require_ownership_for_settings
|
||||||
|
|
||||||
|
@site.update(
|
||||||
|
profile_comments_enabled: params[:site][:profile_comments_enabled],
|
||||||
|
profile_enabled: params[:site][:profile_enabled]
|
||||||
|
)
|
||||||
|
flash[:success] = 'Profile settings changed.'
|
||||||
|
redirect "/settings/#{@site.username}#profile"
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/:username/tags/add' do
|
||||||
|
require_login
|
||||||
|
require_ownership_for_settings
|
||||||
|
|
||||||
|
@site.new_tags_string = params[:tags]
|
||||||
|
|
||||||
|
if @site.valid?
|
||||||
|
@site.save_tags
|
||||||
|
flash[:success] = 'Tags updated.'
|
||||||
|
else
|
||||||
|
flash[:error] = @site.errors.first.last.first
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect "/settings/#{@site.username}#tags"
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/:username/tags/remove' do
|
||||||
|
require_login
|
||||||
|
require_ownership_for_settings
|
||||||
|
|
||||||
|
removed = false
|
||||||
|
|
||||||
|
if params[:tags].is_a?(Array)
|
||||||
|
DB.transaction {
|
||||||
|
params[:tags].each do |tag|
|
||||||
|
tag_to_remove = @site.tags.detect {|t| t.name == tag }
|
||||||
|
if tag_to_remove
|
||||||
|
@site.remove_tag(tag_to_remove)
|
||||||
|
removed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[:success] = removed ? 'Tags updated.' : 'No tags selected.'
|
||||||
|
|
||||||
|
redirect "/settings/#{@site.username}#tags"
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/:username/change_name' do
|
||||||
|
require_login
|
||||||
|
require_ownership_for_settings
|
||||||
|
|
||||||
|
old_site = Site[username: @site.username]
|
||||||
|
old_username = @site.username
|
||||||
|
|
||||||
|
if params[:name] == nil || params[:name] == ''
|
||||||
|
flash[:error] = 'Name cannot be blank.'
|
||||||
|
redirect "/settings/#{@site.username}#username"
|
||||||
|
end
|
||||||
|
|
||||||
|
if old_username.downcase == params[:name].downcase
|
||||||
|
flash[:error] = 'You already have this name.'
|
||||||
|
redirect "/settings/#{@site.username}#username"
|
||||||
|
end
|
||||||
|
|
||||||
|
old_host = @site.host
|
||||||
|
old_site_file_paths = @site.site_files.collect {|site_file| site_file.path}
|
||||||
|
|
||||||
|
@site.username = params[:name]
|
||||||
|
|
||||||
|
if @site.valid?
|
||||||
|
DB.transaction {
|
||||||
|
@site.save_changes
|
||||||
|
@site.move_files_from old_username
|
||||||
|
}
|
||||||
|
|
||||||
|
old_site.delete_all_thumbnails_and_screenshots
|
||||||
|
old_site.purge_all_cache
|
||||||
|
@site.purge_all_cache
|
||||||
|
@site.regenerate_thumbnails_and_screenshots
|
||||||
|
|
||||||
|
flash[:success] = "Site/user name has been changed. You will need to use this name to login, <b>don't forget it!</b>"
|
||||||
|
redirect "/settings/#{@site.username}#username"
|
||||||
|
else
|
||||||
|
flash[:error] = @site.errors.first.last.first
|
||||||
|
redirect "/settings/#{old_username}#username"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/:username/tipping' do
|
||||||
|
require_login
|
||||||
|
require_ownership_for_settings
|
||||||
|
|
||||||
|
current_site.tipping_enabled = params[:site][:tipping_enabled]
|
||||||
|
current_site.tipping_paypal = params[:site][:tipping_paypal]
|
||||||
|
current_site.tipping_bitcoin = params[:site][:tipping_bitcoin]
|
||||||
|
|
||||||
|
if current_site.valid?
|
||||||
|
current_site.save_changes
|
||||||
|
flash[:success] = "Tip settings have been updated."
|
||||||
|
else
|
||||||
|
flash[:error] = current_site.errors.first.last.first
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect "/settings/#{current_site.username}#tipping"
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/:username/change_nsfw' do
|
||||||
|
require_login
|
||||||
|
require_ownership_for_settings
|
||||||
|
|
||||||
|
redirect "/settings/#{@site.username}" if @site.admin_nsfw == true
|
||||||
|
|
||||||
|
@site.is_nsfw = params[:is_nsfw]
|
||||||
|
@site.save_changes validate: false
|
||||||
|
flash[:success] = @site.is_nsfw ? 'Marked 18+' : 'Unmarked 18+'
|
||||||
|
redirect "/settings/#{@site.username}#nsfw"
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/:username/custom_domain' do
|
||||||
|
require_login
|
||||||
|
require_ownership_for_settings
|
||||||
|
|
||||||
|
original_domain = @site.domain
|
||||||
|
@site.domain = params[:domain]
|
||||||
|
|
||||||
|
if params[:domain] =~ /^www\..+$/i
|
||||||
|
flash[:error] = 'Cannot begin with www - please only enter the domain name.'
|
||||||
|
redirect "/settings/#{@site.username}/#custom_domain"
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
addr = IPAddr.new @site.values[:domain]
|
||||||
|
if addr.ipv4? || addr.ipv6?
|
||||||
|
flash[:error] = 'IP addresses are not allowed. Please enter a valid domain name.'
|
||||||
|
redirect "/settings/#{@site.username}#custom_domain"
|
||||||
|
end
|
||||||
|
rescue IPAddr::InvalidAddressError
|
||||||
|
end
|
||||||
|
|
||||||
|
unless self.class.test?
|
||||||
|
begin
|
||||||
|
Socket.gethostbyname @site.values[:domain]
|
||||||
|
rescue SocketError, ResolutionError => e
|
||||||
|
flash[:error] = "The domain isn't setup to use Neocities yet, cannot add. Please make the A and CNAME record changes where you registered your domain."
|
||||||
|
redirect "/settings/#{@site.username}#custom_domain"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if @site.valid?
|
||||||
|
@site.save_changes
|
||||||
|
|
||||||
|
if @site.domain != original_domain
|
||||||
|
LetsEncryptWorker.perform_async @site.id
|
||||||
|
# Sometimes the www record isn't ready for some reason, so try a delay to fix that.
|
||||||
|
LetsEncryptWorker.perform_in 40.minutes, @site.id
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[:success] = 'The domain has been successfully updated! Make sure your configuration with the domain registrar is correct. It could take a while for the changes to take effect (15-40 minutes), please be patient.'
|
||||||
|
redirect "/settings/#{@site.username}#custom_domain"
|
||||||
|
else
|
||||||
|
flash[:error] = @site.errors.first.last.first
|
||||||
|
redirect "/settings/#{@site.username}#custom_domain"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/:username/bluesky_set_did' do
|
||||||
|
require_login
|
||||||
|
require_ownership_for_settings
|
||||||
|
|
||||||
|
# todo standards based validation
|
||||||
|
if params[:did].length > 50
|
||||||
|
flash[:error] = 'DID provided was too long'
|
||||||
|
elsif !params[:did].match(/^did:plc:([a-z|0-9)]+)$/)
|
||||||
|
flash[:error] = 'DID was invalid'
|
||||||
|
else
|
||||||
|
tmpfile = Tempfile.new 'atproto-did'
|
||||||
|
tmpfile.write params[:did]
|
||||||
|
tmpfile.close
|
||||||
|
|
||||||
|
@site.store_files [{filename: '.well-known/atproto-did', tempfile: tmpfile}]
|
||||||
|
$redis_proxy.hdel "dns-_atproto.#{@site.username}.neocities.org", 'TXT'
|
||||||
|
flash[:success] = 'DID set! You can now verify the handle on the Bluesky app.'
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect "/settings/#{@site.username}#bluesky"
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/:username/generate_api_key' do
|
||||||
|
require_login
|
||||||
|
require_ownership_for_settings
|
||||||
|
is_new = @site.api_key.nil?
|
||||||
|
@site.generate_api_key!
|
||||||
|
|
||||||
|
msg = is_new ? "New API key has been generated." : "API key has been regenerated."
|
||||||
|
flash[:success] = msg
|
||||||
|
redirect "/settings/#{@site.username}#api_key"
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/change_password' do
|
||||||
|
require_login
|
||||||
|
|
||||||
|
if !current_site.password_reset_confirmed && !Site.valid_login?(parent_site.username, params[:current_password])
|
||||||
|
flash[:error] = 'Your provided password does not match the current one.'
|
||||||
|
redirect "/settings#password"
|
||||||
|
end
|
||||||
|
|
||||||
|
parent_site.password = params[:new_password]
|
||||||
|
parent_site.valid?
|
||||||
|
|
||||||
|
if params[:new_password] != params[:new_password_confirm]
|
||||||
|
parent_site.errors.add :password, 'New passwords do not match.'
|
||||||
|
end
|
||||||
|
|
||||||
|
parent_site.password_reset_token = nil
|
||||||
|
parent_site.password_reset_confirmed = false
|
||||||
|
|
||||||
|
if parent_site.errors.empty?
|
||||||
|
parent_site.save_changes
|
||||||
|
|
||||||
|
parent_site.send_email(
|
||||||
|
subject: "[Neocities] Your password has been changed",
|
||||||
|
body: Tilt.new('./views/templates/email/password_changed.erb', pretty: true).render(self)
|
||||||
|
)
|
||||||
|
|
||||||
|
flash[:success] = 'Successfully changed password.'
|
||||||
|
redirect "/settings#password"
|
||||||
|
else
|
||||||
|
flash[:error] = current_site.errors.first.last.first
|
||||||
|
redirect '/settings#password'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/change_email' do
|
||||||
|
require_login
|
||||||
|
|
||||||
|
if params[:from_confirm]
|
||||||
|
redirect_url = "/site/#{parent_site.username}/confirm_email"
|
||||||
|
else
|
||||||
|
redirect_url = '/settings#email'
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:email] == parent_site.email
|
||||||
|
flash[:error] = 'You are already using this email address for this account.'
|
||||||
|
redirect redirect_url
|
||||||
|
end
|
||||||
|
|
||||||
|
previous_email = parent_site.email
|
||||||
|
parent_site.email = params[:email]
|
||||||
|
parent_site.email_confirmation_token = SecureRandom.hex 3
|
||||||
|
parent_site.email_confirmed = false
|
||||||
|
parent_site.password_reset_token = nil
|
||||||
|
|
||||||
|
if parent_site.valid?
|
||||||
|
parent_site.save_changes
|
||||||
|
send_confirmation_email
|
||||||
|
|
||||||
|
parent_site.send_email(
|
||||||
|
subject: "[Neocities] Your email address has been changed",
|
||||||
|
body: Tilt.new('./views/templates/email/email_changed.erb', pretty: true).render(self, site: parent_site, previous_email: previous_email)
|
||||||
|
)
|
||||||
|
|
||||||
|
if !parent_site.supporter?
|
||||||
|
session[:fromsettings] = true
|
||||||
|
redirect "/site/#{parent_site.email}/confirm_email"
|
||||||
|
else
|
||||||
|
flash[:success] = 'Email address changed.'
|
||||||
|
redirect '/settings#email'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[:error] = parent_site.errors.first.last.first
|
||||||
|
redirect redirect_url
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/change_email_notification' do
|
||||||
|
require_login
|
||||||
|
|
||||||
|
owner = current_site.owner
|
||||||
|
|
||||||
|
owner.send_emails = params[:send_emails]
|
||||||
|
owner.send_comment_emails = params[:send_comment_emails]
|
||||||
|
owner.send_follow_emails = params[:send_follow_emails]
|
||||||
|
owner.email_invoice = params[:email_invoice]
|
||||||
|
owner.save_changes validate: false
|
||||||
|
flash[:success] = 'Email notification settings have been updated.'
|
||||||
|
redirect '/settings#email'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/change_editor_settings' do
|
||||||
|
require_login
|
||||||
|
|
||||||
|
owner = current_site.owner
|
||||||
|
|
||||||
|
owner.editor_autocomplete_enabled = params[:editor_autocomplete_enabled]
|
||||||
|
owner.editor_font_size = params[:editor_font_size]
|
||||||
|
owner.editor_keyboard_mode = params[:editor_keyboard_mode]
|
||||||
|
owner.editor_tab_width = params[:editor_tab_width]
|
||||||
|
owner.editor_help_tooltips = params[:editor_help_tooltips]
|
||||||
|
owner.save_changes validate: false
|
||||||
|
|
||||||
|
@filename = params[:path]
|
||||||
|
redirect '/site_files/text_editor?filename=' + Rack::Utils.escape(@filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/create_child' do
|
||||||
|
require_login
|
||||||
|
|
||||||
|
if !current_site.plan_feature(:unlimited_site_creation)
|
||||||
|
flash[:error] = 'Cannot create a new site with your current plan, please become a supporter.'
|
||||||
|
redirect '/settings#sites'
|
||||||
|
end
|
||||||
|
|
||||||
|
site = Site.new
|
||||||
|
|
||||||
|
site.parent_site_id = parent_site.id
|
||||||
|
site.username = params[:username]
|
||||||
|
|
||||||
|
if site.valid?
|
||||||
|
site.save
|
||||||
|
flash[:success] = 'Your new site has been created! To manage it, click your username in the top right and go to "Switch Site".'
|
||||||
|
redirect '/settings#sites'
|
||||||
|
else
|
||||||
|
flash[:error] = site.errors.first.last.first
|
||||||
|
redirect '/settings#sites'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/settings/unsubscribe_email/?' do
|
||||||
|
redirect "/settings/#email" if signed_in?
|
||||||
|
|
||||||
|
if params[:email] && params[:token] && params[:email] != '' && Site.valid_email_unsubscribe_token?(params[:email], params[:token])
|
||||||
|
Site.where(email: params[:email]).all.each do |site|
|
||||||
|
site.send_emails = false
|
||||||
|
site.save_changes validate: false
|
||||||
|
end
|
||||||
|
|
||||||
|
@message = "You have been successfully unsubscribed from future emails to #{params[:email]}. Our apologies for the inconvenience."
|
||||||
|
else
|
||||||
|
@message = 'There was an error unsubscribing your email address. Please contact support.'
|
||||||
|
end
|
||||||
|
erb :'settings/account/unsubscribe'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/settings/update_card' do
|
||||||
|
require_login
|
||||||
|
|
||||||
|
customer = Stripe::Customer.retrieve parent_site.stripe_customer_id
|
||||||
|
|
||||||
|
old_card_ids = customer.sources.collect {|s| s.id}
|
||||||
|
|
||||||
|
begin
|
||||||
|
customer.sources.create source: params[:stripe_token]
|
||||||
|
rescue Stripe::InvalidRequestError, Stripe::CardError => e
|
||||||
|
case e.message
|
||||||
|
when /cannot use a.+token more than once/
|
||||||
|
flash[:error] = 'This card is already being used.'
|
||||||
|
when /Your card has expired/
|
||||||
|
flash[:error] = 'Your card has expired. Please try a different card.'
|
||||||
|
when /Your card was declined/
|
||||||
|
flash[:error] = 'Your card was declined. Please contact your bank or try a different card.'
|
||||||
|
when /security code is incorrect/
|
||||||
|
flash[:error] = 'The security code (CVV) you entered is incorrect. Please try again.'
|
||||||
|
when /card number is incorrect/
|
||||||
|
flash[:error] = 'The card number you entered is incorrect. Please check and try again.'
|
||||||
|
when /insufficient funds/
|
||||||
|
flash[:error] = 'Your card has insufficient funds. Please try a different card.'
|
||||||
|
when /does not support this type of purchase/
|
||||||
|
flash[:error] = 'Your card does not support online purchases. Please try a different card.'
|
||||||
|
else
|
||||||
|
# Fallback to Stripe's message for uncommon errors
|
||||||
|
flash[:error] = e.message
|
||||||
|
end
|
||||||
|
redirect '/settings#billing'
|
||||||
|
end
|
||||||
|
|
||||||
|
old_card_ids.each do |card_id|
|
||||||
|
customer.sources.retrieve(card_id).delete
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[:success] = 'Card information updated.'
|
||||||
|
redirect '/settings#billing'
|
||||||
|
end
|
||||||
82
app/signin.rb
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
get '/signin/?' do
|
||||||
|
dashboard_if_signed_in
|
||||||
|
@title = 'Sign In'
|
||||||
|
@description = 'Sign in to your Neocities account.'
|
||||||
|
erb :'signin/index'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/signin' do
|
||||||
|
dashboard_if_signed_in
|
||||||
|
|
||||||
|
if Site.valid_login? params[:username], params[:password]
|
||||||
|
site = Site.get_with_identifier params[:username]
|
||||||
|
|
||||||
|
if site.is_banned
|
||||||
|
flash[:error] = 'Invalid login.'
|
||||||
|
flash[:username] = params[:username]
|
||||||
|
redirect '/signin'
|
||||||
|
end
|
||||||
|
|
||||||
|
if site.is_deleted && !site.is_banned
|
||||||
|
session[:deleted_site_id] = site.id
|
||||||
|
redirect '/signin/restore'
|
||||||
|
end
|
||||||
|
|
||||||
|
session[:id] = site.id
|
||||||
|
redirect '/'
|
||||||
|
else
|
||||||
|
flash[:error] = 'Invalid login.'
|
||||||
|
flash[:username] = params[:username]
|
||||||
|
redirect '/signin'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/signin/restore' do
|
||||||
|
redirect '/' unless session[:deleted_site_id]
|
||||||
|
@site = Site[session[:deleted_site_id]]
|
||||||
|
redirect '/' if @site.nil?
|
||||||
|
@title = 'Restore Deleted Site'
|
||||||
|
erb :'signin/restore'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/signin/cancel_restore' do
|
||||||
|
session[:deleted_site_id] = nil
|
||||||
|
flash[:success] = 'Site restore was cancelled.'
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/signin/restore' do
|
||||||
|
redirect '/' unless session[:deleted_site_id]
|
||||||
|
@site = Site[session[:deleted_site_id]]
|
||||||
|
|
||||||
|
session[:deleted_site_id] = nil
|
||||||
|
|
||||||
|
if @site.is_banned || !@site.undelete!
|
||||||
|
flash[:error] = "Sorry, we cannot restore this account."
|
||||||
|
else
|
||||||
|
session[:id] = @site.id
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/signin/:username' do
|
||||||
|
require_login
|
||||||
|
@site = Site[username: params[:username]]
|
||||||
|
|
||||||
|
not_found if @site.nil?
|
||||||
|
|
||||||
|
if @site.owned_by? current_site
|
||||||
|
session[:id] = @site.id
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[:error] = 'You do not have permission to switch to this site.'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/signout' do
|
||||||
|
require_login
|
||||||
|
signout
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
403
app/site.rb
Normal file
|
|
@ -0,0 +1,403 @@
|
||||||
|
get '/site/:username.rss' do |username|
|
||||||
|
site = Site[username: username]
|
||||||
|
halt 404 if site.nil? || (current_site && site.is_blocking?(current_site))
|
||||||
|
content_type :xml
|
||||||
|
site.to_rss
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/site/:username/?' do |username|
|
||||||
|
site = Site[username: username]
|
||||||
|
# TODO: There should probably be a "this site was deleted" page.
|
||||||
|
not_found if site.nil? || site.is_banned || site.is_deleted || (current_site && site.is_blocking?(current_site))
|
||||||
|
|
||||||
|
redirect '/' if site.is_education
|
||||||
|
|
||||||
|
redirect site.uri unless site.profile_enabled
|
||||||
|
|
||||||
|
@title = site.title
|
||||||
|
@description = "#{site.username}'s Neocities site profile, updates, and comments."
|
||||||
|
|
||||||
|
@page = params[:page]
|
||||||
|
@page = 1 if @page.not_an_integer?
|
||||||
|
|
||||||
|
if params[:event_id]
|
||||||
|
not_found if params[:event_id].not_an_integer?
|
||||||
|
not_found if params[:event_id].to_i > 2_147_483_647 # max integer
|
||||||
|
event = Event.where(id: params[:event_id]).exclude(is_deleted: true).first
|
||||||
|
not_found if event.nil?
|
||||||
|
event_site = event.site
|
||||||
|
event_actioning_site = event.actioning_site
|
||||||
|
not_found if current_site && event_site && event_site.is_blocking?(current_site)
|
||||||
|
not_found if current_site && event_actioning_site && event_actioning_site.is_blocking?(current_site)
|
||||||
|
events_dataset = Event.where(id: params[:event_id]).paginate(1, 1)
|
||||||
|
else
|
||||||
|
events_dataset = site.latest_events(@page, current_site)
|
||||||
|
end
|
||||||
|
|
||||||
|
@page_count = events_dataset.page_count || 1
|
||||||
|
@pagination_dataset = events_dataset
|
||||||
|
@latest_events = events_dataset.all
|
||||||
|
|
||||||
|
meta_robots 'noindex, follow'
|
||||||
|
|
||||||
|
erb :'site', locals: {site: site, is_current_site: site == current_site}
|
||||||
|
end
|
||||||
|
|
||||||
|
MAX_STAT_POINTS = 30
|
||||||
|
get '/site/:username/stats' do
|
||||||
|
@default_stat_points = 7
|
||||||
|
@site = Site[username: params[:username]]
|
||||||
|
not_found if @site.nil? || @site.is_banned || @site.is_deleted || (current_site && @site.is_blocking?(current_site))
|
||||||
|
|
||||||
|
@title = "Site stats for #{@site.host}"
|
||||||
|
@description = "Traffic and engagement stats for #{@site.username}'s Neocities site."
|
||||||
|
|
||||||
|
@stats = {}
|
||||||
|
|
||||||
|
%i{referrers locations paths}.each do |stat|
|
||||||
|
@stats[stat] = @site.send("stat_#{stat}_dataset".to_sym).order(:views.desc).limit(100).all
|
||||||
|
end
|
||||||
|
|
||||||
|
@stats[:locations].collect! do |location|
|
||||||
|
location_name = ''
|
||||||
|
|
||||||
|
location_name += location.city_name if location.city_name
|
||||||
|
|
||||||
|
if location.region_name
|
||||||
|
# Some of the region names are numbers for some reason.
|
||||||
|
begin
|
||||||
|
Integer(location.region_name)
|
||||||
|
rescue
|
||||||
|
location_name += ', ' unless location_name == ''
|
||||||
|
location_name += location.region_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if location.country_code2 && !$country_codes[location.country_code2].nil?
|
||||||
|
location_name += ', ' unless location_name == ''
|
||||||
|
location_name += $country_codes[location.country_code2]
|
||||||
|
end
|
||||||
|
|
||||||
|
location_hash = {name: location_name, views: location.views}
|
||||||
|
if location.latitude && location.longitude
|
||||||
|
location_hash.merge! latitude: location.latitude, longitude: location.longitude
|
||||||
|
end
|
||||||
|
location_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
stats_dataset = @site.stats_dataset.order(:created_at.desc).exclude(created_at: Date.today)
|
||||||
|
|
||||||
|
if @site.supporter?
|
||||||
|
unless params[:days].to_s == 'sincethebigbang'
|
||||||
|
unless params[:days].not_an_integer?
|
||||||
|
days_param = params[:days].to_i
|
||||||
|
if days_param < 9000
|
||||||
|
stats_dataset = stats_dataset.limit days_param
|
||||||
|
else
|
||||||
|
params[:days] = 'sincethebigbang'
|
||||||
|
end
|
||||||
|
else
|
||||||
|
params[:days] = @default_stat_points
|
||||||
|
stats_dataset = stats_dataset.limit @default_stat_points
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
stats_dataset = stats_dataset.limit @default_stat_points
|
||||||
|
end
|
||||||
|
|
||||||
|
stats = stats_dataset.all.reverse
|
||||||
|
|
||||||
|
if current_site && @site.owned_by?(current_site) && params[:format] == 'csv'
|
||||||
|
content_type 'application/csv'
|
||||||
|
attachment "#{current_site.username}-stats.csv"
|
||||||
|
|
||||||
|
return CSV.generate do |csv|
|
||||||
|
csv << ['day', 'hits', 'views', 'bandwidth']
|
||||||
|
stats.each do |s|
|
||||||
|
csv << [s[:created_at].to_s, s[:hits], s[:views], s[:bandwidth]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if stats.length > MAX_STAT_POINTS
|
||||||
|
stats = stats.select.with_index {|a, i| (i % (stats.length / MAX_STAT_POINTS.to_f).round) == 0}
|
||||||
|
end
|
||||||
|
|
||||||
|
@stats[:stat_days] = stats
|
||||||
|
@multi_tooltip_template = "<%= datasetLabel %> - <%= value %>"
|
||||||
|
|
||||||
|
erb :'site/stats', locals: {site: @site}
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/site/:username/set_editor_theme' do
|
||||||
|
require_login
|
||||||
|
current_site.editor_theme = params[:editor_theme]
|
||||||
|
current_site.save_changes validate: false
|
||||||
|
'ok'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/site/:username/follows' do |username|
|
||||||
|
@title = "Sites #{username} follows"
|
||||||
|
@description = "See which Neocities sites #{username} follows."
|
||||||
|
@site = Site[username: username]
|
||||||
|
not_found if @site.nil? || @site.is_deleted || (current_site && (@site.is_blocking?(current_site) || current_site.is_blocking?(@site)))
|
||||||
|
|
||||||
|
params[:page] ||= "1"
|
||||||
|
|
||||||
|
@pagination_dataset = @site.followings_dataset.paginate(params[:page].to_i, Site::FOLLOW_PAGINATION_LIMIT)
|
||||||
|
erb :'site/follows'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/site/:username/followers' do |username|
|
||||||
|
@title = "Sites that follow #{username}"
|
||||||
|
@description = "See which Neocities sites follow #{username}."
|
||||||
|
@site = Site[username: username]
|
||||||
|
not_found if @site.nil? || @site.is_deleted || (current_site && (@site.is_blocking?(current_site) || current_site.is_blocking?(@site)))
|
||||||
|
|
||||||
|
params[:page] ||= "1"
|
||||||
|
|
||||||
|
@pagination_dataset = @site.follows_dataset.paginate(params[:page].to_i, Site::FOLLOW_PAGINATION_LIMIT)
|
||||||
|
erb :'site/followers'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/site/:username/comment' do |username|
|
||||||
|
require_login
|
||||||
|
|
||||||
|
site = Site[username: username]
|
||||||
|
message = normalize_comment_message(params[:message])
|
||||||
|
message_error = comment_message_error(message)
|
||||||
|
|
||||||
|
if current_site && (site.is_blocking?(current_site) || current_site.is_blocking?(site))
|
||||||
|
flash[:error] = comment_unavailable_message(site)
|
||||||
|
redirect request.referer
|
||||||
|
end
|
||||||
|
|
||||||
|
last_comment = site.profile_comments_dataset.order(:created_at.desc).first
|
||||||
|
|
||||||
|
if last_comment && last_comment.message == message && last_comment.created_at > 2.hours.ago
|
||||||
|
flash[:error] = 'You already posted that comment.'
|
||||||
|
redirect request.referer
|
||||||
|
end
|
||||||
|
|
||||||
|
if site.profile_comments_enabled == false ||
|
||||||
|
message_error ||
|
||||||
|
site.is_blocking?(current_site) ||
|
||||||
|
current_site.is_blocking?(site) ||
|
||||||
|
current_site.commenting_allowed? == false ||
|
||||||
|
(current_site.is_a_jerk? && site.id != current_site.id && !site.is_following?(current_site))
|
||||||
|
flash[:error] = message_error || comment_unavailable_message(site)
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
site.add_profile_comment(
|
||||||
|
actioning_site_id: current_site.id,
|
||||||
|
message: message
|
||||||
|
)
|
||||||
|
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/site/:site_id/toggle_follow' do |site_id|
|
||||||
|
require_login
|
||||||
|
content_type :json
|
||||||
|
site = Site[id: site_id]
|
||||||
|
return 404 if site.nil?
|
||||||
|
return 403 if site.is_blocking?(current_site)
|
||||||
|
{result: (current_site.toggle_follow(site) ? 'followed' : 'unfollowed')}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/site/:username/confirm_email/:token' do
|
||||||
|
@title = 'Confirm email'
|
||||||
|
|
||||||
|
if current_site && current_site.email_confirmed
|
||||||
|
return erb(:'site_email_confirmed')
|
||||||
|
end
|
||||||
|
|
||||||
|
site = Site[username: params[:username]]
|
||||||
|
|
||||||
|
if site.nil?
|
||||||
|
return erb(:'site_email_not_confirmed')
|
||||||
|
end
|
||||||
|
|
||||||
|
if site.email_confirmed
|
||||||
|
return erb(:'site_email_confirmed')
|
||||||
|
end
|
||||||
|
|
||||||
|
if site.email_confirmation_token == params[:token]
|
||||||
|
site.email_confirmation_token = nil
|
||||||
|
site.email_confirmation_count = 0
|
||||||
|
site.email_confirmed = true
|
||||||
|
site.save_changes
|
||||||
|
|
||||||
|
erb :'site_email_confirmed'
|
||||||
|
else
|
||||||
|
erb :'site_email_not_confirmed'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/site/:username/confirm_email' do
|
||||||
|
require_login
|
||||||
|
@title = 'Confirm your Email Address'
|
||||||
|
@fromsettings = session[:fromsettings]
|
||||||
|
redirect '/' if current_site.username != params[:username] || !current_site.parent? || current_site.email_confirmed
|
||||||
|
erb :'site/confirm_email'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/site/:username/confirm_email/resend' do
|
||||||
|
require_login
|
||||||
|
|
||||||
|
redirect '/' if current_site.username != params[:username] || !current_site.parent? || current_site.email_confirmed
|
||||||
|
|
||||||
|
request.env['HTTP_REFERER'] ||= "/site/#{current_site.username}/confirm_email"
|
||||||
|
|
||||||
|
send_confirmation_email current_site
|
||||||
|
flash[:success] = 'Confirmation email re-sent.'
|
||||||
|
redirect "/site/#{current_site.username}/confirm_email"
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/site/:username/confirm_email' do
|
||||||
|
require_login
|
||||||
|
|
||||||
|
redirect '/' if current_site.username != params[:username] || !current_site.parent? || current_site.email_confirmed
|
||||||
|
|
||||||
|
# Update email, resend token
|
||||||
|
if params[:email]
|
||||||
|
send_confirmation_email @site
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:token].blank?
|
||||||
|
flash[:error] = 'You must enter a valid token.'
|
||||||
|
redirect "/site/#{current_site.username}/confirm_email"
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_site.email_confirmation_token == params[:token]
|
||||||
|
current_site.email_confirmation_token = nil
|
||||||
|
current_site.email_confirmation_count = 0
|
||||||
|
current_site.email_confirmed = true
|
||||||
|
current_site.save_changes
|
||||||
|
|
||||||
|
if session[:fromsettings]
|
||||||
|
session[:fromsettings] = nil
|
||||||
|
flash[:success] = 'Email address changed.'
|
||||||
|
redirect '/settings#email'
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect '/tutorial'
|
||||||
|
else
|
||||||
|
flash[:error] = 'You must enter a valid token.'
|
||||||
|
redirect "/site/#{current_site.username}/confirm_email"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/site/:username/block' do |username|
|
||||||
|
require_login
|
||||||
|
site = Site[username: username]
|
||||||
|
redirect request.referer if current_site.id == site.id
|
||||||
|
|
||||||
|
current_site.block! site
|
||||||
|
|
||||||
|
if request.referer.match /\/site\/#{Regexp.quote(username)}/i
|
||||||
|
redirect '/'
|
||||||
|
else
|
||||||
|
redirect request.referer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/site/:username/unblock' do |username|
|
||||||
|
require_login
|
||||||
|
site = Site[username: username]
|
||||||
|
|
||||||
|
if site.nil? || current_site.id == site.id
|
||||||
|
redirect request.referer
|
||||||
|
end
|
||||||
|
|
||||||
|
current_site.unblock! site
|
||||||
|
redirect request.referer
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/site/:username/confirm_phone' do
|
||||||
|
require_login
|
||||||
|
redirect '/' unless current_site.phone_verification_needed?
|
||||||
|
@title = 'Verify your Phone Number'
|
||||||
|
erb :'site/confirm_phone'
|
||||||
|
end
|
||||||
|
|
||||||
|
def restart_phone_verification
|
||||||
|
current_site.phone_verification_sent_at = nil
|
||||||
|
current_site.phone_verification_sid = nil
|
||||||
|
current_site.save_changes validate: false
|
||||||
|
redirect "/site/#{current_site.username}/confirm_phone"
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/site/:username/confirm_phone' do
|
||||||
|
require_login
|
||||||
|
redirect '/' unless current_site.phone_verification_needed?
|
||||||
|
|
||||||
|
if params[:phone_intl]
|
||||||
|
phone = Phonelib.parse params[:phone_intl]
|
||||||
|
|
||||||
|
if !phone.valid?
|
||||||
|
flash[:error] = "Invalid phone number, please try again."
|
||||||
|
redirect "/site/#{current_site.username}/confirm_phone"
|
||||||
|
end
|
||||||
|
|
||||||
|
if phone.types.include?(:premium_rate) || phone.types.include?(:shared_cost)
|
||||||
|
flash[:error] = 'Neocities does not support this type of number, please use another number.'
|
||||||
|
redirect "/site/#{current_site.username}/confirm_phone"
|
||||||
|
end
|
||||||
|
|
||||||
|
current_site.phone_verification_sent_at = Time.now
|
||||||
|
current_site.phone_verification_attempts += 1
|
||||||
|
|
||||||
|
if current_site.phone_verification_attempts > Site::PHONE_VERIFICATION_LOCKOUT_ATTEMPTS
|
||||||
|
flash[:error] = 'You have exceeded the number of phone verification attempts allowed.'
|
||||||
|
redirect "/site/#{current_site.username}/confirm_phone"
|
||||||
|
end
|
||||||
|
|
||||||
|
current_site.save_changes validate: false
|
||||||
|
|
||||||
|
verification = $twilio.verify
|
||||||
|
.v2
|
||||||
|
.services($config['twilio_service_sid'])
|
||||||
|
.verifications
|
||||||
|
.create(to: phone.e164, channel: 'sms')
|
||||||
|
|
||||||
|
current_site.phone_verification_sid = verification.sid
|
||||||
|
current_site.save_changes validate: false
|
||||||
|
|
||||||
|
flash[:success] = 'Validation message sent! Check your phone and enter the code below.'
|
||||||
|
else
|
||||||
|
|
||||||
|
restart_phone_verification if current_site.phone_verification_sent_at < Time.now - Site::PHONE_VERIFICATION_EXPIRATION_TIME
|
||||||
|
minutes_remaining = ((current_site.phone_verification_sent_at - (Time.now - Site::PHONE_VERIFICATION_EXPIRATION_TIME))/60).round
|
||||||
|
|
||||||
|
begin
|
||||||
|
# Check code
|
||||||
|
vc = $twilio.verify
|
||||||
|
.v2
|
||||||
|
.services($config['twilio_service_sid'])
|
||||||
|
.verification_checks
|
||||||
|
.create(verification_sid: current_site.phone_verification_sid, code: params[:code])
|
||||||
|
|
||||||
|
# puts vc.status (pending if failed, approved if it passed)
|
||||||
|
if vc.status == 'approved'
|
||||||
|
current_site.phone_verified = true
|
||||||
|
current_site.save_changes validate: false
|
||||||
|
else
|
||||||
|
flash[:error] = "Code was not correct, please try again. If the phone number you entered was incorrect, you can re-enter the number after #{minutes_remaining} more minutes have passed."
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue Twilio::REST::RestError => e
|
||||||
|
if e.message =~ /60202/
|
||||||
|
flash[:error] = "You have exhausted your check attempts. Please try again in #{minutes_remaining} minutes."
|
||||||
|
elsif e.message =~ /20404/ # Unable to create record
|
||||||
|
restart_phone_verification
|
||||||
|
else
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Will redirect to / automagically if phone was verified
|
||||||
|
redirect "/site/#{current_site.username}/confirm_phone"
|
||||||
|
end
|
||||||
166
app/site_files.rb
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
post '/site_files/rename' do
|
||||||
|
require_login
|
||||||
|
path = HTMLEntities.new.decode params[:path]
|
||||||
|
new_path = HTMLEntities.new.decode params[:new_path]
|
||||||
|
site_file = current_site.site_files.select {|s| s.path == path}.first
|
||||||
|
|
||||||
|
escaped_path = Rack::Utils.escape_html path
|
||||||
|
escaped_new_path = Rack::Utils.escape_html new_path
|
||||||
|
|
||||||
|
if site_file.nil?
|
||||||
|
flash[:error] = "File #{escaped_path} does not exist."
|
||||||
|
else
|
||||||
|
res = site_file.rename new_path
|
||||||
|
|
||||||
|
if res.first == true
|
||||||
|
flash[:success] = "Renamed #{escaped_path} to #{escaped_new_path}"
|
||||||
|
else
|
||||||
|
flash[:error] = "Failed to rename #{escaped_path} to #{escaped_new_path}: #{Rack::Utils.escape_html res.last}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
dirname = Pathname(path).dirname
|
||||||
|
dir_query = dirname.nil? || dirname.to_s == '.' ? '' : "?dir=#{Rack::Utils.escape dirname}"
|
||||||
|
|
||||||
|
redirect "/dashboard#{dir_query}"
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/site_files/download' do
|
||||||
|
require_login
|
||||||
|
|
||||||
|
if !current_site.dl_queued_at.nil? && current_site.dl_queued_at > 1.hour.ago
|
||||||
|
flash[:error] = 'Site downloads are currently limited to once per hour, please try again later.'
|
||||||
|
redirect request.referer
|
||||||
|
end
|
||||||
|
|
||||||
|
content_type 'application/zip'
|
||||||
|
attachment "neocities-#{current_site.username}.zip"
|
||||||
|
|
||||||
|
current_site.dl_queued_at = Time.now
|
||||||
|
current_site.save_changes validate: false
|
||||||
|
|
||||||
|
directory_path = current_site.files_path
|
||||||
|
|
||||||
|
stream do |out|
|
||||||
|
ZipTricks::Streamer.open(out) do |zip|
|
||||||
|
Dir["#{directory_path}/**/*"].each do |file|
|
||||||
|
next if File.directory?(file)
|
||||||
|
|
||||||
|
zip_path = file.sub("#{directory_path}/", '')
|
||||||
|
zip.write_stored_file(zip_path) do |file_writer|
|
||||||
|
File.open(file, 'rb') do |file|
|
||||||
|
IO.copy_stream(file, file_writer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get %r{\/site_files\/download\/(.+)} do
|
||||||
|
require_login
|
||||||
|
dont_browser_cache
|
||||||
|
not_found if params[:captures].nil? || params[:captures].length != 1
|
||||||
|
filename = params[:captures].first
|
||||||
|
attachment filename
|
||||||
|
send_file current_site.current_files_path(filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
get %r{\/site_files\/text_editor\/(.+)} do
|
||||||
|
require_login
|
||||||
|
dont_browser_cache
|
||||||
|
|
||||||
|
@filename = params[:captures].first
|
||||||
|
redirect '/site_files/text_editor?filename=' + Rack::Utils.escape(@filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/site_files/text_editor' do
|
||||||
|
require_login
|
||||||
|
dont_browser_cache
|
||||||
|
|
||||||
|
@filename = params[:filename]
|
||||||
|
|
||||||
|
if @filename.nil? || @filename.strip.empty?
|
||||||
|
flash[:error] = 'No filename specified.'
|
||||||
|
redirect '/dashboard'
|
||||||
|
end
|
||||||
|
|
||||||
|
extname = File.extname @filename
|
||||||
|
|
||||||
|
@ace_mode = case extname
|
||||||
|
when /htm|html/ then 'html'
|
||||||
|
when /js/ then 'javascript'
|
||||||
|
when /md/ then 'markdown'
|
||||||
|
when /css/ then 'css'
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
file_path = current_site.current_files_path @filename
|
||||||
|
|
||||||
|
if File.directory? file_path
|
||||||
|
flash[:error] = 'Cannot edit a directory.'
|
||||||
|
redirect '/dashboard'
|
||||||
|
end
|
||||||
|
|
||||||
|
if !File.exist?(file_path)
|
||||||
|
flash[:error] = 'We could not find the requested file.'
|
||||||
|
redirect '/dashboard'
|
||||||
|
end
|
||||||
|
|
||||||
|
@title = "Editing #{@filename}"
|
||||||
|
|
||||||
|
erb :'site_files/text_editor'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/site_files/allowed_types' do
|
||||||
|
@title = 'Allowed File Types'
|
||||||
|
@description = 'See which file types can be uploaded to Neocities sites.'
|
||||||
|
erb :'site_files/allowed_types'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/site_files/hotlinking' do
|
||||||
|
@title = 'Hotlinking Information'
|
||||||
|
@description = 'Learn how hotlinking works on Neocities.'
|
||||||
|
erb :'site_files/hotlinking'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/site_files/mount_info' do
|
||||||
|
@title = 'Site Mount Information'
|
||||||
|
@description = 'Read how to mount and manage your Neocities site files.'
|
||||||
|
erb :'site_files/mount_info'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/site_files/chat' do
|
||||||
|
require_login
|
||||||
|
dont_browser_cache
|
||||||
|
headers 'X-Accel-Buffering' => 'no'
|
||||||
|
halt(403) unless parent_site.supporter?
|
||||||
|
|
||||||
|
# Ensure the request is treated as a stream
|
||||||
|
stream do |out|
|
||||||
|
url = 'https://api.anthropic.com/v1/messages'
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"anthropic-version" => "2023-06-01",
|
||||||
|
"anthropic-beta" => "messages-2023-12-15",
|
||||||
|
"content-type" => "application/json",
|
||||||
|
"x-api-key" => $config['anthropic_api_key']
|
||||||
|
}
|
||||||
|
|
||||||
|
body = {
|
||||||
|
model: "claude-3-haiku-20240307",
|
||||||
|
system: params[:system],
|
||||||
|
messages: JSON.parse(params[:messages]),
|
||||||
|
max_tokens: 4096,
|
||||||
|
temperature: 0.5,
|
||||||
|
stream: true
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
res = HTTP.headers(headers).post(url, body: body)
|
||||||
|
|
||||||
|
while(buffer = res.body.readpartial)
|
||||||
|
out << buffer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
23
app/supporter.rb
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
get '/supporter/?' do
|
||||||
|
@title = 'Supporter'
|
||||||
|
erb :'welcome'
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/supporter/update' do
|
||||||
|
require_login
|
||||||
|
|
||||||
|
# Everyone has supporter features nowwwwwwwwwwww yayyayayayy
|
||||||
|
redirect '/supporter/thanks'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/supporter/thanks' do
|
||||||
|
@title = 'Supporter Confirmation'
|
||||||
|
require_login
|
||||||
|
erb :'supporter/thanks'
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_special_upgrade
|
||||||
|
false
|
||||||
|
end
|
||||||
85
app/sysops.rb
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
require 'base64'
|
||||||
|
require 'zlib'
|
||||||
|
require 'rubygems/package'
|
||||||
|
|
||||||
|
get '/sysops/proxy/map.txt' do
|
||||||
|
require_proxy_auth
|
||||||
|
domains = ''
|
||||||
|
Site.exclude(domain: nil).
|
||||||
|
exclude(domain: '').
|
||||||
|
select(:username,:domain).
|
||||||
|
all.
|
||||||
|
collect do |s|
|
||||||
|
domains << "#{s.domain} #{s.username};\n"
|
||||||
|
end
|
||||||
|
content_type :text
|
||||||
|
domains
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/sysops/proxy/sslcerts.tar.gz' do
|
||||||
|
require_proxy_auth
|
||||||
|
sites = Site.ssl_sites
|
||||||
|
|
||||||
|
nginx_config = ''
|
||||||
|
|
||||||
|
tar = StringIO.new
|
||||||
|
|
||||||
|
Gem::Package::TarWriter.new(tar) do |writer|
|
||||||
|
writer.mkdir 'sslcerts', 0740
|
||||||
|
writer.mkdir 'sslcerts/certs', 0740
|
||||||
|
|
||||||
|
sites.each do |site|
|
||||||
|
writer.add_file "sslcerts/certs/#{site.username}.key", 0640 do |f|
|
||||||
|
f.write site.ssl_key
|
||||||
|
end
|
||||||
|
|
||||||
|
writer.add_file "sslcerts/certs/#{site.username}.crt", 0640 do |f|
|
||||||
|
f.write site.ssl_cert
|
||||||
|
end
|
||||||
|
|
||||||
|
nginx_config << %{
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name *.#{site.domain};
|
||||||
|
ssl_certificate sslsites/certs/#{site.username}.crt;
|
||||||
|
ssl_certificate_key sslsites/certs/#{site.username}.key;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host #{site.username}.femslash.city;
|
||||||
|
proxy_pass http://web:9292$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.unindent
|
||||||
|
end
|
||||||
|
|
||||||
|
writer.add_file "sslcerts/sslsites.conf", 0640 do |f|
|
||||||
|
f.write nginx_config
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tar.rewind
|
||||||
|
|
||||||
|
package = StringIO.new 'b'
|
||||||
|
package.set_encoding 'binary'
|
||||||
|
gzip = Zlib::GzipWriter.new package
|
||||||
|
gzip.write tar.read
|
||||||
|
tar.close
|
||||||
|
gzip.finish
|
||||||
|
package.rewind
|
||||||
|
|
||||||
|
attachment
|
||||||
|
package.read
|
||||||
|
end
|
||||||
|
|
||||||
|
class ProxyAccessViolation < StandardError; end
|
||||||
|
|
||||||
|
def require_proxy_auth
|
||||||
|
begin
|
||||||
|
auth = request.env['HTTP_AUTHORIZATION']
|
||||||
|
user, pass = Base64.decode64(auth.match(/Basic (.+)/)[1]).split(':')
|
||||||
|
raise ProxyAccessViolation unless pass == $config['proxy_pass']
|
||||||
|
rescue
|
||||||
|
raise ProxyAccessViolation, "Violator: #{request.ip}" unless pass == $config['proxy_pass']
|
||||||
|
end
|
||||||
|
end
|
||||||
3
app/tags.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
get '/tags/autocomplete/:name.json' do |name|
|
||||||
|
Tag.autocomplete(name).collect {|t| t[:name]}.to_json
|
||||||
|
end
|
||||||
53
app/tutorial.rb
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
def default_tutorial_html
|
||||||
|
<<-EOT.strip
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
Hello World!
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
EOT
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/tutorials' do
|
||||||
|
@description = 'Start web development tutorials on Neocities.'
|
||||||
|
erb :'tutorials'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/tutorial/?' do
|
||||||
|
require_login
|
||||||
|
erb :'tutorial/index'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/tutorial/:section/?' do
|
||||||
|
require_login
|
||||||
|
not_found unless %w{html}.include?(params[:section])
|
||||||
|
redirect "/tutorial/#{params[:section]}/1"
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/tutorial/:section/:page/?' do
|
||||||
|
require_login
|
||||||
|
@page = params[:page]
|
||||||
|
not_found unless @page.match?(/\A[1-9]\z|\A10\z/)
|
||||||
|
not_found unless %w{html}.include?(params[:section])
|
||||||
|
|
||||||
|
@section = params[:section]
|
||||||
|
|
||||||
|
@title = "#{params[:section].upcase} Tutorial - #{@page}/10"
|
||||||
|
|
||||||
|
|
||||||
|
if @page == '9'
|
||||||
|
unless csrf_safe?
|
||||||
|
signout
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
current_site.tutorial_required = false
|
||||||
|
current_site.save_changes validate: false
|
||||||
|
end
|
||||||
|
|
||||||
|
erb "tutorial/layout".to_sym
|
||||||
|
end
|
||||||
221
app/webhooks.rb
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
post '/webhooks/paypal' do
|
||||||
|
return 403 unless valid_paypal_webhook_source?
|
||||||
|
|
||||||
|
if paypal_subscription_ended_ipn?
|
||||||
|
site = paypal_ipn_site
|
||||||
|
|
||||||
|
if site
|
||||||
|
stripe_active = site.stripe_paying_supporter?
|
||||||
|
|
||||||
|
site.paypal_active = false
|
||||||
|
site.paypal_profile_id = nil
|
||||||
|
site.paypal_token = nil
|
||||||
|
|
||||||
|
unless stripe_active
|
||||||
|
site.plan_type = nil
|
||||||
|
site.plan_ended = true
|
||||||
|
end
|
||||||
|
|
||||||
|
site.save_changes validate: false
|
||||||
|
|
||||||
|
unless stripe_active
|
||||||
|
EmailWorker.perform_async({
|
||||||
|
from: Site::FROM_EMAIL,
|
||||||
|
to: site.email,
|
||||||
|
subject: '[Neocities] Supporter plan has ended',
|
||||||
|
body: Tilt.new('./views/templates/email/supporter_ended.erb', pretty: true).render(self)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
'ok'
|
||||||
|
end
|
||||||
|
|
||||||
|
def paypal_subscription_ended_ipn?
|
||||||
|
%w[
|
||||||
|
recurring_payment_profile_cancel
|
||||||
|
recurring_payment_expired
|
||||||
|
recurring_payment_suspended
|
||||||
|
recurring_payment_suspended_due_to_max_failed_payment
|
||||||
|
subscr_cancel
|
||||||
|
subscr_eot
|
||||||
|
].include?(params[:txn_type].to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def paypal_ipn_site
|
||||||
|
profile_id = params[:recurring_payment_id] ||
|
||||||
|
params[:recurring_payment_profile_id] ||
|
||||||
|
params[:rp_profile_id] ||
|
||||||
|
params[:profile_id] ||
|
||||||
|
params[:subscr_id]
|
||||||
|
|
||||||
|
return nil if profile_id.blank?
|
||||||
|
|
||||||
|
Site.where(paypal_profile_id: profile_id).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_paypal_webhook_source?
|
||||||
|
return true if self.class.test?
|
||||||
|
return true if request.ip == '127.0.0.1'
|
||||||
|
|
||||||
|
# Use DNS to resolve notify.paypal.com as PayPal recommends
|
||||||
|
begin
|
||||||
|
resolved_ips = Resolv.getaddresses 'notify.paypal.com'
|
||||||
|
return resolved_ips.include?(request.ip)
|
||||||
|
rescue Resolv::ResolvError
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/webhooks/paypal/tipping_notify' do
|
||||||
|
return 403 unless valid_paypal_webhook_source?
|
||||||
|
|
||||||
|
# Handle missing custom parameter (this seems to happen with recurring donations)
|
||||||
|
if params[:custom].nil? || params[:custom].strip.empty?
|
||||||
|
# Try to extract username from product_name like "Site Donation for Two-reeler (USERNAME_HERE)"
|
||||||
|
if params[:product_name] && params[:product_name].match(/\((.+)\)$/)
|
||||||
|
username = params[:product_name].match(/\((.+)\)$/)[1]
|
||||||
|
site = Site.where(username: username).first
|
||||||
|
end
|
||||||
|
|
||||||
|
# If no site found by username, try to find by payer email
|
||||||
|
if params[:payer_email]
|
||||||
|
actioning_site = Site.where(email: params[:payer_email]).first
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create payload structure for anonymous tip
|
||||||
|
payload = {
|
||||||
|
site_id: site.id,
|
||||||
|
actioning_site_id: (actioning_site ? actioning_site : nil)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
payload = JSON.parse Base64.strict_decode64(params[:custom]), symbolize_names: true
|
||||||
|
site = Site[payload[:site_id]]
|
||||||
|
end
|
||||||
|
|
||||||
|
@tip_hash = {
|
||||||
|
message: (params[:memo] ? params[:memo] : nil),
|
||||||
|
amount: params[:mc_gross],
|
||||||
|
currency: params[:mc_currency],
|
||||||
|
fee: params[:mc_fee],
|
||||||
|
actioning_site: (payload[:actioning_site_id] ? Site[payload[:actioning_site_id]] : nil),
|
||||||
|
paypal_payer_email: params[:payer_email],
|
||||||
|
paypal_receiver_email: params[:receiver_email],
|
||||||
|
paypal_txn_id: params[:txn_id],
|
||||||
|
created_at: DateTime.strptime(params[:payment_date], "%H:%M:%S %b %e, %Y %Z").to_time
|
||||||
|
}
|
||||||
|
|
||||||
|
@tip = site.add_tip @tip_hash
|
||||||
|
|
||||||
|
Event.create(
|
||||||
|
site_id: @tip.site.id,
|
||||||
|
actioning_site_id: (@tip.actioning_site ? @tip.actioning_site.id : nil),
|
||||||
|
tip_id: @tip.id
|
||||||
|
)
|
||||||
|
|
||||||
|
if @tip.actioning_site
|
||||||
|
subject = "You received a #{@tip.amount_string} tip from #{@tip.actioning_site.username}!"
|
||||||
|
else
|
||||||
|
subject = "You received a #{@tip.amount_string} tip!"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tip.site.send_email(
|
||||||
|
subject: subject,
|
||||||
|
body: Tilt.new('./views/templates/email/tip_received.erb', pretty: true).render(self)
|
||||||
|
)
|
||||||
|
|
||||||
|
EmailWorker.perform_async({
|
||||||
|
from: Site::FROM_EMAIL,
|
||||||
|
to: params[:payer_email],
|
||||||
|
subject: "You sent a #{@tip.amount_string} tip!",
|
||||||
|
body: Tilt.new('./views/templates/email/tip_sent.erb', pretty: true).render(self)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/webhooks/stripe' do
|
||||||
|
event = JSON.parse request.body.read
|
||||||
|
|
||||||
|
if event['type'] == 'charge.failed'
|
||||||
|
site = stripe_get_site_from_event event
|
||||||
|
|
||||||
|
EmailWorker.perform_async({
|
||||||
|
from: Site::FROM_EMAIL,
|
||||||
|
to: site.email,
|
||||||
|
subject: "[Neocities] There was an issue charging your card",
|
||||||
|
body: Tilt.new('./views/templates/email/charge_failure.erb', pretty: true).render(self)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
if event['type'] == 'customer.subscription.deleted'
|
||||||
|
site = stripe_get_site_from_event event
|
||||||
|
site.stripe_subscription_id = nil
|
||||||
|
site.plan_type = nil
|
||||||
|
site.plan_ended = true
|
||||||
|
site.save_changes validate: false
|
||||||
|
|
||||||
|
EmailWorker.perform_async({
|
||||||
|
from: Site::FROM_EMAIL,
|
||||||
|
to: site.email,
|
||||||
|
subject: "[Neocities] Supporter plan has ended",
|
||||||
|
body: Tilt.new('./views/templates/email/supporter_ended.erb', pretty: true).render(self)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
if event['type'] == 'invoice.payment_succeeded'
|
||||||
|
site = stripe_get_site_from_event event
|
||||||
|
|
||||||
|
if site.email_invoice && site.stripe_paying_supporter?
|
||||||
|
invoice_obj = event['data']['object']
|
||||||
|
|
||||||
|
EmailWorker.perform_async({
|
||||||
|
from: Site::FROM_EMAIL,
|
||||||
|
to: site.email,
|
||||||
|
subject: "[Neocities] Invoice",
|
||||||
|
body: Tilt.new('./views/templates/email/invoice.erb', pretty: true).render(
|
||||||
|
self,
|
||||||
|
site: site,
|
||||||
|
amount: invoice_obj['amount_due'],
|
||||||
|
period_start: Time.at(invoice_obj['period_start']),
|
||||||
|
period_end: Time.at(invoice_obj['period_end']),
|
||||||
|
date: Time.at(invoice_obj['date'])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
'ok'
|
||||||
|
end
|
||||||
|
|
||||||
|
def stripe_get_site_from_event(event)
|
||||||
|
customer_id = event['data']['object']['customer']
|
||||||
|
halt 'ok' if customer_id.nil? # Likely a fraudulent card report
|
||||||
|
|
||||||
|
retries = 0
|
||||||
|
begin
|
||||||
|
customer = Stripe::Customer.retrieve customer_id
|
||||||
|
rescue Stripe::APIConnectionError, Stripe::RateLimitError => e
|
||||||
|
retries += 1
|
||||||
|
if retries <= 3
|
||||||
|
sleep(2 ** retries) # exponential backoff: 2s, 4s, 8s
|
||||||
|
retry
|
||||||
|
else
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Some old accounts only have a username for the desc
|
||||||
|
desc_split = customer.description.split(' - ')
|
||||||
|
|
||||||
|
if desc_split.length == 1
|
||||||
|
site_where = {username: desc_split.first}
|
||||||
|
end
|
||||||
|
|
||||||
|
if desc_split.last.not_an_integer?
|
||||||
|
site_where = {username: desc_split.first}
|
||||||
|
else
|
||||||
|
site_where = {id: desc_split.last}
|
||||||
|
end
|
||||||
|
|
||||||
|
Site.where(site_where).first
|
||||||
|
end
|
||||||
228
app_helpers.rb
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
def dashboard_if_signed_in
|
||||||
|
redirect '/dashboard' if signed_in?
|
||||||
|
end
|
||||||
|
|
||||||
|
def csrf_safe?
|
||||||
|
csrf_token == params[:csrf_token] || csrf_token == request.env['HTTP_X_CSRF_TOKEN']
|
||||||
|
end
|
||||||
|
|
||||||
|
def csrf_token
|
||||||
|
session[:_csrf_token] ||= SecureRandom.base64(32)
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_education?
|
||||||
|
current_site && current_site.is_education
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_login
|
||||||
|
redirect '/' unless signed_in? && current_site
|
||||||
|
end
|
||||||
|
|
||||||
|
def signed_in?
|
||||||
|
return false if current_site.nil?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def signout
|
||||||
|
@_site = nil
|
||||||
|
@_parent_site = nil
|
||||||
|
session[:id] = nil
|
||||||
|
session.clear
|
||||||
|
response.delete_cookie 'neocities', path: '/'
|
||||||
|
request.env['rack.session.options'][:drop] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_site
|
||||||
|
return nil if session[:id].nil?
|
||||||
|
@_site ||= Site[id: session[:id]]
|
||||||
|
@_parent_site ||= @_site.parent
|
||||||
|
|
||||||
|
if @_site.is_banned || @_site.is_deleted || (@_parent_site && (@_parent_site.is_banned || @_parent_site.is_deleted))
|
||||||
|
signout
|
||||||
|
end
|
||||||
|
|
||||||
|
@_site
|
||||||
|
end
|
||||||
|
|
||||||
|
def parent_site
|
||||||
|
@_parent_site || current_site
|
||||||
|
end
|
||||||
|
|
||||||
|
def meta_robots(newtag=nil)
|
||||||
|
if newtag
|
||||||
|
@_meta_robots = newtag
|
||||||
|
end
|
||||||
|
|
||||||
|
@_meta_robots
|
||||||
|
end
|
||||||
|
|
||||||
|
def meta_description(new_description=nil)
|
||||||
|
if new_description
|
||||||
|
@_meta_description = new_description
|
||||||
|
end
|
||||||
|
|
||||||
|
description = @_meta_description || @description
|
||||||
|
return nil if description.nil? || description.strip.empty?
|
||||||
|
|
||||||
|
description
|
||||||
|
end
|
||||||
|
|
||||||
|
def title
|
||||||
|
out = "Neocities"
|
||||||
|
return out if request.path == '/'
|
||||||
|
|
||||||
|
full_title = if @title
|
||||||
|
"#{out} - #{@title}"
|
||||||
|
else
|
||||||
|
path_parts = request.path.split('/').reject(&:empty?)
|
||||||
|
formatted_parts = path_parts.map do |part|
|
||||||
|
part.split(/[_\-]/).map(&:capitalize).join(' ')
|
||||||
|
end
|
||||||
|
"#{out} - #{formatted_parts.join(' - ')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if full_title.length >= 70
|
||||||
|
full_title[0..65] + "..."
|
||||||
|
else
|
||||||
|
full_title
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def encoding_fix(file)
|
||||||
|
begin
|
||||||
|
Rack::Utils.escape_html file
|
||||||
|
rescue ArgumentError => e
|
||||||
|
if e.message =~ /invalid byte sequence in UTF-8/ ||
|
||||||
|
e.message =~ /incompatible character encodings/
|
||||||
|
return Rack::Utils.escape_html(file.force_encoding('BINARY'))
|
||||||
|
end
|
||||||
|
fail
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_confirmation_email(site=current_site)
|
||||||
|
# Child sites don't need email confirmation - they use parent site's email
|
||||||
|
return if !site.parent?
|
||||||
|
|
||||||
|
if site.email_confirmation_count > Site::MAXIMUM_EMAIL_CONFIRMATIONS
|
||||||
|
flash[:error] = 'You sent too many email confirmation requests, cannot continue.'
|
||||||
|
redirect request.referrer
|
||||||
|
end
|
||||||
|
|
||||||
|
DB['UPDATE sites set email_confirmation_count=email_confirmation_count+1 WHERE id=?', site.id].first
|
||||||
|
|
||||||
|
EmailWorker.perform_async({
|
||||||
|
from: 'web@neocities.org',
|
||||||
|
reply_to: 'noreply@neocities.org',
|
||||||
|
to: site.email,
|
||||||
|
subject: "[Neocities] Confirm your email address",
|
||||||
|
body: Tilt.new('./views/templates/email/confirm.erb', pretty: true).render(self, site: site)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def dont_browser_cache
|
||||||
|
headers['Cache-Control'] = 'private, no-store, max-age=0, no-cache, must-revalidate, post-check=0, pre-check=0'
|
||||||
|
headers['Pragma'] = 'no-cache'
|
||||||
|
headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
|
||||||
|
@dont_browser_cache = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def sanitize_comment(text)
|
||||||
|
Rinku.auto_link Sanitize.fragment(text), :all, 'target="_blank" rel="nofollow"'
|
||||||
|
end
|
||||||
|
|
||||||
|
def flash_message_keys
|
||||||
|
flash.keys.select {|k| [:success, :error, :errors].include?(k) && !flash[k].to_s.empty?}
|
||||||
|
end
|
||||||
|
|
||||||
|
def flash_message_entries
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
Array(@error).each do |message|
|
||||||
|
entries << {type: 'error', message: message} unless message.to_s.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
flash_message_keys.each do |key|
|
||||||
|
type = key == :success ? 'success' : 'error'
|
||||||
|
|
||||||
|
Array(flash[key]).each do |message|
|
||||||
|
entries << {type: type, message: message} unless message.to_s.empty?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
entries
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_comment_message(message)
|
||||||
|
message.to_s.gsub(/\r\n?/, "\n").gsub(/\n{3,}/, "\n\n").strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def comment_message_error(message)
|
||||||
|
return 'Comment cannot be empty.' if message.empty?
|
||||||
|
return "Comments must be #{Site::MAX_COMMENT_SIZE} characters or fewer." if message.length > Site::MAX_COMMENT_SIZE
|
||||||
|
|
||||||
|
lines = message.split("\n")
|
||||||
|
return "Comments must be #{Site::MAX_COMMENT_LINES} lines or fewer." if lines.length > Site::MAX_COMMENT_LINES
|
||||||
|
|
||||||
|
return nil if lines.length < Site::MULTILINE_COMMENT_AVERAGE_LINE_THRESHOLD
|
||||||
|
|
||||||
|
nonblank_lines = lines.map(&:strip).reject(&:empty?)
|
||||||
|
return 'Comment cannot be empty.' if nonblank_lines.empty?
|
||||||
|
|
||||||
|
average_line_length = nonblank_lines.sum(&:length).to_f / nonblank_lines.length
|
||||||
|
return nil if average_line_length >= Site::MIN_MULTILINE_COMMENT_AVERAGE_LINE_LENGTH
|
||||||
|
|
||||||
|
'Multiline comments need a little more text on each line.'
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_comment_message?(message)
|
||||||
|
comment_message_error(message).nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def comment_unavailable_message(site=nil)
|
||||||
|
return 'Comments are disabled for this site.' if site && site.profile_comments_enabled == false
|
||||||
|
|
||||||
|
if current_site && !current_site.commenting_allowed?
|
||||||
|
if current_site.commenting_too_much?
|
||||||
|
return "To prevent spam, comments are limited to #{Site::MAX_COMMENTS_PER_DAY} per day. Please try again tomorrow."
|
||||||
|
end
|
||||||
|
|
||||||
|
return "To prevent spam, you cannot comment until you have updated your site #{Site::COMMENTING_ALLOWED_UPDATED_COUNT} times on separate days, and your account is one week old."
|
||||||
|
end
|
||||||
|
|
||||||
|
'You cannot comment on this right now.'
|
||||||
|
end
|
||||||
|
|
||||||
|
def flash_display(opts={})
|
||||||
|
erb :'_flash', layout: false, locals: {opts: opts}
|
||||||
|
end
|
||||||
|
|
||||||
|
def hcaptcha_valid?
|
||||||
|
return true if ENV['RACK_ENV'] == 'test' || ENV['CI']
|
||||||
|
return false unless params[:'h-captcha-response']
|
||||||
|
|
||||||
|
resp = HTTP.get('https://hcaptcha.com/siteverify', params: {
|
||||||
|
secret: $config['hcaptcha_secret_key'],
|
||||||
|
response: params[:'h-captcha-response']
|
||||||
|
})
|
||||||
|
|
||||||
|
resp = JSON.parse resp
|
||||||
|
|
||||||
|
if resp['success'] == true
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
JS_ESCAPE_MAP = {"\\" => "\\\\", "</" => '<\/', "\r\n" => '\n', "\n" => '\n', "\r" => '\n', '"' => '\\"', "'" => "\\'", "`" => "\\`", "$" => "\\$"}
|
||||||
|
|
||||||
|
def escape_javascript(javascript)
|
||||||
|
javascript = javascript.to_s
|
||||||
|
if javascript.empty?
|
||||||
|
result = ""
|
||||||
|
else
|
||||||
|
result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"']|[`]|[$])/u, JS_ESCAPE_MAP)
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
9
code-of-conduct.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
These guidelines apply to Neocities contributor communications, including
|
||||||
|
messages in code repositories, collaboration tools, outreach channels, etc.
|
||||||
|
|
||||||
|
* Language and actions must be free of personal attacks, harassment,
|
||||||
|
discrimination, and NSFW content.
|
||||||
|
|
||||||
|
Instances of unacceptable behavior can be reported by contacting the project
|
||||||
|
team at abuse@neocities.org. All complaints will be investigated and reporter
|
||||||
|
confidentiality will be maintained.
|
||||||
377
config.ru
Normal file
|
|
@ -0,0 +1,377 @@
|
||||||
|
|
||||||
|
require 'rubygems'
|
||||||
|
require './app.rb'
|
||||||
|
require 'sidekiq/web'
|
||||||
|
require 'airbrake/sidekiq'
|
||||||
|
require 'rack/mime'
|
||||||
|
require 'rack/utils'
|
||||||
|
require 'time'
|
||||||
|
|
||||||
|
use Airbrake::Rack::Middleware
|
||||||
|
|
||||||
|
class HostedSiteMiddleware
|
||||||
|
def initialize(app)
|
||||||
|
@app = app
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
host = env['HTTP_HOST'].to_s.downcase.split(':').first
|
||||||
|
site = Site.find_by_host(host)
|
||||||
|
|
||||||
|
return @app.call(env) unless site
|
||||||
|
|
||||||
|
path = env['PATH_INFO'].to_s
|
||||||
|
|
||||||
|
begin
|
||||||
|
path = Rack::Utils.unescape_path(path)
|
||||||
|
scrubbed_path = site.scrubbed_path(path)
|
||||||
|
rescue ArgumentError
|
||||||
|
return [400, { 'Content-Type' => 'text/plain' }, ['Invalid path']]
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
if scrubbed_path.end_with?('.html')
|
||||||
|
redirect_path =
|
||||||
|
if scrubbed_path == 'index.html'
|
||||||
|
'/'
|
||||||
|
else
|
||||||
|
"/#{scrubbed_path.sub(/\.html\z/, '')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
return [
|
||||||
|
301,
|
||||||
|
{
|
||||||
|
'Location' => redirect_path,
|
||||||
|
'Content-Type' => 'text/plain'
|
||||||
|
},
|
||||||
|
['Moved Permanently']
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
if scrubbed_path.empty?
|
||||||
|
scrubbed_path = 'index.html'
|
||||||
|
elsif site.is_directory?(scrubbed_path)
|
||||||
|
scrubbed_path = File.join(scrubbed_path, 'index.html')
|
||||||
|
elsif !site.file_exists?(scrubbed_path)
|
||||||
|
html_path = "#{scrubbed_path}.html"
|
||||||
|
scrubbed_path = html_path if site.file_exists?(html_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
return [404, { 'Content-Type' => 'text/plain' }, ['Not Found']] unless site.file_exists?(scrubbed_path)
|
||||||
|
|
||||||
|
file_path = site.files_path(scrubbed_path)
|
||||||
|
|
||||||
|
begin
|
||||||
|
stat = File.stat(file_path)
|
||||||
|
rescue Errno::ENOENT
|
||||||
|
return [404, { 'Content-Type' => 'text/plain' }, ['Not Found']]
|
||||||
|
end
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type' => Rack::Mime.mime_type(
|
||||||
|
File.extname(scrubbed_path),
|
||||||
|
'application/octet-stream'
|
||||||
|
),
|
||||||
|
'Content-Length' => stat.size.to_s,
|
||||||
|
'Last-Modified' => stat.mtime.httpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
return [200, headers, []] if env['REQUEST_METHOD'] == 'HEAD'
|
||||||
|
|
||||||
|
[200, headers, [File.binread(file_path)]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
use HostedSiteMiddleware
|
||||||
|
|
||||||
|
map('/') do
|
||||||
|
run Sinatra::Application
|
||||||
|
end
|
||||||
|
|
||||||
|
map '/webdav' do
|
||||||
|
run lambda { |env|
|
||||||
|
auth = Rack::Auth::Basic::Request.new(env)
|
||||||
|
unless auth.provided? && auth.basic? && auth.credentials
|
||||||
|
return [401, { 'WWW-Authenticate' => 'Basic realm="Restricted Area"' }, ['Not authorized']]
|
||||||
|
end
|
||||||
|
|
||||||
|
username, password = auth.credentials
|
||||||
|
site = Site.get_site_from_login(username, password)
|
||||||
|
unless site
|
||||||
|
return [401, { 'WWW-Authenticate' => 'Basic realm="Restricted Area"' }, ['Not authorized']]
|
||||||
|
end
|
||||||
|
|
||||||
|
request_method = env['REQUEST_METHOD']
|
||||||
|
begin
|
||||||
|
path = Rack::Utils.unescape_path(env['PATH_INFO'])
|
||||||
|
rescue ArgumentError
|
||||||
|
return [400, {}, ['Invalid path']]
|
||||||
|
end
|
||||||
|
|
||||||
|
unless site.owner.supporter?
|
||||||
|
return [
|
||||||
|
402,
|
||||||
|
{
|
||||||
|
'Content-Type' => 'application/xml',
|
||||||
|
'X-Upgrade-Required' => 'https://neocities.org/supporter'
|
||||||
|
},
|
||||||
|
[
|
||||||
|
<<~XML
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<error xmlns="DAV:">
|
||||||
|
<message>WebDAV access requires a supporter account.</message>
|
||||||
|
</error>
|
||||||
|
XML
|
||||||
|
]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
case request_method
|
||||||
|
when 'OPTIONS'
|
||||||
|
return [200, {'Allow' => 'OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, MKCOL, MOVE', 'DAV' => '1'}, ['']]
|
||||||
|
|
||||||
|
when 'GET', 'HEAD'
|
||||||
|
begin
|
||||||
|
scrubbed_path = site.scrubbed_path(path)
|
||||||
|
rescue ArgumentError
|
||||||
|
return [400, {}, ['Invalid path']]
|
||||||
|
end
|
||||||
|
|
||||||
|
if scrubbed_path.empty? || site.is_directory?(scrubbed_path)
|
||||||
|
return [403, {}, ['Cannot download a directory']]
|
||||||
|
end
|
||||||
|
|
||||||
|
return [404, {}, ['']] unless site.file_exists?(scrubbed_path)
|
||||||
|
|
||||||
|
site_file = site.site_files_dataset.where(path: scrubbed_path).first
|
||||||
|
file_path = site.files_path(scrubbed_path)
|
||||||
|
|
||||||
|
begin
|
||||||
|
file_stat = File.stat(file_path)
|
||||||
|
rescue Errno::ENOENT
|
||||||
|
return [404, {}, ['']]
|
||||||
|
end
|
||||||
|
|
||||||
|
content_length = site_file&.size || file_stat.size
|
||||||
|
last_modified =
|
||||||
|
if site_file&.updated_at
|
||||||
|
site_file.updated_at
|
||||||
|
elsif site_file&.created_at
|
||||||
|
site_file.created_at
|
||||||
|
else
|
||||||
|
file_stat.mtime
|
||||||
|
end
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type' => Rack::Mime.mime_type(File.extname(scrubbed_path), 'application/octet-stream'),
|
||||||
|
'Content-Length' => content_length.to_s,
|
||||||
|
'Last-Modified' => last_modified.httpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
if site_file&.sha1_hash
|
||||||
|
headers['ETag'] = %("#{site_file.sha1_hash}")
|
||||||
|
end
|
||||||
|
|
||||||
|
if request_method == 'HEAD'
|
||||||
|
return [200, headers, []]
|
||||||
|
end
|
||||||
|
|
||||||
|
file_io = File.open(file_path, 'rb')
|
||||||
|
return [200, headers, file_io]
|
||||||
|
|
||||||
|
when 'PROPFIND'
|
||||||
|
begin
|
||||||
|
scrubbed_path = site.scrubbed_path(path)
|
||||||
|
rescue ArgumentError
|
||||||
|
return [400, {}, ['Invalid path']]
|
||||||
|
end
|
||||||
|
|
||||||
|
target_is_root = scrubbed_path.empty?
|
||||||
|
|
||||||
|
site_file =
|
||||||
|
unless target_is_root
|
||||||
|
site.site_files_dataset.where(path: scrubbed_path).first
|
||||||
|
end
|
||||||
|
|
||||||
|
if !target_is_root && site_file.nil?
|
||||||
|
return [404, {}, ['']]
|
||||||
|
end
|
||||||
|
|
||||||
|
if site_file && !site_file.is_directory && !site.file_exists?(scrubbed_path)
|
||||||
|
return [404, {}, ['']]
|
||||||
|
end
|
||||||
|
|
||||||
|
depth_header = env['HTTP_DEPTH']
|
||||||
|
depth = depth_header == '0' ? 0 : 1
|
||||||
|
|
||||||
|
responses = []
|
||||||
|
|
||||||
|
build_response_info = lambda do |relative_path, file_info|
|
||||||
|
is_directory = file_info[:is_directory]
|
||||||
|
href_path_segments = relative_path.split('/').reject(&:empty?).map { |segment| Rack::Utils.escape_path(segment) }
|
||||||
|
href = '/webdav'
|
||||||
|
href += '/' unless href_path_segments.empty?
|
||||||
|
href += href_path_segments.join('/')
|
||||||
|
href += '/' if is_directory && !href.end_with?('/')
|
||||||
|
|
||||||
|
display_name =
|
||||||
|
if relative_path.empty?
|
||||||
|
'/'
|
||||||
|
else
|
||||||
|
relative_path.split('/').last
|
||||||
|
end
|
||||||
|
|
||||||
|
updated_at = file_info[:updated_at] || file_info[:created_at]
|
||||||
|
|
||||||
|
<<~XML
|
||||||
|
<D:response>
|
||||||
|
<D:href>#{Rack::Utils.escape_html(href)}</D:href>
|
||||||
|
<D:propstat>
|
||||||
|
<D:prop>
|
||||||
|
<D:displayname>#{Rack::Utils.escape_html(display_name)}</D:displayname>
|
||||||
|
<D:resourcetype>#{is_directory ? '<D:collection/>' : ''}</D:resourcetype>
|
||||||
|
#{file_info[:created_at] ? "<D:creationdate>#{file_info[:created_at].utc.iso8601}</D:creationdate>" : ''}
|
||||||
|
#{updated_at ? "<D:getlastmodified>#{updated_at.httpdate}</D:getlastmodified>" : ''}
|
||||||
|
#{is_directory ? '' : "<D:getcontentlength>#{file_info[:size]}</D:getcontentlength>"}
|
||||||
|
#{file_info[:content_type] ? "<D:getcontenttype>#{Rack::Utils.escape_html(file_info[:content_type])}</D:getcontenttype>" : ''}
|
||||||
|
#{file_info[:etag] ? "<D:getetag>#{Rack::Utils.escape_html(file_info[:etag])}</D:getetag>" : ''}
|
||||||
|
</D:prop>
|
||||||
|
<D:status>HTTP/1.1 200 OK</D:status>
|
||||||
|
</D:propstat>
|
||||||
|
</D:response>
|
||||||
|
XML
|
||||||
|
end
|
||||||
|
|
||||||
|
add_response_for = lambda do |relative_path, file_info|
|
||||||
|
responses << build_response_info.call(relative_path, file_info)
|
||||||
|
end
|
||||||
|
|
||||||
|
target_info =
|
||||||
|
if target_is_root
|
||||||
|
{
|
||||||
|
is_directory: true,
|
||||||
|
size: 0,
|
||||||
|
created_at: site.created_at,
|
||||||
|
updated_at: site.site_updated_at || site.updated_at,
|
||||||
|
content_type: nil,
|
||||||
|
etag: nil
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
is_directory: site_file.is_directory,
|
||||||
|
size: site_file.is_directory ? 0 : site_file.size.to_i,
|
||||||
|
created_at: site_file.created_at,
|
||||||
|
updated_at: site_file.updated_at,
|
||||||
|
content_type: site_file.is_directory ? nil : Rack::Mime.mime_type(File.extname(scrubbed_path), 'application/octet-stream'),
|
||||||
|
etag: site_file.sha1_hash ? %("#{site_file.sha1_hash}") : nil
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
add_response_for.call(scrubbed_path, target_info)
|
||||||
|
|
||||||
|
if depth > 0 && (target_is_root || target_info[:is_directory])
|
||||||
|
site.file_list(scrubbed_path).each do |entry|
|
||||||
|
child_info = {
|
||||||
|
is_directory: entry[:is_directory],
|
||||||
|
size: entry[:is_directory] ? 0 : entry[:size].to_i,
|
||||||
|
created_at: entry[:created_at],
|
||||||
|
updated_at: entry[:updated_at],
|
||||||
|
content_type: entry[:is_directory] ? nil : Rack::Mime.mime_type(File.extname(entry[:path]), 'application/octet-stream'),
|
||||||
|
etag: entry[:sha1_hash] ? %("#{entry[:sha1_hash]}") : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
add_response_for.call(entry[:path], child_info)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
xml = <<~XML
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<D:multistatus xmlns:D="DAV:">
|
||||||
|
#{responses.join}
|
||||||
|
</D:multistatus>
|
||||||
|
XML
|
||||||
|
|
||||||
|
return [207, {'Content-Type' => 'application/xml; charset=utf-8', 'DAV' => '1'}, [xml]]
|
||||||
|
|
||||||
|
when 'PUT'
|
||||||
|
tmpfile = Tempfile.new('davfile', encoding: 'binary')
|
||||||
|
tmpfile.write(env['rack.input'].read)
|
||||||
|
tmpfile.close
|
||||||
|
|
||||||
|
result = site.store_files([{ filename: path, tempfile: tmpfile }])
|
||||||
|
|
||||||
|
if result.is_a?(Hash) && result[:error]
|
||||||
|
# Map error types to appropriate HTTP status codes
|
||||||
|
status_code = case result[:error_type]
|
||||||
|
when 'too_large', 'file_too_large', 'too_many_files'
|
||||||
|
507 # Insufficient Storage
|
||||||
|
when 'directory_exists'
|
||||||
|
409 # Conflict
|
||||||
|
when 'invalid_file_type'
|
||||||
|
415 # Unsupported Media Type
|
||||||
|
else
|
||||||
|
400 # Bad Request
|
||||||
|
end
|
||||||
|
return [status_code, {}, [result[:message]]]
|
||||||
|
end
|
||||||
|
|
||||||
|
return [201, {}, ['']]
|
||||||
|
|
||||||
|
when 'MKCOL'
|
||||||
|
return [400, {}, ['Invalid path']] if site.invalid_path?(path)
|
||||||
|
return [400, {}, ['Path too long']] if SiteFile.path_too_long?(path)
|
||||||
|
return [409, {}, ['Already exists']] if site.file_exists?(path)
|
||||||
|
|
||||||
|
site.create_directory(path)
|
||||||
|
return [201, {}, ['']]
|
||||||
|
|
||||||
|
when 'MOVE'
|
||||||
|
destination = env['HTTP_DESTINATION'][/\/webdav(.+)$/i, 1]
|
||||||
|
return [400, {}, ['Bad Request']] unless destination
|
||||||
|
|
||||||
|
begin
|
||||||
|
destination = Rack::Utils.unescape_path(destination)
|
||||||
|
rescue ArgumentError
|
||||||
|
return [400, {}, ['Invalid destination path']]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Remove leading and trailing slashes if present
|
||||||
|
path.sub!(/^\//, '')
|
||||||
|
path.sub!(/\/$/, '')
|
||||||
|
site_file = site.site_files.find { |s| s.path == path }
|
||||||
|
return [404, {}, ['']] unless site_file
|
||||||
|
|
||||||
|
return [400, {}, ['Invalid destination path']] if site.invalid_path?(destination)
|
||||||
|
return [400, {}, ['Destination path too long']] if SiteFile.path_too_long?(destination)
|
||||||
|
return [400, {}, ['Destination filename too long']] if SiteFile.name_too_long?(destination)
|
||||||
|
return [403, {}, ['Cannot rename to index.html at root']] if destination == '/index.html' && path != '/index.html'
|
||||||
|
|
||||||
|
res = site_file.rename(destination)
|
||||||
|
if res.first == true
|
||||||
|
return [201, {}, ['']]
|
||||||
|
else
|
||||||
|
return [400, {}, [res.last]]
|
||||||
|
end
|
||||||
|
|
||||||
|
when 'DELETE'
|
||||||
|
return [403, {}, ['Cannot delete index.html']] if path == '/index.html' || path == 'index.html'
|
||||||
|
return [403, {}, ['Cannot delete root directory']] if site.files_path(path) == site.files_path
|
||||||
|
return [404, {}, ['File not found']] unless site.file_exists?(path)
|
||||||
|
|
||||||
|
site.delete_file(path)
|
||||||
|
return [201, {}, ['']]
|
||||||
|
else
|
||||||
|
return [501, {}, ['Not Implemented']]
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
map '/sidekiq' do
|
||||||
|
use Rack::Auth::Basic, "Protected Area" do |username, password|
|
||||||
|
raise 'missing sidekiq auth' unless $config['sidekiq_user'] && $config['sidekiq_pass']
|
||||||
|
username == $config['sidekiq_user'] && password == $config['sidekiq_pass']
|
||||||
|
end
|
||||||
|
|
||||||
|
use Rack::Session::Cookie, key: 'sidekiq.session', secret: Base64.strict_decode64($config['session_secret'])
|
||||||
|
use Rack::Protection::AuthenticityToken
|
||||||
|
run Sidekiq::Web
|
||||||
|
end
|
||||||
37
config.yml.ci
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
database: 'postgres://postgres:citestpassword@localhost/ci_test'
|
||||||
|
database_pool: 1
|
||||||
|
session_secret: 'SSBqdXN0IHdhbnRlZCB0byBzZWUgd2hhdCB5b3UgbG9va2VkIGxpa2UgaW4gYSBkcmVzcywgRGFkZSBNdXJwaHk='
|
||||||
|
email_unsubscribe_token: "somethingrandomderrrrp"
|
||||||
|
paypal_api_username: derp
|
||||||
|
paypal_api_password: ing
|
||||||
|
paypal_api_signature: tonz
|
||||||
|
logs_path: "/tmp/neocitiestestlogs"
|
||||||
|
letsencrypt_key: ./tests/files/letsencrypt.key
|
||||||
|
letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/
|
||||||
|
proxy_ips:
|
||||||
|
- 10.0.0.1
|
||||||
|
- 10.0.0.2
|
||||||
|
education_tag_whitelist:
|
||||||
|
- mrteacher
|
||||||
|
stop_forum_spam_api_key: testkey
|
||||||
|
screenshot_urls:
|
||||||
|
- http://screenshots:derp@screenshotssite.com
|
||||||
|
cache_control_ips:
|
||||||
|
- 1.2.3.4
|
||||||
|
- 4.5.6.7
|
||||||
|
hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001"
|
||||||
|
hcaptcha_secret_key: "0x0000000000000000000000000000000000000000"
|
||||||
|
twilio_account_sid: ACEDERPDERP
|
||||||
|
twilio_auth_token: derpderpderp
|
||||||
|
twilio_service_sid: VADERPDERPDERP
|
||||||
|
minfraud_account_id: 696969420
|
||||||
|
minfraud_license_key: DERPDERPDERP
|
||||||
|
google_custom_search_key: herpderp
|
||||||
|
google_custom_search_cx: herpderp
|
||||||
|
google_custom_search_query_limit: 69
|
||||||
|
support_email: derp@example.com
|
||||||
|
moderation_blur_categories: [ cats, dogs ]
|
||||||
|
search_url: https://example.com/customsearch/v1
|
||||||
|
search_connect_timeout: 1
|
||||||
|
search_read_timeout: 1
|
||||||
|
search_write_timeout: 1
|
||||||
82
config.yml.template
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
development:
|
||||||
|
database: 'postgres://localhost/neocities'
|
||||||
|
database_pool: 1
|
||||||
|
redis_url: "redis://localhost"
|
||||||
|
session_secret: "SSBqdXN0IHdhbnRlZCB0byBzZWUgd2hhdCB5b3UgbG9va2VkIGxpa2UgaW4gYSBkcmVzcywgRGFkZSBNdXJwaHk="
|
||||||
|
hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001"
|
||||||
|
hcaptcha_secret_key: "0x0000000000000000000000000000000000000000"
|
||||||
|
sidekiq_user: "ENTER USER HERE"
|
||||||
|
sidekiq_pass: "ENTER PASS HERE"
|
||||||
|
stripe_publishable_key: "ENTER KEY HERE"
|
||||||
|
stripe_api_key: "ENTER KEY HERE"
|
||||||
|
ip_hash_salt: "400$8$1$fc21863da5d531c1"
|
||||||
|
proxy_pass: 'somethinglongandrandom'
|
||||||
|
email_unsubscribe_token: 'somethingrandom'
|
||||||
|
logs_path: /path/to/nginx/logs
|
||||||
|
paypal_api_username: derp
|
||||||
|
paypal_api_password: ing
|
||||||
|
paypal_api_signature: tonz
|
||||||
|
letsencrypt_key: ./tests/files/letsencrypt.key
|
||||||
|
letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/
|
||||||
|
minfraud_account_id: 696969420
|
||||||
|
minfraud_license_key: DERPDERPDERP
|
||||||
|
proxy_ips:
|
||||||
|
- 10.0.0.1
|
||||||
|
- 10.0.0.2
|
||||||
|
education_tag_whitelist:
|
||||||
|
- mrteacher
|
||||||
|
screenshot_urls:
|
||||||
|
- http://screenshots:derp@127.0.0.1:12345
|
||||||
|
stop_forum_spam_api_key: testkey
|
||||||
|
google_custom_search_key: herpderp
|
||||||
|
google_custom_search_cx: herpderp
|
||||||
|
google_custom_search_query_limit: 69
|
||||||
|
support_email: derp@example.com
|
||||||
|
moderation_blur_categories: [ cats, dogs ]
|
||||||
|
search_url: https://example.com/customsearch/v1
|
||||||
|
search_connect_timeout: 1
|
||||||
|
search_read_timeout: 1
|
||||||
|
search_write_timeout: 1
|
||||||
|
test:
|
||||||
|
database: 'postgres://localhost/neocities_test'
|
||||||
|
database_pool: 1
|
||||||
|
session_secret: "SSBqdXN0IHdhbnRlZCB0byBzZWUgd2hhdCB5b3UgbG9va2VkIGxpa2UgaW4gYSBkcmVzcywgRGFkZSBNdXJwaHk="
|
||||||
|
hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001"
|
||||||
|
hcaptcha_secret_key: "0x0000000000000000000000000000000000000000"
|
||||||
|
sidekiq_user: "ENTER USER HERE"
|
||||||
|
sidekiq_pass: "ENTER PASS HERE"
|
||||||
|
stripe_publishable_key: "ENTER KEY HERE"
|
||||||
|
stripe_api_key: "ENTER KEY HERE"
|
||||||
|
ip_hash_salt: "400$8$1$fc21863da5d531c1"
|
||||||
|
proxy_pass: 'somethinglongandrandom'
|
||||||
|
email_unsubscribe_token: 'somethingrandom'
|
||||||
|
paypal_api_username: derp
|
||||||
|
paypal_api_password: ing
|
||||||
|
paypal_api_signature: tonz
|
||||||
|
letsencrypt_key: ./tests/files/letsencrypt.key
|
||||||
|
letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/
|
||||||
|
proxy_ips:
|
||||||
|
- 10.0.0.1
|
||||||
|
- 10.0.0.2
|
||||||
|
education_tag_whitelist:
|
||||||
|
- mrteacher
|
||||||
|
stop_forum_spam_api_key: testkey
|
||||||
|
screenshot_urls:
|
||||||
|
- http://screenshots:derp@screenshotssite.com
|
||||||
|
cache_control_ips:
|
||||||
|
- 1.2.3.4
|
||||||
|
- 4.5.6.7
|
||||||
|
twilio_account_sid: ACEDERPDERP
|
||||||
|
twilio_auth_token: derpderpderp
|
||||||
|
twilio_service_sid: VADERPDERPDERP
|
||||||
|
minfraud_account_id: 696969420
|
||||||
|
minfraud_license_key: DERPDERPDERP
|
||||||
|
google_custom_search_key: herpderp
|
||||||
|
google_custom_search_cx: herpderp
|
||||||
|
google_custom_search_query_limit: 69
|
||||||
|
support_email: derp@example.com
|
||||||
|
moderation_blur_categories: [ cats, dogs ]
|
||||||
|
search_url: https://example.com/customsearch/v1
|
||||||
|
search_connect_timeout: 1
|
||||||
|
search_read_timeout: 1
|
||||||
|
search_write_timeout: 1
|
||||||
106
docker-compose.yml
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
container_name: ${WEB_INTERFACE_CONTAINER}
|
||||||
|
image: baldcap/neocities
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "9292:9292"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- ./docker/config.yml.docker:/app/config.yml:ro
|
||||||
|
- site_files:/app/public/sites
|
||||||
|
- clamav_data:/var/lib/clamav
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "bash -c '</dev/tcp/localhost/9292' 2>/dev/null"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 60s
|
||||||
|
environment:
|
||||||
|
RACK_ENV: production
|
||||||
|
SMTP_ADDRESS: ${SMTP_ADDRESS}
|
||||||
|
SMTP_PORT: ${SMTP_PORT:-587}
|
||||||
|
SMTP_USERNAME: ${SMTP_USERNAME}
|
||||||
|
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||||
|
worker:
|
||||||
|
image: neocities
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
command: worker
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- ./docker/config.yml.docker:/app/config.yml:ro
|
||||||
|
- site_files:/app/public/sites
|
||||||
|
depends_on:
|
||||||
|
web:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
RACK_ENV: production
|
||||||
|
SMTP_ADDRESS: ${SMTP_ADDRESS}
|
||||||
|
SMTP_PORT: ${SMTP_PORT:-587}
|
||||||
|
SMTP_USERNAME: ${SMTP_USERNAME}
|
||||||
|
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "8081:80"
|
||||||
|
- "4433:443"
|
||||||
|
volumes:
|
||||||
|
- ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
# - ./docker/sslcerts:/etc/nginx/sslcerts:ro
|
||||||
|
depends_on:
|
||||||
|
web:
|
||||||
|
condition: service_healthy
|
||||||
|
db:
|
||||||
|
container_name: ${DATABASE_CONTAINER}
|
||||||
|
image: postgres:16
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DATABASE_USER}
|
||||||
|
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||||
|
POSTGRES_DB: ${DATABASE}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./docker/postgres-init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
container_name: ${REDIS_CONTAINER}
|
||||||
|
image: redis:7-alpine
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
# ClamAV is opt-in for dev: docker compose --profile clamav up
|
||||||
|
clamav:
|
||||||
|
image: clamav/clamav:stable
|
||||||
|
volumes:
|
||||||
|
- clamav_data:/var/lib/clamav
|
||||||
|
profiles:
|
||||||
|
- clamav
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
redis_data:
|
||||||
|
site_files:
|
||||||
|
clamav_data:
|
||||||
221
environment.rb
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
RubyVM::YJIT.enable
|
||||||
|
ENV['RACK_ENV'] ||= 'production'
|
||||||
|
ENV['TZ'] = 'UTC'
|
||||||
|
DIR_ROOT = File.expand_path File.dirname(__FILE__)
|
||||||
|
Encoding.default_internal = 'UTF-8'
|
||||||
|
Encoding.default_external = 'UTF-8'
|
||||||
|
|
||||||
|
require 'yaml'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
|
Bundler.require
|
||||||
|
Bundler.require :development if ENV['RACK_ENV'] == 'development'
|
||||||
|
|
||||||
|
require 'logger'
|
||||||
|
|
||||||
|
require 'tilt/erubi'
|
||||||
|
require 'active_support'
|
||||||
|
require 'active_support/time'
|
||||||
|
|
||||||
|
ActiveSupport.to_time_preserves_timezone = :zone
|
||||||
|
|
||||||
|
class File
|
||||||
|
def self.exists?(val)
|
||||||
|
self.exist?(val)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Dir['./ext/**/*.rb'].each {|f| require f}
|
||||||
|
|
||||||
|
# :nocov:
|
||||||
|
if ENV['CI']
|
||||||
|
$config = YAML.load_file File.join(DIR_ROOT, 'config.yml.ci')
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
$config = YAML.load_file(File.join(DIR_ROOT, 'config.yml'))[ENV['RACK_ENV']]
|
||||||
|
rescue Errno::ENOENT
|
||||||
|
puts "ERROR: Please provide a config.yml file."
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# :nocov:
|
||||||
|
|
||||||
|
DB = Sequel.connect $config['database'], sslmode: 'disable', max_connections: $config['database_pool']
|
||||||
|
DB.extension :pagination
|
||||||
|
DB.extension :auto_literal_strings
|
||||||
|
Sequel.split_symbols = true
|
||||||
|
Sidekiq.strict_args!(false)
|
||||||
|
|
||||||
|
require 'will_paginate/sequel'
|
||||||
|
|
||||||
|
# :nocov:
|
||||||
|
=begin
|
||||||
|
if defined?(Pry)
|
||||||
|
Pry.commands.alias_command 'c', 'continue'
|
||||||
|
Pry.commands.alias_command 's', 'step'
|
||||||
|
Pry.commands.alias_command 'n', 'next'
|
||||||
|
Pry.commands.alias_command 'f', 'finish'
|
||||||
|
end
|
||||||
|
=end
|
||||||
|
# :nocov:
|
||||||
|
|
||||||
|
unless ENV['RACK_ENV'] == 'production'
|
||||||
|
Sidekiq.configure_server do |config|
|
||||||
|
config.logger = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sidekiq_redis_config = {}
|
||||||
|
sidekiq_redis_config[:url] = $config['sidekiq_url'] if $config['sidekiq_url']
|
||||||
|
|
||||||
|
# :nocov:
|
||||||
|
Sidekiq.configure_server do |config|
|
||||||
|
config.redis = sidekiq_redis_config
|
||||||
|
end
|
||||||
|
# :nocov:
|
||||||
|
|
||||||
|
Sidekiq.configure_client do |config|
|
||||||
|
config.logger = nil
|
||||||
|
config.redis = sidekiq_redis_config
|
||||||
|
end
|
||||||
|
|
||||||
|
if ENV['RACK_ENV'] == 'test'
|
||||||
|
$redis = MockRedis.new
|
||||||
|
else
|
||||||
|
$redis = Redis.new(
|
||||||
|
url: $config['redis_url'],
|
||||||
|
read_timeout: 5.0,
|
||||||
|
write_timeout: 5.0,
|
||||||
|
connect_timeout: 5.0
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
$redis_cache = Redis::Namespace.new :cache, redis: $redis
|
||||||
|
|
||||||
|
if ENV['RACK_ENV'] == 'test'
|
||||||
|
$redis_proxy = MockRedis.new
|
||||||
|
else
|
||||||
|
$redis_proxy = Redis.new(
|
||||||
|
url: $config['redis_proxy'],
|
||||||
|
read_timeout: 5.0,
|
||||||
|
write_timeout: 5.0,
|
||||||
|
connect_timeout: 5.0
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nocov:
|
||||||
|
if ENV['RACK_ENV'] == 'development'
|
||||||
|
# Run async jobs immediately in development.
|
||||||
|
=begin
|
||||||
|
module Sidekiq
|
||||||
|
module Worker
|
||||||
|
module ClassMethods
|
||||||
|
def perform_async(*args)
|
||||||
|
Thread.new {
|
||||||
|
self.new.perform *args
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
=end
|
||||||
|
end
|
||||||
|
# :nocov:
|
||||||
|
|
||||||
|
Sequel.datetime_class = Time
|
||||||
|
Sequel.extension :core_extensions
|
||||||
|
Sequel.extension :migration
|
||||||
|
Sequel::Model.plugin :validation_helpers
|
||||||
|
Sequel::Model.plugin :force_encoding, 'UTF-8'
|
||||||
|
Sequel::Model.plugin :defaults_setter
|
||||||
|
Sequel::Model.plugin :create_timestamp
|
||||||
|
Sequel.default_timezone = :utc
|
||||||
|
Sequel::Migrator.apply DB, './migrations'
|
||||||
|
|
||||||
|
Stripe.api_key = $config['stripe_api_key']
|
||||||
|
|
||||||
|
Dir.glob('models/*.rb').each {|m| require File.join(DIR_ROOT, "#{m}") }
|
||||||
|
Dir.glob('workers/*.rb').each {|w| require File.join(DIR_ROOT, "/#{w}") }
|
||||||
|
|
||||||
|
DB.loggers << Logger.new(STDOUT) if ENV['RACK_ENV'] == 'development'
|
||||||
|
|
||||||
|
#Mail.defaults do
|
||||||
|
# options = {}
|
||||||
|
#delivery_method :sendmail, options
|
||||||
|
#end
|
||||||
|
Mail.defaults do
|
||||||
|
options = {
|
||||||
|
address: ENV.fetch("SMTP_ADDRESS"),
|
||||||
|
port: ENV.fetch("SMTP_PORT", 587).to_i,
|
||||||
|
user_name: ENV.fetch("SMTP_USERNAME"),
|
||||||
|
password: ENV.fetch("SMTP_PASSWORD"),
|
||||||
|
authentication: :login,
|
||||||
|
enable_starttls_auto: true
|
||||||
|
}
|
||||||
|
|
||||||
|
delivery_method :smtp, options
|
||||||
|
end
|
||||||
|
Sinatra::Application.set :erb, escape_html: true
|
||||||
|
|
||||||
|
require 'sass/plugin/rack'
|
||||||
|
Sinatra::Application.use Sass::Plugin::Rack
|
||||||
|
|
||||||
|
Sass::Plugin.options[:template_location] = 'sass'
|
||||||
|
Sass::Plugin.options[:css_location] = './public/css'
|
||||||
|
Sass::Plugin.options[:style] = :nested
|
||||||
|
|
||||||
|
if ENV['RACK_ENV'] != 'development'
|
||||||
|
Sass::Plugin.options[:style] = :compressed
|
||||||
|
# Sass::Plugin.options[:never_update] = true
|
||||||
|
Sass::Plugin.options[:full_exception] = false
|
||||||
|
end
|
||||||
|
|
||||||
|
PayPalRecurring.configure do |config|
|
||||||
|
config.sandbox = false
|
||||||
|
config.username = $config['paypal_api_username']
|
||||||
|
config.password = $config['paypal_api_password']
|
||||||
|
config.signature = $config['paypal_api_signature']
|
||||||
|
end
|
||||||
|
|
||||||
|
require 'csv'
|
||||||
|
|
||||||
|
$country_codes = {}
|
||||||
|
|
||||||
|
CSV.foreach("./files/country_codes.csv") do |row|
|
||||||
|
$country_codes[row.last] = row.first
|
||||||
|
end
|
||||||
|
|
||||||
|
gandi_opts = {}
|
||||||
|
gandi_opts[:env] = :test # unless ENV['RACK_ENV'] == 'production'
|
||||||
|
$gandi = Gandi::Session.new $config['gandi_api_key'], gandi_opts
|
||||||
|
|
||||||
|
$image_optim = ImageOptim.new pngout: false, svgo: false
|
||||||
|
|
||||||
|
Money.locale_backend = nil
|
||||||
|
Money.default_currency = Money::Currency.new("USD")
|
||||||
|
Money.rounding_mode = BigDecimal::ROUND_HALF_UP
|
||||||
|
|
||||||
|
$twilio = Twilio::REST::Client.new $config['twilio_account_sid'], $config['twilio_auth_token']
|
||||||
|
|
||||||
|
Minfraud.configure do |c|
|
||||||
|
c.account_id = $config['minfraud_account_id']
|
||||||
|
c.license_key = $config['minfraud_license_key']
|
||||||
|
c.enable_validation = true
|
||||||
|
end
|
||||||
|
|
||||||
|
Airbrake.configure do |c|
|
||||||
|
c.project_id = $config['airbrake_project_id']
|
||||||
|
c.project_key = $config['airbrake_project_key']
|
||||||
|
end
|
||||||
|
|
||||||
|
Airbrake.add_filter do |notice|
|
||||||
|
if notice[:params][:password]
|
||||||
|
# Filter out password.
|
||||||
|
notice[:params][:password] = '[Filtered]'
|
||||||
|
end
|
||||||
|
|
||||||
|
notice.ignore! if notice.stash[:exception].is_a?(Sinatra::NotFound)
|
||||||
|
end
|
||||||
|
|
||||||
|
Airbrake.add_filter Airbrake::Sidekiq::RetryableJobsFilter.new
|
||||||
13
ext/NilClass.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
class NilClass
|
||||||
|
def empty?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def blank?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def not_an_integer?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
169
ext/ago.rb
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
|
||||||
|
module Ago
|
||||||
|
module VERSION
|
||||||
|
MAJOR = 0
|
||||||
|
MINOR = 1
|
||||||
|
TINY = 5
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def pretty
|
||||||
|
"#{MAJOR}.#{MINOR}.#{TINY}"
|
||||||
|
end
|
||||||
|
alias_method :print, :pretty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Ago::Order = [:year, :month, :week, :day, :hour, :minute, :second]
|
||||||
|
Ago::Units = {
|
||||||
|
:year => {
|
||||||
|
:basic => 60 * 60 * 24 * 365,
|
||||||
|
:gregorian => 86400 * 365.2425,
|
||||||
|
},
|
||||||
|
:month => {
|
||||||
|
:basic => 60 * 60 * 24 * 30,
|
||||||
|
:gregorian => 86400 * 30.436875,
|
||||||
|
},
|
||||||
|
:week => {
|
||||||
|
:basic => 60 * 60 * 24 * 7,
|
||||||
|
:gregorian => 86400 * 7.02389423076923,
|
||||||
|
},
|
||||||
|
:day => {
|
||||||
|
:basic => 60 * 60 * 24
|
||||||
|
},
|
||||||
|
:hour => {
|
||||||
|
:basic => 60 * 60
|
||||||
|
},
|
||||||
|
:minute => {
|
||||||
|
:basic => 60
|
||||||
|
},
|
||||||
|
:second => {
|
||||||
|
:basic => 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def Ago.calendar_check(calendar)
|
||||||
|
error = ":calendar => value must be either :basic or :gregorian."
|
||||||
|
unless calendar == :basic || calendar == :gregorian
|
||||||
|
raise ArgumentError, error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
module Ago::TimeAgo
|
||||||
|
# Generate List of valid unit :symbols
|
||||||
|
valids = ""
|
||||||
|
Ago::Order.each do |u|
|
||||||
|
unless u == :second
|
||||||
|
valids += ":#{u.to_s}, "
|
||||||
|
else
|
||||||
|
valids += "and :#{u.to_s}."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Valids = valids
|
||||||
|
|
||||||
|
def ago_in_words(opts={})
|
||||||
|
# Process options {hash}
|
||||||
|
focus = opts[:focus] ? opts[:focus] : 0
|
||||||
|
start_at = opts[:start_at] ? opts[:start_at] : :year
|
||||||
|
now = opts[:now] ? opts[:now] : Time.now
|
||||||
|
in_time = opts[:in_time] ? opts[:in_time] : :past
|
||||||
|
calendar = opts[:calendar] ? opts[:calendar] : :basic
|
||||||
|
|
||||||
|
# Filter out invalid arguments for :in_time
|
||||||
|
in_time_error = ":in_time => value must be either :past or :future, " \
|
||||||
|
+ "depending on whether the Time object is before or after Time.now."
|
||||||
|
unless in_time == :past || in_time == :future
|
||||||
|
raise ArgumentError, in_time_error
|
||||||
|
end
|
||||||
|
|
||||||
|
# Filter out invalid arguments for :calendar
|
||||||
|
Ago.calendar_check(calendar)
|
||||||
|
|
||||||
|
# Filter out invalid arguments for :start_at and :focus
|
||||||
|
base_error = " => value must either be a number " +
|
||||||
|
"between 0 and 6 (inclusive),\nor one of the following " +
|
||||||
|
"symbols: " + Valids
|
||||||
|
{:focus => focus, :start_at => start_at}.each do |key, opt|
|
||||||
|
opt_error = ":" + key.to_s + base_error
|
||||||
|
if opt.class == Integer
|
||||||
|
raise ArgumentError, opt_error unless opt >= 0 && opt <= 6
|
||||||
|
elsif opt.class == Symbol
|
||||||
|
raise ArgumentError, opt_error unless Ago::Units[opt]
|
||||||
|
else
|
||||||
|
raise ArgumentError, opt_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create Variables necessary for processing.
|
||||||
|
frags = []
|
||||||
|
output = ""
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
now = calendar == :basic ? now.to_i : now.to_f
|
||||||
|
my_time = calendar == :basic ? self.to_i : self.to_f
|
||||||
|
if now > my_time
|
||||||
|
diff = now - my_time
|
||||||
|
tail = " ago"
|
||||||
|
elsif my_time > now
|
||||||
|
diff = my_time - now
|
||||||
|
tail = " from now"
|
||||||
|
else
|
||||||
|
diff = 0
|
||||||
|
tail = "just now"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Begin Ago.ago processing
|
||||||
|
Ago::Order.each do |u|
|
||||||
|
if calendar == :gregorian && Ago::Units[u][:gregorian]
|
||||||
|
value = Ago::Units[u][:gregorian]
|
||||||
|
else
|
||||||
|
value = Ago::Units[u][:basic]
|
||||||
|
end
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
# Move further ahead in the Ago::Units array if start_at is farther back than
|
||||||
|
# the current point in the array.
|
||||||
|
if start_at.class == Integer
|
||||||
|
next if count <= start_at
|
||||||
|
elsif start_at.class == Symbol
|
||||||
|
next if Ago::Order.index(u) < Ago::Order.index(start_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
n = (diff/value).floor
|
||||||
|
if n > 0
|
||||||
|
plural = n > 1 ? "s" : ""
|
||||||
|
frags << "#{n} #{u.to_s + plural}"
|
||||||
|
|
||||||
|
# If the argument passed into ago() is a symbol, focus the ago statement
|
||||||
|
# down to the level specified in the symbol
|
||||||
|
if focus.class == Symbol
|
||||||
|
break if u == focus || u == :second
|
||||||
|
elsif focus.class == Integer
|
||||||
|
if focus == 0 || u == :second
|
||||||
|
break
|
||||||
|
else
|
||||||
|
focus -= 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
diff -= n * value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Der Kommissar
|
||||||
|
frags.size.times do |n|
|
||||||
|
output += frags[n]
|
||||||
|
output += ", " unless n == frags.size - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return output + "#{tail}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_now_in_words(opts={})
|
||||||
|
ago_in_words(opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Time
|
||||||
|
include Ago::TimeAgo
|
||||||
|
end
|
||||||
13
ext/float.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
class Float
|
||||||
|
def round_to(x)
|
||||||
|
(self * 10**x).round.to_f / 10**x
|
||||||
|
end
|
||||||
|
|
||||||
|
def ceil_to(x)
|
||||||
|
(self * 10**x).ceil.to_f / 10**x
|
||||||
|
end
|
||||||
|
|
||||||
|
def floor_to(x)
|
||||||
|
(self * 10**x).floor.to_f / 10**x
|
||||||
|
end
|
||||||
|
end
|
||||||
83
ext/numeric.rb
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
class Numeric
|
||||||
|
ONE_MEGABYTE = 1000000
|
||||||
|
|
||||||
|
def roundup(nearest=10)
|
||||||
|
self % nearest == 0 ? self : self + nearest - (self % nearest)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_mb
|
||||||
|
self/ONE_MEGABYTE.to_f
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_bytes_pretty
|
||||||
|
computed = nil
|
||||||
|
unit = nil
|
||||||
|
{
|
||||||
|
'B' => 1000,
|
||||||
|
'KB' => 1000 * 1000,
|
||||||
|
'MB' => 1000 * 1000 * 1000,
|
||||||
|
'GB' => 1000 * 1000 * 1000 * 1000,
|
||||||
|
'TB' => 1000 * 1000 * 1000 * 1000 * 1000
|
||||||
|
}.each_pair { |e, s|
|
||||||
|
if self < s
|
||||||
|
computed = (self.to_f / (s / 1000)).round(2)
|
||||||
|
unit = e
|
||||||
|
break
|
||||||
|
end
|
||||||
|
}
|
||||||
|
computed = computed.to_i if computed.modulo(1) == 0.0
|
||||||
|
|
||||||
|
"#{computed} #{unit}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_gigabytes_pretty
|
||||||
|
self.to_gigabytes.to_s + ' GB'
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_gigabytes
|
||||||
|
self / (1000**3)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_comma_separated
|
||||||
|
self.to_i.to_s.chars.to_a.reverse.each_slice(3).map(&:join).join(",").reverse
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_large_number
|
||||||
|
return self.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
||||||
|
if self > 9999
|
||||||
|
if self > 999999999
|
||||||
|
unit_char = 'B' #billion
|
||||||
|
unit_amount = 1000000000.0
|
||||||
|
elsif self > 999999
|
||||||
|
unit_char = 'M' #million
|
||||||
|
unit_amount = 1000000.0
|
||||||
|
elsif self > 9999
|
||||||
|
unit_char = 'K' #thousand
|
||||||
|
unit_amount = 1000.0
|
||||||
|
end
|
||||||
|
|
||||||
|
self_divided = self.to_f / unit_amount
|
||||||
|
self_rounded = self_divided.round(1)
|
||||||
|
|
||||||
|
if self_rounded.denominator == 1
|
||||||
|
return sprintf ("%.0f" + unit_char), self_divided
|
||||||
|
else
|
||||||
|
return sprintf ("%.1f" + unit_char), self_divided
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if self > 999
|
||||||
|
return self.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
||||||
|
else
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_space_pretty
|
||||||
|
to_bytes_pretty
|
||||||
|
end
|
||||||
|
|
||||||
|
def not_an_integer?
|
||||||
|
!self.integer?
|
||||||
|
end
|
||||||
|
end
|
||||||
8
ext/sequel/plugins/create_timestamp.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
module Sequel::Plugins::CreateTimestamp
|
||||||
|
module InstanceMethods
|
||||||
|
def before_create
|
||||||
|
self.created_at = Time.now if respond_to?(:created_at) && !self.created_at
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
33
ext/sequel/plugins/paranoid_delete.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
module Sequel
|
||||||
|
module ParanoidDelete
|
||||||
|
|
||||||
|
def self.included(base)
|
||||||
|
base.extend(ClassMethods)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Instead of actually deleting, we just set is_deleted to true,
|
||||||
|
# and look for it with our default dataset filter.
|
||||||
|
def delete
|
||||||
|
self.is_deleted = true
|
||||||
|
save :validate => false
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
|
||||||
|
# There's no hook for setting default filters after inheritance (that I'm aware of),
|
||||||
|
# so this adds the filter for the first time the class' dataset is accessed for the new model.
|
||||||
|
def dataset
|
||||||
|
if @_is_deleted_filter_set.nil?
|
||||||
|
#KD: I turned this off because I think it's easier to do this manually.
|
||||||
|
#@dataset.filter! is_deleted: false
|
||||||
|
@_is_deleted_filter_set = true
|
||||||
|
end
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Model.include ParanoidDelete
|
||||||
|
end
|
||||||
7
ext/sinatra/base.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
class Sinatra::Base
|
||||||
|
alias_method :render_original, :render
|
||||||
|
def render(engine, data, options = {}, locals = {}, &block)
|
||||||
|
options.merge!(pretty: self.class.development?) if engine == :slim && options[:pretty].nil?
|
||||||
|
render_original engine, data, options, locals, &block
|
||||||
|
end
|
||||||
|
end
|
||||||
5
ext/sinatra/indifferent_hash.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
class Sinatra::IndifferentHash
|
||||||
|
def not_an_integer?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
26
ext/string.rb
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
class String
|
||||||
|
def shorten(length, usedots=true)
|
||||||
|
if usedots
|
||||||
|
return self if self.length < length
|
||||||
|
"#{self[0..length-3]}..."
|
||||||
|
else
|
||||||
|
self[0..length]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unindent
|
||||||
|
gsub /^#{Regexp.quote(scan(/^\s*/).min_by{|l|l.length})}/, ""
|
||||||
|
end
|
||||||
|
|
||||||
|
def blank?
|
||||||
|
return true if self == ''
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def not_an_integer?
|
||||||
|
Integer(self)
|
||||||
|
false
|
||||||
|
rescue ArgumentError
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
7
ext/tempfile.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
class Tempfile
|
||||||
|
alias_method :size_original, :size
|
||||||
|
def size
|
||||||
|
s = size_original
|
||||||
|
s.nil? ? 0 : s
|
||||||
|
end
|
||||||
|
end
|
||||||
9
ext/time.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
class Time
|
||||||
|
def ago
|
||||||
|
self.ago_in_words
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_now
|
||||||
|
self.from_now_in_words
|
||||||
|
end
|
||||||
|
end
|
||||||
BIN
files/GeoLiteCity.dat
Normal file
250
files/country_codes.csv
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
Afghanistan,AF
|
||||||
|
Åland Islands,AX
|
||||||
|
Albania,AL
|
||||||
|
Algeria,DZ
|
||||||
|
American Samoa,AS
|
||||||
|
Andorra,AD
|
||||||
|
Angola,AO
|
||||||
|
Anguilla,AI
|
||||||
|
Antarctica,AQ
|
||||||
|
Antigua and Barbuda,AG
|
||||||
|
Argentina,AR
|
||||||
|
Armenia,AM
|
||||||
|
Aruba,AW
|
||||||
|
Australia,AU
|
||||||
|
Austria,AT
|
||||||
|
Azerbaijan,AZ
|
||||||
|
Bahamas,BS
|
||||||
|
Bahrain,BH
|
||||||
|
Bangladesh,BD
|
||||||
|
Barbados,BB
|
||||||
|
Belarus,BY
|
||||||
|
Belgium,BE
|
||||||
|
Belize,BZ
|
||||||
|
Benin,BJ
|
||||||
|
Bermuda,BM
|
||||||
|
Bhutan,BT
|
||||||
|
"Bolivia, Plurinational State of",BO
|
||||||
|
"Bonaire, Sint Eustatius and Saba",BQ
|
||||||
|
Bosnia and Herzegovina,BA
|
||||||
|
Botswana,BW
|
||||||
|
Bouvet Island,BV
|
||||||
|
Brazil,BR
|
||||||
|
British Indian Ocean Territory,IO
|
||||||
|
Brunei Darussalam,BN
|
||||||
|
Bulgaria,BG
|
||||||
|
Burkina Faso,BF
|
||||||
|
Burundi,BI
|
||||||
|
Cambodia,KH
|
||||||
|
Cameroon,CM
|
||||||
|
Canada,CA
|
||||||
|
Cape Verde,CV
|
||||||
|
Cayman Islands,KY
|
||||||
|
Central African Republic,CF
|
||||||
|
Chad,TD
|
||||||
|
Chile,CL
|
||||||
|
China,CN
|
||||||
|
Christmas Island,CX
|
||||||
|
Cocos (Keeling) Islands,CC
|
||||||
|
Colombia,CO
|
||||||
|
Comoros,KM
|
||||||
|
Congo,CG
|
||||||
|
"Congo, the Democratic Republic of the",CD
|
||||||
|
Cook Islands,CK
|
||||||
|
Costa Rica,CR
|
||||||
|
Côte d'Ivoire,CI
|
||||||
|
Croatia,HR
|
||||||
|
Cuba,CU
|
||||||
|
Curaçao,CW
|
||||||
|
Cyprus,CY
|
||||||
|
Czech Republic,CZ
|
||||||
|
Denmark,DK
|
||||||
|
Djibouti,DJ
|
||||||
|
Dominica,DM
|
||||||
|
Dominican Republic,DO
|
||||||
|
Ecuador,EC
|
||||||
|
Egypt,EG
|
||||||
|
El Salvador,SV
|
||||||
|
Equatorial Guinea,GQ
|
||||||
|
Eritrea,ER
|
||||||
|
Estonia,EE
|
||||||
|
Ethiopia,ET
|
||||||
|
Falkland Islands (Malvinas),FK
|
||||||
|
Faroe Islands,FO
|
||||||
|
Fiji,FJ
|
||||||
|
Finland,FI
|
||||||
|
France,FR
|
||||||
|
French Guiana,GF
|
||||||
|
French Polynesia,PF
|
||||||
|
French Southern Territories,TF
|
||||||
|
Gabon,GA
|
||||||
|
Gambia,GM
|
||||||
|
Georgia,GE
|
||||||
|
Germany,DE
|
||||||
|
Ghana,GH
|
||||||
|
Gibraltar,GI
|
||||||
|
Greece,GR
|
||||||
|
Greenland,GL
|
||||||
|
Grenada,GD
|
||||||
|
Guadeloupe,GP
|
||||||
|
Guam,GU
|
||||||
|
Guatemala,GT
|
||||||
|
Guernsey,GG
|
||||||
|
Guinea,GN
|
||||||
|
Guinea-Bissau,GW
|
||||||
|
Guyana,GY
|
||||||
|
Haiti,HT
|
||||||
|
Heard Island and McDonald Mcdonald Islands,HM
|
||||||
|
Holy See (Vatican City State),VA
|
||||||
|
Honduras,HN
|
||||||
|
Hong Kong,HK
|
||||||
|
Hungary,HU
|
||||||
|
Iceland,IS
|
||||||
|
India,IN
|
||||||
|
Indonesia,ID
|
||||||
|
"Iran, Islamic Republic of",IR
|
||||||
|
Iraq,IQ
|
||||||
|
Ireland,IE
|
||||||
|
Isle of Man,IM
|
||||||
|
Israel,IL
|
||||||
|
Italy,IT
|
||||||
|
Jamaica,JM
|
||||||
|
Japan,JP
|
||||||
|
Jersey,JE
|
||||||
|
Jordan,JO
|
||||||
|
Kazakhstan,KZ
|
||||||
|
Kenya,KE
|
||||||
|
Kiribati,KI
|
||||||
|
"Korea, Democratic People's Republic of",KP
|
||||||
|
"Korea, Republic of",KR
|
||||||
|
Kuwait,KW
|
||||||
|
Kyrgyzstan,KG
|
||||||
|
Lao People's Democratic Republic,LA
|
||||||
|
Latvia,LV
|
||||||
|
Lebanon,LB
|
||||||
|
Lesotho,LS
|
||||||
|
Liberia,LR
|
||||||
|
Libya,LY
|
||||||
|
Liechtenstein,LI
|
||||||
|
Lithuania,LT
|
||||||
|
Luxembourg,LU
|
||||||
|
Macao,MO
|
||||||
|
"Macedonia, the Former Yugoslav Republic of",MK
|
||||||
|
Madagascar,MG
|
||||||
|
Malawi,MW
|
||||||
|
Malaysia,MY
|
||||||
|
Maldives,MV
|
||||||
|
Mali,ML
|
||||||
|
Malta,MT
|
||||||
|
Marshall Islands,MH
|
||||||
|
Martinique,MQ
|
||||||
|
Mauritania,MR
|
||||||
|
Mauritius,MU
|
||||||
|
Mayotte,YT
|
||||||
|
Mexico,MX
|
||||||
|
"Micronesia, Federated States of",FM
|
||||||
|
"Moldova, Republic of",MD
|
||||||
|
Monaco,MC
|
||||||
|
Mongolia,MN
|
||||||
|
Montenegro,ME
|
||||||
|
Montserrat,MS
|
||||||
|
Morocco,MA
|
||||||
|
Mozambique,MZ
|
||||||
|
Myanmar,MM
|
||||||
|
Namibia,NA
|
||||||
|
Nauru,NR
|
||||||
|
Nepal,NP
|
||||||
|
Netherlands,NL
|
||||||
|
New Caledonia,NC
|
||||||
|
New Zealand,NZ
|
||||||
|
Nicaragua,NI
|
||||||
|
Niger,NE
|
||||||
|
Nigeria,NG
|
||||||
|
Niue,NU
|
||||||
|
Norfolk Island,NF
|
||||||
|
Northern Mariana Islands,MP
|
||||||
|
Norway,NO
|
||||||
|
Oman,OM
|
||||||
|
Pakistan,PK
|
||||||
|
Palau,PW
|
||||||
|
"Palestine, State of",PS
|
||||||
|
Panama,PA
|
||||||
|
Papua New Guinea,PG
|
||||||
|
Paraguay,PY
|
||||||
|
Peru,PE
|
||||||
|
Philippines,PH
|
||||||
|
Pitcairn,PN
|
||||||
|
Poland,PL
|
||||||
|
Portugal,PT
|
||||||
|
Puerto Rico,PR
|
||||||
|
Qatar,QA
|
||||||
|
Réunion,RE
|
||||||
|
Romania,RO
|
||||||
|
Russian Federation,RU
|
||||||
|
Rwanda,RW
|
||||||
|
Saint Barthélemy,BL
|
||||||
|
"Saint Helena, Ascension and Tristan da Cunha",SH
|
||||||
|
Saint Kitts and Nevis,KN
|
||||||
|
Saint Lucia,LC
|
||||||
|
Saint Martin (French part),MF
|
||||||
|
Saint Pierre and Miquelon,PM
|
||||||
|
Saint Vincent and the Grenadines,VC
|
||||||
|
Samoa,WS
|
||||||
|
San Marino,SM
|
||||||
|
Sao Tome and Principe,ST
|
||||||
|
Saudi Arabia,SA
|
||||||
|
Senegal,SN
|
||||||
|
Serbia,RS
|
||||||
|
Seychelles,SC
|
||||||
|
Sierra Leone,SL
|
||||||
|
Singapore,SG
|
||||||
|
Sint Maarten (Dutch part),SX
|
||||||
|
Slovakia,SK
|
||||||
|
Slovenia,SI
|
||||||
|
Solomon Islands,SB
|
||||||
|
Somalia,SO
|
||||||
|
South Africa,ZA
|
||||||
|
South Georgia and the South Sandwich Islands,GS
|
||||||
|
South Sudan,SS
|
||||||
|
Spain,ES
|
||||||
|
Sri Lanka,LK
|
||||||
|
Sudan,SD
|
||||||
|
Suriname,SR
|
||||||
|
Svalbard and Jan Mayen,SJ
|
||||||
|
Swaziland,SZ
|
||||||
|
Sweden,SE
|
||||||
|
Switzerland,CH
|
||||||
|
Syrian Arab Republic,SY
|
||||||
|
"Taiwan, Province of China",TW
|
||||||
|
Tajikistan,TJ
|
||||||
|
"Tanzania, United Republic of",TZ
|
||||||
|
Thailand,TH
|
||||||
|
Timor-Leste,TL
|
||||||
|
Togo,TG
|
||||||
|
Tokelau,TK
|
||||||
|
Tonga,TO
|
||||||
|
Trinidad and Tobago,TT
|
||||||
|
Tunisia,TN
|
||||||
|
Turkey,TR
|
||||||
|
Turkmenistan,TM
|
||||||
|
Turks and Caicos Islands,TC
|
||||||
|
Tuvalu,TV
|
||||||
|
Uganda,UG
|
||||||
|
Ukraine,UA
|
||||||
|
United Arab Emirates,AE
|
||||||
|
United Kingdom,GB
|
||||||
|
United States,US
|
||||||
|
United States Minor Outlying Islands,UM
|
||||||
|
Uruguay,UY
|
||||||
|
Uzbekistan,UZ
|
||||||
|
Vanuatu,VU
|
||||||
|
"Venezuela, Bolivarian Republic of",VE
|
||||||
|
Viet Nam,VN
|
||||||
|
"Virgin Islands, British",VG
|
||||||
|
"Virgin Islands, U.S.",VI
|
||||||
|
Wallis and Futuna,WF
|
||||||
|
Western Sahara,EH
|
||||||
|
Yemen,YE
|
||||||
|
Zambia,ZM
|
||||||
|
Zimbabwe,ZW
|
||||||
|
European Union, EU
|
||||||
|
85
files/fullhitsmigration.rb
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
raise 'nope'
|
||||||
|
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
raise 'derp'
|
||||||
|
DB.drop_table :stats
|
||||||
|
DB.drop_table :stat_referrers
|
||||||
|
DB.drop_table :stat_paths
|
||||||
|
DB.drop_table :stat_locations
|
||||||
|
|
||||||
|
DB.create_table! :hits do
|
||||||
|
primary_key :id
|
||||||
|
Integer :site_id, index: true
|
||||||
|
Integer :hit_referrer_id
|
||||||
|
Integer :hit_path_id
|
||||||
|
Integer :hit_location_id
|
||||||
|
Bignum :bandwidth
|
||||||
|
Time :accessed_at, index: true
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :hit_referrers do
|
||||||
|
primary_key :id
|
||||||
|
String :uri, index: {unique: true}
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :hit_locations do
|
||||||
|
primary_key :id
|
||||||
|
String :country_code2
|
||||||
|
String :region_name
|
||||||
|
String :city_name
|
||||||
|
Float :latitude
|
||||||
|
Float :longitude
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :hit_paths do
|
||||||
|
primary_key :id
|
||||||
|
String :path, index: {unique: true}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
raise 'No.' if ENV['RACK_ENV'] == 'production'
|
||||||
|
|
||||||
|
%i{hits hit_referrers hit_locations hit_paths}.each do |t|
|
||||||
|
DB.drop_table t
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :stats do
|
||||||
|
primary_key :id
|
||||||
|
Integer :site_id
|
||||||
|
Date :created_at
|
||||||
|
Integer :hits
|
||||||
|
Integer :views
|
||||||
|
Integer :comments
|
||||||
|
Integer :follows
|
||||||
|
Integer :site_updates
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :stat_referrers do
|
||||||
|
primary_key :id
|
||||||
|
Integer :stat_id
|
||||||
|
String :url
|
||||||
|
String :views
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :stat_locations do
|
||||||
|
primary_key :id
|
||||||
|
Integer :stat_id
|
||||||
|
String :country_code2
|
||||||
|
String :region_name
|
||||||
|
String :city_name
|
||||||
|
Decimal :latitude
|
||||||
|
Decimal :longitude
|
||||||
|
Integer :views
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table :stat_paths do
|
||||||
|
primary_key :id
|
||||||
|
Integer :stat_id
|
||||||
|
String :name
|
||||||
|
Integer :views
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
56
files/phantomjs_screenshot.js
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
var page = require('webpage').create()
|
||||||
|
var system = require('system')
|
||||||
|
|
||||||
|
var maxTimeout = 15000
|
||||||
|
|
||||||
|
if (system.args.length === 1) {
|
||||||
|
console.log('required args: <siteURL> <outputFilePath>');
|
||||||
|
phantom.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = system.args[1]
|
||||||
|
var outputPath = system.args[2]
|
||||||
|
|
||||||
|
page.viewportSize = { width: 1280, height: 960 }
|
||||||
|
page.clipRect = { top: 0, left: 0, width: 1280, height: 960}
|
||||||
|
|
||||||
|
/*
|
||||||
|
In development, not working yet.
|
||||||
|
|
||||||
|
page.settings.scriptTimeout = 1000
|
||||||
|
|
||||||
|
page.onLongRunningScript = function() {
|
||||||
|
page.stopJavaScript()
|
||||||
|
phantom.exit(4)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
var t = Date.now()
|
||||||
|
|
||||||
|
console.log('Loading ' + address)
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
console.log('timeout')
|
||||||
|
phantom.exit(62)
|
||||||
|
}, maxTimeout)
|
||||||
|
|
||||||
|
page.settings.resourceTimeout = maxTimeout
|
||||||
|
|
||||||
|
page.onResourceTimeout = function(e) {
|
||||||
|
console.log(e.errorCode)
|
||||||
|
console.log(e.errorString)
|
||||||
|
console.log(e.url)
|
||||||
|
phantom.exit(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
page.open(address, function(status) {
|
||||||
|
if(status !== 'success') {
|
||||||
|
console.log('failed')
|
||||||
|
phantom.exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
page.render(outputPath)
|
||||||
|
console.log('Loading time ' + (Date.now() - t) + ' msec');
|
||||||
|
phantom.exit()
|
||||||
|
})
|
||||||
|
|
||||||
24
files/root_site_uris.txt
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
1,weekly,https://femslash.city
|
||||||
|
0.9,daily,https://femslash.city/browse
|
||||||
|
0.9,monthly,https://femslash.city/signin
|
||||||
|
0.9,hourly,https://femslash.city/activity
|
||||||
|
0.9,monthly,https://femslash.city/tutorials
|
||||||
|
0.9,monthly,https://femslash.city/supporter
|
||||||
|
0.9,monthly,https://femslash.city/cli
|
||||||
|
0.9,monthly,https://femslash.city/about
|
||||||
|
0.9,monthly,https://femslash.city/donate
|
||||||
|
0.9,monthly,https://femslash.city/api
|
||||||
|
0.9,monthly,https://femslash.city/press
|
||||||
|
0.8,monthly,https://femslash.city/terms
|
||||||
|
0.8,weekly,https://blog.femslash.city
|
||||||
|
0.7,monthly,https://femslash.city/contact
|
||||||
|
0.6,daily,https://femslash.city/browse?sort_by=special_sauce&tag=
|
||||||
|
0.6,hourly,https://femslash.city/browse?sort_by=last_updated&tag=
|
||||||
|
0.6,daily,https://femslash.city/browse?sort_by=supporters&tag=
|
||||||
|
0.6,weekly,https://femslash.city/browse?sort_by=featured&tag=
|
||||||
|
0.6,daily,https://femslash.city/browse?sort_by=tipping_enabled&tag=
|
||||||
|
0.6,weekly,https://femslash.city/browse?sort_by=views&tag=
|
||||||
|
0.6,weekly,https://femslash.city/browse?sort_by=hits&tag=
|
||||||
|
0.6,hourly,https://femslash.city/browse?sort_by=newest&tag=
|
||||||
|
0.6,monthly,https://femslash.city/browse?sort_by=oldest&tag=
|
||||||
|
0.6,always,https://femslash.city/browse?sort_by=random&tag=
|
||||||
BIN
files/wireframes/homepage01bg.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
files/wireframes/homepage01x.png
Normal file
|
After Width: | Height: | Size: 476 KiB |
BIN
files/wireframes/homepage02x.jpg
Normal file
|
After Width: | Height: | Size: 329 KiB |
BIN
files/wireframes/homepage03.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
files/wireframes/homepage03.psd
Normal file
BIN
files/wireframes/homepage03x.png
Normal file
|
After Width: | Height: | Size: 504 KiB |
9
files/wireframes/index.html
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
</head>
|
||||||
|
<body style="background:#E9E0D5 url(homepage01bg.png) repeat-x;text-align:center; margin:0;padding:0">
|
||||||
|
<img src="homepage01x.png">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
files/wireframes/newindex01x.png
Normal file
|
After Width: | Height: | Size: 837 KiB |
BIN
files/wireframes/newsfeed01.png
Normal file
|
After Width: | Height: | Size: 956 KiB |
BIN
files/wireframes/profile01.png
Normal file
|
After Width: | Height: | Size: 863 KiB |
BIN
files/wireframes/wireframes01x.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
17
migrations/001_create_sites.rb
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.create_table! :sites do
|
||||||
|
primary_key :id
|
||||||
|
String :username
|
||||||
|
String :email
|
||||||
|
String :password
|
||||||
|
Integer :server_id
|
||||||
|
DateTime :created_at
|
||||||
|
DateTime :updated_at
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_table :sites
|
||||||
|
}
|
||||||
|
end
|
||||||
15
migrations/002_create_servers.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.create_table! :servers do
|
||||||
|
primary_key :id
|
||||||
|
String :ip
|
||||||
|
Integer :slots_available
|
||||||
|
DateTime :created_at
|
||||||
|
DateTime :updated_at
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_table :servers
|
||||||
|
}
|
||||||
|
end
|
||||||
12
migrations/003_create_tags.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.create_table! :tags do
|
||||||
|
primary_key :id
|
||||||
|
String :name
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_table :tags
|
||||||
|
}
|
||||||
|
end
|
||||||
12
migrations/004_create_sites_tags.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.create_table! :sites_tags do
|
||||||
|
foreign_key :site_id, :sites
|
||||||
|
foreign_key :tag_id, :tags
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_table :sites_tags
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/005_mark_changed_sites.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :initial_index_changed, :boolean, default: false
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :initial_index_changed
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/006_remove_change_marker.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.drop_column :sites, :initial_index_changed
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.add_column :sites, :initial_index_changed, :boolean, default: false
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/007_add_hits_counter.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :hits, :integer, default: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :hits
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/008_add_nsfw_flag.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :is_nsfw, :boolean, default: false
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :is_nsfw
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/009_add_indexes.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_index :sites, [:hits, :created_at]
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_index :sites, [:hits, :created_at]
|
||||||
|
}
|
||||||
|
end
|
||||||
11
migrations/010_add_more_indexes.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_index :sites, :created_at
|
||||||
|
DB.add_index :sites, :hits
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_index :sites, :created_at
|
||||||
|
DB.drop_index :sites, :hits
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/011_add_admin_flag.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :is_admin, :boolean, default: false
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :is_admin
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/012_add_banned_flag.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :is_banned, :boolean, default: false
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :is_banned
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/013_add_ip_address_of_creator.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :ip, :text
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :ip
|
||||||
|
}
|
||||||
|
end
|
||||||
10
migrations/014_add_password_reset_token.rb
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :password_reset_token, :text
|
||||||
|
DB.add_index :sites, :password_reset_token
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :password_reset_token
|
||||||
|
}
|
||||||
|
end
|
||||||
10
migrations/015_mark_site_changed.rb
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :site_changed, :boolean, default: false
|
||||||
|
DB.add_index :sites, :site_changed
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :site_changed
|
||||||
|
}
|
||||||
|
end
|
||||||
10
migrations/016_site_updated_count.rb
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :changed_count, :integer, default: 0
|
||||||
|
DB.add_index :sites, :changed_count
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :changed_count
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/017_add_domain.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :domain, :text, default: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :domain
|
||||||
|
}
|
||||||
|
end
|
||||||
90
migrations/018_add_social_schema.rb
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :title, :text, default: nil
|
||||||
|
DB.add_column :sites, :twitter_handle, :text, default: nil
|
||||||
|
DB.add_column :sites, :views, :integer, default: 0
|
||||||
|
DB.add_column :sites, :stripe_token, :text, default: nil
|
||||||
|
|
||||||
|
DB.create_table! :follows do
|
||||||
|
primary_key :id
|
||||||
|
Integer :site_id, index: true
|
||||||
|
Integer :actioning_site_id, index: true
|
||||||
|
DateTime :created_at, index: true
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :tips do
|
||||||
|
primary_key :id
|
||||||
|
Integer :site_id, index: true
|
||||||
|
Integer :actioning_site_id, index: true
|
||||||
|
DateTime :created_at, index: true
|
||||||
|
BigDecimal :amount
|
||||||
|
Integer :stripe_charge_id
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :changes do
|
||||||
|
primary_key :id
|
||||||
|
Integer :site_id, index: true
|
||||||
|
DateTime :created_at, index: true
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :events do
|
||||||
|
primary_key :id
|
||||||
|
Integer :site_id, index: true
|
||||||
|
Integer :follow_id
|
||||||
|
Integer :tip_id
|
||||||
|
Integer :tag_id
|
||||||
|
Integer :site_update_id
|
||||||
|
Integer :comment_id
|
||||||
|
Boolean :notification_seen, default: false
|
||||||
|
Integer :created_at, index: true
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :comments do
|
||||||
|
primary_key :id
|
||||||
|
Integer :event_id, index: true
|
||||||
|
Integer :actioning_site_id
|
||||||
|
Integer :parent_comment_id
|
||||||
|
Text :message
|
||||||
|
DateTime :created_at
|
||||||
|
DateTime :updated_at
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :likes do
|
||||||
|
primary_key :id
|
||||||
|
Integer :event_id, index: true
|
||||||
|
Integer :actioning_site_id
|
||||||
|
DateTime :created_at
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :stats do
|
||||||
|
primary_key :id
|
||||||
|
Integer :site_id, index: true
|
||||||
|
Integer :hits, default: 0
|
||||||
|
Integer :views, default: 0
|
||||||
|
DateTime :created_at, index: true
|
||||||
|
end
|
||||||
|
|
||||||
|
DB.create_table! :blocks do
|
||||||
|
primary_key :id
|
||||||
|
Integer :site_id, index: true
|
||||||
|
Integer :actioning_site_id, index: true
|
||||||
|
DateTime :created_at
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :title
|
||||||
|
DB.drop_column :sites, :twitter_handle
|
||||||
|
DB.drop_column :sites, :views
|
||||||
|
DB.drop_column :sites, :stripe_token
|
||||||
|
|
||||||
|
DB.drop_table :follows
|
||||||
|
DB.drop_table :tips
|
||||||
|
DB.drop_table :changes
|
||||||
|
DB.drop_table :events
|
||||||
|
DB.drop_table :comments
|
||||||
|
DB.drop_table :likes
|
||||||
|
DB.drop_table :stats
|
||||||
|
DB.drop_table :blocks
|
||||||
|
}
|
||||||
|
end
|
||||||
11
migrations/019_add_stripe_customer_id.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.drop_column :sites, :stripe_token
|
||||||
|
DB.add_column :sites, :stripe_customer_id, :text, default: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :stripe_customer_id
|
||||||
|
DB.add_column :sites, :stripe_token, :text, default: nil
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/020_add_plan_ended.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :plan_ended, :boolean, default: false
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :plan_ended
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/021_add_site_is_crashing.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :is_crashing, :boolean, default: false
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :is_crashing
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/022_fix_changes.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.rename_table :changes, :site_changes
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.rename_table :site_changes, :changes
|
||||||
|
}
|
||||||
|
end
|
||||||
19
migrations/023_add_profile_comments.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.rename_column :events, :comment_id, :profile_comment_id
|
||||||
|
|
||||||
|
DB.create_table! :profile_comments do
|
||||||
|
primary_key :id
|
||||||
|
Integer :site_id
|
||||||
|
Integer :actioning_site_id
|
||||||
|
Text :message
|
||||||
|
DateTime :created_at
|
||||||
|
DateTime :updated_at
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.rename_column :events, :profile_comment_id, :comment_id
|
||||||
|
DB.drop_table :profile_comments
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/024_add_paranoid_site_delete.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :sites, :is_deleted, :boolean, default: false
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :sites, :is_deleted
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/025_add_event_paranoid_delete.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :events, :is_deleted, :boolean, default: false
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :events, :is_deleted
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/026_add_comment_paranoid_delete.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.add_column :comments, :is_deleted, :boolean, default: false
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_column :comments, :is_deleted
|
||||||
|
}
|
||||||
|
end
|
||||||
15
migrations/027_add_comment_likes.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.create_table! :comment_likes do
|
||||||
|
primary_key :id
|
||||||
|
Integer :comment_id
|
||||||
|
Integer :site_id
|
||||||
|
Integer :actioning_site_id
|
||||||
|
DateTime :created_at
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.drop_table :comment_likes
|
||||||
|
}
|
||||||
|
end
|
||||||
9
migrations/028_remove_comment_like_site_id.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Sequel.migration do
|
||||||
|
up {
|
||||||
|
DB.drop_column :comment_likes, :site_id
|
||||||
|
}
|
||||||
|
|
||||||
|
down {
|
||||||
|
DB.add_column :comment_likes, :site_id, :integer
|
||||||
|
}
|
||||||
|
end
|
||||||