commit 84b39e1b7f1733a6cc71a00596021ac8b93bd21f Author: Donovan Daniels Date: Thu May 2 22:04:43 2024 -0500 Initial Commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6910dc0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +# Ignore everything +** + +# Allow only the files that are actually needed +!/package.json +!/yarn.lock +!/Gemfile +!/Gemfile.lock diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..88a9c60 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "extends": ["@uwu-codes/eslint-config/esm"], + "rules": { + "@typescript-eslint/no-unsafe-declaration-merging": "off", + "@typescript-eslint/explicit-function-return-type": "error", + "unicorn/prefer-string-replace-all": "off", + "unicorn/consistent-function-scoping": "off", + "default-case": "off", + "init-declarations": "off" + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8dc4323 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a170c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle +/.yarn + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key + +/app/assets/builds/* +!/app/assets/builds/.keep + +/node_modules +/scripts +/.idea +/.run diff --git a/.image_optim.yml b/.image_optim.yml new file mode 100644 index 0000000..a1bd253 --- /dev/null +++ b/.image_optim.yml @@ -0,0 +1,15 @@ +nice: 10 +timeout: 120 + +advpng: false +gifsicle: true +jhead: false +jpegoptim: false +jpegrecompress: false +jpegtran: false +optipng: false +oxipng: false +pngcrush: false +pngout: false +pngquant: false +svgo: false diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..3efdc42 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +18.8.0 diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..634d797 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,201 @@ +inherit_mode: + merge: + - Exclude + - Include + +require: + - rubocop-erb + - rubocop-rails + +AllCops: + NewCops: enable + SuggestExtensions: + rubocop-factory_bot: false + +Bundler/OrderedGems: + Enabled: true + +Layout/EmptyLineAfterGuardClause: + Enabled: false + +Layout/FirstArrayElementIndentation: + EnforcedStyle: consistent + +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent + +Layout/LineLength: + Enabled: false + +Lint/SymbolConversion: + EnforcedStyle: consistent + +Metrics/AbcSize: + Enabled: false + +Metrics/BlockLength: + AllowedMethods: + - class_methods + - concerning + - context + - create_table + - factory + - FactoryBot.define + - should + - should_eventually + Exclude: + - config/**/*.rb + +Metrics/ClassLength: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Metrics/ModuleLength: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Naming/PredicateName: + Enabled: false + +Rails/BulkChangeTable: + Enabled: false + +Rails/HasManyOrHasOneDependent: + Enabled: false + +Rails/HttpStatus: + EnforcedStyle: numeric + +Rails/I18nLocaleTexts: + Enabled: false + +Rails/InverseOf: + Enabled: false + +Rails/Output: + Exclude: + - db/seeds.rb + - db/fixes/*.rb + +Rails/ReversibleMigration: + Enabled: false + +Rails/SkipsModelValidations: + Enabled: false + +Rails/TimeZone: + Enabled: false + +Rails/WhereEquals: + Enabled: false + +Rails/WhereExists: + EnforcedStyle: where + +Rails/WhereNotWithMultipleConditions: + Enabled: false + +Style/ConditionalAssignment: + Enabled: false + +Style/Documentation: + Enabled: false + +Style/EmptyMethod: + EnforcedStyle: expanded + +Style/FloatDivision: + Enabled: false + +Style/GuardClause: + Enabled: false + +Style/HashSyntax: + EnforcedShorthandSyntax: never + +Style/IfUnlessModifier: + Enabled: true + +Style/Lambda: + EnforcedStyle: literal + +Style/NumericPredicate: + EnforcedStyle: comparison + +Style/PerlBackrefs: + Enabled: false + +Rails/OutputSafety: + Enabled: false + +Style/QuotedSymbols: + Enabled: false + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/TrailingCommaInArguments: + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: consistent_comma + +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: consistent_comma + +Style/RescueModifier: + Enabled: false + +Lint/StructNewOverride: + Enabled: false + +Layout/HashAlignment: + Enabled: true + EnforcedHashRocketStyle: table + EnforcedColonStyle: table + +Style/MethodCallWithArgsParentheses: + Enabled: true + +Style/MissingRespondToMissing: + Enabled: false + +Style/ModuleFunction: + Enabled: false + +Rails/ApplicationMailer: + Enabled: false + +Style/ClassAndModuleChildren: + Enabled: false + +Lint/MissingSuper: + Enabled: false + +Lint/EmptyBlock: + Enabled: false + +Rails/ActionControllerFlashBeforeRender: + Enabled: false + +Rails/NegateInclude: + Enabled: false + +Rails/DynamicFindBy: + Enabled: false + +Rails/TransactionExitStatement: + Enabled: false + +Style/NestedTernaryOperator: + Enabled: false + +Metrics/ParameterLists: + Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..be94e6f --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.2.2 diff --git a/.yarnclean b/.yarnclean new file mode 100644 index 0000000..af7c06e --- /dev/null +++ b/.yarnclean @@ -0,0 +1 @@ +@types/node diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..66748c9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +FROM ruby:3.2.2-alpine3.18 as ruby-builder + +RUN apk --no-cache add build-base cmake git glib-dev postgresql14-dev + +COPY Gemfile Gemfile.lock ./ +RUN gem i foreman && BUNDLE_IGNORE_CONFIG=true bundle install -j$(nproc) \ + && rm -rf /usr/local/bundle/cache/*.gem \ + && find /usr/local/bundle/gems/ -name "*.c" -delete \ + && find /usr/local/bundle/gems/ -name "*.o" -delete + +FROM node:18-alpine3.18 as node-builder +RUN apk --no-cache add git +WORKDIR /app +COPY package.json yarn.lock ./ +RUN corepack enable && corepack prepare --activate && yarn install +RUN corepack install -g pnpm + +FROM ruby:3.2.2-alpine3.18 + +RUN apk --no-cache add ffmpeg vips gifsicle \ + postgresql14-client \ + git jemalloc tzdata + +WORKDIR /app + +RUN git config --global --add safe.directory $(pwd) + +ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2 +ENV RUBY_YJIT_ENABLE=1 + +# Setup node and yarn +COPY --from=node-builder /usr/lib /usr/lib +COPY --from=node-builder /usr/local/share /usr/local/share +COPY --from=node-builder /usr/local/lib /usr/local/lib +COPY --from=node-builder /usr/local/include /usr/local/include +COPY --from=node-builder /usr/local/bin /usr/local/bin +COPY --from=node-builder /root/.cache/node /root/.cache/node + +# Copy gems and js packages +COPY --from=node-builder /app/node_modules node_modules +COPY --from=ruby-builder /usr/local/bundle /usr/local/bundle + +# Stop bin/rails console from offering autocomplete +RUN echo "IRB.conf[:USE_AUTOCOMPLETE] = false" > ~/.irbrc + +CMD ["foreman", "start", "-f", "Procfile.dev"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..160e3fd --- /dev/null +++ b/Gemfile @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "dotenv-rails", require: "dotenv/rails-now" + +ruby "3.2.2" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 7.1.1" + +# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] +gem "sprockets-rails" + +# Use postgresql as the database for Active Record +gem "pg", "~> 1.1" + +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" + +# Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails] +gem "jsbundling-rails" + +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails" + +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails" + +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" + +# Use Redis adapter to run Action Cable in production +# gem "redis", ">= 4.0.1" + +# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] +# gem "kredis" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +# gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[windows jruby] + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +# gem "image_processing", "~> 1.2" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[mri windows] +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" + + # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] + # gem "rack-mini-profiler" + + # Speed up commands on slow machines / big apps [https://github.com/rails/spring] + # gem "spring" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" +end + +gem "github_webhook", "~> 1.4" +gem "httparty", "~> 0.21.0" +gem "redis", "~> 5.0" +gem "rubocop", "~> 1.57" +gem "rubocop-erb", "~> 0.3.0" +gem "rubocop-rails", "~> 2.22" +gem "whenever", "~> 1.0" + +gem "filesize", "~> 0.2.0" + +gem "ed25519", "~> 1.3" + +gem "timeout", "~> 0.4.1" + +gem "image_optim", "~> 0.31.3" + +gem "request_store", "~> 1.5" + +gem "ruby-vips", "~> 2.2" + +gem "simple_form", "~> 5.3" + +gem "responders", "~> 3.1" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..f034ca2 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,352 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.1.1) + actionpack (= 7.1.1) + activesupport (= 7.1.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.1.1) + actionpack (= 7.1.1) + activejob (= 7.1.1) + activerecord (= 7.1.1) + activestorage (= 7.1.1) + activesupport (= 7.1.1) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.1.1) + actionpack (= 7.1.1) + actionview (= 7.1.1) + activejob (= 7.1.1) + activesupport (= 7.1.1) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.2) + actionpack (7.1.1) + actionview (= 7.1.1) + activesupport (= 7.1.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.1) + actionpack (= 7.1.1) + activerecord (= 7.1.1) + activestorage (= 7.1.1) + activesupport (= 7.1.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.1.1) + activesupport (= 7.1.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.1) + activesupport (= 7.1.1) + globalid (>= 0.3.6) + activemodel (7.1.1) + activesupport (= 7.1.1) + activerecord (7.1.1) + activemodel (= 7.1.1) + activesupport (= 7.1.1) + timeout (>= 0.4.0) + activestorage (7.1.1) + actionpack (= 7.1.1) + activejob (= 7.1.1) + activerecord (= 7.1.1) + activesupport (= 7.1.1) + marcel (~> 1.0) + activesupport (7.1.1) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + addressable (2.8.5) + public_suffix (>= 2.0.2, < 6.0) + ast (2.4.2) + base64 (0.2.0) + better_html (2.0.2) + actionview (>= 6.0) + activesupport (>= 6.0) + ast (~> 2.0) + erubi (~> 1.4) + parser (>= 2.4) + smart_properties + bigdecimal (3.1.4) + bindex (0.8.1) + bootsnap (1.17.0) + msgpack (~> 1.2) + builder (3.2.4) + capybara (3.39.2) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + chronic (0.10.2) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) + crass (1.0.6) + date (3.3.4) + debug (1.8.0) + irb (>= 1.5.0) + reline (>= 0.3.1) + dotenv (2.8.1) + dotenv-rails (2.8.1) + dotenv (= 2.8.1) + railties (>= 3.2) + drb (2.2.0) + ruby2_keywords + ed25519 (1.3.0) + erubi (1.12.0) + exifr (1.4.0) + ffi (1.16.3) + filesize (0.2.0) + fspath (3.1.2) + github_webhook (1.4.2) + activesupport (>= 4) + rack (>= 1.3) + railties (>= 4) + globalid (1.2.1) + activesupport (>= 6.1) + httparty (0.21.0) + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + image_optim (0.31.3) + 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_size (3.3.0) + in_threads (1.6.0) + io-console (0.6.0) + irb (1.8.3) + rdoc + reline (>= 0.3.8) + jbuilder (2.11.5) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + jsbundling-rails (1.2.1) + railties (>= 6.0.0) + json (2.6.3) + language_server-protocol (3.17.0.3) + loofah (2.21.4) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.2) + matrix (0.4.2) + mini_mime (1.1.5) + minitest (5.20.0) + msgpack (1.7.2) + multi_xml (0.6.0) + mutex_m (0.2.0) + net-imap (0.4.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.4.0) + net-protocol + nio4r (2.5.9) + nokogiri (1.15.4-x86_64-linux) + racc (~> 1.4) + parallel (1.23.0) + parser (3.2.2.4) + ast (~> 2.4.1) + racc + pg (1.5.4) + progress (3.6.0) + psych (5.1.1.1) + stringio + public_suffix (5.0.3) + puma (6.4.0) + nio4r (~> 2.0) + racc (1.7.3) + rack (3.0.8) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails (7.1.1) + actioncable (= 7.1.1) + actionmailbox (= 7.1.1) + actionmailer (= 7.1.1) + actionpack (= 7.1.1) + actiontext (= 7.1.1) + actionview (= 7.1.1) + activejob (= 7.1.1) + activemodel (= 7.1.1) + activerecord (= 7.1.1) + activestorage (= 7.1.1) + activesupport (= 7.1.1) + bundler (>= 1.15.0) + railties (= 7.1.1) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.1) + actionpack (= 7.1.1) + activesupport (= 7.1.1) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.1.0) + rdoc (6.6.0) + psych (>= 4.0.0) + redis (5.0.8) + redis-client (>= 0.17.0) + redis-client (0.18.0) + connection_pool + regexp_parser (2.8.2) + reline (0.4.0) + io-console (~> 0.5) + request_store (1.5.1) + rack (>= 1.4) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) + rexml (3.2.6) + rubocop (1.57.2) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.2.2.4) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) + rubocop-erb (0.3.0) + better_html + rubocop (~> 1.45) + rubocop-rails (2.22.1) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + ruby-progressbar (1.13.0) + ruby-vips (2.2.0) + ffi (~> 1.12) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + selenium-webdriver (4.15.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + simple_form (5.3.0) + actionpack (>= 5.2) + activemodel (>= 5.2) + smart_properties (1.17.0) + sprockets (4.2.1) + concurrent-ruby (~> 1.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + sprockets (>= 3.0.0) + stimulus-rails (1.3.0) + railties (>= 6.0.0) + stringio (3.0.8) + thor (1.3.0) + timeout (0.4.1) + turbo-rails (1.5.0) + actionpack (>= 6.0.0) + activejob (>= 6.0.0) + railties (>= 6.0.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.5.0) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webrick (1.8.1) + websocket (1.2.10) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + whenever (1.0.0) + chronic (>= 0.6.3) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.6.12) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + bootsnap + capybara + debug + dotenv-rails + ed25519 (~> 1.3) + filesize (~> 0.2.0) + github_webhook (~> 1.4) + httparty (~> 0.21.0) + image_optim (~> 0.31.3) + jbuilder + jsbundling-rails + pg (~> 1.1) + puma (>= 5.0) + rails (~> 7.1.1) + redis (~> 5.0) + request_store (~> 1.5) + responders (~> 3.1) + rubocop (~> 1.57) + rubocop-erb (~> 0.3.0) + rubocop-rails (~> 2.22) + ruby-vips (~> 2.2) + selenium-webdriver + simple_form (~> 5.3) + sprockets-rails + stimulus-rails + timeout (~> 0.4.1) + turbo-rails + tzinfo-data + web-console + whenever (~> 1.0) + +RUBY VERSION + ruby 3.2.2p53 + +BUNDLED WITH + 2.4.21 diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..5b316b9 --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +server: bin/rails server -p 3000 -b 0.0.0.0 +cron: bundler exec whenever --set environment="$RAILS_ENV" --update-crontab && crond -f diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000..d2dd46e --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,3 @@ +server: bin/rails server -p 3000 -b 0.0.0.0 +assets: yarn build --watch +cron: bundler exec whenever --set environment="$RAILS_ENV" --update-crontab && crond -f diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd0b63a --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# README + +* https://butts-are.cool +* https://e621.ws +* https://furry.cool +* https://maidboye.cafe +* https://oceanic.ws +* https://yiff.media +* https://yiff.rest +* https://yiff.rocks + +# Production +`docker compose -f docker-compose.prod.yml run --rm -e RAILS_ENV=production websites /app/bin/setup` +`docker compose -f docker-compose.prod.yml up -d` + +# Development +`docker compose run --rm websites /app/bin/setup` +`docker compose up` diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..d2a78aa --- /dev/null +++ b/Rakefile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/assets/builds/.keep b/app/assets/builds/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000..9a99757 --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,2 @@ +//= link_tree ../images +//= link_tree ../builds diff --git a/app/assets/images/butts-are.cool/android-icon-144x144.png b/app/assets/images/butts-are.cool/android-icon-144x144.png new file mode 100644 index 0000000..2f7250c Binary files /dev/null and b/app/assets/images/butts-are.cool/android-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/android-icon-192x192.png b/app/assets/images/butts-are.cool/android-icon-192x192.png new file mode 100644 index 0000000..975a0ba Binary files /dev/null and b/app/assets/images/butts-are.cool/android-icon-192x192.png differ diff --git a/app/assets/images/butts-are.cool/android-icon-36x36.png b/app/assets/images/butts-are.cool/android-icon-36x36.png new file mode 100644 index 0000000..721212a Binary files /dev/null and b/app/assets/images/butts-are.cool/android-icon-36x36.png differ diff --git a/app/assets/images/butts-are.cool/android-icon-48x48.png b/app/assets/images/butts-are.cool/android-icon-48x48.png new file mode 100644 index 0000000..6bc8d84 Binary files /dev/null and b/app/assets/images/butts-are.cool/android-icon-48x48.png differ diff --git a/app/assets/images/butts-are.cool/android-icon-72x72.png b/app/assets/images/butts-are.cool/android-icon-72x72.png new file mode 100644 index 0000000..fa72f7d Binary files /dev/null and b/app/assets/images/butts-are.cool/android-icon-72x72.png differ diff --git a/app/assets/images/butts-are.cool/android-icon-96x96.png b/app/assets/images/butts-are.cool/android-icon-96x96.png new file mode 100644 index 0000000..611cb5a Binary files /dev/null and b/app/assets/images/butts-are.cool/android-icon-96x96.png differ diff --git a/app/assets/images/butts-are.cool/apple-icon-114x114.png b/app/assets/images/butts-are.cool/apple-icon-114x114.png new file mode 100644 index 0000000..1e29926 Binary files /dev/null and b/app/assets/images/butts-are.cool/apple-icon-114x114.png differ diff --git a/app/assets/images/butts-are.cool/apple-icon-120x120.png b/app/assets/images/butts-are.cool/apple-icon-120x120.png new file mode 100644 index 0000000..dabf6f7 Binary files /dev/null and b/app/assets/images/butts-are.cool/apple-icon-120x120.png differ diff --git a/app/assets/images/butts-are.cool/apple-icon-144x144.png b/app/assets/images/butts-are.cool/apple-icon-144x144.png new file mode 100644 index 0000000..2f7250c Binary files /dev/null and b/app/assets/images/butts-are.cool/apple-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/apple-icon-152x152.png b/app/assets/images/butts-are.cool/apple-icon-152x152.png new file mode 100644 index 0000000..21d95d5 Binary files /dev/null and b/app/assets/images/butts-are.cool/apple-icon-152x152.png differ diff --git a/app/assets/images/butts-are.cool/apple-icon-180x180.png b/app/assets/images/butts-are.cool/apple-icon-180x180.png new file mode 100644 index 0000000..c1bd617 Binary files /dev/null and b/app/assets/images/butts-are.cool/apple-icon-180x180.png differ diff --git a/app/assets/images/butts-are.cool/apple-icon-57x57.png b/app/assets/images/butts-are.cool/apple-icon-57x57.png new file mode 100644 index 0000000..060fab9 Binary files /dev/null and b/app/assets/images/butts-are.cool/apple-icon-57x57.png differ diff --git a/app/assets/images/butts-are.cool/apple-icon-60x60.png b/app/assets/images/butts-are.cool/apple-icon-60x60.png new file mode 100644 index 0000000..de12435 Binary files /dev/null and b/app/assets/images/butts-are.cool/apple-icon-60x60.png differ diff --git a/app/assets/images/butts-are.cool/apple-icon-72x72.png b/app/assets/images/butts-are.cool/apple-icon-72x72.png new file mode 100644 index 0000000..fa72f7d Binary files /dev/null and b/app/assets/images/butts-are.cool/apple-icon-72x72.png differ diff --git a/app/assets/images/butts-are.cool/apple-icon-76x76.png b/app/assets/images/butts-are.cool/apple-icon-76x76.png new file mode 100644 index 0000000..682fab9 Binary files /dev/null and b/app/assets/images/butts-are.cool/apple-icon-76x76.png differ diff --git a/app/assets/images/butts-are.cool/apple-icon-precomposed.png b/app/assets/images/butts-are.cool/apple-icon-precomposed.png new file mode 100644 index 0000000..2fca194 Binary files /dev/null and b/app/assets/images/butts-are.cool/apple-icon-precomposed.png differ diff --git a/app/assets/images/butts-are.cool/apple-icon.png b/app/assets/images/butts-are.cool/apple-icon.png new file mode 100644 index 0000000..2fca194 Binary files /dev/null and b/app/assets/images/butts-are.cool/apple-icon.png differ diff --git a/app/assets/images/butts-are.cool/butts.mp4 b/app/assets/images/butts-are.cool/butts.mp4 new file mode 100644 index 0000000..432fe38 Binary files /dev/null and b/app/assets/images/butts-are.cool/butts.mp4 differ diff --git a/app/assets/images/butts-are.cool/custom/balls/android-icon-144x144.png b/app/assets/images/butts-are.cool/custom/balls/android-icon-144x144.png new file mode 100644 index 0000000..1bc741e Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/android-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/android-icon-192x192.png b/app/assets/images/butts-are.cool/custom/balls/android-icon-192x192.png new file mode 100644 index 0000000..c99d7a7 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/android-icon-192x192.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/android-icon-36x36.png b/app/assets/images/butts-are.cool/custom/balls/android-icon-36x36.png new file mode 100644 index 0000000..6967dfa Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/android-icon-36x36.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/android-icon-48x48.png b/app/assets/images/butts-are.cool/custom/balls/android-icon-48x48.png new file mode 100644 index 0000000..483c12c Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/android-icon-48x48.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/android-icon-72x72.png b/app/assets/images/butts-are.cool/custom/balls/android-icon-72x72.png new file mode 100644 index 0000000..c8d49cf Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/android-icon-72x72.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/android-icon-96x96.png b/app/assets/images/butts-are.cool/custom/balls/android-icon-96x96.png new file mode 100644 index 0000000..00bd5f3 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/android-icon-96x96.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/apple-icon-114x114.png b/app/assets/images/butts-are.cool/custom/balls/apple-icon-114x114.png new file mode 100644 index 0000000..f2dd065 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/apple-icon-114x114.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/apple-icon-120x120.png b/app/assets/images/butts-are.cool/custom/balls/apple-icon-120x120.png new file mode 100644 index 0000000..bbc2b0a Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/apple-icon-120x120.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/apple-icon-144x144.png b/app/assets/images/butts-are.cool/custom/balls/apple-icon-144x144.png new file mode 100644 index 0000000..1bc741e Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/apple-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/apple-icon-152x152.png b/app/assets/images/butts-are.cool/custom/balls/apple-icon-152x152.png new file mode 100644 index 0000000..f7debc2 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/apple-icon-152x152.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/apple-icon-180x180.png b/app/assets/images/butts-are.cool/custom/balls/apple-icon-180x180.png new file mode 100644 index 0000000..86c8b5c Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/apple-icon-180x180.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/apple-icon-57x57.png b/app/assets/images/butts-are.cool/custom/balls/apple-icon-57x57.png new file mode 100644 index 0000000..9d7321e Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/apple-icon-57x57.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/apple-icon-60x60.png b/app/assets/images/butts-are.cool/custom/balls/apple-icon-60x60.png new file mode 100644 index 0000000..ed2c1c3 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/apple-icon-60x60.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/apple-icon-72x72.png b/app/assets/images/butts-are.cool/custom/balls/apple-icon-72x72.png new file mode 100644 index 0000000..c8d49cf Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/apple-icon-72x72.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/apple-icon-76x76.png b/app/assets/images/butts-are.cool/custom/balls/apple-icon-76x76.png new file mode 100644 index 0000000..5857e10 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/apple-icon-76x76.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/apple-icon-precomposed.png b/app/assets/images/butts-are.cool/custom/balls/apple-icon-precomposed.png new file mode 100644 index 0000000..9fbc4ef Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/apple-icon-precomposed.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/apple-icon.png b/app/assets/images/butts-are.cool/custom/balls/apple-icon.png new file mode 100644 index 0000000..9fbc4ef Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/apple-icon.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/browserconfig.xml b/app/assets/images/butts-are.cool/custom/balls/browserconfig.xml new file mode 100644 index 0000000..80cf645 --- /dev/null +++ b/app/assets/images/butts-are.cool/custom/balls/browserconfig.xml @@ -0,0 +1,2 @@ + +#00ffff \ No newline at end of file diff --git a/app/assets/images/butts-are.cool/custom/balls/favicon-16x16.png b/app/assets/images/butts-are.cool/custom/balls/favicon-16x16.png new file mode 100644 index 0000000..eb3c60e Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/favicon-16x16.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/favicon-32x32.png b/app/assets/images/butts-are.cool/custom/balls/favicon-32x32.png new file mode 100644 index 0000000..7d868db Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/favicon-32x32.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/favicon-96x96.png b/app/assets/images/butts-are.cool/custom/balls/favicon-96x96.png new file mode 100644 index 0000000..a832364 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/favicon-96x96.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/favicon.ico b/app/assets/images/butts-are.cool/custom/balls/favicon.ico new file mode 100644 index 0000000..528c43a Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/favicon.ico differ diff --git a/app/assets/images/butts-are.cool/custom/balls/full.jpg b/app/assets/images/butts-are.cool/custom/balls/full.jpg new file mode 100644 index 0000000..de2cead Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/full.jpg differ diff --git a/app/assets/images/butts-are.cool/custom/balls/image-ogp.png b/app/assets/images/butts-are.cool/custom/balls/image-ogp.png new file mode 100644 index 0000000..a2263d7 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/image-ogp.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/image.png b/app/assets/images/butts-are.cool/custom/balls/image.png new file mode 100644 index 0000000..80dc440 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/image.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/manifest.json b/app/assets/images/butts-are.cool/custom/balls/manifest.json new file mode 100644 index 0000000..131db2b --- /dev/null +++ b/app/assets/images/butts-are.cool/custom/balls/manifest.json @@ -0,0 +1,92 @@ +{ + "name": "Butts Are Cool - Balls", + "short_name": "Butts Are Cool - Balls", + "theme_color": "#00ffff", + "background_color": "#00ffff", + "display": "browser", + "scope": "/", + "start_url": "/", + "icons": [ + { + "src": "https://balls.butts-are.cool/favicon-16x16.png", + "sizes": "16x16", + "type": "image/png" + }, + { + "src": "https://balls.butts-are.cool/favicon-32x32.png", + "sizes": "32x32", + "type": "image/png" + }, + { + "src": "https://balls.butts-are.cool/android-icon-36x36.png", + "sizes": "36x36", + "type": "image/png", + "density": "0.75" + }, + { + "src": "https://balls.butts-are.cool/android-icon-48x48.png", + "sizes": "48x48", + "type": "image/png", + "density": "1.0" + }, + { + "src": "https://balls.butts-are.cool/apple-icon-57x57.png", + "sizes": "57x57", + "type": "image/png" + }, + { + "src": "https://balls.butts-are.cool/apple-icon-60x60.png", + "sizes": "60x60", + "type": "image/png" + }, + { + "src": "https://balls.butts-are.cool/android-icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "density": "1.5" + }, + { + "src": "https://balls.butts-are.cool/apple-icon-76x76.png", + "sizes": "76x76", + "type": "image/png" + }, + { + "src": "https://balls.butts-are.cool/android-icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "density": "2.0" + }, + { + "src": "https://balls.butts-are.cool/apple-icon-114x114.png", + "sizes": "114x114", + "type": "image/png" + }, + { + "src": "https://balls.butts-are.cool/apple-icon-120x120.png", + "sizes": "120x120", + "type": "image/png" + }, + { + "src": "https://balls.butts-are.cool/android-icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "density": "3.0" + }, + { + "src": "https://balls.butts-are.cool/apple-icon-152x152.png", + "sizes": "152x152", + "type": "image/png" + }, + { + "src": "https://balls.butts-are.cool/apple-icon-180x180.png", + "sizes": "180x180", + "type": "image/png" + }, + { + "src": "https://balls.butts-are.cool/android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/app/assets/images/butts-are.cool/custom/balls/ms-icon-144x144.png b/app/assets/images/butts-are.cool/custom/balls/ms-icon-144x144.png new file mode 100644 index 0000000..82b9a9b Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/ms-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/ms-icon-150x150.png b/app/assets/images/butts-are.cool/custom/balls/ms-icon-150x150.png new file mode 100644 index 0000000..41f1dd3 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/ms-icon-150x150.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/ms-icon-310x310.png b/app/assets/images/butts-are.cool/custom/balls/ms-icon-310x310.png new file mode 100644 index 0000000..3831109 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/ms-icon-310x310.png differ diff --git a/app/assets/images/butts-are.cool/custom/balls/ms-icon-70x70.png b/app/assets/images/butts-are.cool/custom/balls/ms-icon-70x70.png new file mode 100644 index 0000000..5332de0 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/balls/ms-icon-70x70.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/android-icon-144x144.png b/app/assets/images/butts-are.cool/custom/cocks/android-icon-144x144.png new file mode 100644 index 0000000..220fde0 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/android-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/android-icon-192x192.png b/app/assets/images/butts-are.cool/custom/cocks/android-icon-192x192.png new file mode 100644 index 0000000..22131e0 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/android-icon-192x192.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/android-icon-36x36.png b/app/assets/images/butts-are.cool/custom/cocks/android-icon-36x36.png new file mode 100644 index 0000000..cbc848b Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/android-icon-36x36.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/android-icon-48x48.png b/app/assets/images/butts-are.cool/custom/cocks/android-icon-48x48.png new file mode 100644 index 0000000..9347bf9 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/android-icon-48x48.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/android-icon-72x72.png b/app/assets/images/butts-are.cool/custom/cocks/android-icon-72x72.png new file mode 100644 index 0000000..7a711c9 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/android-icon-72x72.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/android-icon-96x96.png b/app/assets/images/butts-are.cool/custom/cocks/android-icon-96x96.png new file mode 100644 index 0000000..7bbb588 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/android-icon-96x96.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/apple-icon-114x114.png b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-114x114.png new file mode 100644 index 0000000..bbd02d1 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-114x114.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/apple-icon-120x120.png b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-120x120.png new file mode 100644 index 0000000..5149426 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-120x120.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/apple-icon-144x144.png b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-144x144.png new file mode 100644 index 0000000..220fde0 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/apple-icon-152x152.png b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-152x152.png new file mode 100644 index 0000000..7c3548c Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-152x152.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/apple-icon-180x180.png b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-180x180.png new file mode 100644 index 0000000..9b15c6d Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-180x180.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/apple-icon-57x57.png b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-57x57.png new file mode 100644 index 0000000..af714f8 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-57x57.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/apple-icon-60x60.png b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-60x60.png new file mode 100644 index 0000000..d129adc Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-60x60.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/apple-icon-72x72.png b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-72x72.png new file mode 100644 index 0000000..7a711c9 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-72x72.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/apple-icon-76x76.png b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-76x76.png new file mode 100644 index 0000000..1234197 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-76x76.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/apple-icon-precomposed.png b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-precomposed.png new file mode 100644 index 0000000..d6d2a70 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/apple-icon-precomposed.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/apple-icon.png b/app/assets/images/butts-are.cool/custom/cocks/apple-icon.png new file mode 100644 index 0000000..d6d2a70 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/apple-icon.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/browserconfig.xml b/app/assets/images/butts-are.cool/custom/cocks/browserconfig.xml new file mode 100644 index 0000000..ab59577 --- /dev/null +++ b/app/assets/images/butts-are.cool/custom/cocks/browserconfig.xml @@ -0,0 +1,2 @@ + +#00ffff \ No newline at end of file diff --git a/app/assets/images/butts-are.cool/custom/cocks/favicon-16x16.png b/app/assets/images/butts-are.cool/custom/cocks/favicon-16x16.png new file mode 100644 index 0000000..2bdd62b Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/favicon-16x16.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/favicon-32x32.png b/app/assets/images/butts-are.cool/custom/cocks/favicon-32x32.png new file mode 100644 index 0000000..5b1a523 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/favicon-32x32.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/favicon-96x96.png b/app/assets/images/butts-are.cool/custom/cocks/favicon-96x96.png new file mode 100644 index 0000000..1b45dd1 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/favicon-96x96.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/favicon.ico b/app/assets/images/butts-are.cool/custom/cocks/favicon.ico new file mode 100644 index 0000000..605b649 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/favicon.ico differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/full.jpg b/app/assets/images/butts-are.cool/custom/cocks/full.jpg new file mode 100644 index 0000000..37ae172 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/full.jpg differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/image-ogp.png b/app/assets/images/butts-are.cool/custom/cocks/image-ogp.png new file mode 100644 index 0000000..19c84bf Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/image-ogp.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/image.png b/app/assets/images/butts-are.cool/custom/cocks/image.png new file mode 100644 index 0000000..098156d Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/image.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/manifest.json b/app/assets/images/butts-are.cool/custom/cocks/manifest.json new file mode 100644 index 0000000..3a3581f --- /dev/null +++ b/app/assets/images/butts-are.cool/custom/cocks/manifest.json @@ -0,0 +1,92 @@ +{ + "name": "Butts Are Cool - Penises", + "short_name": "Butts Are Cool - Penises", + "theme_color": "#00ffff", + "background_color": "#00ffff", + "display": "browser", + "scope": "/", + "start_url": "/", + "icons": [ + { + "src": "https://penises.butts-are.cool/favicon-16x16.png", + "sizes": "16x16", + "type": "image/png" + }, + { + "src": "https://penises.butts-are.cool/favicon-32x32.png", + "sizes": "32x32", + "type": "image/png" + }, + { + "src": "https://penises.butts-are.cool/android-icon-36x36.png", + "sizes": "36x36", + "type": "image/png", + "density": "0.75" + }, + { + "src": "https://penises.butts-are.cool/android-icon-48x48.png", + "sizes": "48x48", + "type": "image/png", + "density": "1.0" + }, + { + "src": "https://penises.butts-are.cool/apple-icon-57x57.png", + "sizes": "57x57", + "type": "image/png" + }, + { + "src": "https://penises.butts-are.cool/apple-icon-60x60.png", + "sizes": "60x60", + "type": "image/png" + }, + { + "src": "https://penises.butts-are.cool/android-icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "density": "1.5" + }, + { + "src": "https://penises.butts-are.cool/apple-icon-76x76.png", + "sizes": "76x76", + "type": "image/png" + }, + { + "src": "https://penises.butts-are.cool/android-icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "density": "2.0" + }, + { + "src": "https://penises.butts-are.cool/apple-icon-114x114.png", + "sizes": "114x114", + "type": "image/png" + }, + { + "src": "https://penises.butts-are.cool/apple-icon-120x120.png", + "sizes": "120x120", + "type": "image/png" + }, + { + "src": "https://penises.butts-are.cool/android-icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "density": "3.0" + }, + { + "src": "https://penises.butts-are.cool/apple-icon-152x152.png", + "sizes": "152x152", + "type": "image/png" + }, + { + "src": "https://penises.butts-are.cool/apple-icon-180x180.png", + "sizes": "180x180", + "type": "image/png" + }, + { + "src": "https://penises.butts-are.cool/android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/app/assets/images/butts-are.cool/custom/cocks/ms-icon-144x144.png b/app/assets/images/butts-are.cool/custom/cocks/ms-icon-144x144.png new file mode 100644 index 0000000..72a273d Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/ms-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/ms-icon-150x150.png b/app/assets/images/butts-are.cool/custom/cocks/ms-icon-150x150.png new file mode 100644 index 0000000..d03ebb8 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/ms-icon-150x150.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/ms-icon-310x310.png b/app/assets/images/butts-are.cool/custom/cocks/ms-icon-310x310.png new file mode 100644 index 0000000..e40c413 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/ms-icon-310x310.png differ diff --git a/app/assets/images/butts-are.cool/custom/cocks/ms-icon-70x70.png b/app/assets/images/butts-are.cool/custom/cocks/ms-icon-70x70.png new file mode 100644 index 0000000..2df5d5f Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/cocks/ms-icon-70x70.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/android-icon-144x144.png b/app/assets/images/butts-are.cool/custom/knots/android-icon-144x144.png new file mode 100644 index 0000000..959296b Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/android-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/android-icon-192x192.png b/app/assets/images/butts-are.cool/custom/knots/android-icon-192x192.png new file mode 100644 index 0000000..8d27cb9 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/android-icon-192x192.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/android-icon-36x36.png b/app/assets/images/butts-are.cool/custom/knots/android-icon-36x36.png new file mode 100644 index 0000000..64cb89e Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/android-icon-36x36.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/android-icon-48x48.png b/app/assets/images/butts-are.cool/custom/knots/android-icon-48x48.png new file mode 100644 index 0000000..5abe69b Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/android-icon-48x48.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/android-icon-72x72.png b/app/assets/images/butts-are.cool/custom/knots/android-icon-72x72.png new file mode 100644 index 0000000..b8c5b7b Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/android-icon-72x72.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/android-icon-96x96.png b/app/assets/images/butts-are.cool/custom/knots/android-icon-96x96.png new file mode 100644 index 0000000..ae1143c Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/android-icon-96x96.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/apple-icon-114x114.png b/app/assets/images/butts-are.cool/custom/knots/apple-icon-114x114.png new file mode 100644 index 0000000..c854ba6 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/apple-icon-114x114.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/apple-icon-120x120.png b/app/assets/images/butts-are.cool/custom/knots/apple-icon-120x120.png new file mode 100644 index 0000000..cc3e7f8 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/apple-icon-120x120.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/apple-icon-144x144.png b/app/assets/images/butts-are.cool/custom/knots/apple-icon-144x144.png new file mode 100644 index 0000000..959296b Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/apple-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/apple-icon-152x152.png b/app/assets/images/butts-are.cool/custom/knots/apple-icon-152x152.png new file mode 100644 index 0000000..f60126e Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/apple-icon-152x152.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/apple-icon-180x180.png b/app/assets/images/butts-are.cool/custom/knots/apple-icon-180x180.png new file mode 100644 index 0000000..93e8688 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/apple-icon-180x180.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/apple-icon-57x57.png b/app/assets/images/butts-are.cool/custom/knots/apple-icon-57x57.png new file mode 100644 index 0000000..f319bc9 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/apple-icon-57x57.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/apple-icon-60x60.png b/app/assets/images/butts-are.cool/custom/knots/apple-icon-60x60.png new file mode 100644 index 0000000..893fabd Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/apple-icon-60x60.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/apple-icon-72x72.png b/app/assets/images/butts-are.cool/custom/knots/apple-icon-72x72.png new file mode 100644 index 0000000..b8c5b7b Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/apple-icon-72x72.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/apple-icon-76x76.png b/app/assets/images/butts-are.cool/custom/knots/apple-icon-76x76.png new file mode 100644 index 0000000..44c58f7 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/apple-icon-76x76.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/apple-icon-precomposed.png b/app/assets/images/butts-are.cool/custom/knots/apple-icon-precomposed.png new file mode 100644 index 0000000..c563640 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/apple-icon-precomposed.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/apple-icon.png b/app/assets/images/butts-are.cool/custom/knots/apple-icon.png new file mode 100644 index 0000000..c563640 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/apple-icon.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/browserconfig.xml b/app/assets/images/butts-are.cool/custom/knots/browserconfig.xml new file mode 100644 index 0000000..508fb93 --- /dev/null +++ b/app/assets/images/butts-are.cool/custom/knots/browserconfig.xml @@ -0,0 +1,2 @@ + +#00ffff \ No newline at end of file diff --git a/app/assets/images/butts-are.cool/custom/knots/favicon-16x16.png b/app/assets/images/butts-are.cool/custom/knots/favicon-16x16.png new file mode 100644 index 0000000..f2b4bbf Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/favicon-16x16.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/favicon-32x32.png b/app/assets/images/butts-are.cool/custom/knots/favicon-32x32.png new file mode 100644 index 0000000..3d06c61 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/favicon-32x32.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/favicon-96x96.png b/app/assets/images/butts-are.cool/custom/knots/favicon-96x96.png new file mode 100644 index 0000000..ae1143c Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/favicon-96x96.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/favicon.ico b/app/assets/images/butts-are.cool/custom/knots/favicon.ico new file mode 100644 index 0000000..90dd2c2 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/favicon.ico differ diff --git a/app/assets/images/butts-are.cool/custom/knots/full.jpg b/app/assets/images/butts-are.cool/custom/knots/full.jpg new file mode 100644 index 0000000..ccc956d Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/full.jpg differ diff --git a/app/assets/images/butts-are.cool/custom/knots/image-ogp.png b/app/assets/images/butts-are.cool/custom/knots/image-ogp.png new file mode 100644 index 0000000..7b82585 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/image-ogp.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/image.png b/app/assets/images/butts-are.cool/custom/knots/image.png new file mode 100644 index 0000000..9143f75 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/image.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/manifest.json b/app/assets/images/butts-are.cool/custom/knots/manifest.json new file mode 100644 index 0000000..f1ce641 --- /dev/null +++ b/app/assets/images/butts-are.cool/custom/knots/manifest.json @@ -0,0 +1,92 @@ +{ + "name": "Butts Are Cool - Knots", + "short_name": "Butts Are Cool - Knots", + "theme_color": "#00ffff", + "background_color": "#00ffff", + "display": "browser", + "scope": "/", + "start_url": "/", + "icons": [ + { + "src": "https://knots.butts-are.cool/favicon-16x16.png", + "sizes": "16x16", + "type": "image/png" + }, + { + "src": "https://knots.butts-are.cool/favicon-32x32.png", + "sizes": "32x32", + "type": "image/png" + }, + { + "src": "https://knots.butts-are.cool/android-icon-36x36.png", + "sizes": "36x36", + "type": "image/png", + "density": "0.75" + }, + { + "src": "https://knots.butts-are.cool/android-icon-48x48.png", + "sizes": "48x48", + "type": "image/png", + "density": "1.0" + }, + { + "src": "https://knots.butts-are.cool/apple-icon-57x57.png", + "sizes": "57x57", + "type": "image/png" + }, + { + "src": "https://knots.butts-are.cool/apple-icon-60x60.png", + "sizes": "60x60", + "type": "image/png" + }, + { + "src": "https://knots.butts-are.cool/android-icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "density": "1.5" + }, + { + "src": "https://knots.butts-are.cool/apple-icon-76x76.png", + "sizes": "76x76", + "type": "image/png" + }, + { + "src": "https://knots.butts-are.cool/android-icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "density": "2.0" + }, + { + "src": "https://knots.butts-are.cool/apple-icon-114x114.png", + "sizes": "114x114", + "type": "image/png" + }, + { + "src": "https://knots.butts-are.cool/apple-icon-120x120.png", + "sizes": "120x120", + "type": "image/png" + }, + { + "src": "https://knots.butts-are.cool/android-icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "density": "3.0" + }, + { + "src": "https://knots.butts-are.cool/apple-icon-152x152.png", + "sizes": "152x152", + "type": "image/png" + }, + { + "src": "https://knots.butts-are.cool/apple-icon-180x180.png", + "sizes": "180x180", + "type": "image/png" + }, + { + "src": "https://knots.butts-are.cool/android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/app/assets/images/butts-are.cool/custom/knots/ms-icon-144x144.png b/app/assets/images/butts-are.cool/custom/knots/ms-icon-144x144.png new file mode 100644 index 0000000..959296b Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/ms-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/ms-icon-150x150.png b/app/assets/images/butts-are.cool/custom/knots/ms-icon-150x150.png new file mode 100644 index 0000000..627866d Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/ms-icon-150x150.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/ms-icon-310x310.png b/app/assets/images/butts-are.cool/custom/knots/ms-icon-310x310.png new file mode 100644 index 0000000..c7490f9 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/ms-icon-310x310.png differ diff --git a/app/assets/images/butts-are.cool/custom/knots/ms-icon-70x70.png b/app/assets/images/butts-are.cool/custom/knots/ms-icon-70x70.png new file mode 100644 index 0000000..8da137d Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/knots/ms-icon-70x70.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/android-icon-144x144.png b/app/assets/images/butts-are.cool/custom/sheaths/android-icon-144x144.png new file mode 100644 index 0000000..c28d1a6 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/android-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/android-icon-192x192.png b/app/assets/images/butts-are.cool/custom/sheaths/android-icon-192x192.png new file mode 100644 index 0000000..5e9bf72 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/android-icon-192x192.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/android-icon-36x36.png b/app/assets/images/butts-are.cool/custom/sheaths/android-icon-36x36.png new file mode 100644 index 0000000..4144b53 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/android-icon-36x36.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/android-icon-48x48.png b/app/assets/images/butts-are.cool/custom/sheaths/android-icon-48x48.png new file mode 100644 index 0000000..b04abde Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/android-icon-48x48.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/android-icon-72x72.png b/app/assets/images/butts-are.cool/custom/sheaths/android-icon-72x72.png new file mode 100644 index 0000000..9e681c3 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/android-icon-72x72.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/android-icon-96x96.png b/app/assets/images/butts-are.cool/custom/sheaths/android-icon-96x96.png new file mode 100644 index 0000000..224f1f9 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/android-icon-96x96.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-114x114.png b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-114x114.png new file mode 100644 index 0000000..2b22dfd Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-114x114.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-120x120.png b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-120x120.png new file mode 100644 index 0000000..9841a48 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-120x120.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-144x144.png b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-144x144.png new file mode 100644 index 0000000..c28d1a6 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-152x152.png b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-152x152.png new file mode 100644 index 0000000..feeaa8b Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-152x152.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-180x180.png b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-180x180.png new file mode 100644 index 0000000..992a03b Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-180x180.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-57x57.png b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-57x57.png new file mode 100644 index 0000000..d6fc926 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-57x57.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-60x60.png b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-60x60.png new file mode 100644 index 0000000..1d308e8 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-60x60.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-72x72.png b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-72x72.png new file mode 100644 index 0000000..9e681c3 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-72x72.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-76x76.png b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-76x76.png new file mode 100644 index 0000000..885061e Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-76x76.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-precomposed.png b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-precomposed.png new file mode 100644 index 0000000..6a32c80 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon-precomposed.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/apple-icon.png b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon.png new file mode 100644 index 0000000..6a32c80 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/apple-icon.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/browserconfig.xml b/app/assets/images/butts-are.cool/custom/sheaths/browserconfig.xml new file mode 100644 index 0000000..1f3c9d3 --- /dev/null +++ b/app/assets/images/butts-are.cool/custom/sheaths/browserconfig.xml @@ -0,0 +1,2 @@ + +#00ffff \ No newline at end of file diff --git a/app/assets/images/butts-are.cool/custom/sheaths/favicon-16x16.png b/app/assets/images/butts-are.cool/custom/sheaths/favicon-16x16.png new file mode 100644 index 0000000..df55d31 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/favicon-16x16.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/favicon-32x32.png b/app/assets/images/butts-are.cool/custom/sheaths/favicon-32x32.png new file mode 100644 index 0000000..fe697d8 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/favicon-32x32.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/favicon-96x96.png b/app/assets/images/butts-are.cool/custom/sheaths/favicon-96x96.png new file mode 100644 index 0000000..293cd2f Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/favicon-96x96.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/favicon.ico b/app/assets/images/butts-are.cool/custom/sheaths/favicon.ico new file mode 100644 index 0000000..a362b98 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/favicon.ico differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/full.png b/app/assets/images/butts-are.cool/custom/sheaths/full.png new file mode 100644 index 0000000..586dba9 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/full.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/image-ogp.png b/app/assets/images/butts-are.cool/custom/sheaths/image-ogp.png new file mode 100644 index 0000000..b8e7a4d Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/image-ogp.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/image.png b/app/assets/images/butts-are.cool/custom/sheaths/image.png new file mode 100644 index 0000000..b571e4f Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/image.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/manifest.json b/app/assets/images/butts-are.cool/custom/sheaths/manifest.json new file mode 100644 index 0000000..fcf2292 --- /dev/null +++ b/app/assets/images/butts-are.cool/custom/sheaths/manifest.json @@ -0,0 +1,92 @@ +{ + "name": "Butts Are Cool - Sheaths", + "short_name": "Butts Are Cool - Sheaths", + "theme_color": "#00ffff", + "background_color": "#00ffff", + "display": "browser", + "scope": "/", + "start_url": "/", + "icons": [ + { + "src": "https://sheaths.butts-are.cool/favicon-16x16.png", + "sizes": "16x16", + "type": "image/png" + }, + { + "src": "https://sheaths.butts-are.cool/favicon-32x32.png", + "sizes": "32x32", + "type": "image/png" + }, + { + "src": "https://sheaths.butts-are.cool/android-icon-36x36.png", + "sizes": "36x36", + "type": "image/png", + "density": "0.75" + }, + { + "src": "https://sheaths.butts-are.cool/android-icon-48x48.png", + "sizes": "48x48", + "type": "image/png", + "density": "1.0" + }, + { + "src": "https://sheaths.butts-are.cool/apple-icon-57x57.png", + "sizes": "57x57", + "type": "image/png" + }, + { + "src": "https://sheaths.butts-are.cool/apple-icon-60x60.png", + "sizes": "60x60", + "type": "image/png" + }, + { + "src": "https://sheaths.butts-are.cool/android-icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "density": "1.5" + }, + { + "src": "https://sheaths.butts-are.cool/apple-icon-76x76.png", + "sizes": "76x76", + "type": "image/png" + }, + { + "src": "https://sheaths.butts-are.cool/android-icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "density": "2.0" + }, + { + "src": "https://sheaths.butts-are.cool/apple-icon-114x114.png", + "sizes": "114x114", + "type": "image/png" + }, + { + "src": "https://sheaths.butts-are.cool/apple-icon-120x120.png", + "sizes": "120x120", + "type": "image/png" + }, + { + "src": "https://sheaths.butts-are.cool/android-icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "density": "3.0" + }, + { + "src": "https://sheaths.butts-are.cool/apple-icon-152x152.png", + "sizes": "152x152", + "type": "image/png" + }, + { + "src": "https://sheaths.butts-are.cool/apple-icon-180x180.png", + "sizes": "180x180", + "type": "image/png" + }, + { + "src": "https://sheaths.butts-are.cool/android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/app/assets/images/butts-are.cool/custom/sheaths/ms-icon-144x144.png b/app/assets/images/butts-are.cool/custom/sheaths/ms-icon-144x144.png new file mode 100644 index 0000000..3ee9289 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/ms-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/ms-icon-150x150.png b/app/assets/images/butts-are.cool/custom/sheaths/ms-icon-150x150.png new file mode 100644 index 0000000..44db894 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/ms-icon-150x150.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/ms-icon-310x310.png b/app/assets/images/butts-are.cool/custom/sheaths/ms-icon-310x310.png new file mode 100644 index 0000000..1f5f119 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/ms-icon-310x310.png differ diff --git a/app/assets/images/butts-are.cool/custom/sheaths/ms-icon-70x70.png b/app/assets/images/butts-are.cool/custom/sheaths/ms-icon-70x70.png new file mode 100644 index 0000000..4865763 Binary files /dev/null and b/app/assets/images/butts-are.cool/custom/sheaths/ms-icon-70x70.png differ diff --git a/app/assets/images/butts-are.cool/favicon-16x16.png b/app/assets/images/butts-are.cool/favicon-16x16.png new file mode 100644 index 0000000..966d92e Binary files /dev/null and b/app/assets/images/butts-are.cool/favicon-16x16.png differ diff --git a/app/assets/images/butts-are.cool/favicon-32x32.png b/app/assets/images/butts-are.cool/favicon-32x32.png new file mode 100644 index 0000000..764c540 Binary files /dev/null and b/app/assets/images/butts-are.cool/favicon-32x32.png differ diff --git a/app/assets/images/butts-are.cool/favicon-96x96.png b/app/assets/images/butts-are.cool/favicon-96x96.png new file mode 100644 index 0000000..611cb5a Binary files /dev/null and b/app/assets/images/butts-are.cool/favicon-96x96.png differ diff --git a/app/assets/images/butts-are.cool/favicon.ico b/app/assets/images/butts-are.cool/favicon.ico new file mode 100644 index 0000000..b80c952 Binary files /dev/null and b/app/assets/images/butts-are.cool/favicon.ico differ diff --git a/app/assets/images/butts-are.cool/image.png b/app/assets/images/butts-are.cool/image.png new file mode 100644 index 0000000..0e49c31 Binary files /dev/null and b/app/assets/images/butts-are.cool/image.png differ diff --git a/app/assets/images/butts-are.cool/ms-icon-144x144.png b/app/assets/images/butts-are.cool/ms-icon-144x144.png new file mode 100644 index 0000000..2f7250c Binary files /dev/null and b/app/assets/images/butts-are.cool/ms-icon-144x144.png differ diff --git a/app/assets/images/butts-are.cool/ms-icon-150x150.png b/app/assets/images/butts-are.cool/ms-icon-150x150.png new file mode 100644 index 0000000..b2020a0 Binary files /dev/null and b/app/assets/images/butts-are.cool/ms-icon-150x150.png differ diff --git a/app/assets/images/butts-are.cool/ms-icon-310x310.png b/app/assets/images/butts-are.cool/ms-icon-310x310.png new file mode 100644 index 0000000..5ac3265 Binary files /dev/null and b/app/assets/images/butts-are.cool/ms-icon-310x310.png differ diff --git a/app/assets/images/butts-are.cool/ms-icon-70x70.png b/app/assets/images/butts-are.cool/ms-icon-70x70.png new file mode 100644 index 0000000..cd0de51 Binary files /dev/null and b/app/assets/images/butts-are.cool/ms-icon-70x70.png differ diff --git a/app/assets/images/e621.ws/android-icon-144x144.png b/app/assets/images/e621.ws/android-icon-144x144.png new file mode 100644 index 0000000..75e382f Binary files /dev/null and b/app/assets/images/e621.ws/android-icon-144x144.png differ diff --git a/app/assets/images/e621.ws/android-icon-192x192.png b/app/assets/images/e621.ws/android-icon-192x192.png new file mode 100644 index 0000000..0896453 Binary files /dev/null and b/app/assets/images/e621.ws/android-icon-192x192.png differ diff --git a/app/assets/images/e621.ws/android-icon-36x36.png b/app/assets/images/e621.ws/android-icon-36x36.png new file mode 100644 index 0000000..ea66c41 Binary files /dev/null and b/app/assets/images/e621.ws/android-icon-36x36.png differ diff --git a/app/assets/images/e621.ws/android-icon-48x48.png b/app/assets/images/e621.ws/android-icon-48x48.png new file mode 100644 index 0000000..a302deb Binary files /dev/null and b/app/assets/images/e621.ws/android-icon-48x48.png differ diff --git a/app/assets/images/e621.ws/android-icon-72x72.png b/app/assets/images/e621.ws/android-icon-72x72.png new file mode 100644 index 0000000..44397d8 Binary files /dev/null and b/app/assets/images/e621.ws/android-icon-72x72.png differ diff --git a/app/assets/images/e621.ws/android-icon-96x96.png b/app/assets/images/e621.ws/android-icon-96x96.png new file mode 100644 index 0000000..70dac5a Binary files /dev/null and b/app/assets/images/e621.ws/android-icon-96x96.png differ diff --git a/app/assets/images/e621.ws/apple-icon-114x114.png b/app/assets/images/e621.ws/apple-icon-114x114.png new file mode 100644 index 0000000..7973cd1 Binary files /dev/null and b/app/assets/images/e621.ws/apple-icon-114x114.png differ diff --git a/app/assets/images/e621.ws/apple-icon-120x120.png b/app/assets/images/e621.ws/apple-icon-120x120.png new file mode 100644 index 0000000..98fb87b Binary files /dev/null and b/app/assets/images/e621.ws/apple-icon-120x120.png differ diff --git a/app/assets/images/e621.ws/apple-icon-144x144.png b/app/assets/images/e621.ws/apple-icon-144x144.png new file mode 100644 index 0000000..75e382f Binary files /dev/null and b/app/assets/images/e621.ws/apple-icon-144x144.png differ diff --git a/app/assets/images/e621.ws/apple-icon-152x152.png b/app/assets/images/e621.ws/apple-icon-152x152.png new file mode 100644 index 0000000..fdf6219 Binary files /dev/null and b/app/assets/images/e621.ws/apple-icon-152x152.png differ diff --git a/app/assets/images/e621.ws/apple-icon-180x180.png b/app/assets/images/e621.ws/apple-icon-180x180.png new file mode 100644 index 0000000..b88cb1c Binary files /dev/null and b/app/assets/images/e621.ws/apple-icon-180x180.png differ diff --git a/app/assets/images/e621.ws/apple-icon-57x57.png b/app/assets/images/e621.ws/apple-icon-57x57.png new file mode 100644 index 0000000..e11bae4 Binary files /dev/null and b/app/assets/images/e621.ws/apple-icon-57x57.png differ diff --git a/app/assets/images/e621.ws/apple-icon-60x60.png b/app/assets/images/e621.ws/apple-icon-60x60.png new file mode 100644 index 0000000..ee6e7a1 Binary files /dev/null and b/app/assets/images/e621.ws/apple-icon-60x60.png differ diff --git a/app/assets/images/e621.ws/apple-icon-72x72.png b/app/assets/images/e621.ws/apple-icon-72x72.png new file mode 100644 index 0000000..44397d8 Binary files /dev/null and b/app/assets/images/e621.ws/apple-icon-72x72.png differ diff --git a/app/assets/images/e621.ws/apple-icon-76x76.png b/app/assets/images/e621.ws/apple-icon-76x76.png new file mode 100644 index 0000000..be7f62a Binary files /dev/null and b/app/assets/images/e621.ws/apple-icon-76x76.png differ diff --git a/app/assets/images/e621.ws/apple-icon-precomposed.png b/app/assets/images/e621.ws/apple-icon-precomposed.png new file mode 100644 index 0000000..35ad854 Binary files /dev/null and b/app/assets/images/e621.ws/apple-icon-precomposed.png differ diff --git a/app/assets/images/e621.ws/apple-icon.png b/app/assets/images/e621.ws/apple-icon.png new file mode 100644 index 0000000..35ad854 Binary files /dev/null and b/app/assets/images/e621.ws/apple-icon.png differ diff --git a/app/assets/images/e621.ws/favicon-16x16.png b/app/assets/images/e621.ws/favicon-16x16.png new file mode 100644 index 0000000..e0dd014 Binary files /dev/null and b/app/assets/images/e621.ws/favicon-16x16.png differ diff --git a/app/assets/images/e621.ws/favicon-32x32.png b/app/assets/images/e621.ws/favicon-32x32.png new file mode 100644 index 0000000..62d413b Binary files /dev/null and b/app/assets/images/e621.ws/favicon-32x32.png differ diff --git a/app/assets/images/e621.ws/favicon-96x96.png b/app/assets/images/e621.ws/favicon-96x96.png new file mode 100644 index 0000000..70dac5a Binary files /dev/null and b/app/assets/images/e621.ws/favicon-96x96.png differ diff --git a/app/assets/images/e621.ws/favicon.ico b/app/assets/images/e621.ws/favicon.ico new file mode 100644 index 0000000..1e10c35 Binary files /dev/null and b/app/assets/images/e621.ws/favicon.ico differ diff --git a/app/assets/images/e621.ws/icon.png b/app/assets/images/e621.ws/icon.png new file mode 100644 index 0000000..8dc7f77 Binary files /dev/null and b/app/assets/images/e621.ws/icon.png differ diff --git a/app/assets/images/e621.ws/ms-icon-144x144.png b/app/assets/images/e621.ws/ms-icon-144x144.png new file mode 100644 index 0000000..75e382f Binary files /dev/null and b/app/assets/images/e621.ws/ms-icon-144x144.png differ diff --git a/app/assets/images/e621.ws/ms-icon-150x150.png b/app/assets/images/e621.ws/ms-icon-150x150.png new file mode 100644 index 0000000..1929d1e Binary files /dev/null and b/app/assets/images/e621.ws/ms-icon-150x150.png differ diff --git a/app/assets/images/e621.ws/ms-icon-310x310.png b/app/assets/images/e621.ws/ms-icon-310x310.png new file mode 100644 index 0000000..d41f2c5 Binary files /dev/null and b/app/assets/images/e621.ws/ms-icon-310x310.png differ diff --git a/app/assets/images/e621.ws/ms-icon-70x70.png b/app/assets/images/e621.ws/ms-icon-70x70.png new file mode 100644 index 0000000..0a93d53 Binary files /dev/null and b/app/assets/images/e621.ws/ms-icon-70x70.png differ diff --git a/app/assets/images/furry.cool/DonPride.png b/app/assets/images/furry.cool/DonPride.png new file mode 100644 index 0000000..338093e Binary files /dev/null and b/app/assets/images/furry.cool/DonPride.png differ diff --git a/app/assets/images/furry.cool/DonPrideTransparent.png b/app/assets/images/furry.cool/DonPrideTransparent.png new file mode 100644 index 0000000..e4cbdc1 Binary files /dev/null and b/app/assets/images/furry.cool/DonPrideTransparent.png differ diff --git a/app/assets/images/furry.cool/android-icon-144x144.png b/app/assets/images/furry.cool/android-icon-144x144.png new file mode 100644 index 0000000..af6ce7a Binary files /dev/null and b/app/assets/images/furry.cool/android-icon-144x144.png differ diff --git a/app/assets/images/furry.cool/android-icon-192x192.png b/app/assets/images/furry.cool/android-icon-192x192.png new file mode 100644 index 0000000..2d64bb1 Binary files /dev/null and b/app/assets/images/furry.cool/android-icon-192x192.png differ diff --git a/app/assets/images/furry.cool/android-icon-36x36.png b/app/assets/images/furry.cool/android-icon-36x36.png new file mode 100644 index 0000000..5474e93 Binary files /dev/null and b/app/assets/images/furry.cool/android-icon-36x36.png differ diff --git a/app/assets/images/furry.cool/android-icon-48x48.png b/app/assets/images/furry.cool/android-icon-48x48.png new file mode 100644 index 0000000..e473a66 Binary files /dev/null and b/app/assets/images/furry.cool/android-icon-48x48.png differ diff --git a/app/assets/images/furry.cool/android-icon-72x72.png b/app/assets/images/furry.cool/android-icon-72x72.png new file mode 100644 index 0000000..37149a5 Binary files /dev/null and b/app/assets/images/furry.cool/android-icon-72x72.png differ diff --git a/app/assets/images/furry.cool/android-icon-96x96.png b/app/assets/images/furry.cool/android-icon-96x96.png new file mode 100644 index 0000000..9ebfabd Binary files /dev/null and b/app/assets/images/furry.cool/android-icon-96x96.png differ diff --git a/app/assets/images/furry.cool/apple-icon-114x114.png b/app/assets/images/furry.cool/apple-icon-114x114.png new file mode 100644 index 0000000..7d6232c Binary files /dev/null and b/app/assets/images/furry.cool/apple-icon-114x114.png differ diff --git a/app/assets/images/furry.cool/apple-icon-120x120.png b/app/assets/images/furry.cool/apple-icon-120x120.png new file mode 100644 index 0000000..920703a Binary files /dev/null and b/app/assets/images/furry.cool/apple-icon-120x120.png differ diff --git a/app/assets/images/furry.cool/apple-icon-144x144.png b/app/assets/images/furry.cool/apple-icon-144x144.png new file mode 100644 index 0000000..af6ce7a Binary files /dev/null and b/app/assets/images/furry.cool/apple-icon-144x144.png differ diff --git a/app/assets/images/furry.cool/apple-icon-152x152.png b/app/assets/images/furry.cool/apple-icon-152x152.png new file mode 100644 index 0000000..db13cf5 Binary files /dev/null and b/app/assets/images/furry.cool/apple-icon-152x152.png differ diff --git a/app/assets/images/furry.cool/apple-icon-180x180.png b/app/assets/images/furry.cool/apple-icon-180x180.png new file mode 100644 index 0000000..ff1aec1 Binary files /dev/null and b/app/assets/images/furry.cool/apple-icon-180x180.png differ diff --git a/app/assets/images/furry.cool/apple-icon-57x57.png b/app/assets/images/furry.cool/apple-icon-57x57.png new file mode 100644 index 0000000..fb3a350 Binary files /dev/null and b/app/assets/images/furry.cool/apple-icon-57x57.png differ diff --git a/app/assets/images/furry.cool/apple-icon-60x60.png b/app/assets/images/furry.cool/apple-icon-60x60.png new file mode 100644 index 0000000..462fc1f Binary files /dev/null and b/app/assets/images/furry.cool/apple-icon-60x60.png differ diff --git a/app/assets/images/furry.cool/apple-icon-72x72.png b/app/assets/images/furry.cool/apple-icon-72x72.png new file mode 100644 index 0000000..37149a5 Binary files /dev/null and b/app/assets/images/furry.cool/apple-icon-72x72.png differ diff --git a/app/assets/images/furry.cool/apple-icon-76x76.png b/app/assets/images/furry.cool/apple-icon-76x76.png new file mode 100644 index 0000000..ca3129f Binary files /dev/null and b/app/assets/images/furry.cool/apple-icon-76x76.png differ diff --git a/app/assets/images/furry.cool/apple-icon-precomposed.png b/app/assets/images/furry.cool/apple-icon-precomposed.png new file mode 100644 index 0000000..764e935 Binary files /dev/null and b/app/assets/images/furry.cool/apple-icon-precomposed.png differ diff --git a/app/assets/images/furry.cool/apple-icon.png b/app/assets/images/furry.cool/apple-icon.png new file mode 100644 index 0000000..764e935 Binary files /dev/null and b/app/assets/images/furry.cool/apple-icon.png differ diff --git a/app/assets/images/furry.cool/browserconfig.xml b/app/assets/images/furry.cool/browserconfig.xml new file mode 100644 index 0000000..c554148 --- /dev/null +++ b/app/assets/images/furry.cool/browserconfig.xml @@ -0,0 +1,2 @@ + +#ffffff \ No newline at end of file diff --git a/app/assets/images/furry.cool/favicon-16x16.png b/app/assets/images/furry.cool/favicon-16x16.png new file mode 100644 index 0000000..da09cd8 Binary files /dev/null and b/app/assets/images/furry.cool/favicon-16x16.png differ diff --git a/app/assets/images/furry.cool/favicon-32x32.png b/app/assets/images/furry.cool/favicon-32x32.png new file mode 100644 index 0000000..fd4b67f Binary files /dev/null and b/app/assets/images/furry.cool/favicon-32x32.png differ diff --git a/app/assets/images/furry.cool/favicon-96x96.png b/app/assets/images/furry.cool/favicon-96x96.png new file mode 100644 index 0000000..216a323 Binary files /dev/null and b/app/assets/images/furry.cool/favicon-96x96.png differ diff --git a/app/assets/images/furry.cool/favicon.ico b/app/assets/images/furry.cool/favicon.ico new file mode 100644 index 0000000..ed9818d Binary files /dev/null and b/app/assets/images/furry.cool/favicon.ico differ diff --git a/app/assets/images/furry.cool/manifest.json b/app/assets/images/furry.cool/manifest.json new file mode 100644 index 0000000..013d4a6 --- /dev/null +++ b/app/assets/images/furry.cool/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "App", + "icons": [ + { + "src": "\/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "\/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "\/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "\/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "\/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "\/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/app/assets/images/furry.cool/ms-icon-144x144.png b/app/assets/images/furry.cool/ms-icon-144x144.png new file mode 100644 index 0000000..5b8e09d Binary files /dev/null and b/app/assets/images/furry.cool/ms-icon-144x144.png differ diff --git a/app/assets/images/furry.cool/ms-icon-150x150.png b/app/assets/images/furry.cool/ms-icon-150x150.png new file mode 100644 index 0000000..53dd11c Binary files /dev/null and b/app/assets/images/furry.cool/ms-icon-150x150.png differ diff --git a/app/assets/images/furry.cool/ms-icon-310x310.png b/app/assets/images/furry.cool/ms-icon-310x310.png new file mode 100644 index 0000000..fc189fc Binary files /dev/null and b/app/assets/images/furry.cool/ms-icon-310x310.png differ diff --git a/app/assets/images/furry.cool/ms-icon-70x70.png b/app/assets/images/furry.cool/ms-icon-70x70.png new file mode 100644 index 0000000..bcd5515 Binary files /dev/null and b/app/assets/images/furry.cool/ms-icon-70x70.png differ diff --git a/app/assets/images/maidboye.cafe/MaidAngry.png b/app/assets/images/maidboye.cafe/MaidAngry.png new file mode 100644 index 0000000..d796c7b Binary files /dev/null and b/app/assets/images/maidboye.cafe/MaidAngry.png differ diff --git a/app/assets/images/maidboye.cafe/MaidBeta.png b/app/assets/images/maidboye.cafe/MaidBeta.png new file mode 100644 index 0000000..4e72a1b Binary files /dev/null and b/app/assets/images/maidboye.cafe/MaidBeta.png differ diff --git a/app/assets/images/maidboye.cafe/MaidHappy.png b/app/assets/images/maidboye.cafe/MaidHappy.png new file mode 100644 index 0000000..6ca7957 Binary files /dev/null and b/app/assets/images/maidboye.cafe/MaidHappy.png differ diff --git a/app/assets/images/maidboye.cafe/MaidRef.png b/app/assets/images/maidboye.cafe/MaidRef.png new file mode 100644 index 0000000..9a990f3 Binary files /dev/null and b/app/assets/images/maidboye.cafe/MaidRef.png differ diff --git a/app/assets/images/maidboye.cafe/MaidShy.png b/app/assets/images/maidboye.cafe/MaidShy.png new file mode 100644 index 0000000..28b645f Binary files /dev/null and b/app/assets/images/maidboye.cafe/MaidShy.png differ diff --git a/app/assets/images/maidboye.cafe/android-icon-144x144.png b/app/assets/images/maidboye.cafe/android-icon-144x144.png new file mode 100644 index 0000000..521dc5d Binary files /dev/null and b/app/assets/images/maidboye.cafe/android-icon-144x144.png differ diff --git a/app/assets/images/maidboye.cafe/android-icon-192x192.png b/app/assets/images/maidboye.cafe/android-icon-192x192.png new file mode 100644 index 0000000..76fdbf3 Binary files /dev/null and b/app/assets/images/maidboye.cafe/android-icon-192x192.png differ diff --git a/app/assets/images/maidboye.cafe/android-icon-36x36.png b/app/assets/images/maidboye.cafe/android-icon-36x36.png new file mode 100644 index 0000000..e6e8d3c Binary files /dev/null and b/app/assets/images/maidboye.cafe/android-icon-36x36.png differ diff --git a/app/assets/images/maidboye.cafe/android-icon-48x48.png b/app/assets/images/maidboye.cafe/android-icon-48x48.png new file mode 100644 index 0000000..fbfedf5 Binary files /dev/null and b/app/assets/images/maidboye.cafe/android-icon-48x48.png differ diff --git a/app/assets/images/maidboye.cafe/android-icon-72x72.png b/app/assets/images/maidboye.cafe/android-icon-72x72.png new file mode 100644 index 0000000..22dbbe4 Binary files /dev/null and b/app/assets/images/maidboye.cafe/android-icon-72x72.png differ diff --git a/app/assets/images/maidboye.cafe/android-icon-96x96.png b/app/assets/images/maidboye.cafe/android-icon-96x96.png new file mode 100644 index 0000000..f1ad45e Binary files /dev/null and b/app/assets/images/maidboye.cafe/android-icon-96x96.png differ diff --git a/app/assets/images/maidboye.cafe/apple-icon-114x114.png b/app/assets/images/maidboye.cafe/apple-icon-114x114.png new file mode 100644 index 0000000..8ac2cbb Binary files /dev/null and b/app/assets/images/maidboye.cafe/apple-icon-114x114.png differ diff --git a/app/assets/images/maidboye.cafe/apple-icon-120x120.png b/app/assets/images/maidboye.cafe/apple-icon-120x120.png new file mode 100644 index 0000000..1d30558 Binary files /dev/null and b/app/assets/images/maidboye.cafe/apple-icon-120x120.png differ diff --git a/app/assets/images/maidboye.cafe/apple-icon-144x144.png b/app/assets/images/maidboye.cafe/apple-icon-144x144.png new file mode 100644 index 0000000..521dc5d Binary files /dev/null and b/app/assets/images/maidboye.cafe/apple-icon-144x144.png differ diff --git a/app/assets/images/maidboye.cafe/apple-icon-152x152.png b/app/assets/images/maidboye.cafe/apple-icon-152x152.png new file mode 100644 index 0000000..0ae9f24 Binary files /dev/null and b/app/assets/images/maidboye.cafe/apple-icon-152x152.png differ diff --git a/app/assets/images/maidboye.cafe/apple-icon-180x180.png b/app/assets/images/maidboye.cafe/apple-icon-180x180.png new file mode 100644 index 0000000..b7bd155 Binary files /dev/null and b/app/assets/images/maidboye.cafe/apple-icon-180x180.png differ diff --git a/app/assets/images/maidboye.cafe/apple-icon-57x57.png b/app/assets/images/maidboye.cafe/apple-icon-57x57.png new file mode 100644 index 0000000..8538b4b Binary files /dev/null and b/app/assets/images/maidboye.cafe/apple-icon-57x57.png differ diff --git a/app/assets/images/maidboye.cafe/apple-icon-60x60.png b/app/assets/images/maidboye.cafe/apple-icon-60x60.png new file mode 100644 index 0000000..84b7a49 Binary files /dev/null and b/app/assets/images/maidboye.cafe/apple-icon-60x60.png differ diff --git a/app/assets/images/maidboye.cafe/apple-icon-72x72.png b/app/assets/images/maidboye.cafe/apple-icon-72x72.png new file mode 100644 index 0000000..22dbbe4 Binary files /dev/null and b/app/assets/images/maidboye.cafe/apple-icon-72x72.png differ diff --git a/app/assets/images/maidboye.cafe/apple-icon-76x76.png b/app/assets/images/maidboye.cafe/apple-icon-76x76.png new file mode 100644 index 0000000..8173ea5 Binary files /dev/null and b/app/assets/images/maidboye.cafe/apple-icon-76x76.png differ diff --git a/app/assets/images/maidboye.cafe/apple-icon-precomposed.png b/app/assets/images/maidboye.cafe/apple-icon-precomposed.png new file mode 100644 index 0000000..337801f Binary files /dev/null and b/app/assets/images/maidboye.cafe/apple-icon-precomposed.png differ diff --git a/app/assets/images/maidboye.cafe/apple-icon.png b/app/assets/images/maidboye.cafe/apple-icon.png new file mode 100644 index 0000000..337801f Binary files /dev/null and b/app/assets/images/maidboye.cafe/apple-icon.png differ diff --git a/app/assets/images/maidboye.cafe/favicon-16x16.png b/app/assets/images/maidboye.cafe/favicon-16x16.png new file mode 100644 index 0000000..2ebdb5f Binary files /dev/null and b/app/assets/images/maidboye.cafe/favicon-16x16.png differ diff --git a/app/assets/images/maidboye.cafe/favicon-32x32.png b/app/assets/images/maidboye.cafe/favicon-32x32.png new file mode 100644 index 0000000..e652268 Binary files /dev/null and b/app/assets/images/maidboye.cafe/favicon-32x32.png differ diff --git a/app/assets/images/maidboye.cafe/favicon-96x96.png b/app/assets/images/maidboye.cafe/favicon-96x96.png new file mode 100644 index 0000000..f1ad45e Binary files /dev/null and b/app/assets/images/maidboye.cafe/favicon-96x96.png differ diff --git a/app/assets/images/maidboye.cafe/favicon.ico b/app/assets/images/maidboye.cafe/favicon.ico new file mode 100644 index 0000000..21fa4d4 Binary files /dev/null and b/app/assets/images/maidboye.cafe/favicon.ico differ diff --git a/app/assets/images/maidboye.cafe/ms-icon-144x144.png b/app/assets/images/maidboye.cafe/ms-icon-144x144.png new file mode 100644 index 0000000..521dc5d Binary files /dev/null and b/app/assets/images/maidboye.cafe/ms-icon-144x144.png differ diff --git a/app/assets/images/maidboye.cafe/ms-icon-150x150.png b/app/assets/images/maidboye.cafe/ms-icon-150x150.png new file mode 100644 index 0000000..a821ab3 Binary files /dev/null and b/app/assets/images/maidboye.cafe/ms-icon-150x150.png differ diff --git a/app/assets/images/maidboye.cafe/ms-icon-310x310.png b/app/assets/images/maidboye.cafe/ms-icon-310x310.png new file mode 100644 index 0000000..615fc72 Binary files /dev/null and b/app/assets/images/maidboye.cafe/ms-icon-310x310.png differ diff --git a/app/assets/images/maidboye.cafe/ms-icon-70x70.png b/app/assets/images/maidboye.cafe/ms-icon-70x70.png new file mode 100644 index 0000000..cb60cba Binary files /dev/null and b/app/assets/images/maidboye.cafe/ms-icon-70x70.png differ diff --git a/app/assets/images/oceanic.ws/android-icon-144x144.png b/app/assets/images/oceanic.ws/android-icon-144x144.png new file mode 100644 index 0000000..07f2026 Binary files /dev/null and b/app/assets/images/oceanic.ws/android-icon-144x144.png differ diff --git a/app/assets/images/oceanic.ws/android-icon-192x192.png b/app/assets/images/oceanic.ws/android-icon-192x192.png new file mode 100644 index 0000000..bc0e17c Binary files /dev/null and b/app/assets/images/oceanic.ws/android-icon-192x192.png differ diff --git a/app/assets/images/oceanic.ws/android-icon-36x36.png b/app/assets/images/oceanic.ws/android-icon-36x36.png new file mode 100644 index 0000000..34a1578 Binary files /dev/null and b/app/assets/images/oceanic.ws/android-icon-36x36.png differ diff --git a/app/assets/images/oceanic.ws/android-icon-48x48.png b/app/assets/images/oceanic.ws/android-icon-48x48.png new file mode 100644 index 0000000..34d97a6 Binary files /dev/null and b/app/assets/images/oceanic.ws/android-icon-48x48.png differ diff --git a/app/assets/images/oceanic.ws/android-icon-72x72.png b/app/assets/images/oceanic.ws/android-icon-72x72.png new file mode 100644 index 0000000..036b2e4 Binary files /dev/null and b/app/assets/images/oceanic.ws/android-icon-72x72.png differ diff --git a/app/assets/images/oceanic.ws/android-icon-96x96.png b/app/assets/images/oceanic.ws/android-icon-96x96.png new file mode 100644 index 0000000..762ac8a Binary files /dev/null and b/app/assets/images/oceanic.ws/android-icon-96x96.png differ diff --git a/app/assets/images/oceanic.ws/apple-icon-114x114.png b/app/assets/images/oceanic.ws/apple-icon-114x114.png new file mode 100644 index 0000000..5f204c2 Binary files /dev/null and b/app/assets/images/oceanic.ws/apple-icon-114x114.png differ diff --git a/app/assets/images/oceanic.ws/apple-icon-120x120.png b/app/assets/images/oceanic.ws/apple-icon-120x120.png new file mode 100644 index 0000000..3bb04d2 Binary files /dev/null and b/app/assets/images/oceanic.ws/apple-icon-120x120.png differ diff --git a/app/assets/images/oceanic.ws/apple-icon-144x144.png b/app/assets/images/oceanic.ws/apple-icon-144x144.png new file mode 100644 index 0000000..07f2026 Binary files /dev/null and b/app/assets/images/oceanic.ws/apple-icon-144x144.png differ diff --git a/app/assets/images/oceanic.ws/apple-icon-152x152.png b/app/assets/images/oceanic.ws/apple-icon-152x152.png new file mode 100644 index 0000000..79c59b6 Binary files /dev/null and b/app/assets/images/oceanic.ws/apple-icon-152x152.png differ diff --git a/app/assets/images/oceanic.ws/apple-icon-180x180.png b/app/assets/images/oceanic.ws/apple-icon-180x180.png new file mode 100644 index 0000000..24e8ea5 Binary files /dev/null and b/app/assets/images/oceanic.ws/apple-icon-180x180.png differ diff --git a/app/assets/images/oceanic.ws/apple-icon-57x57.png b/app/assets/images/oceanic.ws/apple-icon-57x57.png new file mode 100644 index 0000000..26b94ba Binary files /dev/null and b/app/assets/images/oceanic.ws/apple-icon-57x57.png differ diff --git a/app/assets/images/oceanic.ws/apple-icon-60x60.png b/app/assets/images/oceanic.ws/apple-icon-60x60.png new file mode 100644 index 0000000..3d4abe4 Binary files /dev/null and b/app/assets/images/oceanic.ws/apple-icon-60x60.png differ diff --git a/app/assets/images/oceanic.ws/apple-icon-72x72.png b/app/assets/images/oceanic.ws/apple-icon-72x72.png new file mode 100644 index 0000000..036b2e4 Binary files /dev/null and b/app/assets/images/oceanic.ws/apple-icon-72x72.png differ diff --git a/app/assets/images/oceanic.ws/apple-icon-76x76.png b/app/assets/images/oceanic.ws/apple-icon-76x76.png new file mode 100644 index 0000000..93f4ded Binary files /dev/null and b/app/assets/images/oceanic.ws/apple-icon-76x76.png differ diff --git a/app/assets/images/oceanic.ws/apple-icon-precomposed.png b/app/assets/images/oceanic.ws/apple-icon-precomposed.png new file mode 100644 index 0000000..bd36fc9 Binary files /dev/null and b/app/assets/images/oceanic.ws/apple-icon-precomposed.png differ diff --git a/app/assets/images/oceanic.ws/apple-icon.png b/app/assets/images/oceanic.ws/apple-icon.png new file mode 100644 index 0000000..bd36fc9 Binary files /dev/null and b/app/assets/images/oceanic.ws/apple-icon.png differ diff --git a/app/assets/images/oceanic.ws/favicon-16x16.png b/app/assets/images/oceanic.ws/favicon-16x16.png new file mode 100644 index 0000000..c084472 Binary files /dev/null and b/app/assets/images/oceanic.ws/favicon-16x16.png differ diff --git a/app/assets/images/oceanic.ws/favicon-32x32.png b/app/assets/images/oceanic.ws/favicon-32x32.png new file mode 100644 index 0000000..42b7b5e Binary files /dev/null and b/app/assets/images/oceanic.ws/favicon-32x32.png differ diff --git a/app/assets/images/oceanic.ws/favicon-96x96.png b/app/assets/images/oceanic.ws/favicon-96x96.png new file mode 100644 index 0000000..762ac8a Binary files /dev/null and b/app/assets/images/oceanic.ws/favicon-96x96.png differ diff --git a/app/assets/images/oceanic.ws/favicon.ico b/app/assets/images/oceanic.ws/favicon.ico new file mode 100644 index 0000000..e7b0b5a Binary files /dev/null and b/app/assets/images/oceanic.ws/favicon.ico differ diff --git a/app/assets/images/oceanic.ws/icon.png b/app/assets/images/oceanic.ws/icon.png new file mode 100644 index 0000000..2fff148 Binary files /dev/null and b/app/assets/images/oceanic.ws/icon.png differ diff --git a/app/assets/images/oceanic.ws/ms-icon-144x144.png b/app/assets/images/oceanic.ws/ms-icon-144x144.png new file mode 100644 index 0000000..07f2026 Binary files /dev/null and b/app/assets/images/oceanic.ws/ms-icon-144x144.png differ diff --git a/app/assets/images/oceanic.ws/ms-icon-150x150.png b/app/assets/images/oceanic.ws/ms-icon-150x150.png new file mode 100644 index 0000000..8a29812 Binary files /dev/null and b/app/assets/images/oceanic.ws/ms-icon-150x150.png differ diff --git a/app/assets/images/oceanic.ws/ms-icon-310x310.png b/app/assets/images/oceanic.ws/ms-icon-310x310.png new file mode 100644 index 0000000..fd41922 Binary files /dev/null and b/app/assets/images/oceanic.ws/ms-icon-310x310.png differ diff --git a/app/assets/images/oceanic.ws/ms-icon-70x70.png b/app/assets/images/oceanic.ws/ms-icon-70x70.png new file mode 100644 index 0000000..be88150 Binary files /dev/null and b/app/assets/images/oceanic.ws/ms-icon-70x70.png differ diff --git a/app/assets/images/yiff.media/Blep.png b/app/assets/images/yiff.media/Blep.png new file mode 100644 index 0000000..8e6a626 Binary files /dev/null and b/app/assets/images/yiff.media/Blep.png differ diff --git a/app/assets/images/yiff.media/android-icon-144x144.png b/app/assets/images/yiff.media/android-icon-144x144.png new file mode 100644 index 0000000..1baea13 Binary files /dev/null and b/app/assets/images/yiff.media/android-icon-144x144.png differ diff --git a/app/assets/images/yiff.media/android-icon-192x192.png b/app/assets/images/yiff.media/android-icon-192x192.png new file mode 100644 index 0000000..e97424c Binary files /dev/null and b/app/assets/images/yiff.media/android-icon-192x192.png differ diff --git a/app/assets/images/yiff.media/android-icon-36x36.png b/app/assets/images/yiff.media/android-icon-36x36.png new file mode 100644 index 0000000..ce08cf2 Binary files /dev/null and b/app/assets/images/yiff.media/android-icon-36x36.png differ diff --git a/app/assets/images/yiff.media/android-icon-48x48.png b/app/assets/images/yiff.media/android-icon-48x48.png new file mode 100644 index 0000000..95d7c10 Binary files /dev/null and b/app/assets/images/yiff.media/android-icon-48x48.png differ diff --git a/app/assets/images/yiff.media/android-icon-72x72.png b/app/assets/images/yiff.media/android-icon-72x72.png new file mode 100644 index 0000000..67fbb29 Binary files /dev/null and b/app/assets/images/yiff.media/android-icon-72x72.png differ diff --git a/app/assets/images/yiff.media/android-icon-96x96.png b/app/assets/images/yiff.media/android-icon-96x96.png new file mode 100644 index 0000000..5983dc5 Binary files /dev/null and b/app/assets/images/yiff.media/android-icon-96x96.png differ diff --git a/app/assets/images/yiff.media/apple-icon-114x114.png b/app/assets/images/yiff.media/apple-icon-114x114.png new file mode 100644 index 0000000..d7da659 Binary files /dev/null and b/app/assets/images/yiff.media/apple-icon-114x114.png differ diff --git a/app/assets/images/yiff.media/apple-icon-120x120.png b/app/assets/images/yiff.media/apple-icon-120x120.png new file mode 100644 index 0000000..0c522ec Binary files /dev/null and b/app/assets/images/yiff.media/apple-icon-120x120.png differ diff --git a/app/assets/images/yiff.media/apple-icon-144x144.png b/app/assets/images/yiff.media/apple-icon-144x144.png new file mode 100644 index 0000000..3a5dcff Binary files /dev/null and b/app/assets/images/yiff.media/apple-icon-144x144.png differ diff --git a/app/assets/images/yiff.media/apple-icon-152x152.png b/app/assets/images/yiff.media/apple-icon-152x152.png new file mode 100644 index 0000000..8d5b36b Binary files /dev/null and b/app/assets/images/yiff.media/apple-icon-152x152.png differ diff --git a/app/assets/images/yiff.media/apple-icon-180x180.png b/app/assets/images/yiff.media/apple-icon-180x180.png new file mode 100644 index 0000000..f1f5e8e Binary files /dev/null and b/app/assets/images/yiff.media/apple-icon-180x180.png differ diff --git a/app/assets/images/yiff.media/apple-icon-57x57.png b/app/assets/images/yiff.media/apple-icon-57x57.png new file mode 100644 index 0000000..02088ba Binary files /dev/null and b/app/assets/images/yiff.media/apple-icon-57x57.png differ diff --git a/app/assets/images/yiff.media/apple-icon-60x60.png b/app/assets/images/yiff.media/apple-icon-60x60.png new file mode 100644 index 0000000..a0ace14 Binary files /dev/null and b/app/assets/images/yiff.media/apple-icon-60x60.png differ diff --git a/app/assets/images/yiff.media/apple-icon-72x72.png b/app/assets/images/yiff.media/apple-icon-72x72.png new file mode 100644 index 0000000..ab3270a Binary files /dev/null and b/app/assets/images/yiff.media/apple-icon-72x72.png differ diff --git a/app/assets/images/yiff.media/apple-icon-76x76.png b/app/assets/images/yiff.media/apple-icon-76x76.png new file mode 100644 index 0000000..8ce34f2 Binary files /dev/null and b/app/assets/images/yiff.media/apple-icon-76x76.png differ diff --git a/app/assets/images/yiff.media/apple-icon-precomposed.png b/app/assets/images/yiff.media/apple-icon-precomposed.png new file mode 100644 index 0000000..f2e6257 Binary files /dev/null and b/app/assets/images/yiff.media/apple-icon-precomposed.png differ diff --git a/app/assets/images/yiff.media/apple-icon.png b/app/assets/images/yiff.media/apple-icon.png new file mode 100644 index 0000000..f2e6257 Binary files /dev/null and b/app/assets/images/yiff.media/apple-icon.png differ diff --git a/app/assets/images/yiff.media/favicon-16x16.png b/app/assets/images/yiff.media/favicon-16x16.png new file mode 100644 index 0000000..46d78f5 Binary files /dev/null and b/app/assets/images/yiff.media/favicon-16x16.png differ diff --git a/app/assets/images/yiff.media/favicon-32x32.png b/app/assets/images/yiff.media/favicon-32x32.png new file mode 100644 index 0000000..cc3f14c Binary files /dev/null and b/app/assets/images/yiff.media/favicon-32x32.png differ diff --git a/app/assets/images/yiff.media/favicon-96x96.png b/app/assets/images/yiff.media/favicon-96x96.png new file mode 100644 index 0000000..e4051be Binary files /dev/null and b/app/assets/images/yiff.media/favicon-96x96.png differ diff --git a/app/assets/images/yiff.media/favicon.ico b/app/assets/images/yiff.media/favicon.ico new file mode 100644 index 0000000..48319e5 Binary files /dev/null and b/app/assets/images/yiff.media/favicon.ico differ diff --git a/app/assets/images/yiff.media/ms-icon-144x144.png b/app/assets/images/yiff.media/ms-icon-144x144.png new file mode 100644 index 0000000..3a5dcff Binary files /dev/null and b/app/assets/images/yiff.media/ms-icon-144x144.png differ diff --git a/app/assets/images/yiff.media/ms-icon-150x150.png b/app/assets/images/yiff.media/ms-icon-150x150.png new file mode 100644 index 0000000..1c465dc Binary files /dev/null and b/app/assets/images/yiff.media/ms-icon-150x150.png differ diff --git a/app/assets/images/yiff.media/ms-icon-310x310.png b/app/assets/images/yiff.media/ms-icon-310x310.png new file mode 100644 index 0000000..4a0d419 Binary files /dev/null and b/app/assets/images/yiff.media/ms-icon-310x310.png differ diff --git a/app/assets/images/yiff.media/ms-icon-70x70.png b/app/assets/images/yiff.media/ms-icon-70x70.png new file mode 100644 index 0000000..2c04568 Binary files /dev/null and b/app/assets/images/yiff.media/ms-icon-70x70.png differ diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..9aec230 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..8d6c2a1 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..30ad64a --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,256 @@ +# frozen_string_literal: true + +class ApplicationController < ActionController::Base + class ReadOnlyError < StandardError; end + class FeatureUnavailableError < StandardError; end + + before_action :set_view_path + before_action :initialize_session + before_action :normalize_search + before_action :set_common_headers + helper_method :site_domain, :assets_path, :safe_site_name, :site_title, :site_color, :controller_param, :action_param, :body_class, :stimulus_class + skip_before_action :verify_authenticity_token + + rescue_from Exception, with: :rescue_exception + + def set_view_path + prepend_view_path(Rails.root.join("app", "views", safe_site_name)) + end + + def set_common_headers + Websites.config.common_headers(request.domain).each do |key, value| + response.headers[key] = value + end + end + + def initialize_session + WebLogger.initialize(request) + CurrentUser.user = APIUser.anonymous + CurrentUser.ip_addr = request.remote_ip + end + + def site_domain + "unknown" + end + + def assets_path + site_domain + end + + def site_title + "Unknown" + end + + def site_color + "#2C2F33" + end + + def safe_site_name + site_domain.parameterize.dasherize + end + + def controller_param + return "unknown" unless params[:controller] + name = params[:controller].parameterize.dasherize + name = name.gsub("#{safe_site_name}-", "") if name.include?(safe_site_name) + name + end + + def action_param + return "unknown" unless params[:action] + params[:action] + end + + def body_class + "s-#{safe_site_name} c-#{controller_param} a-#{action_param}" + end + + def stimulus_class + params[:controller].gsub("/", "--").dasherize + end + + def track_usage(service) + APIUsage.create!( + user_id: CurrentUser.is_anonymous? ? nil : CurrentUser.id, + api_key: @apikey.nil? || @apikey.is_anon? ? nil : @apikey, + user_agent: request.headers["user-agent"], + method: request.method, + path: request.path, + params: request.params.to_json, + service: service, + ip_addr: request.remote_ip, + ) + end + + module CommonAssetRoutes + def manifest + render(partial: "manifest", layout: false) + end + + def browserconfig + render(partial: "browserconfig", layout: false) + end + end + + module CommonRoutes + def online + render(json: { + success: true, + uptime: Time.now - Websites::STARTED_AT, + }) + end + + def access_denied(message: nil, code: nil) + @message = message.present? ? "Access Denied: #{message}" : "Access Denied" + @code = code + respond_to do |fmt| + fmt.html { render("static/access_denied", status: 403) } + fmt.json do + render(json: { + success: false, + message: @message, + code: @code, + }, status: 403) + end + end + end + + def not_found(code: nil) + @code = code + respond_to do |fmt| + fmt.any { render("static/not_found", status: 404) } + fmt.json do + render(json: { + success: false, + message: "Not found.", + code: @code, + }, status: 404) + end + end + end + + def readonly + respond_to do |fmt| + fmt.html { render("static/readonly", status: YiffyAPIErrorCodes::READONLY.status) } + fmt.json { render_error(YiffyAPIErrorCodes::READONLY, message: "This service is currently in read only mode. Try again later.") } + end + end + + private + + def render_error(*, **) + extend(YiffyAPIUtil).render_error(*, **) + end + end + + module RenderMethods + def handle_error + @exception = request.env["action_dispatch.exception"] + @status_code = @exception.try(:status_code) || + ActionDispatch::ExceptionWrapper.new( + request.env, @exception + ).status_code + return not_found if @status_code == 404 + return rescue_exception(@exception) if @exception + render_error_page(@status_code, Exception.new("An unexpected error occurred.")) + end + + def rescue_exception(exception) + @exception = exception + case exception + when ActiveRecord::RecordNotFound + not_found + when ReadOnlyError + readonly + when PG::Error + render_error_page(503, exception, message: "The database is unavailable. Try again later.") + when ActionController::ParameterMissing, ActionController::UnpermittedParameters + render_expected_error(400, exception.message) + when ActionController::RoutingError + render_error_page(405, exception) + when ActionController::UnknownFormat, ActionView::MissingTemplate + render_unsupported_format + when ActionController::InvalidAuthenticityToken + access_denied(message: "Invalid CSRF Token") + else + render_error_page(500, exception) + end + end + + def render_unsupported_format + return not_found if request.format.nil? + render_expected_error(406, "#{request.format} is not a supported format for this page", format: :html) + end + + def render_expected_error(status, message, format: request.format.symbol) + format = :html unless format.in?(%i[html json]) + @message = message + @log_code = nil + render("static/error", status: status, formats: format) + end + + def render_error_page(status, exception, message: exception.message, format: request.format.symbol) + @exception = exception + @expected = status < 500 + @message = message.encode("utf-8", invalid: :replace, undef: :replace) + @backtrace = Rails.backtrace_cleaner.clean(@exception.backtrace) + format = :html unless format.in?(%i[html json]) + + @message = "An unexpected error occurred." if Rails.env.production? && message == exception.message + + WebLogger.log_exception(@exception, expected: @expected) + log = ExceptionLog.add(exception, request) unless @expected + @log_code = log&.code + render("static/error", status: status, formats: format) + end + end + + module SearchMethods + def normalize_search + return unless request.get? || request.head? + params[:search] ||= ActionController::Parameters.new + + deep_reject_blank = ->(hash) do + hash.reject { |_k, v| v.blank? || (v.is_a?(Hash) && deep_reject_blank.call(v).blank?) } + end + if params[:search].is_a?(ActionController::Parameters) + nonblank_search_params = deep_reject_blank.call(params[:search]) + else + nonblank_search_params = ActionController::Parameters.new + end + + if nonblank_search_params != params[:search] + params[:search] = nonblank_search_params + redirect_to(url_for(params: params.except(:controller, :action, :index).permit!)) + end + end + + def search_params + params.fetch(:search, {}).permit! + end + + def permit_search_params(permitted_params) + params.fetch(:search, {}).permit(%i[id created_at updated_at] + permitted_params) + end + end + + module ReadonlyMethods + extend ActiveSupport::Concern + def enforce_readonly + return unless Websites.config.readonly? + raise(ReadOnlyError) unless allowed_readonly_actions.include?(action_name) + end + + def allowed_readonly_actions + %w[index show] + end + + included do + before_action :enforce_readonly + end + end + + include CommonRoutes + include RenderMethods + include SearchMethods +end diff --git a/app/controllers/butts_are_cool/application_controller.rb b/app/controllers/butts_are_cool/application_controller.rb new file mode 100644 index 0000000..2d666fd --- /dev/null +++ b/app/controllers/butts_are_cool/application_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ButtsAreCool + class ApplicationController < ::ApplicationController + def site_domain + ButtsAreCoolRoutes::DOMAIN + end + + def site_title + "Butts Are Cool" + end + + def site_color + "#2C2F33" + end + end +end diff --git a/app/controllers/butts_are_cool/balls_controller.rb b/app/controllers/butts_are_cool/balls_controller.rb new file mode 100644 index 0000000..7cc2afd --- /dev/null +++ b/app/controllers/butts_are_cool/balls_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ButtsAreCool + class BallsController < ButtsAreCool::ApplicationController + include ::ApplicationController::CommonAssetRoutes + + def index + render("butts_are_cool/home/custom", locals: { type: "balls", post_id: "1422653", file_ext: "jpg" }) + end + + def assets_path + "#{ButtsAreCoolRoutes::DOMAIN}/custom/balls" + end + + def site_domain + ButtsAreCoolRoutes::BALLS_DOMAIN + end + end +end diff --git a/app/controllers/butts_are_cool/cocks_controller.rb b/app/controllers/butts_are_cool/cocks_controller.rb new file mode 100644 index 0000000..53815f0 --- /dev/null +++ b/app/controllers/butts_are_cool/cocks_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ButtsAreCool + class CocksController < ButtsAreCool::ApplicationController + include ::ApplicationController::CommonAssetRoutes + + def index + render("butts_are_cool/home/custom", locals: { type: "cocks", post_id: "2442378", file_ext: "jpg" }) + end + + def assets_path + "#{ButtsAreCoolRoutes::DOMAIN}/custom/cocks" + end + + def site_domain + ButtsAreCoolRoutes::COCKS_DOMAIN + end + end +end diff --git a/app/controllers/butts_are_cool/home_controller.rb b/app/controllers/butts_are_cool/home_controller.rb new file mode 100644 index 0000000..a12acc2 --- /dev/null +++ b/app/controllers/butts_are_cool/home_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module ButtsAreCool + class HomeController < ButtsAreCool::ApplicationController + include ::ApplicationController::CommonAssetRoutes + + def index + end + end +end diff --git a/app/controllers/butts_are_cool/knots_controller.rb b/app/controllers/butts_are_cool/knots_controller.rb new file mode 100644 index 0000000..4f6b13e --- /dev/null +++ b/app/controllers/butts_are_cool/knots_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ButtsAreCool + class KnotsController < ButtsAreCool::ApplicationController + include ::ApplicationController::CommonAssetRoutes + + def index + render("butts_are_cool/home/custom", locals: { type: "knots", post_id: "2535662", file_ext: "jpg" }) + end + + def assets_path + "#{ButtsAreCoolRoutes::DOMAIN}/custom/knots" + end + + def site_domain + ButtsAreCoolRoutes::KNOTS_DOMAIN + end + end +end diff --git a/app/controllers/butts_are_cool/sheaths_controller.rb b/app/controllers/butts_are_cool/sheaths_controller.rb new file mode 100644 index 0000000..11a317a --- /dev/null +++ b/app/controllers/butts_are_cool/sheaths_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ButtsAreCool + class SheathsController < ButtsAreCool::ApplicationController + include ::ApplicationController::CommonAssetRoutes + + def index + render("butts_are_cool/home/custom", locals: { type: "sheaths", post_id: "2670356", file_ext: "png" }) + end + + def assets_path + "#{ButtsAreCoolRoutes::DOMAIN}/custom/sheaths" + end + + def site_domain + ButtsAreCoolRoutes::SHEATHS_DOMAIN + end + end +end diff --git a/app/controllers/e621_ws/application_controller.rb b/app/controllers/e621_ws/application_controller.rb new file mode 100644 index 0000000..3389083 --- /dev/null +++ b/app/controllers/e621_ws/application_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module E621Ws + class ApplicationController < ::ApplicationController + def site_domain + E621WsRoutes::DOMAIN + end + + def assset_domin + E621WsRoutes::DOMAIN + end + + def site_title + "E621" + end + + def site_color + "#012E56" + end + end +end diff --git a/app/controllers/e621_ws/status/application_controller.rb b/app/controllers/e621_ws/status/application_controller.rb new file mode 100644 index 0000000..17efc11 --- /dev/null +++ b/app/controllers/e621_ws/status/application_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module E621Ws + module Status + class ApplicationController < ::E621Ws::ApplicationController + def site_title + "E621 Status" + end + + def site_domain + E621WsRoutes::STATUS_DOMAIN + end + + def assets_path + E621WsRoutes::DOMAIN + end + end + end +end diff --git a/app/controllers/e621_ws/status/schema_controller.rb b/app/controllers/e621_ws/status/schema_controller.rb new file mode 100644 index 0000000..368eac6 --- /dev/null +++ b/app/controllers/e621_ws/status/schema_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module E621Ws + module Status + class SchemaController < ApplicationController + def index + render("combined") + end + + def combined + end + + def current + end + + def history + end + end + end +end diff --git a/app/controllers/e621_ws/status/webhooks_controller.rb b/app/controllers/e621_ws/status/webhooks_controller.rb new file mode 100644 index 0000000..08b5163 --- /dev/null +++ b/app/controllers/e621_ws/status/webhooks_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module E621Ws + module Status + class WebhooksController < ApplicationController + def index + end + + def discord + redirect_to(oauth_url(min: params[:min] == "true"), allow_other_host: true) + end + + def discord_callback + res = client.authorize(redirect_uri: Websites.config.e621_status_check_discord_redirect, code: params[:code]) + + return redirect_to(e621_ws_status_webhook_path, alert: "Error: #{res.error_description}") if res.is_a?(Requests::DiscordOauth::Error) + return redirect_to(e621_ws_status_webhook_path, alert: "Error: Failed to get webhook.") if res.webhook.nil? + + uid = res.scope.include?("identify") ? res.user_id : nil + hook = E621Webhook.from_struct(res.webhook, uid) + + if hook.is_a?(Symbol) + # noinspection RubyCaseWithoutElseBlockInspection + case hook + when :too_many_channel + return redirect_to(e621_ws_status_webhook_path, alert: "Error: You already have a status check enabled in that channel. Delete the other webhook to use a new webhook. The newly created webhook will be automatically deleted.") + when :too_many_guild + return redirect_to(e621_ws_status_webhook_path, alert: "Error: You've already enabled #{E621Webhook::MAX_PER_GUILD} status checks in that server. Please delete the other webhooks before adding a new check. The newly created webhook will be automatically deleted.") + end + end + redirect_to(e621_ws_root_path, notice: "Check successfully setup. Delete the webhook to disable the updates.") + end + + private + + def client + Requests::DiscordOauth.new( + client_id: Websites.config.e621_status_check_discord_id, + client_secret: Websites.config.e621_status_check_discord_secret, + ) + end + + def oauth_url(min: false) + DiscordLink.for( + client_id: Websites.config.e621_status_check_discord_id, + redirect_uri: Websites.config.e621_status_check_discord_redirect, + scope: Websites.config.e621_status_check_discord_scopes[min ? :min : :all], + ) + end + end + end +end diff --git a/app/controllers/e621_ws/status_controller.rb b/app/controllers/e621_ws/status_controller.rb new file mode 100644 index 0000000..0b9e815 --- /dev/null +++ b/app/controllers/e621_ws/status_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module E621Ws + class StatusController < Status::ApplicationController + include ::ApplicationController::CommonAssetRoutes + + def index + respond_to do |fmt| + fmt.html do + @current = E621Status.current + end + fmt.json do + render(json: E621Status.combined) + end + end + end + + def current + render(json: E621Status.current) + end + + def history + render(json: E621Status.history) + end + end +end diff --git a/app/controllers/furry_cool/application_controller.rb b/app/controllers/furry_cool/application_controller.rb new file mode 100644 index 0000000..2c42476 --- /dev/null +++ b/app/controllers/furry_cool/application_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module FurryCool + class ApplicationController < ::ApplicationController + def site_domain + FurryCoolRoutes::DOMAIN + end + + def site_title + "Donovan_DMC" + end + + def site_color + "#00FFFF" + end + end +end diff --git a/app/controllers/furry_cool/home_controller.rb b/app/controllers/furry_cool/home_controller.rb new file mode 100644 index 0000000..3440e4c --- /dev/null +++ b/app/controllers/furry_cool/home_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module FurryCool + class HomeController < FurryCool::ApplicationController + include ::ApplicationController::CommonAssetRoutes + + def index + end + + private + + def site_title + "Donovan_DMC - Info" + end + end +end diff --git a/app/controllers/maidboye_cafe/application_controller.rb b/app/controllers/maidboye_cafe/application_controller.rb new file mode 100644 index 0000000..54ec728 --- /dev/null +++ b/app/controllers/maidboye_cafe/application_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module MaidboyeCafe + class ApplicationController < ::ApplicationController + def site_domain + MaidboyeCafeRoutes::DOMAIN + end + + def site_title + "Maid Boye" + end + + def site_color + "#A7A4AA" + end + end +end diff --git a/app/controllers/maidboye_cafe/home_controller.rb b/app/controllers/maidboye_cafe/home_controller.rb new file mode 100644 index 0000000..67ed8df --- /dev/null +++ b/app/controllers/maidboye_cafe/home_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module MaidboyeCafe + class HomeController < MaidboyeCafe::ApplicationController + include ::ApplicationController::CommonAssetRoutes + + def index + end + + def privacy + end + + private + + def site_title + "Maid Boye - Home" + end + end +end diff --git a/app/controllers/oceanic_ws/application_controller.rb b/app/controllers/oceanic_ws/application_controller.rb new file mode 100644 index 0000000..44a8e7d --- /dev/null +++ b/app/controllers/oceanic_ws/application_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module OceanicWs + class ApplicationController < ::ApplicationController + def site_domain + OceanicWsRoutes::DOMAIN + end + + def site_title + "Oceanic" + end + + def site_color + "#2E9DD7" + end + end +end diff --git a/app/controllers/oceanic_ws/docs_controller.rb b/app/controllers/oceanic_ws/docs_controller.rb new file mode 100644 index 0000000..336acbd --- /dev/null +++ b/app/controllers/oceanic_ws/docs_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module OceanicWs + GITHUB_REPO = "https://github.com/OceanicJS/Oceanic" + + class DocsController < OceanicWs::ApplicationController + include ::ApplicationController::CommonAssetRoutes + + def index + @versions = versions + end + + def json + render(json: { + versions: versions, + }) + end + + def latest + redirect_to("/#{latest_version}#{params[:other]}") + end + + private + + def versions + ver = Cache.redis.smembers("oceanic:versions") + branches = ver.filter { |v| !v.start_with?("v") } + tags = ver.filter { |v| v.start_with?("v") } + { + branches: branches.sort, + tags: tags.sort_by { |v| Gem::Version.new(v[1..]) }, + } + end + + def latest_version + `git ls-remote --tags #{GITHUB_REPO}`.split("\n").map { |line| line.split("\t")[-1].gsub("refs/tags/", "") }.max_by { |ver| Gem::Version.new(ver[1..]) } + end + end +end diff --git a/app/controllers/oceanic_ws/github_webhooks_controller.rb b/app/controllers/oceanic_ws/github_webhooks_controller.rb new file mode 100644 index 0000000..e2beca0 --- /dev/null +++ b/app/controllers/oceanic_ws/github_webhooks_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module OceanicWs + class GithubWebhooksController < ActionController::API + include GithubWebhook::Processor + + def github_push(payload) + if payload[:ref].starts_with?("refs/tags/") + BuildOceanicDocsJob.perform_later(:tag, payload) + elsif payload[:ref] == "refs/heads/dev" + BuildOceanicDocsJob.perform_later(:dev, payload) + end + + head(204) + end + + private + + def webhook_secret(_payload) + Websites.config.github_webhook_secret + end + end +end diff --git a/app/controllers/oceanic_ws/home_controller.rb b/app/controllers/oceanic_ws/home_controller.rb new file mode 100644 index 0000000..84a7e38 --- /dev/null +++ b/app/controllers/oceanic_ws/home_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module OceanicWs + class HomeController < OceanicWs::ApplicationController + include ::ApplicationController::CommonAssetRoutes + + def index + end + end +end diff --git a/app/controllers/yiff_media/application_controller.rb b/app/controllers/yiff_media/application_controller.rb new file mode 100644 index 0000000..9b872e7 --- /dev/null +++ b/app/controllers/yiff_media/application_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module YiffMedia + class ApplicationController < ::ApplicationController + def site_domain + YiffMediaRoutes::DOMAIN + end + + def site_title + "YiffyAPI" + end + + def site_color + "#222222" + end + end +end diff --git a/app/controllers/yiff_media/home_controller.rb b/app/controllers/yiff_media/home_controller.rb new file mode 100644 index 0000000..09dfcec --- /dev/null +++ b/app/controllers/yiff_media/home_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module YiffMedia + class HomeController < YiffMedia::ApplicationController + include ::ApplicationController::CommonAssetRoutes + + def index + end + + private + + def site_title + "Yiffy Media - Home" + end + end +end diff --git a/app/controllers/yiff_media/reports_controller.rb b/app/controllers/yiff_media/reports_controller.rb new file mode 100644 index 0000000..d8d98e6 --- /dev/null +++ b/app/controllers/yiff_media/reports_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module YiffMedia + class ReportsController < YiffMedia::ApplicationController + include ::ApplicationController::CommonAssetRoutes + + def index + end + + private + + def site_title + "Yiffy Media - Reports" + end + end +end diff --git a/app/controllers/yiff_rest/api_v2_controller.rb b/app/controllers/yiff_rest/api_v2_controller.rb new file mode 100644 index 0000000..7f64ae1 --- /dev/null +++ b/app/controllers/yiff_rest/api_v2_controller.rb @@ -0,0 +1,207 @@ +# frozen_string_literal: true + +module YiffRest + class APIV2Controller < ApplicationController + include ::ApplicationController::ReadonlyMethods + before_action :handle_ratelimit, except: %i[robots] + before_action :validate_images_access, only: %i[index] + before_action :validate_images_bulk_access, only: %i[bulk] + before_action -> { track_usage("images") }, only: %i[index categories category image] + + def index + return render_error(YiffyAPIErrorCodes::IMAGES_IMAGE_RESPONSE_DISABLED, error: "Image response has been disabled. Please use the json response.") if params[:category].ends_with?("/image") + + category = params[:category]&.downcase&.gsub("/", ".") || "" + return redirect_to("https://yiff.rest") if category.blank? + + limit = params[:amount]&.to_i || 1 + return render_error(YiffyAPIErrorCodes::IMAGES_AMOUNT_LT_ONE, error: "Amount must be 1 or more.") if limit < 1 + return render_error(YiffyAPIErrorCodes::IMAGES_AMOUNT_LT_ONE, error: "Amount must be 5 or less.") if limit > 5 + + size_limit = params[:sizeLimit].blank? ? nil : Filesize.from(params[:sizeLimit].to_s).to_i + return render_error(YiffyAPIErrorCodes::IMAGES_CATEGORY_NOT_FOUND, error: "Category not found.") unless api_categories.include?(category) + + images = APIImage.random(category, limit, size_limit) + + return render_error(YiffyAPIErrorCodes::IMAGES_NO_RESULTS, error: "No results were found. Try changing your search parameters.") if images.empty? + + Cache.redis.multi do |r| + r.incr("yiffy2:stats:images:#{category}") + r.incr("yiffy2:stats:images:ip:#{request.remote_ip}") + r.incr("yiffy2:stats:images:ip:#{request.remote_ip}:#{category}") + r.incr("yiffy2:stats:images:total") + r.incr("yiffy2:stats:images:total:#{category}") + if @apikey.present? && !@apikey&.is_anon? + r.incr("yiffy2:stats:images:key:#{@apikey.id}") + r.incr("yiffy2:stats:images:key:#{@apikey.id}:#{category}") + end + end + + render(json: { + "$schema": "https://schema.yiff.rest/V2.json", + images: images, + success: true, + notes: notes_for_request, + }) + end + + def robots + render(plain: <<~ROBOTS) + User-agent: * + Disallow: / + ROBOTS + end + + def stats + render(json: { + success: true, + data: APIKey.stats(ip: request.remote_ip, key: @apikey), + }) + end + + def categories + render(json: { + success: true, + data: APIImage.categories, + }) + end + + def category + category = params[:category]&.downcase&.gsub("/", ".") || "" + return render_error(YiffyAPIErrorCodes::IMAGES_CATEGORY_NOT_FOUND, error: "Category not found.") unless api_categories.include?(category) + count = APIImage.cached_count(category) + render(json: { + success: true, + data: { + **APIImage.categories.find { |c| c[:db] == category }, + count: count, + }, + }) + end + + def image + image = APIImage.find_by(id: params[:id].gsub("-", "")) + return render_error(YiffyAPIErrorCodes::IMAGES_NOT_FOUND, error: "Image not found.") unless image + render(json: { + success: true, + data: { + **image.as_json, + category: image.category, + }, + }) + end + + def bulk + req = params[:api_v2] || {} + return render_error(YiffyAPIErrorCodes::BULK_IMAGES_INVALID_BODY, error: "Invalid body, or no categories specified.") if req.blank? + size_limit = params[:sizeLimit].blank? ? nil : Filesize.from(params[:sizeLimit].to_s).to_i + total = 0 + valid = true + req.each do |category, amount| + total += amount.to_i + next if api_categories.include?(category) + + render_error(YiffyAPIErrorCodes::IMAGES_CATEGORY_NOT_FOUND, error: "Invalid category specified: #{category}") + valid = false + break + end + + return unless valid + return render_error(YiffyAPIErrorCodes::BULK_IMAGES_NUMBER_GT_MAX, error: "Total amount of images requested is greater than #{@apikey.bulk_limit} (#{total}).") if total > @apikey.bulk_limit + + images = APIImage.bulk(req, size_limit) + + return render_error(YiffyAPIErrorCodes::IMAGES_NO_RESULTS, error: "No results were found. Try changing your search parameters.") if images.empty? + + Cache.redis.multi do |r| + r.incrby("yiffy2:stats:images:ip:#{request.remote_ip}", total) + r.incr("yiffy2:stats:images:ip:#{request.remote_ip}:bulk") + r.incrby("yiffy2:stats:images:total", total) + r.incr("yiffy2:stats:images:total:bulk") + r.incrby("yiffy2:stats:images:key:#{@apikey.id}", total) + r.incr("yiffy2:stats:images:key:#{@apikey.id}:bulk") + req.each do |category, amount| + r.incrby("yiffy2:stats:images:#{category}", amount.to_i) + r.incr("yiffy2:stats:images:#{category}:bulk") + r.incrby("yiffy2:stats:images:ip:#{request.remote_ip}:#{category}", amount.to_i) + r.incr("yiffy2:stats:images:ip:#{request.remote_ip}:#{category}:bulk") + r.incrby("yiffy2:stats:images:total:#{category}", amount.to_i) + r.incr("yiffy2:stats:images:total:#{category}:bulk") + r.incrby("yiffy2:stats:images:key:#{@apikey.id}:#{category}", amount.to_i) + r.incr("yiffy2:stats:images:key:#{@apikey.id}:#{category}:bulk") + end + end + + execute_webhook("bulk", req) + + render(json: { + success: true, + data: images, + }) + end + + private + + def site_domain + YiffRestRoutes::V2_DOMAIN + end + + def api_categories + categories = APIImage.categories.pluck(:db) + [*categories, "chris"] # gay little polar cutie + end + + def category_info + [*APIImage.categories, { name: "Gay Polar Cutie", db: "chris", sfw: true }] # gay little polar cutie + end + + def notes_for_request + return [] if params[:notes].to_s.downcase == "disabled" + notes = [] + # notes << { id: 1, content: "This api host (api.furry.bot) is being removed on June 9th, 2021. Please migrate to https://yiff.rest." } if headers["Host"] == "api.furry.bot" + notes << { id: 2, content: "We've moved to using subdomains for api versioning! e.g. https://v2.yiff.rest. They have the same functionality, just without the version in the path. The /V2 route will not be removed, but v3 and forward will only use the subdomain." } if request.path.start_with?("/V2") + notes << { id: 3, content: "Hey, we see you aren't using an api key. They're free! To get one, visit https://yiff.rest/apikeys." } if headers["Authorization"].blank? + # notes << { id: 4, content: "WARNING! This list is STATIC, it can be inaccurate! We recommended parsing the dot notation in https://v2.yiff.rest/categories instead of this!" } + # notes << { id: 5, content: "Since images are getting bigger, we're adding a size limit parameter. Add ?sizeLimit= to limit the size of images we provide you." } if params[:sizeLimit].blank? + notes << { id: 6, content: "You can now hide these notes by setting the notes parameter to disabled. (ex: ?notes=disabled)" } + notes << { id: 7, content: "We now have proper documentation: https://docs.yiff.rest" } + notes << { id: 8, content: "We have a new service available, a thumbnailer for e621. You can see its documentation at https://docs.yiff.rest/thumbnails." } + end + + def execute_webhook(category, bulk_categories = nil) + bulk = "" + color = 0x008000 + if bulk_categories.present? + bulk = <<~BULK + + **Categories:** + #{bulk_categories.permit!.to_h.map { |c, l| "- **#{c}**: #{l}" }.join("\n")} + BULK + color = 0xDC143C if bulk_categories.keys.any? { |k| category_info.find { |c| c[:db] == k }[:sfw] } + elsif !(category_info.find { |c| c[:db] == category }[:sfw]) + color = 0xDC143C + end + Websites.config.yiffyapi_usage_webhook.execute(embeds: [ + { + title: "V2 API Request", + description: <<~DESC.strip, + Host: **#{request.host}** + Path: **#{request.path}** + Category: `#{category}` + Auth: #{@apikey.nil? || @apikey.is_anon? ? '**No**' : "**Yes** (##{@apikey.id})"} + Size Limit: **#{params[:sizeLimit] || 'None'}** + User Agent: **#{request.user_agent}** + IP: **#{request.remote_ip}** + #{bulk} + DESC + color: color, + timestamp: Time.now.iso8601, + }, + ]) + end + + def allowed_readonly_actions + super + %w[bulk] + end + end +end diff --git a/app/controllers/yiff_rest/apikeys_controller.rb b/app/controllers/yiff_rest/apikeys_controller.rb new file mode 100644 index 0000000..225a600 --- /dev/null +++ b/app/controllers/yiff_rest/apikeys_controller.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +module YiffRest + class ApikeysController < ApplicationController + include ::ApplicationController::ReadonlyMethods + before_action :validate_discord, except: %i[logout] + before_action :handle_ratelimit, except: %i[index logout] + before_action :prepare_user, except: %i[logout] + before_action :load_apikey, except: %i[index new create logout] + before_action :manager_only, only: %i[disable] + before_action :admin_only, only: %i[edit] + before_action :check_can_create_apikeys, only: %i[new create] + before_action -> { check_apikey_access(:can_edit?) }, only: %i[update] + before_action -> { check_apikey_access(:can_delete?) }, only: %i[destroy] + before_action -> { check_apikey_access(:can_disable?) }, only: %i[disable enable] + before_action -> { check_apikey_access(:can_deactivate?) }, only: %i[deactivate reactivate] + before_action -> { check_apikey_access(:can_regenerate?) }, only: %i[regenerate] + + protect_from_forgery with: :exception + def index + @apikeys = APIKey.all + @apikeys = @apikeys.where(owner_id: CurrentUser.id) unless CurrentUser.is_manager? + @apikeys = @apikeys.search(search_params) + end + + def new + @apikey = APIKey.new(owner_id: CurrentUser.id) + end + + def edit + end + + def create + allowed_params = apikey_params + allowed_params[:owner_id] ||= CurrentUser.id + apiuser = APIUser.find_by(id: allowed_params[:owner_id]) + APIUser.create!(id: allowed_params[:owner_id], name: "User#{allowed_params[:owner_id]}") if apiuser.nil? + @apikey = APIKey.create(allowed_params) + if @apikey.errors.any? + redirect_back(fallback_location: new_yiff_rest_apikey_path, notice: @apikey.errors.full_messages.join("; ")) + else + redirect_to(yiff_rest_apikeys_path, notice: "API Key created. You can view the documentation via the link in the top bar. Click \"View\" to see your apikey.") + end + end + + def update + @apikey.update(apikey_params) + return redirect_back(fallback_location: edit_yiff_rest_apikey_path(@apikey), notice: @apikey.errors.full_messages.join("; ")) if @apikey.errors.any? + redirect_to(yiff_rest_apikeys_path, notice: "API Key Updated.") + end + + def destroy + @apikey.destroy + redirect_back(fallback_location: yiff_rest_apikeys_path) + end + + def disable + @apikey.update(disabled: true, disabled_reason: disable_params[:disabled_reason]) + redirect_back(fallback_location: yiff_rest_apikeys_path, notice: @apikey.errors.any? ? @apikey.errors.full_messages : "API Key disabled.") + end + + def enable + @apikey.update(disabled: false, disabled_reason: nil) + redirect_back(fallback_location: yiff_rest_apikeys_path, notice: @apikey.errors.any? ? @apikey.errors.full_messages : "API Key enabled.") + end + + def deactivate + @apikey.update(active: false) + redirect_back(fallback_location: yiff_rest_apikeys_path, notice: @apikey.errors.any? ? @apikey.errors.full_messages : "API Key deactivated.") + end + + def reactivate + @apikey.update(active: true) + redirect_back(fallback_location: yiff_rest_apikeys_path, notice: @apikey.errors.any? ? @apikey.errors.full_messages : "API Key reactivated.") + end + + def regenerate + @apikey.regenerate! + redirect_back(fallback_location: yiff_rest_apikeys_path, notice: @apikey.errors.any? ? @apikey.errors.full_messages : "API Key regenerated.") + end + + def logout + session.delete("discord_user") + CurrentUser.user = nil + redirect_to(yiff_rest_root_path, notice: "You have been logged out.") + end + + private + + def site_domain + YiffRestRoutes::DOMAIN + end + + def site_title + "YiffyAPI - API Keys" + end + + def assets_path + YiffMediaRoutes::DOMAIN + end + + def validate_discord + redirect_to(Websites.config.yiffyapi_discord_redirect("apikey"), allow_other_host: true) if session[:discord_user].blank? + end + + def apikey_params + permitted_params = %i[application_name usage] + permitted_params += %i[super unlimited bulk_limit limit_long limit_short window_long window_short flags_images flags_thumbs flags_shortener flags_images_bulk owner_id] if CurrentUser.is_admin? + params.require(:api_key).permit(permitted_params) + end + + def disable_params + params.require(:api_key).permit(:disabled_reason) + end + + def search_params + permitted_params = %i[] + permitted_params += %i[owner_id application_name usage active disabled disabled_reason] if CurrentUser.is_manager? + permit_search_params(permitted_params) + end + + def handle_ratelimit + info, body, rlheaders = RateLimiter.process(request, ignore_auth: true) + headers.merge!(rlheaders) + return if info.nil? + # noinspection RubyCaseWithoutElseBlockInspection + case info + when :RATELIMIT_SHORT + render_error(YiffyAPIErrorCodes::RATELIMIT_ROUTE, error: "Request Limit Exceeded", info: body) + when :RATELIMIT_LONG + render_error(YiffyAPIErrorCodes::RATELIMIT_GLOBAL, error: "Request Limit Exceeded", info: body) + end + end + + def prepare_user + name = session.dig("discord_user", "global_name") || "#{session.dig('discord_user', 'username')}#{@session.dig('discord_user', 'discriminator')}" + CurrentUser.user = APIUser.find_or_create_by(id: session.dig("discord_user", "id")) + CurrentUser.update!(name: name, discord_data: session["discord_user"]) + CurrentUser.update_avatar(session.dig("discord_user", "avatar")) + end + + def load_apikey + @apikey = APIKey.find(params[:id]) + end + + def check_apikey_access(method) + return if @apikey.nil? + access_denied(message: "You don't have access to that.") unless @apikey.send(method, CurrentUser) + end + + def check_can_create_apikeys + return if CurrentUser.is_admin? + access_denied(message: "You already have the maximum amount of apikeys.") unless CurrentUser.can_create_apikey? + end + end +end diff --git a/app/controllers/yiff_rest/application_controller.rb b/app/controllers/yiff_rest/application_controller.rb new file mode 100644 index 0000000..fdd6e0b --- /dev/null +++ b/app/controllers/yiff_rest/application_controller.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module YiffRest + class ApplicationController < ::ApplicationController + include YiffyAPIUtil + include ::ApplicationController::CommonAssetRoutes + + before_action :check_ip_block + before_action :validate_api_key + + def site_domain + YiffRestRoutes::DOMAIN + end + + def site_title + "YiffyAPI" + end + + def assets_path + YiffMediaRoutes::DOMAIN + end + + def site_color + "#222222" + end + + def validate_api_key_required + return render_error(YiffyAPIErrorCodes::API_KEY_REQUIRED, error: "An API key is required to access this service.") if request.headers["Authorization"].blank? + validate_api_key + end + + def validate_api_key + return nil if request.headers["Authorization"].blank? + @apikey = APIKey.from_request(request, with_anon: false) + return render_error(YiffyAPIErrorCodes::INVALID_API_KEY, error: "Invalid api key.") unless @apikey + return render_error(YiffyAPIErrorCodes::INACTIVE_API_KEY, error: "Api key is inactive.") unless @apikey.active? + if @apikey.disabled? + extra = { + reason: @apikey.disabled_reason, + support: "https://yiff.rest/support", + code: YiffyAPIErrorCodes::DISABLED_API_KEY.code, + } + render_error(YiffyAPIErrorCodes::DISABLED_API_KEY, error: "Your api key has been disabled by an administrator. See \"extra.reason\" for the reasoning.", extra: extra) + end + CurrentUser.user = @apikey.owner + end + + def validate_images_access + @apikey = APIKey.from_request(request) + render_error(YiffyAPIErrorCodes::SERVICE_NO_ACCESS, error: "You do not have access to this service.") unless @apikey.images_access? + end + + def validate_thumbs_access + @apikey = APIKey.from_request(request) + render_error(YiffyAPIErrorCodes::SERVICE_NO_ACCESS, error: "You do not have access to this service.") unless @apikey.thumbs_access? + end + + def validate_shortener_access + @apikey = APIKey.from_request(request) + render_error(YiffyAPIErrorCodes::SERVICE_NO_ACCESS, error: "You do not have access to this service.") unless @apikey.shortener_access? + end + + def validate_images_bulk_access + @apikey = APIKey.from_request(request) + render_error(YiffyAPIErrorCodes::SERVICE_NO_ACCESS, error: "You do not have access to this service.") unless @apikey.images_bulk_access? + end + + def handle_ratelimit + info, body, rlheaders = RateLimiter.process(request) + headers.merge!(rlheaders) + return if info.nil? + # noinspection RubyCaseWithoutElseBlockInspection + case info + when :INVALID_KEY + render_error(YiffyAPIErrorCodes::INVALID_API_KEY, error: "Invalid api key.") + when :RATELIMIT_SHORT + render_error(YiffyAPIErrorCodes::RATELIMIT_ROUTE, error: "Request Limit Exceeded", info: body) + when :RATELIMIT_LONG + render_error(YiffyAPIErrorCodes::RATELIMIT_GLOBAL, error: "Request Limit Exceeded", info: body) + end + end + + def check_ip_block + Websites.config.blocked_ip_addresses.each do |block| + next unless block[:ip] == request.remote_ip + render_error(YiffyAPIErrorCodes::IP_BLOCKED, error: "You have been blocked from accessing this service.", extra: { + reason: block[:reason], + help: "https://yiff.rest/support", + }) + break + end + end + + def user_access_check(method) + access_denied(message: "You do not have access to that.") unless CurrentUser.send(method) + end + + APIUser::Levels.constants.each do |constant| + define_method("#{constant.downcase}_only") do + user_access_check("is_#{constant.downcase}?") + end + end + end +end diff --git a/app/controllers/yiff_rest/discord_controller.rb b/app/controllers/yiff_rest/discord_controller.rb new file mode 100644 index 0000000..176aa23 --- /dev/null +++ b/app/controllers/yiff_rest/discord_controller.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module YiffRest + class DiscordController < ApplicationController + before_action :handle_oauth, only: %i[count_servers flags apikey] + + def index + end + + def count_servers + end + + def flags + end + + def apikey + return redirect_to("http://websites4.containers.local:3000/apikeys?domain=yiff.rest") if Rails.env.development? + redirect_to("https://yiff.rest/apikeys", allow_other_host: true) + end + + def interactions + begin + verify_request! + rescue Ed25519::VerifyError + return head(401) + end + + render(json: DiscordInteractions::YiffyAPI.new(params[:discord]).handle) + end + + private + + def site_domain + YiffRestRoutes::DISCORD_DOMAIN + end + + def site_title + "YiffyAPI - Discord Utilities" + end + + def client + Requests::DiscordOauth.new( + client_id: Websites.config.yiffyapi_discord_id, + client_secret: Websites.config.yiffyapi_discord_secret, + ) + end + + def oauth_url(type) + DiscordLink.for( + client_id: Websites.config.yiffyapi_discord_id, + redirect_uri: Websites.config.yiffyapi_discord_redirect(type), + scope: Websites.config.yiffyapi_discord_scopes(type), + ) + end + + def handle_oauth + return redirect_to(oauth_url(params[:action]), allow_other_host: true) if params[:code].blank? + res = client.authorize(redirect_uri: Websites.config.yiffyapi_discord_redirect(params[:action]), code: params[:code]) + + if res.is_a?(Requests::DiscordOauth::Error) + err = "Error: #{res.error}" + err += " (#{res.error_description})" if res.error_description + return redirect_to(yiff_rest_discord_root_path, alert: err) + end + + # noinspection RubyCaseWithoutElseBlockInspection + case params[:action] + when "count_servers" + guilds = res.get_guilds + @info = { + total: guilds.length, + owner: guilds.count { |g| g["owner"] }, + admin: guilds.count { |g| g["permissions"] & 0x8 != 0 }, + admin_owner: guilds.count { |g| g["permissions"] & 0x8 != 0 && !g["owner"] }, + } + when "flags" + user = res.get_authorization["user"] + @info = { + public_number: user["public_flags"], + all_number: user["flags"], + public_flags: DiscordConstants::UserFlags.parse_flags(user["public_flags"]).map(&:title), + all_flags: DiscordConstants::UserFlags.parse_flags(user["flags"] - user["public_flags"]).map(&:title), + } + when "apikey" + session[:discord_user] = res.get_authorization["user"].slice("id", "username", "discriminator", "global_name", "avatar") + end + end + + def verify_request! + signature = request.headers["X-Signature-Ed25519"] + timestamp = request.headers["X-Signature-Timestamp"] + verify_key.verify([signature].pack("H*"), "#{timestamp}#{request.raw_post}") + end + + def verify_key + Ed25519::VerifyKey.new([Websites.config.yiffyapi_public_key].pack("H*")).freeze + end + end +end diff --git a/app/controllers/yiff_rest/home_controller.rb b/app/controllers/yiff_rest/home_controller.rb new file mode 100644 index 0000000..7b65f15 --- /dev/null +++ b/app/controllers/yiff_rest/home_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module YiffRest + class HomeController < ApplicationController + def index + end + + private + + def site_domain + YiffRestRoutes::DOMAIN + end + + def site_title + "YiffyAPI" + end + + def assets_path + YiffMediaRoutes::DOMAIN + end + end +end diff --git a/app/controllers/yiff_rest/state_controller.rb b/app/controllers/yiff_rest/state_controller.rb new file mode 100644 index 0000000..06a7b18 --- /dev/null +++ b/app/controllers/yiff_rest/state_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module YiffRest + class StateController < ApplicationController + def index + @state = APIImage.state + end + + private + + def site_domain + YiffRestRoutes::STATE_DOMAIN + end + + def site_title + "YiffyAPI - API V2 State" + end + + def assets_path + YiffMediaRoutes::DOMAIN + end + end +end diff --git a/app/controllers/yiff_rest/thumbs_controller.rb b/app/controllers/yiff_rest/thumbs_controller.rb new file mode 100644 index 0000000..e6e0d2e --- /dev/null +++ b/app/controllers/yiff_rest/thumbs_controller.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module YiffRest + class ThumbsController < ApplicationController + include ::ApplicationController::ReadonlyMethods + before_action :handle_ratelimit + before_action :validate_api_key_required + before_action :validate_thumbs_access + before_action :validate_post + before_action -> { track_usage("thumbs") } + + def show + render(json: E621Thumbnail.urls_from_post(@post)) + end + + def create + return render_error(YiffyAPIErrorCodes::THUMBS_GIF_DISABLED, error: "The gif type has been disabled.") if params[:type] == "gif" && Websites.config.e621_thumbnails_disable_gif + thumb = E621Thumbnail.find_by_post(@post, params[:type]) + if thumb.present? + return render_error(YiffyAPIErrorCodes::THUMBS_TIMEOUT, error: "Generation timed out.", expiresAt: thumb.expires_at&.to_i) if thumb.timeout? + if thumb.error? + message = "Generation encountered an error." + message += " (#{thumb.error_code})" if thumb.error_code.present? + return render_error(YiffyAPIErrorCodes::THUMBS_GENERIC_ERROR, error: message, expiresAt: thumb.expires_at&.to_i) + end + if thumb.complete? + return render(json: { + success: true, + status: "done", + url: thumb.url, + }) + end + else + thumb = E621Thumbnail.from_post(@post, params[:type], api_key_id: @apikey.id) + end + + thumb.generate! if thumb.pending? + render(status: 202, json: { + success: true, + status: "processing", + checkURL: thumb.check_url, + checkAt: (Time.now + thumb.check_time).to_i, + time: thumb.check_time, + startedAt: thumb.created_at.to_i, + }) + end + + def check + return render_error(YiffyAPIErrorCodes::THUMBS_GIF_DISABLED, error: "The gif type has been disabled.") if params[:type] == "gif" && Websites.config.e621_thumbnails_disable_gif + thumb = E621Thumbnail.find_by_post(@post, params[:type]) + return render_error(YiffyAPIErrorCodes::THUMBS_CHECK_NOT_FOUND, error: "Not Found") if thumb.blank? + return render_error(YiffyAPIErrorCodes::THUMBS_TIMEOUT, error: "Generation timed out.", expiresAt: thumb.expires_at&.to_i) if thumb.timeout? + if thumb.error? + message = "Generation encountered an error." + message += " (#{thumb.error_code})" if thumb.error_code.present? + return render_error(YiffyAPIErrorCodes::THUMBS_GENERIC_ERROR, error: message, expiresAt: thumb.expires_at&.to_i) + end + if thumb.complete? + return render(status: 201, json: { + success: true, + status: "done", + url: thumb.url, + }) + end + + render(status: 200, json: { + success: true, + status: "processing", + checkURL: thumb.check_url, + checkAt: (Time.now + thumb.check_time).to_i, + time: thumb.check_time, + startedAt: thumb.created_at.to_i, + }) + end + + private + + def validate_post + @md5 = params[:id].to_s + @post = nil + if /^\d+$/.match?(@md5) + @post = Requests::E621.get_post(id: params[:id]) + return render_error(YiffyAPIErrorCodes::THUMBS_INVALID_POST_ID, error: "Invalid Post ID") if @post.nil? + @md5 = post["file"]["md5"] + end + + @post = Requests::E621.get_post(md5: params[:id]) if /[a-f\d]{32}/i.match?(@md5) + render_error(YiffyAPIErrorCodes::THUMBS_INVALID_MD5, error: "Invalid Post") if @post.nil? + end + end +end diff --git a/app/controllers/yiff_rocks/home_controller.rb b/app/controllers/yiff_rocks/home_controller.rb new file mode 100644 index 0000000..9e5002c --- /dev/null +++ b/app/controllers/yiff_rocks/home_controller.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module YiffRocks + class HomeController < YiffRest::ApplicationController + include ::ApplicationController::ReadonlyMethods + before_action :handle_ratelimit, only: %i[create update destroy] + before_action :validate_api_key_required, only: %i[create update destroy] + before_action :validate_shortener_access, only: %i[create update destroy] + before_action :load_shorturl, only: %i[update destroy] + before_action :ensure_edit_access, only: %i[update destroy] + before_action -> { track_usage("shortener") }, except: %i[index] + wrap_parameters format: :json, name: "short_url" + + def index + end + + def show + code = params[:code] + preview = code.ends_with?("+") + code = code[..-2] if preview + if params[:format] == "json" + return if handle_ratelimit && performed? + return if validate_api_key_required && performed? + return if validate_shortener_access && performed? + @short = ShortUrl.find_by(code: code) + return render_error(YiffyAPIErrorCodes::SHORTENER_NOT_FOUND, error: "A short url with that code was not found.") if @short.nil? + return render(json: { + success: true, + data: @short, + }) + end + @short = ShortUrl.find_by(code: code) + return render(plain: "Unknown short url code.", status: 404) if @short.nil? + return render("preview") if preview + redirect_to(@short.url, allow_other_host: true) + end + + def create + @short_url = ShortUrl.create(creator_ua: request.headers["User-Agent"], **short_url_params(:create)) + + handle_errors + return if performed? + + render(json: @short_url) + end + + def update + @short_url.update(short_url_params(:update)) + + handle_errors + return if performed? + + return render_error(YiffyAPIErrorCodes::SHORTENER_NO_CHANGES, message: "No changes were detected.") unless @short_url.saved_change_to_url? || @short_url.saved_change_to_creator_name? + + render(json: @short_url) + end + + def destroy + @short_url.destroy + end + + private + + def site_domain + YiffRocksRoutes::DOMAIN + end + + def site_title + "Yiff Rocks - URL Shortener" + end + + def assets_path + YiffMediaRoutes::DOMAIN + end + + def short_url_params(context = nil) + permitted_params = %i[url credit creator_name] + permitted_params += %i[code] if context == :create + params.require(:short_url).permit(permitted_params) + end + + def handle_errors + if @short_url.errors.any? + @short_url.errors.full_messages.each do |error| + return render_error(YiffyAPIErrorCodes::SHORTENER_CODE_TOO_LONG, message: error, errors: @short_url.errors.full_messages) if error.to_s =~ /Code is too long/ + return render_error(YiffyAPIErrorCodes::SHORTENER_INVALID_CODE, message: error, errors: @short_url.errors.full_messages) if error.to_s =~ /Code is invalid/ + return render_error(YiffyAPIErrorCodes::SHORTENER_INVALID_URL, message: error, errors: @short_url.errors.full_messages) if error.to_s =~ /Url is invalid/ + return render_error(YiffyAPIErrorCodes::SHORTENER_URL_TOO_LONG, message: error, errors: @short_url.errors.full_messages) if error.to_s =~ /Url is too long/ + return render_error(YiffyAPIErrorCodes::SHORTENER_CREDIT_TOO_LONG, message: error, errors: @short_url.errors.full_messages) if error.to_s =~ /Creator name is too long/ + return render_error(YiffyAPIErrorCodes::SHORTENER_CODE_IN_USE, message: error, errors: @short_url.errors.full_messages) if error.to_s =~ /Code has already been taken/ + @short_url.save! # Make further failed validations raise + end + end + end + + def load_shorturl + @short_url = ShortUrl.find_by(code: params[:code]) + render_error(YiffyAPIErrorCodes::SHORTENER_NOT_FOUND, message: "A short url with that code was not found.") if @short_url.nil? + end + + def ensure_edit_access + management_code = params.fetch(:short_url, {}).permit(:managementCode)[:managementCode] + params[:short_url].delete(:managementCode) if management_code.present? + + if CurrentUser.user.id != @short_url.creator_id + return render_error(YiffyAPIErrorCodes::SHORTENER_NO_MANAGEMENT_CODE, message: "That short url cannot be edited by you.") if @short_url.management_code.blank? || management_code.blank? + render_error(YiffyAPIErrorCodes::SHORTENER_MANAGEMENT_CODE_MISMATCH, message: "That management code does not match this short url.") if management_code != @short_url.management_code + end + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..60c6bd6 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module ApplicationHelper + def page_title + return content_for(:page_title) if content_for?(:page_title) + return site_title if defined?(site_title) + "Unknown" + end + + def domain + return content_for(:domain) if content_for?(:domain) + return site_domain if defined?(site_domain) + "unknown" + end + + def color + return content_for(:color) if content_for?(:color) + return site_color if defined?(site_color) + "#2C2F33" + end + + def with_params(**) + params.except(:controller, :action, :index).merge(**).permit! + end + + def without_search(*args) + p = params.except(:controller, :action, :index, :search) + return p.permit! if args.empty? + p.merge(search: params.fetch(:search, {}).except(*args)).permit! + end + + def if_active(css, path:) + return css if request.path == path + "" + end +end diff --git a/app/javascript/@types/global.d.ts b/app/javascript/@types/global.d.ts new file mode 100644 index 0000000..6d60ccc --- /dev/null +++ b/app/javascript/@types/global.d.ts @@ -0,0 +1,6 @@ + +interface Window { + Stimulus: import("@hotwired/stimulus").Application + jQuery: JQueryStatic; + $: JQueryStatic; +} diff --git a/app/javascript/@types/node/index.d.ts b/app/javascript/@types/node/index.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/app/javascript/@types/node/index.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/app/javascript/application.ts b/app/javascript/application.ts new file mode 100644 index 0000000..63a5845 --- /dev/null +++ b/app/javascript/application.ts @@ -0,0 +1,14 @@ +/// +// Entry point for the build script in your package.json +import "@hotwired/turbo-rails"; +import "bootstrap"; +import "bootstrap/dist/css/bootstrap.min.css"; +import jQuery from "jquery"; +window.jQuery = window.$ = jQuery; +import Rails from "@rails/ujs"; +import "jquery-ujs"; +import "./ujs-prompt.js" +import "./controllers/index.js"; +import "./styles/application.scss"; + +Rails.start(); diff --git a/app/javascript/controllers/application.ts b/app/javascript/controllers/application.ts new file mode 100644 index 0000000..b13ce7c --- /dev/null +++ b/app/javascript/controllers/application.ts @@ -0,0 +1,11 @@ +import {Application} from "@hotwired/stimulus"; +import Notice from "./notice.js"; + +const application = Application.start(); + +// Configure Stimulus development experience +application.debug = false; +window.Stimulus = application; +application.register("notice", Notice); + +export { application }; diff --git a/app/javascript/controllers/e621_ws/status_controller.ts b/app/javascript/controllers/e621_ws/status_controller.ts new file mode 100644 index 0000000..78857f0 --- /dev/null +++ b/app/javascript/controllers/e621_ws/status_controller.ts @@ -0,0 +1,19 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="e621-ws--status" +export default class extends Controller { + static override targets = ["rawTime", "time"]; + declare rawTimeTarget: HTMLElement; + declare timeTarget: HTMLElement; + + + + override connect() { + const fmt = new Intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "long", + hour12: false + }); + this.timeTarget.innerHTML = fmt.format(new Date(this.rawTimeTarget.innerText)); + } +} diff --git a/app/javascript/controllers/index.ts b/app/javascript/controllers/index.ts new file mode 100644 index 0000000..9117a96 --- /dev/null +++ b/app/javascript/controllers/index.ts @@ -0,0 +1,8 @@ +// This file is auto-generated by ./bin/rails stimulus:manifest:update +// Run that command whenever you add a new controller or create them with +// ./bin/rails generate stimulus controllerName + +import "./application.js"; + +// import E621WsStatusController from "./e621_ws/status_controller.js"; +// application.register("e621-ws--status", E621WsStatusController); diff --git a/app/javascript/controllers/maidboye_cafe/home_controller.ts b/app/javascript/controllers/maidboye_cafe/home_controller.ts new file mode 100644 index 0000000..53f4f93 --- /dev/null +++ b/app/javascript/controllers/maidboye_cafe/home_controller.ts @@ -0,0 +1,18 @@ +import { Controller } from "@hotwired/stimulus" + +interface KofiWidgetOverlay { + draw: (pid: string, config: object) => void; +} + +declare const kofiWidgetOverlay: KofiWidgetOverlay; + +export default class extends Controller { + override connect() { + kofiWidgetOverlay.draw("maidboye", { + "type": "floating-chat", + "floating-chat.donateButton.text": "Donate", + "floating-chat.donateButton.background-color": "#00bfa5", + "floating-chat.donateButton.text-color": "#fff" + }); + } +} diff --git a/app/javascript/controllers/notice.ts b/app/javascript/controllers/notice.ts new file mode 100644 index 0000000..1d08f30 --- /dev/null +++ b/app/javascript/controllers/notice.ts @@ -0,0 +1,11 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static override targets = ["notice"]; + declare noticeTarget: HTMLElement; + + close(event: KeyboardEvent) { + event.preventDefault(); + $(this.noticeTarget).fadeOut("fast"); + } +} diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss new file mode 100644 index 0000000..6ef3cb8 --- /dev/null +++ b/app/javascript/styles/application.scss @@ -0,0 +1,49 @@ +@import "butts-are.cool/home"; +@import "e621.ws/status"; +@import "furry.cool/home"; +@import "maidboye.cafe/home"; +@import "oceanic.ws/home"; +@import "yiff.media/home"; +@import "yiff.rest/home"; +@import "yiff.rocks/home"; + +div#notice { + padding: 0.25em; + position: fixed; + top: 1rem; + left: 25%; + width: 50%; + z-index: 100; + color: #FFF; + background-color: darkgreen; + border: 1px solid #333333; + + &.ui-state-error { + background-color: maroon; + } + + &.ui-corner-all { + border-radius: 3px; + } + + #close-notice-link { + display: flex; + float: right; + padding: 0 0.75em; + text-decoration: none; + color: #B4C7D9; + } +} + +body.c-application { + background-color: #2C2F33; + color: #FFFDD0; + + a { + color: #FFF; + } + + div#page { + text-align: center; + } +} diff --git a/app/javascript/styles/butts-are.cool/home.scss b/app/javascript/styles/butts-are.cool/home.scss new file mode 100644 index 0000000..9a1deb9 --- /dev/null +++ b/app/javascript/styles/butts-are.cool/home.scss @@ -0,0 +1,11 @@ +body.s-butts-are-cool, body.s-balls-butts-are-cool, body.s-cocks-butts-are-cool, body.s-knots-butts-are-cool, body.s-sheaths-butts-are-cool { + background-color: #2C2F33; + + * { + color: #FFFFEA; + } + + #page { + text-align: center; + } +} diff --git a/app/javascript/styles/e621.ws/status.scss b/app/javascript/styles/e621.ws/status.scss new file mode 100644 index 0000000..ac9cd66 --- /dev/null +++ b/app/javascript/styles/e621.ws/status.scss @@ -0,0 +1,84 @@ +body.s-status-e621-ws { + background-color: #012E57; + text-align: center; + + #c-e621-ws-status { + h1 { + color: #B8BEC4; + margin-top: 30vh; + } + + h2, h3 { + color: #B8BEC4; + margin-top: 5vh; + } + + span#state.up, span#status.success, span#available.success { + color: #008000; + } + + span#state.down, span#status.error, span#available.error { + color: #FF0000; + } + + span#state.partially.down, span#status.partially.down { + color: #FFA500; + } + + a { + color: #E9F2FA; + } + } + + #c-e621-ws-status-webhooks { + * { + font-family: "Raleway", sans-serif; + line-height: 1.3; + } + + h1, h2 { + color: #B8BEC4; + margin-top: 2vh; + } + + a { + color: #E9F2FA; + } + + p { + color: #B8BEC4; + margin-left: 7.5vw; + margin-right: 7.5vw; + font-size: 18px; + } + + .btn-authorize { + color: #E9F2FA; + } + + @media screen and (max-width: 900px) { + div#fuck-off { + text-align: center !important; + } + + p { + margin-left: 2.5vw; + margin-right: 2.5vw; + font-size: 13px; + } + } + + @media screen and (min-width: 901px) { + div#fuck-off { + position: absolute; + top: 7px; + left: 8px; + } + } + + div#auth-buttons a { + display: inline-block; + vertical-align: top; + } + } +} diff --git a/app/javascript/styles/furry.cool/home.scss b/app/javascript/styles/furry.cool/home.scss new file mode 100644 index 0000000..885784e --- /dev/null +++ b/app/javascript/styles/furry.cool/home.scss @@ -0,0 +1,36 @@ +body.s-furry-cool { + height: 100%; + margin: 0; + padding: 0; + width: 100%; + background-color: #2C2F33; + display: table; + font-family: "Montserrat", sans-serif; + + #c-home { + #a-index { + #page { + text-align: center; + + h1, h2, a { + color: #2C9EDA; + } + + span.spacer { + margin-left: 0.2rem; + margin-right: 0.2rem; + } + + a:visited { + color: #56B9ED; + } + + img.logo { + max-width: 20%; + margin-bottom: 0.5rem; + margin-top: 5rem; + } + } + } + } +} diff --git a/app/javascript/styles/maidboye.cafe/home.scss b/app/javascript/styles/maidboye.cafe/home.scss new file mode 100644 index 0000000..624798f --- /dev/null +++ b/app/javascript/styles/maidboye.cafe/home.scss @@ -0,0 +1,36 @@ +body.s-maidboye-cafe { + background-color: #2C2F33; + + &.c-home { + &.a-index { + text-align: center; + + * { color: #FFFDD0; } + + div#right { + position: fixed; + bottom: 5px; + right: 5px; + } + + a { color: #FFF; } + + div#left a { + text-decoration: none; + } + + div#icon img { + width: 30%; + height: 30%; + border-radius: 135px; + padding: 20px; + } + } + + &.a-privacy { + * { + color: #56B9ED; + } + } + } +} diff --git a/app/javascript/styles/oceanic.ws/home.scss b/app/javascript/styles/oceanic.ws/home.scss new file mode 100644 index 0000000..3b07e0c --- /dev/null +++ b/app/javascript/styles/oceanic.ws/home.scss @@ -0,0 +1,42 @@ +body.s-oceanic-ws { + position: relative; + height: 100vh; + background-color: #2C2F33; + font-family: "Montserrat", sans-serif; + text-align: center; + + h1, h2, a { + color: #2C9EDA; + } + + div#page { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 50%; + height: 50%; + } + + span.spacer { + margin-left: 0.2rem; + margin-right: 0.2rem; + } + + a:visited { + color: #56B9ED; + } + + &.c-home.a-index { + img.logo { + max-width: 20%; + margin-bottom: 3rem; + } + } + + &.c-docs.a-index { + a:visited { + color: #56B9ED; + } + } +} diff --git a/app/javascript/styles/yiff.media/home.scss b/app/javascript/styles/yiff.media/home.scss new file mode 100644 index 0000000..7a05281 --- /dev/null +++ b/app/javascript/styles/yiff.media/home.scss @@ -0,0 +1,32 @@ +body.s-yiff-media { + background-color: #2c2f33; + color: #FFFDD0; + + a { + color: #FFF; + } + + .hide { + color: #2c2f33; + } + + h1, h2 { + color: #0ff; + } + + div#buttons button.stop { + position: fixed; + left: 5px; + bottom: 5px; + } + + div#buttons button.start { + position: fixed; + right: 5px; + bottom: 5px; + } + + button.hidden { + display: none; + } +} diff --git a/app/javascript/styles/yiff.rest/apikeys.scss b/app/javascript/styles/yiff.rest/apikeys.scss new file mode 100644 index 0000000..6e2bc2f --- /dev/null +++ b/app/javascript/styles/yiff.rest/apikeys.scss @@ -0,0 +1,17 @@ +body.s-yiff-rest.c-apikeys { + background-color: #2C2F33; + color: #FFFDD0; + + div.apikey-super { + border: 3px solid goldenrod; + } + + div.apikey-disabled { + background-color: #5b0000 !important; + } + + div.apikey-inactive { + background-color: #00437d !important; + } +} +// diff --git a/app/javascript/styles/yiff.rest/discord.scss b/app/javascript/styles/yiff.rest/discord.scss new file mode 100644 index 0000000..a1bea9c --- /dev/null +++ b/app/javascript/styles/yiff.rest/discord.scss @@ -0,0 +1,8 @@ +body.s-discord-yiff-rest { + background-color: #2C2F33; + color: #FFFDD0; + + a { + color: #FFF; + } +} diff --git a/app/javascript/styles/yiff.rest/home.scss b/app/javascript/styles/yiff.rest/home.scss new file mode 100644 index 0000000..461a08b --- /dev/null +++ b/app/javascript/styles/yiff.rest/home.scss @@ -0,0 +1,33 @@ +@import "apikeys"; +@import "discord"; +@import "state"; + +body.s-yiff-rest { + background-color: #2C2F33; + color: #FFFDD0; + + a { + color: #FFF; + } + + div#left { + position: fixed; + bottom: 5px; + left: 5px; + } + + div#left a { + text-decoration: none; + } + + div#page { + text-align: center; + } + + #logo { + text-align: center; + img { + width: 25%; + } + } +} diff --git a/app/javascript/styles/yiff.rest/state.scss b/app/javascript/styles/yiff.rest/state.scss new file mode 100644 index 0000000..c2ab23f --- /dev/null +++ b/app/javascript/styles/yiff.rest/state.scss @@ -0,0 +1,40 @@ +body.s-state-yiff-rest { + background-color: #2C2F33; + color: #2A2A1D; + + table, + thead, + th, + tbody, + tr, + td { + text-align: center; + } + + table { + width: 100%; + } + + table, + th, + td { + border: 1px solid black; + border-collapse: collapse; + } + + .red { + background-color: #f00; + } + + .green { + background-color: #008000; + } + + .yellow { + background-color: #FF0; + } + + .total { + background-color: plum; + } +} diff --git a/app/javascript/styles/yiff.rocks/home.scss b/app/javascript/styles/yiff.rocks/home.scss new file mode 100644 index 0000000..9ef60aa --- /dev/null +++ b/app/javascript/styles/yiff.rocks/home.scss @@ -0,0 +1,19 @@ +body.s-yiff-rocks { + background-color: #2C2F33; + color: #FFFDD0; + + a { + color: #FFF; + } + + div#page { + text-align: center; + } + + #logo { + text-align: center; + img { + width: 25%; + } + } +} diff --git a/app/javascript/ujs-prompt.ts b/app/javascript/ujs-prompt.ts new file mode 100644 index 0000000..5ffa4c8 --- /dev/null +++ b/app/javascript/ujs-prompt.ts @@ -0,0 +1,31 @@ +/// +import $ from "jquery"; + +function ujsPrompt(el: HTMLElement) { + const link = $(el); + + const href = link.attr("href"), + method = link.data("method"), + csrfToken = $("meta[name=csrf-token]").attr("content"), + csrfParam = $("meta[name=csrf-param]").attr("content"), + form = $(`
`), + msg = link.data("prompt"), + value = prompt(msg), + paramName = link.data("param-name") || "prompt-value", + promptParamInput = ``; + + let metadataInput = `` + + if (csrfParam !== undefined && csrfToken !== undefined) { + metadataInput += ``; + } + + form.hide().append(metadataInput).append(promptParamInput).appendTo("body"); + form.submit(); +} + +$("a[data-prompt]").on("click", function(e) { + e.stopImmediatePropagation(); + e.preventDefault(); + ujsPrompt(this); +}); diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..92e0e23 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/jobs/build_oceanic_docs_job.rb b/app/jobs/build_oceanic_docs_job.rb new file mode 100644 index 0000000..87d742f --- /dev/null +++ b/app/jobs/build_oceanic_docs_job.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class BuildOceanicDocsJob < ApplicationJob + queue_as :low_priority + GITHUB_REPO = "https://github.com/OceanicJS/Oceanic" + + def perform(_type, payload) + name = payload[:ref].split("/").slice(2..).join("/") + return if name.start_with?("dependabot") + Dir.mktmpdir(["oceanic-github-", name]) do |dir| + Dir.chdir(dir) do + system("git clone #{GITHUB_REPO} .", exception: true) + system("git fetch -u origin #{name}:#{name}", exception: true) + system("git checkout #{name}", exception: true) + FileUtils.rm("#{dir}/.npmrc") + system("npx pnpm install --ignore-scripts --frozen-lockfile", exception: true) + system("npx pnpm run test:docs", exception: true) + FileUtils.cp_r("#{dir}/docs", "/data/oceanic-docs/#{name}") + Cache.redis.sadd("oceanic:versions", name) + end + end + end +end diff --git a/app/jobs/e621_thumbnail_error_cleanup_job.rb b/app/jobs/e621_thumbnail_error_cleanup_job.rb new file mode 100644 index 0000000..802c5a8 --- /dev/null +++ b/app/jobs/e621_thumbnail_error_cleanup_job.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class E621ThumbnailErrorCleanupJob < ApplicationJob + queue_as :default + + def perform(entry) + raise(StandardError, "Attempted to call E621ThumbnailErrorCleanupJob with a non-error status entry: #{entry.status}") unless entry.error? || entry.timeout? + entry.destroy + end +end diff --git a/app/jobs/e621_thumbnail_job.rb b/app/jobs/e621_thumbnail_job.rb new file mode 100644 index 0000000..8984fd2 --- /dev/null +++ b/app/jobs/e621_thumbnail_job.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class E621ThumbnailJob < ApplicationJob + queue_as :default + + def perform(entry) + infile = Tempfile.new(%W[e621-thumbnail-#{entry.stripped_md5} .webm]) + outfile = Tempfile.new(%W[e621-thumbnail-#{entry.stripped_md5} .#{entry.filetype}]) + cutfile = Tempfile.new(%W[e621-thumbnail-#{entry.stripped_md5} .cut.webm]) + palettefile = Tempfile.new(%W[e621-thumbnail-#{entry.stripped_md5} .palette.png]) + Timeout.timeout(1000) do + infile.binmode + HTTParty.get("https://static1.e621.net/data/#{entry.stripped_md5[0..1]}/#{entry.stripped_md5[2..3]}/#{entry.stripped_md5}.webm", stream_body: true) do |fragment| + infile.write(fragment) + end + duration = `ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 #{infile.path}`.to_f + offset = duration > 10 ? rand(0..(duration - 10)) : duration / 2 + if entry.filetype == "gif" + `ffmpeg -y -i #{infile.path} -ss #{offset} -t 3 -c:v copy -an #{cutfile.path} 1>&2` + `ffmpeg -y -i #{cutfile.path} -filter_complex "[0:v] palettegen" #{palettefile.path} 1>&2` + `ffmpeg -y -i #{cutfile.path} -i #{palettefile.path} -filter_complex "[0:v][1:v] paletteuse" #{outfile.path} 1>&2` + else + `ffmpeg -y -i #{infile.path} -ss #{offset} -vframes 1 #{outfile.path} 1>&2` + end + ImageOptim.new.optimize_image!(outfile.path) + StorageManager::E621Thumbnails.upload("#{entry.stripped_md5}.#{entry.filetype}", outfile.read) + entry.update!(status: "complete") + execute_webhook(entry, title: "Thumbnail Generated (#{entry.filetype})") + end + rescue Timeout::Error + entry.update!(status: "timeout") + execute_webhook(entry, title: "Thumbnail Generation Timed Out (#{entry.filetype})", error: true) + rescue StandardError => e + code = Requests::Pastebin.default.create(title: "E621 Thumbnail Generation Error (#{entry.stripped_md5})", content: "#{e}\n#{e.backtrace&.join("\n") || ''}") + entry.update!(status: "error", error_code: code) + execute_webhook(entry, title: "Thumbnail Generation Errored (#{entry.filetype})", body: "Backtrace: https://passtebin.com/#{code}", error: true) + raise(e) + ensure + [infile, outfile, palettefile, cutfile].each do |file| + file&.close + file&.unlink + end + end + + def execute_webhook(entry, title:, body: "", error: false) + if error + entry.update(expires_at: 5.minutes.from_now) + E621ThumbnailErrorCleanupJob.set(wait: 5.minutes).perform_later(entry) + end + thumb = {} + if entry.complete? + thumb = { + thumbnail: { + url: entry.url, + }, + } + end + Websites.config.e621_thumbnails_webhook.execute({ + embeds: [ + { + title: title, + description: <<~DESC, + Key: ##{entry.api_key_id} (`#{entry.api_key.application_name}`) + Post: [##{entry.post_id}](https://e621.net/posts/#{entry.post_id}) + #{body} + DESC + color: error ? 0xDC143C : 0x008000, + timestamp: Time.now.iso8601, + **thumb, + }, + ], + }) + end +end diff --git a/app/logical/cache.rb b/app/logical/cache.rb new file mode 100644 index 0000000..f4d3b24 --- /dev/null +++ b/app/logical/cache.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class Cache + def self.read_multi(keys, prefix) + sanitized_key_to_key_hash = keys.index_by { |key| "#{prefix}:#{key}" } + + sanitized_keys = sanitized_key_to_key_hash.keys + sanitized_key_to_value_hash = Rails.cache.read_multi(*sanitized_keys) + + sanitized_key_to_value_hash.transform_keys(&sanitized_key_to_key_hash) + end + + def self.fetch(key, expires_in: nil, &) + Rails.cache.fetch(key, expires_in: expires_in, &) + end + + def self.write(key, value, expires_in: nil) + Rails.cache.write(key, value, expires_in: expires_in) + end + + def self.delete(key) + Rails.cache.delete(key) + end + + def self.clear + Rails.cache.clear + end + + def self.redis + # Using a shared variable like this here is OK + # since unicorn spawns a new process for each worker + @redis ||= Redis.new(url: Websites.config.redis_url) + end +end diff --git a/app/logical/current_user.rb b/app/logical/current_user.rb new file mode 100644 index 0000000..ebe337f --- /dev/null +++ b/app/logical/current_user.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class CurrentUser + def self.scoped(user, ip_addr = "127.0.0.1") + old_user = self.user + old_ip_addr = self.ip_addr + + self.user = user + self.ip_addr = ip_addr + + begin + yield + ensure + self.user = old_user + self.ip_addr = old_ip_addr + end + end + + def self.as_system(&) + scoped(::APIUser.system, &) + end + + def self.user=(user) + RequestStore[:current_user] = user + end + + def self.ip_addr=(ip_addr) + RequestStore[:current_ip_addr] = ip_addr + end + + def self.user + RequestStore[:current_user] + end + + def self.ip_addr + RequestStore[:current_ip_addr] + end + + def self.id + user&.id + end + + def self.name + user&.name + end + + def self.method_missing(method, *, &) + user.__send__(method, *, &) + end +end diff --git a/app/logical/discord_constants.rb b/app/logical/discord_constants.rb new file mode 100644 index 0000000..3767552 --- /dev/null +++ b/app/logical/discord_constants.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +module DiscordConstants + class UserFlags + attr_reader :flag, :title + + def initialize(flag, title) + @flag = flag + @title = title + end + + def include?(flag) + (@flag & flag) == @flag + end + + STAFF = new(1 << 0, "Discord Employee") + PARTNER = new(1 << 1, "Discord Partner") + HYPESQUAD = new(1 << 2, "Hypesquad Events") + BUG_HUNTER_LEVEL_1 = new(1 << 3, "Bug Hunter Level 1") + MFA_SMS = new(1 << 4, "2FA SMS") + PREMIUM_PROMO_DISMISSED = new(1 << 5, "Premium Promotion Dismissed") + HYPESQUAD_BRAVERY = new(1 << 6, "House of Bravery") + HYPESQUAD_BRILLIANCE = new(1 << 7, "House of Brilliance") + HYPESQUAD_BALANCE = new(1 << 8, "House of Balance") + EARLY_SUPPORTER = new(1 << 9, "Early Supporter") + PSEUDO_TEAM_USER = new(1 << 10, "Team User") + INTERNAL_APPLICATION = new(1 << 11, "Internal Application") + SYSTEM = new(1 << 12, "System") + HAS_UNREAD_URGENT_MESSAGES = new(1 << 13, "Has Unread Urgent Messages") + BUG_HUNTER_LEVEL_2 = new(1 << 14, "Bug Hunter Level 2") + # 15 + VERIFIED_BOT = new(1 << 16, "Verified Bot") + VERIFIED_DEVELOPER = new(1 << 17, "Early Verified Bot Developer") + CERTIFIED_MODERATOR = new(1 << 18, "Certified Moderator") + BOT_HTTP_INTERACTIONS = new(1 << 19, "Bot HTTP Interactions") + SPAMMER = new(1 << 20, "Spammer") + # 21 + ACTIVE_DEVELOPER = new(1 << 22, "Active Developer") + # 23-32 + HIGH_GLOBAL_RATE_LIMIT = new(1 << 33, "High Global Rate Limit") + DELETED = new(1 << 34, "Deleted") + DISABLED_SUSPICIOUS_ACTIVITY = new(1 << 35, "Disabled Suspicious Activity") + SELF_DELETED = new(1 << 36, "Self Deleted") + PREMIUM_DISCRIMINATOR = new(1 << 37, "Premium Discriminator") + USED_DESKTOP_CLIENT = new(1 << 38, "Used Desktop Client") + USED_WEB_CLIENT = new(1 << 39, "Used Web Client") + USED_MOBILE_CLIENT = new(1 << 40, "Used Mobile Client") + DISABLED = new(1 << 41, "Disabled") + # 42 + VERIFIED_EMAIL = new(1 << 43, "Verified Email") + QUARANTINED = new(1 << 44, "Quarantined") + # 45-49 + COLLABORATOR = new(1 << 50, "Collaborator") + RESTRICTED_COLLABORATOR = new(1 << 51, "Restricted Collaborator") + + def self.parse_flags(flags) + flags = flags.to_i + return [] if flags == 0 + constants.filter { |c| const_get(c).include?(flags) }.map { |c| const_get(c) } + end + end + + module InteractionTypes + PING = 1 + APPLICATION_COMMAND = 2 + MESSAGE_COMPONENT = 3 + APPLICATION_COMMAND_AUTOCOMPLETE = 4 + MODAL_SUBMIT = 5 + end + + module InteractionResponseTypes + PONG = 1 + CHANNEL_MESSAGE_WITH_SOURCE = 4 + DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5 + DEFERRED_UPDATE_MESSAGE = 6 + UPDATE_MESSAGE = 7 + APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8 + MODAL = 9 + PREMIUM_REQUIRED = 10 + end + + module ApplicationCommandTypes + CHAT_INPUT = 1 + USER = 2 + MESSAGE = 3 + end + + module ApplicationCommandOptionTypes + SUB_COMMAND = 1 + SUB_COMMAND_GROUP = 2 + STRING = 3 + INTEGER = 4 + BOOLEAN = 5 + USER = 6 + CHANNEL = 7 + ROLE = 8 + MENTIONABLE = 9 + NUMBER = 10 + ATTACHMENT = 11 + end + + module MessageFlags + CROSSPOSTED = 1 << 0 + IS_CROSSPOST = 1 << 1 + SUPPRESS_EMBEDS = 1 << 2 + SOURCE_MESSAGE_DELETED = 1 << 3 + URGENT = 1 << 4 + HAS_THREAD = 1 << 5 + EPHEMERAL = 1 << 6 + LOADING = 1 << 7 + FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8 + SUPPRESS_NOTIFICATIONS = 1 << 12 + IS_VOICE_MESSAGE = 1 << 13 + end + + module ComponentTypes + ACTION_ROW = 1 + BUTTON = 2 + STRING_SELECT = 3 + TEXT_INPUT = 4 + USER_SELECT = 5 + ROLE_SELECT = 6 + MENTIONABLE_SELECT = 7 + CHANNEL_SELECT = 8 + end + + module ButtonStyles + PRIMARY = 1 # Blurple + SECONDARY = 2 # Grey + SUCCESS = 3 # Green + DANGER = 4 # Red + LINK = 5 # Grey (Link) + end + + module TextInputStyles + SHORT = 1 + PARAGRAPH = 2 + end +end diff --git a/app/logical/discord_interactions.rb b/app/logical/discord_interactions.rb new file mode 100644 index 0000000..6dcbe03 --- /dev/null +++ b/app/logical/discord_interactions.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module DiscordInteractions +end diff --git a/app/logical/discord_interactions/commands.rb b/app/logical/discord_interactions/commands.rb new file mode 100644 index 0000000..e1d8106 --- /dev/null +++ b/app/logical/discord_interactions/commands.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +module DiscordInteractions + module Commands + module_function + + def commands + [ + { + description: "Manage your API keys", + name: "apikey", + type: DiscordConstants::ApplicationCommandTypes::CHAT_INPUT, + options: [ + { + type: DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND, + name: "create", + description: "Create an API key", + options: [], + }, + { + type: DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND, + name: "delete", + description: "Delete an API key", + options: [ + { + type: DiscordConstants::ApplicationCommandOptionTypes::STRING, + name: "key", + description: "The API key to delete", + autocomplete: true, + required: true, + choices: [], + }, + ], + }, + { + type: DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND, + name: "list", + description: "List your API keys", + options: [], + }, + ], + }, + { + description: "Manage API keys (developer only)", + name: "apidev", + type: DiscordConstants::ApplicationCommandTypes::CHAT_INPUT, + options: [ + { + type: DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND, + name: "list", + description: "List a user's api keys.", + options: [ + { + type: DiscordConstants::ApplicationCommandOptionTypes::USER, + name: "user", + description: "The user to list the api keys of.", + required: true, + }, + ], + }, + { + type: DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND, + name: "disable", + description: "Disable an api key.", + options: [ + { + type: DiscordConstants::ApplicationCommandOptionTypes::STRING, + name: "key", + description: "The api key to disable.", + choices: [], + required: true, + }, + { + type: DiscordConstants::ApplicationCommandOptionTypes::STRING, + name: "reason", + description: "The reason for deactivating the key.", + choices: [], + }, + ], + }, + { + type: DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND, + name: "enable", + description: "Enable an api key.", + options: [ + { + type: DiscordConstants::ApplicationCommandOptionTypes::STRING, + name: "key", + description: "The api key to enable.", + choices: [], + required: true, + }, + ], + }, + { + type: DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND, + name: "stats", + description: "Get the stats of an api key.", + options: [ + { + type: DiscordConstants::ApplicationCommandOptionTypes::STRING, + name: "key", + description: "The api key to get the stats of.", + choices: [], + required: false, + }, + { + type: DiscordConstants::ApplicationCommandOptionTypes::STRING, + name: "ip", + description: "The ip to get the stats of.", + choices: [], + required: false, + }, + ], + }, + ], + }, + ] + end + + def register + cmd = Cache.redis.get("discord-commands") + return false if cmd.present? && cmd == commands.to_json + if Rails.env.development? + Requests::DiscordGuildCommands.default.update(commands) + else + Requests::DiscordGlobalCommands.default.update(commands) + end + Cache.redis.set("discord-commands", commands.to_json) + true + end + end +end diff --git a/app/logical/discord_interactions/interaction_options.rb b/app/logical/discord_interactions/interaction_options.rb new file mode 100644 index 0000000..7a15512 --- /dev/null +++ b/app/logical/discord_interactions/interaction_options.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +module DiscordInteractions + class InteractionOptions + class MissingOptionError < StandardError; end + class MissingResolvedError < StandardError; end + attr_reader :options, :resolved + + def initialize(options, resolved) + @options = options + @resolved = resolved + end + + def get_attachment(name, required: false) + raise(MissingResolvedError, "Attempted to use get_attachment with nil resolved.") if resolved.blank? + value = get_attachment_option(name, required: required).try(:[], "value") + return nil if value.blank? + res = resolved["attachments"].try(:[], value) + raise(MissingOptionError, "Attachment not present for required option: #{name}") if res.blank? && required + res + end + + def get_attachment_option(name, required: false) + get_option(name, required, DiscordConstants::ApplicationCommandOptionTypes::ATTACHMENT) + end + + def get_boolean(name, required: false) + get_boolean_option(name, required: required).try(:[], "value") + end + + def get_boolean_option(name, required: false) + get_option(name, required, DiscordConstants::ApplicationCommandOptionTypes::BOOLEAN) + end + + def get_channel(name, required: false) + raise(MissingResolvedError, "Attempted to use get_channel with nil resolved.") if resolved.blank? + value = get_channel_option(name, required: required).try(:[], "value") + return nil if value.blank? + res = resolved["channels"].try(:[], value) + raise(MissingOptionError, "Channel not present for required option: #{name}") if res.blank? && required + res + end + + def get_channel_option(name, required: false) + get_option(name, required, DiscordConstants::ApplicationCommandOptionTypes::CHANNEL) + end + + def get_focused_option(required: false) + opt = get_options.find { |o| o["focused"] == true } + raise(MissingOptionError, "Missing required focused option") if !opt && required + opt + end + + def get_integer(name, required: false) + get_integer_option(name, required: required).try(:[], "value") + end + + def get_integer_option(name, required: false) + get_option(name, required, DiscordConstants::ApplicationCommandOptionTypes::INTEGER) + end + + def get_member(name, required: false) + raise(MissingResolvedError, "Attempted to use get_member with nil resolved.") if resolved.blank? + value = get_user_option(name, required: required).try(:[], "value") + return nil if value.blank? + res = resolved["members"].try(:[], value) + raise(MissingOptionError, "Member not present for required option: #{name}") if res.blank? && required + end + + def get_mentionable(name, required: false) + raise(MissingResolvedError, "Attempted to use get_mentionable with nil resolved.") if resolved.blank? + value = get_mentionable_option(name, required: required).try(:[], "value") + return nil if value.blank? + role = resolved["roles"].try(:[], value) + user = resolved["users"].try(:[], value) + raise(MissingOptionError, "Value not present for required option: #{name}") if role.blank? && user.blank? && required + role || user + end + + def get_mentionable_option(name, required: false) + get_option(name, required, DiscordConstants::ApplicationCommandOptionTypes::MENTIONABLE) + end + + def get_number(name, required: false) + get_number_option(name, required: required).try(:[], "value") + end + + def get_number_option(name, required: false) + get_option(name, required, DiscordConstants::ApplicationCommandOptionTypes::NUMBER) + end + + def get_options # rubocop:disable Naming/AccessorMethodName + sub = get_subcommand || [] + case sub.length + when 0 + base = options + when 1 + base = options.find { |o| o["name"] == sub[0] && o["type"] == DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND }.try(:[], "options") + when 2 + base = options.find { |o| o["name"] == sub[0] && o["type"] == DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND_GROUP }.try(:[], "options").try(:find, &(->(o) { o["name"] == sub[1] && o["type"] == DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND })).try(:[], "options") + else + base = nil + end + base || [] + end + + def get_role(name, required: false) + raise(MissingResolvedError, "Attempted to use get_role with nil resolved.") if resolved.blank? + value = get_role_option(name, required: required).try(:[], "value") + return nil if value.blank? + res = resolved["roles"].try(:[], value) + raise(MissingOptionError, "Role not present for required option: #{name}") if res.blank? && required + res + end + + def get_role_option(name, required: false) + get_option(name, required, DiscordConstants::ApplicationCommandOptionTypes::ROLE) + end + + def get_string(name, required: false) + get_string_option(name, required: required).try(:[], "value") + end + + def get_string_option(name, required: false) + get_option(name, required, DiscordConstants::ApplicationCommandOptionTypes::STRING) + end + + def get_subcommand(required: false) + opt = options.find { |o| o["type"] == DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND || o["type"] == DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND_GROUP } + if opt.try(:[], "options") + if opt["options"].length == 1 && opt["type"] == DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND_GROUP + sub = opt["options"].find { |o| o["type"] == DiscordConstants::ApplicationCommandOptionTypes::SUB_COMMAND } + sub.try(:[], "options") ? [opt["name"], sub["name"]] : [opt["name"]] + else + [opt["name"]] + end + elsif required + raise(MissingOptionError, "Missing required option: SubCommand/SubCommandGroup") + end + end + + def get_user(name, required: false) + raise(MissingResolvedError, "Attempted to use get_user with nil resolved.") if resolved.blank? + value = get_user_option(name, required: required).try(:[], "value") + return nil if value.blank? + res = resolved["users"].try(:[], value) + raise(MissingOptionError, "User not present for required option: #{name}") if res.blank? && required + res + end + + def get_user_option(name, required: false) + get_option(name, required, DiscordConstants::ApplicationCommandOptionTypes::USER) + end + + private + + def get_option(name, required, type) + opt = get_options.find { |o| o["name"] == name && o["type"] == type } + raise(MissingOptionError, "Missing required option: #{name}") if !opt && required + opt + end + end +end diff --git a/app/logical/discord_interactions/yiffyapi.rb b/app/logical/discord_interactions/yiffyapi.rb new file mode 100644 index 0000000..ff8561e --- /dev/null +++ b/app/logical/discord_interactions/yiffyapi.rb @@ -0,0 +1,480 @@ +# frozen_string_literal: true + +module DiscordInteractions + class YiffyAPI + attr_reader :raw + + GREEN_TICK = APIKey::WebhookMethods::GREEN_TICK + RED_TICK = APIKey::WebhookMethods::RED_TICK + + def initialize(raw) + @raw = raw + end + + def [](key) + @raw[key.to_s] + end + + # rubocop:disable Metrics/BlockNesting + # noinspection RubyCaseWithoutElseBlockInspection + def handle + return { type: DiscordConstants::InteractionResponseTypes::PONG } if raw["type"] == DiscordConstants::InteractionTypes::PING + + if raw["guild_id"].nil? + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "My commands cannot be used in Direct Messages.", + **common, + }, + } + end + CurrentUser.user = CurrentUser.user = APIUser.find_or_create_by(id: raw["member"]["user"]["id"]) + CurrentUser.ip_addr = "0.0.0.0" + + case raw["type"] + when DiscordConstants::InteractionTypes::APPLICATION_COMMAND + options = DiscordInteractions::InteractionOptions.new(raw["data"]["options"], raw["data"]["resolved"]) + sub = options.get_subcommand + case raw["data"]["name"] + when "apikey" + keys = APIKey.for_owner(raw["member"]["user"]["id"]) + case sub[0] + when "create" + if keys.length >= 3 + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "You already have the maximum amount of api keys. Contact a developer if you believe an exception should be made.", + **common, + }, + } + end + + { + type: DiscordConstants::InteractionResponseTypes::MODAL, + data: { + custom_id: "apikey-create", + components: [ + { + type: DiscordConstants::ComponentTypes::ACTION_ROW, + components: [ + { + custom_id: "apikey-create.name", + placeholder: "My Awesome Application", + min_length: 3, + max_length: 50, + label: "Name", + style: DiscordConstants::TextInputStyles::SHORT, + type: DiscordConstants::ComponentTypes::TEXT_INPUT, + }, + ], + }, + { + type: DiscordConstants::ComponentTypes::ACTION_ROW, + components: [ + { + custom_id: "apikey-create.usage", + placeholder: "Showing porn to my friends", + min_length: 5, + max_length: 400, + label: "Usage", + style: DiscordConstants::TextInputStyles::PARAGRAPH, + type: DiscordConstants::ComponentTypes::TEXT_INPUT, + }, + ], + }, + ], + title: "Create API Key", + }, + } + when "delete" + key = keys.find { |k| k.encoded == options.get_string("key", required: true) } + if key.nil? || key.owner_id.to_s != raw["member"]["user"]["id"] + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Invalid key specified.", + **common, + }, + } + end + + if key.disabled? + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "This key has been disabled by a developer. To have this key deleted or removed, concat a developer.\n\nDisable Reason: **#{key.disabled_reason || '(None)'}**", + **common, + }, + } + end + + { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Are you sure you want to delete the key **#{key.application_name}**? This action cannot be undone.", + components: [ + { + type: DiscordConstants::ComponentTypes::ACTION_ROW, + components: [ + { + custom_id: "apikey-delete-yes.#{key.encoded}.#{raw['member']['user']['id']}", + label: "Yes", + style: DiscordConstants::ButtonStyles::SUCCESS, + type: DiscordConstants::ComponentTypes::BUTTON, + }, + { + custom_id: "apikey-delete-no.#{raw['member']['user']['id']}", + label: "No", + style: DiscordConstants::ButtonStyles::DANGER, + type: DiscordConstants::ComponentTypes::BUTTON, + }, + ], + }, + ], + **common, + }, + } + + when "list" + if keys.empty? + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "You do not have any API keys.", + **common, + }, + } + end + text = [] + + keys.each_with_index do |apikey, index| + text.push( + <<~TEXT, + #{index + 1}.) + - Key: ||#{apikey.key}|| + - Application: `#{apikey.application_name}` + - Usage: `#{apikey.usage || 'None Specified'}` + - Active: #{apikey.active? ? GREEN_TICK : RED_TICK} + - Disabled: #{apikey.disabled? ? "#{GREEN_TICK} (Reason: #{apikey.disabled_reason || 'None Specified'})" : RED_TICK} + - Unlimited: #{apikey.unlimited? ? GREEN_TICK : RED_TICK} + - Services: #{apikey.access_string} + TEXT + ) + end + + { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "We found the following api keys:\n\n#{text.join("\n\n")}", + **common, + }, + } + end + when "apidev" + unless Websites.config.yiffyapi_administrators.include?(raw["member"]["user"]["id"]) + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "You are not allowed to use that.", + **common, + }, + } + end + + case sub[0] + when "list" + user_id = options.get_user("user", required: true)["id"] + keys = APIKey.for_owner(user_id) + + if keys.empty? + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "That user does not have any API keys.", + **common, + }, + } + end + text = [] + + keys.each_with_index do |apikey, index| + text.push( + <<~TEXT, + #{index + 1}.) + - Key: ||#{apikey.key}|| + - Application: `#{apikey.application_name}` + - Usage: `#{apikey.usage || 'None Specified'}` + - Active: #{apikey.active? ? GREEN_TICK : RED_TICK} + - Disabled: #{apikey.disabled? ? "#{GREEN_TICK} (Reason: #{apikey.disabled_reason || 'None Specified'})" : RED_TICK} + - Unlimited: #{apikey.unlimited? ? GREEN_TICK : RED_TICK} + - Services: #{apikey.access_string} + TEXT + ) + end + + { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "We found the following api keys for <@#{user_id}>:\n\n#{text.join("\n\n")}", + **common, + }, + } + + when "disable" + key = options.get_string("key", required: true) + reason = options.get_string("reason") || "None Provided" + apikey = APIKey.find_by(key: key) + if apikey.nil? + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "I couldn't find that api key.", + **common, + }, + } + end + if apikey.disabled? + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "That api key is already disabled.", + **common, + }, + } + end + + apikey.update(disabled: true, disabled_reason: reason) + + { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Api key successfully disabled.", + **common, + }, + } + + when "enable" + key = options.get_string("key", required: true) + apikey = APIKey.find_by(key: key) + if apikey.nil? + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "I couldn't find that api key.", + **common, + }, + } + end + unless apikey.disabled? + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "That api key is not disabled.", + **common, + }, + } + end + + apikey.update(disabled: false, disabled_reason: nil) + + { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Api key successfully enabled.", + **common, + }, + } + when "stats" + key = options.get_string("key") + ip = options.get_string("ip") + if key + apikey = APIKey.find_by(key: key) + if apikey.nil? + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "I couldn't find that api key.", + **common, + }, + } + end + end + + stats = APIKey.stats(ip: ip, key: key) + if key.present? + stats = flatten_hash(stats[:key]) + text = "Stats for **#{apikey.application_name}** (||#{apikey.key}||)\n\n" + elsif ip.present? + stats = flatten_hash(stats[:ip]) + text = "Stats for **#{ip}**\n\n" + else + stats = flatten_hash(stats[:global]) + text = "Global stats\n\n" + end + + stats.each do |name, value| + text += "**#{name}**: #{ActiveSupport::NumberHelper.number_to_delimited(value)}\n" + end + + { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: text, + **common, + }, + } + end + end + when DiscordConstants::InteractionTypes::MESSAGE_COMPONENT + action, *other, user = raw["data"]["custom_id"].split(".") + if user != raw["member"]["user"]["id"] + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "That is not yours to play with.", + **common, + }, + } + end + + case action + when "apikey-delete-yes" + key = APIKey.for_owner(user).find { |k| k.encoded == other[0] } + if key.nil? || key.owner_id.to_s != user + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Invalid key specified.", + **common, + }, + } + end + + key.destroy + + { + type: DiscordConstants::InteractionResponseTypes::UPDATE_MESSAGE, + data: { + content: "Key deleted.", + components: [], + **common, + }, + } + when "apikey-delete-no" + { + type: DiscordConstants::InteractionResponseTypes::UPDATE_MESSAGE, + data: { + content: "Cancelled.", + components: [], + **common, + }, + } + end + when DiscordConstants::InteractionTypes::APPLICATION_COMMAND_AUTOCOMPLETE + options = DiscordInteractions::InteractionOptions.new(raw["data"]["options"], raw["data"]["resolved"]) + sub, = options.get_subcommand + opt = options.get_focused_option(required: true) + case raw["data"]["name"] + when "apikey" + case sub + when "delete" + keys = APIKey.for_owner(raw["member"]["user"]["id"]) + value = opt["value"] + keys = keys.filter { |k| k.application_name.start_with?(value) } + { + type: DiscordConstants::InteractionResponseTypes::APPLICATION_COMMAND_AUTOCOMPLETE_RESULT, + data: { + choices: keys.map do |k| + { + name: k.application_name, + value: k.encoded, + } + end, + }, + } + end + end + when DiscordConstants::InteractionTypes::MODAL_SUBMIT + case raw["data"]["custom_id"] + when "apikey-create" + name = raw["data"]["components"][0]["components"][0]["value"] + usage = raw["data"]["components"][1]["components"][0]["value"] + if name.length < 3 || name.length > 50 + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Name must be between 3 and 50 characters.", + components: [], + **common, + }, + } + end + + if usage.length < 5 || usage.length > 400 + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Usage must be between 5 and 400 characters.", + **common, + }, + } + end + + key = APIKey.create( + owner_id: raw["member"]["user"]["id"], + application_name: name, + usage: usage, + ) + + if key.errors.present? + Rails.logger.error("Failed to create apikey for #{raw['member']['user']['id']}: #{key.errors.full_messages.join(', ')}") + return { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "An error occurred while creating your API key.", + **common, + }, + } + end + + { + type: DiscordConstants::InteractionResponseTypes::CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Your API key: `#{key.key}`. Provide this in the `Authorization` header. You must still provide a unique user agent. If you have any issues, contact a developer.", + **common, + }, + } + end + end + end + # rubocop:enable Metrics/BlockNesting + + private + + def flatten_hash(hash) + hash.each_with_object({}) do |(k, v), h| + if v.is_a?(Hash) + flatten_hash(v).map do |h_k, h_v| + h["#{k}.#{h_k}".to_sym] = h_v + end + else + h[k] = v + end + end + end + + def common + { + flags: DiscordConstants::MessageFlags::EPHEMERAL, + allowed_mentions: { + users: [], + roles: [], + everyone: [], + replied_user: false, + }, + } + end + end +end diff --git a/app/logical/discord_link.rb b/app/logical/discord_link.rb new file mode 100644 index 0000000..212f78f --- /dev/null +++ b/app/logical/discord_link.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module DiscordLink + module_function + + def for(client_id:, redirect_uri:, scope:, response_type: "code") + params = %W[client_id=#{client_id} redirect_uri=#{CGI.escape(redirect_uri)} response_type=#{response_type} scope=#{scope.join('%20')} prompt=none] + "https://discord.com/api/oauth2/authorize?#{params.join('&')}" + end +end diff --git a/app/logical/e621_exports_parser.rb b/app/logical/e621_exports_parser.rb new file mode 100644 index 0000000..439d8ef --- /dev/null +++ b/app/logical/e621_exports_parser.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module E621ExportsParser +end diff --git a/app/logical/e621_status_updater.rb b/app/logical/e621_status_updater.rb new file mode 100644 index 0000000..e8c9e16 --- /dev/null +++ b/app/logical/e621_status_updater.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module E621StatusUpdater + def self.run + return if Rails.env.development? + status = Requests::E621.status + previous_status = Cache.redis.get("e621_status:current").to_i + last_change_time = Cache.redis.get("e621_status:last_change_time") + last_change_time = Time.parse(last_change_time) if last_change_time.present? + if previous_status == status + Cache.redis.set("e621_status:last_change_time", (Time.now - 2.seconds).to_s) + elsif last_change_time.nil? || (Time.now - last_change_time) >= 3.minutes + Rails.logger.debug { "Updating status: #{previous_status} -> #{status}" } + Cache.redis.set("e621_status:current", status.to_s) + Cache.redis.set("e621_status:last_change_time", (Time.now - 2.seconds).to_s) + E621Status.create!(status: status) + end + end +end diff --git a/app/logical/git_helper.rb b/app/logical/git_helper.rb new file mode 100644 index 0000000..1cd790d --- /dev/null +++ b/app/logical/git_helper.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module GitHelper + def self.init + if Rails.root.join("REVISION").exist? + @hash = Rails.root.join("REVISION").read.strip + elsif system("type git > /dev/null && git rev-parse --show-toplevel > /dev/null") + @hash = `git rev-parse HEAD`.strip + else + @hash = nil + end + end + + def self.short_hash + return nil if @hash.nil? + @hash[0..8] + end + + def self.commit_url(commit_hash) + "#{Websites.config.source_code_url}/commit/#{commit_hash}" + end +end diff --git a/app/logical/parse_value.rb b/app/logical/parse_value.rb new file mode 100644 index 0000000..cca6867 --- /dev/null +++ b/app/logical/parse_value.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module ParseValue + extend self + + def range(range, type = :integer) + if range.start_with?("<=") + [:lte, cast(range.delete_prefix("<="), type)] + + elsif range.start_with?("..") + [:lte, cast(range.delete_prefix(".."), type)] + + elsif range.start_with?("<") + [:lt, cast(range.delete_prefix("<"), type)] + + elsif range.start_with?(">=") + [:gte, cast(range.delete_prefix(">="), type)] + + elsif range.end_with?("..") + [:gte, cast(range.delete_suffix(".."), type)] + + elsif range.start_with?(">") + [:gt, cast(range.delete_prefix(">"), type)] + + elsif range.include?("..") + left, right = range.split("..", 2) + [:between, cast(left, type), cast(right, type)] + + elsif range.include?(",") + [:in, range.split(",")[0..99].map { |x| cast(x, type) }] + + else + [:eq, cast(range, type)] + + end + end + + private + + def cast(object, type) + case type + when :integer + object.to_i + + when :float + object.to_f + end + end +end diff --git a/app/logical/rate_limiter.rb b/app/logical/rate_limiter.rb new file mode 100644 index 0000000..7422dc4 --- /dev/null +++ b/app/logical/rate_limiter.rb @@ -0,0 +1,181 @@ +# frozen_string_literal: true + +class RateLimiter + # rubocop:disable Style/EmptyMethod + def get_global(name, ip); end + def get_route(name, domain, path, ip); end + def get_global_ttl(name, ip); end + def get_route_ttl(name, domain, path, ip); end + def consume_global(name, window, limit, ip); end + def consume_route(name, domain, path, window, limit, ip); end + def fix_infinite_expiry(key, val, expiry); end + # rubocop:enable Style/EmptyMethod + + def global_key(name, ip) + "rl:#{name}:global:#{ip}" + end + + def route_bucket(domain, path) + Base64.urlsafe_encode64("domain=#{domain},path=#{path}", padding: false) + end + + def route_key(name, domain, path, ip) + "rl:#{name}:#{route_bucket(domain, path)}:#{ip}" + end + + # TODO: https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers (when completed, it's not practically usable currently) + # `X-RateLimit` here is loosely based on draft 7, with policy merged into one header, and converted to json + # properties: + # * `limit`=integer (required) The maximum amount of requests that can be made in `window` + # * `remaining`=integer (required) The remaining requests in `window` + # * `reset`=integer (required) The time after which `limit` resets + # * `window`=integer (required) The window size in seconds + # * `burst`=integer (optional) The burst limit for window + # * `comment`=string (optional) A comment for the limit + # + # Examples: + # * `X-RateLimit: [{ "limit": 2, "remaining": 1, "reset": 2, "window": 2, "comment": "route; bucket=ZG9tYWluPXYyLnlpZmYucmVzdCxwYXRoPS9mdXJyeS95aWZmL2dheQ" }]` + # * `X-RateLimit: [{ "limit": 7, "remaining": 5, "reset": 8, "window": 10, "comment": "global; authed" }]` + # * `X-RateLimit: [{ "limit": 2, "remaining": 1, "reset": 2, "window": 2, "comment": "route; bucket=ZG9tYWluPXYyLnlpZmYucmVzdCxwYXRoPS9mdXJyeS95aWZmL2dheQ" }, { "limit": 7, "remaining": 5, "reset": 8, "window": 10, "comment": "global" }]` + def process(request, ignore_auth: false, short_limit: nil, short_window: nil, long_limit: nil, long_window: nil) + ip = request.remote_ip + path = request.path.downcase + domain = request.domain + if ignore_auth + apikey = APIKey.anon + else + apikey = APIKey.from_request(request) + end + short_limit ||= apikey.limit_short + short_window ||= apikey.window_short + long_limit ||= apikey.limit_long + long_window ||= apikey.window_long + headers = {} + if domain == "yiff.rest" && path.start_with?("/v2") + domain = "v2.yiff.rest" + path = path[3..] + end + + return [:INVALID_KEY, nil, headers] if apikey.nil? + authed = "" + authed = "; authed" unless apikey.is_anon? + + rroute = consume_route("yiffy2", domain, path, short_window, short_limit, ip) + expires_route = get_route_ttl("yiffy2", domain, path, ip) + remaining = (short_limit - rroute).clamp(0, short_limit) + reset = expires_route.nil? ? short_window : Time.now.to_i + expires_route + reset_after = expires_route.nil? ? 0 : expires_route + bucket = route_bucket(domain, path) + rl = [{ limit: short_limit, remaining: remaining, reset: reset / 1000, window: short_window / 1000, comment: "route; bucket=#{bucket}#{authed}" }] + + headers["X-RateLimit-Limit"] = short_limit.to_s + headers["X-RateLimit-Remaining"] = remaining.to_s + headers["X-RateLimit-Reset"] = reset.to_s + headers["X-RateLimit-Reset-After"] = reset_after.to_s + headers["X-RateLimit-Bucket"] = bucket + headers["X-RateLimit-Precision"] = "millisecond" + + rglobal = consume_global("yiffy2", long_window, long_limit, ip) + expires_global = get_global_ttl("yiffy2", ip) + fix_infinite_expiry(global_key("yiffy2", ip), expires_global, long_window) + global_remaining = (long_limit - rglobal).clamp(0, long_limit) + global_reset = expires_global.nil? ? long_window : Time.now.to_i + expires_global + global_reset_after = expires_global.nil? ? 0 : expires_global + + headers["X-RateLimit-Global-Limit"] = long_limit.to_s + headers["X-RateLimit-Global-Remaining"] = global_remaining.to_s + headers["X-RateLimit-Global-Reset"] = global_reset.to_s + headers["X-RateLimit-Global-Reset-After"] = global_reset_after.to_s + headers["X-RateLimit-Global-Precision"] = "millisecond" + + rl << { limit: long_limit, remaining: global_remaining, reset: global_reset / 1000, window: long_window / 1000, comment: "global#{authed}" } + headers["X-RateLimit"] = rl.to_json + + if (rroute - 1) >= short_limit + route_ra = expires_route.nil? ? short_window / 1000 : expires_route / 1000 + headers["Retry-After"] = route_ra.to_s + + Websites.config.yiffyapi_ratelimit_webhook.execute({ + embeds: [ + { + title: "Rate Limit Exceeded", + description: <<~DESC.strip, + **Host:** **#{domain}** + Path: **#{path}** + Auth: **#{apikey.is_anon? ? 'no' : "Yes (`#{apikey.key}`)"}** + User Agent: `#{request.user_agent}` + IP: `#{request.remote_ip}` + Global: <:redTick:865401803256627221> + Info: + \u25fd Limit: **#{short_limit}** + \u25fd Remaining: **#{remaining}** + \u25fd Reset: **#{reset}** + \u25fd Reset After: **#{reset_after}** + \u25fd Bucket: **#{bucket}** + \u25fd Decoded Bucket: **#{Base64.urlsafe_decode64(bucket)}** + DESC + color: 0xDC143C, + timestamp: Time.now.iso8601, + }, + ], + }) + return [:RATELIMIT_SHORT, { + limit: short_limit, + remaining: remaining, + reset: reset, + resetAfter: reset_after, + retryAfter: route_ra.ceil, + bucket: bucket, + precision: "millisecond", + global: false, + }, headers,] + end + + if (rglobal - 1) >= long_limit + global_ra = expires_global.nil? ? long_window / 1000 : expires_global / 1000 + headers["Retry-After"] = global_ra.to_s + + Websites.config.yiffyapi_ratelimit_webhook.execute({ + embeds: [ + { + title: "Rate Limit Exceeded", + description: <<~DESC.strip, + **Host:** **#{domain}** + Path: **#{path}** + Auth: **#{apikey.is_anon? ? 'no' : "Yes (`#{apikey.key}`)"}** + User Agent: `#{request.user_agent}` + IP: `#{request.remote_ip}` + Global: <:greenTick:865401802920951819> + Info: + \u25fd Limit: **#{long_limit}** + \u25fd Remaining: **#{global_remaining}** + \u25fd Reset: **#{global_reset}** + \u25fd Reset After: **#{global_reset_after}** + DESC + color: 0xDC143C, + timestamp: Time.now.iso8601, + }, + ], + }) + return [:RATELIMIT_LONG, { + limit: long_limit, + remaining: global_remaining, + reset: global_reset, + resetAfter: global_reset_after, + retryAfter: global_ra.ceil, + precision: "millisecond", + global: true, + }, headers,] + end + + [nil, nil, headers] + end + + def self.limiter + @limiter ||= RateLimiter::Redis.new + end + + def self.process(request, ignore_auth: false) + limiter.process(request, ignore_auth: ignore_auth) + end +end diff --git a/app/logical/rate_limiter/in_memory.rb b/app/logical/rate_limiter/in_memory.rb new file mode 100644 index 0000000..45e110d --- /dev/null +++ b/app/logical/rate_limiter/in_memory.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class RateLimiter::InMemory < RateLimiter + attr_reader :list + + def initialize + super + @list = {} + end + + def get_global(name, ip) + list[global_key(name, ip)].try(:[], :count) + end + + def get_route(name, domain, path, ip) + list[route_key(name, domain, path, ip)].try(:[], :count) + end + + def get_global_ttl(name, ip) + list[global_key(name, ip)].try(:[], :expiry) + end + + def get_route_ttl(name, domain, path, ip) + list[route_key(name, domain, path, ip)].try(:[], :expiry) + end + + def consume_global(name, window, limit, ip) + consume(global_key(name, ip), window, limit) + end + + def consume_route(name, domain, path, window, limit, ip) + consume(route_key(name, domain, path, ip), window, limit) + end + + def fix_infinite_expiry(_key, val, expiry) + # noop + end + + private + + def consume(key, window, limit) + (list[key] || { count: 0, expiry: nil }) => { count:, expiry: } + + if expiry && expiry < Time.now.to_i + list.delete(key) + count = expiry = nil + end + + return count + 1 if count && count < limit + + count = (count || 0) + 1 + expiry ||= Time.now.to_i + window + list[key] = { count: count, expiry: expiry } + count + end +end diff --git a/app/logical/rate_limiter/redis.rb b/app/logical/rate_limiter/redis.rb new file mode 100644 index 0000000..843b1ce --- /dev/null +++ b/app/logical/rate_limiter/redis.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class RateLimiter::Redis < RateLimiter + def get_global(name, ip) + r = Cache.redis.get(global_key(name, ip)) + return r.to_i unless r.nil? + nil + end + + def get_route(name, domain, path, ip) + r = Cache.redis.get(route_key(name, domain, path, ip)) + return r.to_i unless r.nil? + nil + end + + def get_global_ttl(name, ip) + r = Cache.redis.pttl(global_key(name, ip)) + return nil if r.nil? || r < 0 + r + end + + def get_route_ttl(name, domain, path, ip) + r = Cache.redis.pttl(route_key(name, domain, path, ip)) + return nil if r.nil? || r < 0 + r + end + + def consume_global(name, window, limit, ip) + consume(global_key(name, ip), window, limit) + end + + def consume_route(name, domain, path, window, limit, ip) + consume(route_key(name, domain, path, ip), window, limit) + end + + def fix_infinite_expiry(key, _val, expiry) + Cache.redis.pexpire(key, expiry) if expiry.to_i == -1 + end + + private + + def consume(key, window, limit) + exists = Cache.redis.exists(key) == 1 + Cache.redis.set(key, "0", px: window) unless exists + val = Cache.redis.get(key).to_i + exp = Cache.redis.pttl(key) + fix_infinite_expiry(key, val, exp) + # don't increase if the request will be rejected + val > limit ? limit + 1 : Cache.redis.incr(key) + end +end diff --git a/app/logical/requests/bunny.rb b/app/logical/requests/bunny.rb new file mode 100644 index 0000000..ce5c102 --- /dev/null +++ b/app/logical/requests/bunny.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Requests + class Bunny + include HTTParty + + base_uri "https://storage.bunnycdn.com" + + attr_reader :access_key, :storage_zone_name + + def initialize(access_key:, storage_zone_name:) + @access_key = access_key + @storage_zone_name = storage_zone_name + end + + def delete(path) + self.class.delete("/#{storage_zone_name}/#{path}", headers: { "AccessKey" => access_key }) + end + + def exists?(path) + # bunny doesn't support HEAD, for some reason - so we have to download the ENTIRE file to check if it exists + # I'm unsure if this counts against us, so it should be used sparingly + !get(path).nil? + end + + def get(path) + r = self.class.get("/#{storage_zone_name}/#{path}", headers: { "AccessKey" => access_key }) + r.success? ? r.body : nil + end + + def put(path, body) + self.class.put("/#{storage_zone_name}/#{path}", body: body, headers: { "AccessKey" => access_key, "Checksum" => Digest::SHA2.hexdigest(body) }) + end + end +end diff --git a/app/logical/requests/discord_global_commands.rb b/app/logical/requests/discord_global_commands.rb new file mode 100644 index 0000000..937e778 --- /dev/null +++ b/app/logical/requests/discord_global_commands.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Requests + class DiscordGlobalCommands + include HTTParty + base_uri "https://discord.com/api/v10" + attr_reader :application_id + + def initialize(application_id:) + @application_id = application_id + end + + def update(commands) + auth = Requests::DiscordOauth.yiffy_client_credentials(scopes: %w[applications.commands.update]) + data = self.class.put("/applications/#{application_id}/commands", { + body: commands.to_json, + headers: { + "Content-Type" => "application/json", + "Authorization" => "Bearer #{auth.access_token}", + }, + }) + if data.code != 200 + Rails.logger.error("Failed to update Discord commands:") + Rails.logger.error(data.body) + end + nil + end + + def self.default + new(application_id: Websites.config.yiffyapi_discord_id) + end + end +end diff --git a/app/logical/requests/discord_guild_commands.rb b/app/logical/requests/discord_guild_commands.rb new file mode 100644 index 0000000..32f41d6 --- /dev/null +++ b/app/logical/requests/discord_guild_commands.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Requests + class DiscordGuildCommands + include HTTParty + base_uri "https://discord.com/api/v10" + attr_reader :application_id, :guild_id + + def initialize(application_id:, guild_id:) + @application_id = application_id + @guild_id = guild_id + end + + def update(commands) + auth = Requests::DiscordOauth.yiffy_client_credentials(scopes: %w[applications.commands.update]) + data = self.class.put("/applications/#{application_id}/guilds/#{guild_id}/commands", { + body: commands.to_json, + headers: { + "Content-Type" => "application/json", + "Authorization" => "Bearer #{auth.access_token}", + }, + }) + if data.code != 200 + Rails.logger.error("Failed to update Discord commands:") + Rails.logger.error(data.body) + end + nil + end + + def self.default + new(application_id: Websites.config.yiffyapi_discord_id, guild_id: Websites.config.yiffyapi_discord_guild) + end + end +end diff --git a/app/logical/requests/discord_oauth.rb b/app/logical/requests/discord_oauth.rb new file mode 100644 index 0000000..5a7e9d5 --- /dev/null +++ b/app/logical/requests/discord_oauth.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Requests + class DiscordOauth + include HTTParty + base_uri "https://discord.com/api/oauth2" + + Authorization = Struct.new(:access_token, :token_type, :expires_in, :refresh_token, :scope, :webhook) do + def get_authorization # rubocop:disable Naming/AccessorMethodName + r = DiscordOauth.get("/@me", { + headers: { + "Authorization" => "#{token_type} #{access_token}", + }, + }) + if r.code != 200 + Rails.logger.error("Failed to get user info: #{r.code} #{r.body}") + return nil + end + JSON.parse(r.body) + end + + def get_guilds # rubocop:disable Naming/AccessorMethodName + r = HTTParty.get("https://discord.com/api/users/@me/guilds", { + headers: { + "Authorization" => "#{token_type} #{access_token}", + }, + }) + if r.code != 200 + Rails.logger.error("Failed to get user guilds: #{r.code} #{r.body}") + return nil + end + JSON.parse(r.body) + end + + def user_id + get_authorization.try(:[], "user").try(:[], "id") + end + end + Webhook = Struct.new(:application_id, :name, :url, :channel_id, :token, :type, :avatar, :guild_id, :id) + Error = Struct.new(:error, :error_description) + + attr_reader :client_id, :client_secret + + def initialize(client_id:, client_secret:) + @client_id = client_id + @client_secret = client_secret + end + + def authorize(redirect_uri:, code:) + data = self.class.post("/token", { + body: URI.encode_www_form({ + grant_type: "authorization_code", + code: code, + redirect_uri: redirect_uri, + }), + basic_auth: { + username: client_id, + password: client_secret, + }, + headers: { + "Content-Type" => "application/x-www-form-urlencoded", + }, + }) + unless data.success? + Rails.logger.info(client_id) + Rails.logger.error("Failed to authorize: #{data.code} #{data.body}") + end + return Error.new(data["error"], data["error_description"]) if data["error"] + + hook = nil + hook = Webhook.new(data["webhook"]["application_id"], data["webhook"]["name"], data["webhook"]["url"], data["webhook"]["channel_id"], data["webhook"]["token"], data["webhook"]["type"], data["webhook"]["avatar"], data["webhook"]["guild_id"], data["webhook"]["id"]) if data["webhook"] + Authorization.new(data["access_token"], data["token_type"], data["expires_in"], data["refresh_token"], data["scope"], hook) + end + + def client_grant(scopes:) + data = self.class.post("/token", { + body: URI.encode_www_form({ + grant_type: "client_credentials", + scope: scopes.join(" "), + }), + basic_auth: { + username: client_id, + password: client_secret, + }, + headers: { + "Content-Type" => "application/x-www-form-urlencoded", + }, + }) + return Error.new(data["error"], data["error_description"]) if data["error"] + + Authorization.new(data["access_token"], data["token_type"], data["expires_in"], data["refresh_token"], data["scope"], nil) + end + + def self.yiffy_client_credentials(scopes:) + new( + client_id: Websites.config.yiffyapi_discord_id, + client_secret: Websites.config.yiffyapi_discord_secret, + ).client_grant(scopes: scopes) + end + end +end diff --git a/app/logical/requests/discord_webhook.rb b/app/logical/requests/discord_webhook.rb new file mode 100644 index 0000000..3b07589 --- /dev/null +++ b/app/logical/requests/discord_webhook.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Requests + class DiscordWebhook + include HTTParty + base_uri "https://discord.com/api/webhooks" + + attr_reader :id, :token + + def initialize(id:, token:) + @id = id + @token = token + end + + def execute(body) + r = self.class.post("/#{id}/#{token}?wait=false", { + body: body.to_json, + headers: { + "Content-Type" => "application/json", + }, + }) + Rails.logger.warn("Discord webhook failed: #{r.code} #{r.body}") unless r.success? + r + end + + def edit(message_id, body) + r = self.class.patch("/#{id}/#{token}/#{message_id}?wait=false", { + body: body.to_json, + headers: { + "Content-Type" => "application/json", + }, + }) + Rails.logger.warn("Discord webhook edit failed: #{r.code} #{r.body}") unless r.success? + r + end + + def delete + self.class.delete("/#{id}/#{token}") + end + + def self.is_deleted(response) + response.code == 404 && JSON.parse(response.body)["code"] == 10_015 + end + end +end diff --git a/app/logical/requests/e621.rb b/app/logical/requests/e621.rb new file mode 100644 index 0000000..ce74ba5 --- /dev/null +++ b/app/logical/requests/e621.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Requests + class E621 + include HTTParty + base_uri "https://e621.net" + + def status + r = self.class.get("/posts.json?limit=0", { + headers: { + "User-Agent" => "E621Status/1.0.0 (https://status.e621.ws; \"donovan_dmc\")", + }, + timeout: 5, + }) + status = r.code + if r.code == 503 && r.headers["Content-Type"]&.start_with?("text/html") + status = 1 + elsif r.code == 501 + status = 200 + Rails.logger.warn("E621 status check returned 501, ignoring") + end + status + rescue Net::OpenTimeout + Rails.logger.warn("E621 status check timed out") + 408 + rescue StandardError => e + Rails.logger.warn("E621 status check failed: #{e}") + 0 + end + + def get_post(id: nil, md5: nil) + raise(ArgumentError, "id or md5 must be given") if id.nil? && md5.nil? + path = "/" + path = "/posts/#{id}.json" if id + path = "/posts.json?md5=#{md5}" if md5 + r = self.class.get(path, { + headers: { + "User-Agent" => "Websites/4.0.0 (https://github.com/DonovanDMC/Websites; \"donovan_dmc\")", + }, + }) + return nil if r.code != 200 + JSON.parse(r.body)["post"] + end + + def get_posts(ids: nil, md5s: nil, status: nil) + raise(ArgumentError, "ids or md5s must be given") if ids.nil? && md5s.nil? + path = "/posts.json?" + path += "tags=id:#{ids.join(',')}%20limit:100" if ids + path += "tags=md5:#{md5s.join(',')}%20limit:100" if md5s + path += "%20status:#{status}" if path.include?("tags=") && !status.nil? + path += "tags=status:#{status}" if !path.include?("tags=") && !status.nil? + r = self.class.get(path, { + headers: { + "User-Agent" => "Websites/4.0.0 (https://github.com/DonovanDMC/Websites; \"donovan_dmc\")", + }, + }) + JSON.parse(r.body)["posts"] || [] + end + + def find_replacement(md5:) + r = self.class.get("/post_replacements.json?search[md5]=#{md5}", { + headers: { + "User-Agent" => "Websites/4.0.0 (https://github.com/DonovanDMC/Websites; \"donovan_dmc\")", + }, + }) + JSON.parse(r.body)&.first + end + + def self.status + new.status + end + + def self.get_post(**) + new.get_post(**) + end + + def self.get_posts(**) + new.get_posts(**) + end + end +end diff --git a/app/logical/requests/pastebin.rb b/app/logical/requests/pastebin.rb new file mode 100644 index 0000000..ecaa290 --- /dev/null +++ b/app/logical/requests/pastebin.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Requests + class Pastebin + include HTTParty + base_uri "https://pastebin.com/api" + attr_reader :dev_key, :user_key, :folder + + def initialize(dev_key:, user_key:, folder: nil) + @dev_key = dev_key + @user_key = user_key + @folder = folder + end + + def create(title:, content:) + data = self.class.post("/api_post.php", { + body: URI.encode_www_form({ + api_option: "paste", + api_dev_key: dev_key, + api_user_key: user_key, + api_paste_code: content, + api_paste_name: title, + api_paste_expire_date: "1W", + api_paste_private: 2, + api_folder_key: folder, + }), + headers: { + "Content-Type" => "application/x-www-form-urlencoded", + }, + }) + if data.code != 200 + Rails.logger.error("Failed to paste to pastebin:") + Rails.logger.error(data) + return nil + end + data.gsub("https://pastebin.com/", "") + end + + def self.default + new(dev_key: Websites.config.pastebin_dev_key, user_key: Websites.config.pastebin_user_key, folder: Websites.config.pastebin_folder) + end + end +end diff --git a/app/logical/storage_manager.rb b/app/logical/storage_manager.rb new file mode 100644 index 0000000..380d6f1 --- /dev/null +++ b/app/logical/storage_manager.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module StorageManager + E621Thumbnails = Local.new(base_url: Websites.config.e621_thumbnails_base_url, base_path: Websites.config.e621_thumbnails_base_path) + # E621Thumbnails = Bunny.new(base_url: Websites.config.e621_thumbnails_base_url, access_key: Websites.config.e621_thumbnails_access_key, storage_zone_name: Websites.config.e621_thumbnails_storage_zone_name) +end diff --git a/app/logical/storage_manager/bunny.rb b/app/logical/storage_manager/bunny.rb new file mode 100644 index 0000000..cd22eb5 --- /dev/null +++ b/app/logical/storage_manager/bunny.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module StorageManager + class Bunny + attr_reader :base_url, :access_key, :storage_zone_name + + def initialize(base_url:, access_key:, storage_zone_name:) + @base_url = base_url + @access_key = access_key + @storage_zone_name = storage_zone_name + end + + def request + Requests::Bunny.new(access_key: access_key, storage_zone_name: storage_zone_name) + end + + delegate :delete, :exists?, :get, :put, to: :request + + def upload(path, body) + r = put(path, body) + return nil unless r.success? + "#{base_url}/#{path}" + end + + def url_for(entry) + "#{base_url}/#{entry.stripped_md5}.#{entry.filetype}" + end + end +end diff --git a/app/logical/storage_manager/local.rb b/app/logical/storage_manager/local.rb new file mode 100644 index 0000000..0dc2346 --- /dev/null +++ b/app/logical/storage_manager/local.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module StorageManager + class Local + attr_reader :base_url, :base_path + + def initialize(base_url:, base_path:) + @base_url = base_url + @base_path = base_path + end + + def delete(path) + return unless exists?(path) + File.delete("#{base_path}/#{path}") + end + + def exists?(path) + File.exist?("#{base_path}/#{path}") + end + + def get(path) + File.read("#{base_path}/#{path}") + end + + def put(path, body) + File.write("#{base_path}/#{path}", body) + end + + def upload(path, body) + put(path, body) + "#{base_url}/#{path}" + end + + def url_for(entry) + "#{base_url}/#{entry.stripped_md5}.#{entry.filetype}" + end + end +end diff --git a/app/logical/web_logger.rb b/app/logical/web_logger.rb new file mode 100644 index 0000000..459d528 --- /dev/null +++ b/app/logical/web_logger.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class WebLogger + def self.log_exception(exception, expected: false, **params) + if expected + Rails.logger.info("#{exception.class}: #{exception.message}") + else + backtrace = Rails.backtrace_cleaner.clean(exception.backtrace).join("\n") + Rails.logger.error("#{exception.class}: #{exception.message}\n#{backtrace}") + ::NewRelic::Agent.notice_error(exception, expected: expected, custom_params: params) if defined?(::NewRelic) + end + end + + def self.initialize(request) + add_attributes( + "request.ip" => request.ip, + "request.domain" => request.params[:domain], + "request.path" => request.params[:path], + ) + end + + def self.add_attributes(**) + return unless defined?(::NewRelic) + + ::NewRelic::Agent.add_custom_attributes(**) + end +end diff --git a/app/logical/yiffyapi_error_codes.rb b/app/logical/yiffyapi_error_codes.rb new file mode 100644 index 0000000..d83bc27 --- /dev/null +++ b/app/logical/yiffyapi_error_codes.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +class YiffyAPIErrorCodes + attr_reader :code, :status + + def initialize(code, status) + @code = code + @status = status + end + + INTERNAL_ERROR = new(0, 500) + ACCESS_DENIED = new(1, 403) + READONLY = new(2, 503) + RATELIMIT_ROUTE = new(1000, 429) + RATELIMIT_GLOBAL = new(1001, 429) + IP_BLOCKED = new(1002, 403) + # unused + DOWN_FOR_MAINTENANCE = new(1003, nil) + + INVALID_API_KEY = new(1010, 401) + INACTIVE_API_KEY = new(1011, 401) + DISABLED_API_KEY = new(1012, 403) + API_KEY_REQUIRED = new(1013, 401) + ANONYMOUS_RESTRICTED = new(1014, 403) + + DISK_FULL = new(1020, 507) + BLOCKED_USERAGENT = new(1021, 403) + SERVICE_NO_ACCESS = new(1022, 403) + UNKNOWN_ROUTE = new(1024, 404) + METHOD_NOT_ALLOWED = new(1025, 405) + + # legacy codes that are spread out + # unused + IMAGES_INVALID_RESPONSE_TYPE = new(1023, nil) + IMAGES_CATEGORY_NOT_FOUND = new(1030, 404) + # unused + IMAGES_EMPTY_CATEGORY = new(1031, nil) + IMAGES_NOT_FOUND = new(1040, 404) + IMAGES_NO_RESULTS = new(1041, 400) + IMAGES_AMOUNT_LT_ONE = new(1051, 400) + IMAGES_AMOUNT_GT_FIVE = new(1052, 400) + IMAGES_IMAGE_RESPONSE_DISABLED = new(1053, 404) + BULK_IMAGES_INVALID_BODY = new(1054, 400) + BULK_IMAGES_INVALID_CATEGORY = new(1055, 400) + BULK_IMAGES_NUMBER_GT_MAX = new(1056, 400) + IMAGES_SFW_ONLY_API_KEY = new(1057, 403) + + THUMBS_GENERIC_ERROR = new(1060, 500) + THUMBS_API_KEY_REQUIRED = new(1061, nil) # unused + THUMBS_INVALID_POST_ID = new(1062, 404) + THUMBS_INVALID_MD5 = new(1063, 404) + THUMBS_INVALID_TYPE = new(1064, 404) + THUMBS_TIMEOUT = new(1065, 500) + THUMBS_CHECK_NOT_FOUND = new(1066, 404) + THUMBS_GIF_DISABLED = new(1067, 400) + + SHORTENER_CODE_TOO_LONG = new(1070, 422) + SHORTENER_INVALID_CODE = new(1071, 422) + SHORTENER_CODE_IN_USE = new(1072, 409) + SHORTENER_INVALID_URL = new(1073, 422) + SHORTENER_CREDIT_TOO_LONG = new(1074, 422) + SHORTENER_NOT_FOUND = new(1075, 404) + SHORTENER_MANAGEMENT_CODE_REQUIRED = new(1076, 401) + SHORTENER_NO_MANAGEMENT_CODE = new(1077, 403) + SHORTENER_MANAGEMENT_CODE_MISMATCH = new(1078, 401) + SHORTENER_URL_IN_USE = new(1079, 409) + SHORTENER_NO_CHANGES = new(1080, 400) + SHORTENER_URL_TOO_LONG = new(1081, 422) +end diff --git a/app/logical/yiffyapi_util.rb b/app/logical/yiffyapi_util.rb new file mode 100644 index 0000000..66aecd0 --- /dev/null +++ b/app/logical/yiffyapi_util.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module YiffyAPIUtil + def build_failure_response(code, **data) + { + success: false, + code: code.code, + **data, + } + end + + def render_error(code, **) + render(json: build_failure_response(code, **), status: code.status) + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..5cc63a0 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/models/api_image.rb b/app/models/api_image.rb new file mode 100644 index 0000000..e4d6212 --- /dev/null +++ b/app/models/api_image.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +class APIImage < ApplicationRecord + CDN_URL = "https://v2.yiff.media/" + + belongs_to_creator + + module SearchMethods + def random(category, limit, size_limit = nil) + q = where(category: category) + q = q.where(file_size: ..size_limit) if size_limit + q.ids.sample(limit).map(&method(:find)) + end + + def bulk(category_map, size_limit = nil) + data = {} + category_map.each do |category, limit| + data[category] = random(category, limit, size_limit) + end + data + end + end + + extend SearchMethods + + def md5 + id.gsub("-", "") + end + + def url + "#{CDN_URL}#{category.gsub('.', '/')}/#{md5}.#{file_ext}" + end + + def short_url + ShortUrl.override(md5, url).shorturl + end + + def serializable_hash(*) + { + artists: artists, + sources: sources, + width: width, + height: height, + url: url, + type: mime_type, + name: "#{md5}.#{file_ext}", + id: md5, + ext: file_ext, + size: file_size, + reportURL: nil, + shortURL: short_url, + } + end + + def self.categories + sfw = %w[animals.birb animals.blep animals.dikdik furry.boop furry.cuddle furry.flop furry.fursuit furry.hold furry.howl furry.hug furry.kiss furry.lick furry.propose] + nsfw = %w[furry.butts furry.bulge furry.yiff.andromorph furry.yiff.gay furry.yiff.gynomorph furry.yiff.lesbian furry.yiff.straight] + titles = { + **sfw.index_with { |c| c.split(".").map(&:capitalize).join(" > ") }, + "animals.dikdik" => "Animals > Dik Dik", + **nsfw.index_with { |c| c.split(".").map(&:capitalize).join(" > ") }, + } + + # noinspection RubyMismatchedArgumentType + sfw.map { |c| { name: titles[c], db: c, sfw: true } } + .concat(nsfw.map { |c| { name: titles[c], db: c, sfw: false } }) + end + + def self.category_title(db) + categories.find { |k| k == db }.try(:db) || db.split(".").map(&:capitalize).join(" > ") + end + + def self.cached_count(category) + count = Cache.redis.get("yiffy2:images:#{category}") + if count.nil? + count = APIImage.where(category: category).count + # images aren't changing so we really don't need any expiry, but I still want an expiry just in case + Cache.redis.set("yiffy2:images:#{category}", count.to_s, ex: 60 * 60 * 24 * 7) + end + count.to_i + end + + def self.state + data = group(:category).count.map do |k, v| + { + count: v, + name: category_title(k), + category: k, + state: v < 5 ? "red" : v < 20 ? "yellow" : "green", + } + end + [*data, { + count: data.pluck(:count).reduce(:+), + name: "Total", + category: "total", + state: "total", + },] + end +end diff --git a/app/models/api_key.rb b/app/models/api_key.rb new file mode 100644 index 0000000..cebf99e --- /dev/null +++ b/app/models/api_key.rb @@ -0,0 +1,393 @@ +# frozen_string_literal: true + +class APIKey < ApplicationRecord + WINDOW_LONG_ANON = 10_000 + LIMIT_LONG_ANON = 7 + WINDOW_SHORT_ANON = 2000 + LIMIT_SHORT_ANON = 2 + MAX_PER_USER = 3 + + belongs_to :owner, class_name: "APIUser" + validate :validate_user_not_limited, unless: :skip_validations, on: :create + validates :application_name, length: { minimum: 3, maximum: 50 }, unless: :skip_validations, uniqueness: { scope: %i[owner_id] } + validates :usage, length: { maximum: 150 }, unless: :skip_validations + validates :limit_short, numericality: { greater_than_or_equal_to: 0 }, unless: :skip_validations + validates :window_short, numericality: { greater_than_or_equal_to: 0 }, unless: :skip_validations + validates :limit_long, numericality: { greater_than_or_equal_to: 0 }, unless: :skip_validations + validates :window_long, numericality: { greater_than_or_equal_to: 0 }, unless: :skip_validations + + before_create do + self.key = SecureRandom.hex(20) if key.blank? + self.flags = Flags.default if flags.blank? + end + + attr_accessor :no_webhook_messages, :skip_validations + + after_create :send_created, unless: :no_webhook_messages + after_update :send_updated, unless: :no_webhook_messages + after_destroy :send_deleted, unless: :no_webhook_messages + + before_save do + self.disabled_reason = "None Provided" if disabled? && disabled_reason.blank? + self.usage = "None provided" if usage.blank? + self.application_name = "None provided" if application_name.blank? + end + + module Flags + module_function + + IMAGES = 1 << 0 + THUMBS = 1 << 1 + SHORTENER = 1 << 2 + IMAGES_BULK = 1 << 3 + + def default + IMAGES | THUMBS | SHORTENER + end + + def anon + IMAGES + end + end + + module AccessMethods + def any + active? && !disabled? + end + + def images_access? + any && (flags & Flags::IMAGES == Flags::IMAGES) + end + + def thumbs_access? + any && (flags & Flags::THUMBS == Flags::THUMBS) + end + + def shortener_access? + any && (flags & Flags::SHORTENER == Flags::SHORTENER) + end + + def images_bulk_access? + any && (flags & Flags::IMAGES_BULK == Flags::IMAGES_BULK) + end + + def access_string + services = [] + services << "Images" if images_access? + services << "Thumbs" if thumbs_access? + services << "Shortener" if shortener_access? + services << "Bulk Images (#{bulk_limit})" if images_bulk_access? + return "None" if services.empty? + services.join(", ") + end + + def can_view?(user) + return true if user.id == owner_id + user.is_manager? + end + + def can_edit?(user) + return true if user.is_admin? + return false if disabled + user.id == owner_id + end + + def can_delete?(user) + return true if user.id == owner_id + user.is_admin? + end + + def can_disable?(user) + user.is_manager? + end + + def can_deactivate?(user) + return false if disabled + user.id == owner_id + end + + def can_regenerate?(user) + return true if user.is_admin? + user.id == owner_id + end + end + + module SearchMethods + def from_request(request, with_anon: true) + return anon if request.headers["Authorization"].blank? && with_anon + find_by(key: request.headers["Authorization"]) + end + + def for_owner(id) + where(owner_id: id) + end + + def search(params) + q = super + + q = q.attribute_matches(:owner_id, params[:owner_id]) + q = q.attribute_matches(:application_name, params[:application_name]) + q = q.attribute_matches(:usage, params[:usage]) + q = q.attribute_matches(:active, params[:active]) + q = q.attribute_matches(:disabled, params[:disabled]) + q = q.attribute_matches(:disabled_reason, params[:disabled_reason]) + + q.order(id: :asc) + end + end + + include AccessMethods + extend SearchMethods + + def is_anon? + self == APIKey.anon + end + + def encoded + # key doesn't actually need to be private, or really even have meaning here - just needs to be consistent + # using sha1 because sha256 is 64 characters, pushing the custom id to 101 or 102 characters + OpenSSL::HMAC.hexdigest("sha1", Websites.config.yiffyapi_public_key, key) + end + + def self.anon + @anon ||= new( + flags: Flags.anon, + limit_long: LIMIT_LONG_ANON, + limit_short: LIMIT_SHORT_ANON, + window_long: WINDOW_LONG_ANON, + window_short: WINDOW_SHORT_ANON, + application_name: "Anonymous", + ) + end + + def flags_images + images_access? + end + + def flags_thumbs + thumbs_access? + end + + def flags_shortener + shortener_access? + end + + def flags_images_bulk + images_bulk_access? + end + + def set_flag(flag, value) + if value.truthy? + update(flags: flags | flag) + else + update(flags: flags & ~flag) + end + end + + def flags_images=(value) + set_flag(Flags::IMAGES, value) + end + + def flags_thumbs=(value) + set_flag(Flags::THUMBS, value) + end + + def flags_shortener=(value) + set_flag(Flags::SHORTENER, value) + end + + def flags_images_bulk=(value) + set_flag(Flags::IMAGES_BULK, value) + end + + def css_class + classes = [] + classes << "apikey-super" if super? + classes << "apikey-disabled" if disabled? + classes << "apikey-inactive" unless active? + classes.join(" ") + end + + def regenerate! + update(key: SecureRandom.hex(20)) + end + + def validate_user_not_limited + return if CurrentUser.is_admin? + unless owner.can_create_apikey? + errors.add(:owner, "cannot create apikeys.") + throw(:abort) + end + end + + module WebhookMethods + GREEN = 0x008000 + YELLOW = 0xFFA500 + RED = 0xFF0000 + GREEN_TICK = "<:GreenTick:1235058920762904576>" + RED_TICK = "<:RedTick:1235058898549870724>" + + def execute(content) + Websites.config.yiffyapi_apikey_logs_webhook.execute({ + embeds: [content], + }) + end + + def send_created + execute({ + title: "API Key ##{id} Created", + description: <<~DESC, + Key: `#{key}` + Application: `#{application_name}` + Usage: `#{usage}` + Active: #{active? ? GREEN_TICK : RED_TICK} + Disabled: #{disabled? ? "#{GREEN_TICK} (Reason: #{disabled_reason || 'None Provided'})" : RED_TICK} + Unlimited: #{unlimited? ? GREEN_TICK : RED_TICK} + Super: #{super? ? GREEN_TICK : RED_TICK} + Services: #{access_string} + Blame: #{blame} + DESC + color: GREEN, + timestamp: Time.now.iso8601, + }) + end + + def send_deleted + execute({ + title: "API Key ##{id} Deleted", + description: <<~DESC, + Key: `#{key}` + Application: `#{application_name}` + Usage: `#{usage}` + Active: #{active? ? GREEN_TICK : RED_TICK} + Disabled: #{disabled? ? "#{GREEN_TICK} (Reason: #{disabled_reason || 'None Provided'})" : RED_TICK} + Unlimited: #{unlimited? ? GREEN_TICK : RED_TICK} + Super: #{super? ? GREEN_TICK : RED_TICK} + Services: #{access_string} + Blame: #{blame} + DESC + color: RED, + timestamp: Time.now.iso8601, + }) + end + + def check_change(attr, changes) + changes << "#{attr.to_s.titleize}: **#{send("#{attr}_before_last_save")}** -> **#{send(attr)}**" if send("saved_change_to_#{attr}?") + end + + def send_updated + changes = [] + changes << "Disabled (**#{disabled_reason}**)" if disabled? && saved_change_to_disabled? + changes << "Enabled" if !disabled? && saved_change_to_disabled? + changes << "Deactivated" if !active? && saved_change_to_active? + changes << "Activated" if active? && saved_change_to_active? + changes << "Super Removed" if !super? && saved_change_to_super? + changes << "Super Added" if super? && saved_change_to_super? + if saved_change_to_flags? + ref = APIKey.new(flags: flags_before_last_save, bulk_limit: bulk_limit_before_last_save) + changes << "Old Services: #{ref.access_string}" + changes << "New Services: #{access_string}" + end + check_change(:application_name, changes) + check_change(:usage, changes) + check_change(:limit_short, changes) + check_change(:window_short, changes) + check_change(:limit_long, changes) + check_change(:window_long, changes) + check_change(:key, changes) + check_change(:unlimited, changes) + check_change(:bulk_limit, changes) + + return if changes.empty? + + changes << "Blame: #{blame}" + execute({ + title: "API Key ##{id} Updated", + description: changes.join("\n"), + color: YELLOW, + timestamp: Time.now.iso8601, + }) + end + + def blame + if CurrentUser.user.present? + "<@#{CurrentUser.id}> (`#{CurrentUser.name}`)" + elsif Rails.const_defined?("Console") + "**Console**" + elsif CurrentUser.ip_addr + "**#{CurrentUser.ip_addr}**" + else + "**Unknown**" + end + end + end + + include WebhookMethods + + def self.stats(ip: nil, key: nil) + categories = APIImage.categories.pluck(:db) + + keys = [ + "yiffy2:stats:images:total", + *categories.map { |c| "yiffy2:stats:images:total:#{c}" }, + ] + + ipstats = ip.present? + if ipstats + keys.push( + "yiffy2:stats:images:ip:#{ip}", + *categories.map { |c| "yiffy2:stats:images:ip:#{ip}:#{c}" }, + ) + end + keystats = key.present? && !key.is_anon? + if keystats + keys.push( + "yiffy2:stats:images:key:#{key.id}", + *categories.map { |c| "yiffy2:stats:images:key:#{key.id}:#{c}" }, + ) + end + + values = Cache.redis.mget(*keys).map(&:to_i) + total = values.shift + total_specific = values.shift(categories.length) + ip_total = values.shift if ipstats + ip_specific = values.shift(categories.length) if ipstats + key_total = values.shift if keystats + key_specific = values.shift(categories.length) if keystats + + fmt = ->(arr) { + categories.each_with_index + .map { |c, i| hasherizer(c.split("."), arr[i]) } + .reduce({}) { |a, e| a.deep_merge(e) } + } + + { + global: { + total: total, + **fmt.call(total_specific), + }, + ip: if ipstats + { + total: ip_total, + **fmt.call(ip_specific), + } + end, + key: if keystats + { + total: key_total, + **fmt.call(key_specific), + } + end, + } + end + + def self.hasherizer(arr, value) + if arr.empty? + value + else + {}.tap do |hash| + hash[arr.shift] = hasherizer(arr, value) + end + end + end +end diff --git a/app/models/api_usage.rb b/app/models/api_usage.rb new file mode 100644 index 0000000..169b272 --- /dev/null +++ b/app/models/api_usage.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class APIUsage < ApplicationRecord + belongs_to :user, class_name: "APIUser", optional: true + belongs_to :api_key, optional: true +end diff --git a/app/models/api_user.rb b/app/models/api_user.rb new file mode 100644 index 0000000..d61a5cd --- /dev/null +++ b/app/models/api_user.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require "open-uri" + +class APIUser < ApplicationRecord + has_many :api_keys, foreign_key: :owner_id + has_many :api_images, foreign_key: :creator_id + has_many :short_urls, foreign_key: :creator_id + has_many :e621_thumbnails, foreign_key: :creator_id + has_one_attached :avatar + attr_accessor :discord_data + + before_create do + self.level = Levels::DEFAULT if level.blank? + self.name = "User#{id}" if name.blank? + end + + module Levels + DEFAULT = 0 + MANAGER = 10 + ADMIN = 20 + + def self.level_name(level) + name = constants.find { |c| const_get(c) == level }.to_s.titleize + return "Unknown: #{level}" if name.blank? + name + end + end + + module LevelMethods + Levels.constants.each do |constant| + define_method("is_#{constant.downcase}?") do + level >= Levels.const_get(constant) + end + + define_method("is_exactly_#{constant.downcase}?") do + level == Levels.const_get(constant) + end + end + + def level_name + Levels.level_name(level) + end + + def is_anonymous? + self == APIUser.anonymous + end + end + + include LevelMethods + + def self.anonymous + user = new(id: "0", name: "Anonymous", level: Levels::DEFAULT, created_at: Time.now) + user.freeze.readonly! + user + end + + def self.system + sys = find_or_create_by(id: "875334238856163368") + sys.update_columns(name: "System") if sys.name != "System" + sys + end + + def method_missing(method, *) + return discord_data[method.to_s] if discord_data.present? && discord_data.key?(method.to_s) + super + end + + def update_avatar(hash) + return if hash == last_avatar_hash && avatar.attached? + return if Cache.fetch("avatar_update:#{id}") + Cache.write("avatar_update:#{id}", "1", expires_in: 1.day) + avatar.purge + url = "https://yiff.rest/Blep.png" + url = "https://cdn.discordapp.com/avatars/#{id}/#{hash}.webp?size=128" unless hash.nil? + image = URI.open(url) # rubocop:disable Security/Open + avatar.attach(io: image, filename: "#{hash}.webp") + update!(last_avatar_hash: hash) + end + + def can_create_apikey? + api_keys.length < 3 + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..8468393 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class + + module ApiMethods + extend ActiveSupport::Concern + + def as_json(options = {}) + options ||= {} + options[:except] ||= [] + options[:except] += hidden_attributes + + options[:methods] ||= [] + options[:methods] += method_attributes + + super(options) + end + + def serializable_hash(*args) + hash = super(*args) + hash.transform_keys { |key| key.delete("?") } + end + + protected + + def hidden_attributes + %i[uploader_ip_addr updater_ip_addr creator_ip_addr user_ip_addr ip_addr] + end + + def method_attributes + [] + end + end + + module UserMethods + def belongs_to_creator(options = {}) + field = options.delete(:field) || :creator + class_eval do + belongs_to(field, **options.merge(class_name: "APIUser")) + before_validation(on: :create) do |rec| + rec.send("#{field}_id=", CurrentUser.id) if rec.send("#{field}_id").nil? + rec.send("#{field}_ip_addr=", CurrentUser.ip_addr) if rec.respond_to?(:"#{field}_ip_addr=") && rec.send("#{field}_ip_addr").nil? + end + end + end + end + + module SearchMethods + def attribute_matches(attribute, value, **) + return all if value.nil? + + column = column_for_attribute(attribute) + case column.sql_type_metadata.type + when :boolean + boolean_attribute_matches(attribute, value, **) + when :integer, :datetime + numeric_attribute_matches(attribute, value, **) + when :string, :text + text_attribute_matches(attribute, value, **) + else + raise(ArgumentError, "unhandled attribute type") + end + end + + def boolean_attribute_matches(attribute, value) + if value.to_s.truthy? + value = true + elsif value.to_s.falsy? + value = false + else + raise(ArgumentError, "value must be truthy or falsy") + end + + where(attribute => value) + end + + # range: "5", ">5", "<5", ">=5", "<=5", "5..10", "5,6,7" + def numeric_attribute_matches(attribute, range) + column = column_for_attribute(attribute) + qualified_column = "#{table_name}.#{column.name}" + parsed_range = ParseValue.range(range, column.type) + + add_range_relation(parsed_range, qualified_column) + end + + def add_range_relation(arr, field) + return all if arr.nil? + + case arr[0] + when :eq + if arr[1].is_a?(Time) + where("#{field} between ? and ?", arr[1].beginning_of_day, arr[1].end_of_day) + else + where(["#{field} = ?", arr[1]]) + end + when :gt + where(["#{field} > ?", arr[1]]) + when :gte + where(["#{field} >= ?", arr[1]]) + when :lt + where(["#{field} < ?", arr[1]]) + when :lte + where(["#{field} <= ?", arr[1]]) + when :in + where(["#{field} in (?)", arr[1]]) + when :between + where(["#{field} BETWEEN ? AND ?", arr[1], arr[2]]) + else + all + end + end + + def text_attribute_matches(attribute, value, convert_to_wildcard: false) + column = column_for_attribute(attribute) + qualified_column = "#{table_name}.#{column.name}" + value = "*#{value}*" if convert_to_wildcard && value.exclude?("*") + + if value =~ /\*/ + where("lower(#{qualified_column}) LIKE :value ESCAPE E'\\\\'", value: value.downcase.to_escaped_for_sql_like) + else + where("to_tsvector(:ts_config, #{qualified_column}) @@ plainto_tsquery(:ts_config, :value)", ts_config: "english", value: value) + end + end + + def apply_basic_order(params) + case params[:order] + when "id_asc" + order(id: :asc) + when "id_desc" + order(id: :desc) + else + default_order + end + end + + def default_order + order(id: :desc) + end + + def search(params) + params ||= {} + + q = all + q = q.attribute_matches(:id, params[:id]) + q = q.attribute_matches(:created_at, params[:created_at]) if attribute_names.include?("created_at") + q = q.attribute_matches(:updated_at, params[:updated_at]) if attribute_names.include?("updated_at") + + q + end + end + + include ApiMethods + extend SearchMethods + extend UserMethods +end diff --git a/app/models/e621_status.rb b/app/models/e621_status.rb new file mode 100644 index 0000000..be6ed33 --- /dev/null +++ b/app/models/e621_status.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +class E621Status < ApplicationRecord + NOTES = { + 0 => "Some internal issue happened while contacting e621.", + 1 => "E621 is currently in maintenance mode.", + 403 => "E621 is likely experiencing some kind of attack right now, so api endpoints may be returning challenges.", + }.freeze + + STATES = { + 0 => "error", + 1 => "maintenance", + 403 => "partially down", + }.freeze + + STATUS_MESSAGES = { + 0 => "Internal Error", + 1 => "Maintenance", + 2 => "No Status", + 200 => "OK", + 204 => "No Content", + 400 => "Bad Request", + 401 => "Unauthorized", + 403 => "Forbidden", + 404 => "Not Found", + 405 => "Method Not Allowed", + 406 => "Not Acceptable", + 408 => "Request Timeout", + 410 => "Gone", + 429 => "Too Many Requests", + 500 => "Internal Server Error", + 502 => "Bad Gateway", + 503 => "Service Unavailable", + 504 => "Gateway Timeout", + 520 => "Unknown Cloudflare Error", + 521 => "Web Server Is Down", + 522 => "Connection Timed Out", + 523 => "Origin Is Unreachable", + 524 => "A Timeout Occurred", + 525 => "SSL Handshake Failed", + 526 => "Invalid SSL Certificate", + 527 => "Railgun Error", + 530 => "Site Is Frozen", + }.freeze + + after_create :send_notifications + + def available + status >= 200 && status <= 299 + end + + def state + (STATES[status] || available ? "up" : "down").gsub(/ /, "-") + end + + def status_message + STATUS_MESSAGES[status] || "Unknown (#{status})" + end + + def note + NOTES[status] + end + + module ApiMethods + def method_attributes + super + %i[available state status_message note] + end + end + + module SearchMethods + def current + order(id: :desc).first || new(status: 2, created_at: Time.now) + end + + def history(limit = 100) + order(id: :desc).limit(limit) + end + + def combined(limit = 100) + { + current: current, + history: history(limit)[1..], + } + end + end + + include ApiMethods + extend SearchMethods + + def send_notifications + E621Webhook.find_each do |webhook| + webhook.send_update(self) + end + end +end diff --git a/app/models/e621_thumbnail.rb b/app/models/e621_thumbnail.rb new file mode 100644 index 0000000..02cba06 --- /dev/null +++ b/app/models/e621_thumbnail.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +class E621Thumbnail < ApplicationRecord + class DeletedPostError < StandardError; end + + belongs_to_creator + belongs_to :api_key, optional: true + + # def post + # p = Requests::E621.get_post(id: post_id) + # current_md5 = p.try(:[], "file").try(:[], "md5") + # if current_md5.present? && current_md5 != stripped_md5 + # update(md5: p["file"]["md5"]) + # regenerate! + # end + # end + + module FileMethods + def regenerate! + delete_files! + generate! + end + + def generate! + update(status: "generating") + E621ThumbnailJob.perform_later(self) + end + + def delete_files! + StorageManager::E621Thumbnails.delete("#{stripped_md5}.#{filetype}") if StorageManager::E621Thumbnails.exists?("#{stripped_md5}.#{filetype}") + end + + def url + StorageManager::E621Thumbnails.url_for(self) + end + end + + module StatusMethods + def pending? + status == "pending" + end + + def generating? + status == "generating" + end + + def complete? + status == "complete" + end + + def error? + status == "error" + end + + def timeout? + status == "timeout" + end + end + + module CheckMethods + def check_url + "https://thumbs.yiff.rest/check/#{stripped_md5}/#{filetype}" + end + + def check_time + time = Time.now - created_at + case filetype + when "gif" + return 5_000 if time > 45_000 + return 10_000 if time > 30_000 + 15_000 + when "png" + return 2_500 if time > 30_000 + return 5_000 if time > 20_000 + 10_000 + else + 0 + end + end + end + + include FileMethods + include StatusMethods + include CheckMethods + + def stripped_md5 + md5.gsub("-", "") + end + + def refresh! + end + + def self.refresh_all! + unique = E621Thumbnail.select(:md5).distinct + unique.each_slice(100) do |slice| + tags = "md5:#{slice.map(&:stripped_md5).join(',')}" + Rails.logger.debug(tags) + end + end + + def self.from_post(post, type, api_key_id:) + raise(DeletedPostError) if post["flags"]["deleted"] + E621Thumbnail.create!( + api_key_id: api_key_id, + md5: post["file"]["md5"], + filetype: type, + post_id: post["id"], + ) + end + + def self.urls_from_post(post) + find_by_post(post).transform_values { |thumb| thumb&.url } + end + + def self.find_by_post(post, type = nil) + raise(DeletedPostError) if post["flags"]["deleted"] + md5 = post["file"]["md5"] + q = where(md5: md5) + return q.find_by(filetype: type) if type.present? + { + gif: q.find_by(md5: md5, filetype: "gif"), + png: q.find_by(md5: md5, filetype: "png"), + } + end +end diff --git a/app/models/e621_webhook.rb b/app/models/e621_webhook.rb new file mode 100644 index 0000000..e263d83 --- /dev/null +++ b/app/models/e621_webhook.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +class E621Webhook < ApplicationRecord + validate :validate_max_per_channel, on: :create + validate :validate_max_per_guild, on: :create + after_create do + send_creation_message + send_update(E621Status.current) + end + after_destroy do + webhook&.delete unless skip_delete_webhook + send_deletion_message + end + attr_reader :skip_delete_webhook + + MAX_PER_CHANNEL = 1 + MAX_PER_GUILD = 5 + SUCCESS_COLOR = 0x008000 + WARNING_COLOR = 0xFFA500 + ERROR_COLOR = 0xFF0000 + E621_COLOR = 0x012E57 + + def self.from_struct(data, creator_id = nil) + hook = create( + webhook_id: data.id, + webhook_token: data.token, + channel_id: data.channel_id, + creator_id: creator_id, + guild_id: data.guild_id, + ) + + if hook.errors[:channel_id].include?("has too many webhooks") + hook.send_too_many_channel_message + hook.destroy + return :too_many_channel + end + + if hook.errors[:guild_id].include?("has too many webhooks") + hook.send_too_many_guild_message + hook.destroy + return :too_many_guild + end + hook + end + + def validate_max_per_channel + return if channel_id.blank? + return if E621Webhook.for_channel(channel_id).count < MAX_PER_CHANNEL + errors.add(:channel_id, "has too many webhooks") + end + + def validate_max_per_guild + return if guild_id.blank? + return if E621Webhook.for_guild(guild_id).count < MAX_PER_GUILD + errors.add(:guild_id, "has too many webhooks") + end + + module WebhookMethods + def webhook + return nil unless webhook_id && webhook_token + Requests::DiscordWebhook.new(id: webhook_id, token: webhook_token) + end + + def embed(**data) + { + thumbnail: { + url: "https://status.e621.ws/icon.png", + }, + url: "https://status.e621.ws", + timestamp: Time.now.iso8601, + color: E621_COLOR, + **data, + } + end + + def notification_webhook + Websites.config.e621_status_check_logs_webhook + end + + def send_creation_message + execute({ + embeds: [ + embed(title: "E621 Status Check", description: "This webhook has been setup to receive status updates for e621's api#{creator_id.present? ? " by <@#{creator_id}>" : ''}."), + ], + }) + + notification_webhook.execute({ + embeds: [ + embed(title: "Status Check Webhook Added", description: "A status check has been added in the channel **#{channel_id}** of the guild **#{guild_id}**#{creator_id.present? ? " by <@#{creator_id}>" : ''}.", color: SUCCESS_COLOR), + ], + }) + end + + def send_deletion_message + notification_webhook.execute({ + embeds: [ + embed(title: "Status Check Webhook Removed", description: "A status check has been removed in the channel **#{channel_id}** of the guild **#{guild_id}**#{creator_id.present? ? " by <@#{creator_id}>" : ''}.", color: ERROR_COLOR), + ], + }) + end + + def send_too_many_guild_message + execute({ + embeds: [ + embed(title: "E621 Status Check", description: "You've already enabled #{MAX_PER_GUILD} status checks in this server. Please delete the other webhooks before adding a new check. This webhook will be automatically deleted.", color: ERROR_COLOR), + ], + }) + end + + def send_too_many_channel_message + execute({ + embeds: [ + embed(title: "E621 Status Check", description: "You already have a status check enabled in this channel. Delete the other webhook to use a new webhook. This webhook will be automatically deleted.", color: ERROR_COLOR), + ], + }) + end + + def send_update(status) + status_text = E621Status::STATUS_MESSAGES[status.status] + fields = [ + { name: "Status", value: "#{status.status} #{status_text.present? ? "(#{status_text})" : ''}", inline: true }, + { name: "State", value: E621Status::STATES[status.state] || (status.available ? "up" : "down"), inline: true }, + ] + # noinspection RubyMismatchedArgumentType + fields << { name: "Note", value: status.note, inline: false } if status.note.present? + color = status.available ? SUCCESS_COLOR : status.status == 403 ? WARNING_COLOR : ERROR_COLOR + execute({ + embeds: [ + embed(title: "E621 Status Update", description: "E621's api is **#{status.available ? 'available' : 'unavailable'}**.", color: color, + fields: fields, + timestamp: status.created_at.iso8601, + footer: { text: "Since" }), + ], + }) + end + + def execute(body) + r = webhook&.execute(body) + if webhook.nil? || Requests::DiscordWebhook.is_deleted(r) + @skip_delete_webhook = true + destroy + end + r + end + end + + module SearchMethods + def for_guild(id) + where(guild_id: id) + end + + def for_channel(id) + where(channel_id: id) + end + end + + include WebhookMethods + extend SearchMethods +end diff --git a/app/models/exception_log.rb b/app/models/exception_log.rb new file mode 100644 index 0000000..1fe140b --- /dev/null +++ b/app/models/exception_log.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class ExceptionLog < ApplicationRecord + serialize :extra_params, coder: JSON + + def self.add(exception, request) + extra_params = { + host: Socket.gethostname, + params: request.filtered_parameters, + referrer: request.referrer, + user_agent: request.user_agent, + } + + # Required to unwrap exceptions that occur inside template rendering. + unwrapped_exception = exception + unwrapped_exception = exception.cause if exception.is_a?(ActionView::Template::Error) + + if unwrapped_exception.is_a?(ActiveRecord::QueryCanceled) + extra_params[:sql] = {} + extra_params[:sql][:query] = unwrapped_exception.sql || "[NOT FOUND?]" + extra_params[:sql][:binds] = unwrapped_exception.binds + end + + create!( + ip_addr: request.remote_ip || "0.0.0.0", + class_name: unwrapped_exception.class.name, + message: unwrapped_exception.message, + trace: unwrapped_exception.backtrace.join("\n"), + code: SecureRandom.uuid, + version: Websites.config.version, + extra_params: extra_params, + ) + end +end diff --git a/app/models/short_url.rb b/app/models/short_url.rb new file mode 100644 index 0000000..ec89ce4 --- /dev/null +++ b/app/models/short_url.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +class ShortUrl < ApplicationRecord + belongs_to_creator + belongs_to :api_key, optional: true + + validates :code, format: { with: /\A[A-Za-z_\-\d]+\z/ }, length: { maximum: 50 }, presence: true, uniqueness: true + validates :creator_name, length: { maximum: 50 } + validates :url, format: { with: /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/ }, length: { maximum: 2048 }, presence: true + before_validation :set_creator_name_if_blank + + before_create do + self.code ||= SecureRandom.alphanumeric(8) + self.url = url.strip + end + + attr_accessor :no_webhook_messages, :skip_validations + + after_create :send_created, unless: :no_webhook_messages + after_update :send_updated, unless: :no_webhook_messages + after_destroy :send_deleted, unless: :no_webhook_messages + + def self.override(code, url) + prev = find_by(code: code) + return prev if prev&.url == url && prev&.management_code.nil? + prev&.update!(code: "#{code}_#{prev.id}") + CurrentUser.as_system do + create!( + code: code, + creator_name: "YiffyAPI", + creator_ua: "YiffyAPI", + url: url, + ) + end + end + + def shorturl + "https://yiff.rocks/#{code}" + end + + def credit=(value) + self.creator_name = value + end + + def set_creator_name_if_blank + self.creator_name = creator.name if creator_name.blank? + end + + def serializable_hash(*) + { + code: code, + createdAt: created_at, + modifiedAt: updated_at, + url: url, + pos: id, + credit: creator_name || creator&.name, + fullURL: shorturl, + } + end + + module WebhookMethods + GREEN = 0x008000 + YELLOW = 0xFFA500 + RED = 0xFF0000 + + def execute(content) + Websites.config.yiffyapi_apikey_shortener_webhook.execute({ + embeds: [content], + }) + end + + def send_created + execute({ + title: "Short URL Created", + description: <<~DESC, + ID: **#{id}** + Code: **#{code}** + URL: #{url} + IP: **#{creator_ip_addr}** + User-Agent: **#{creator_ua}** + Blame: #{blame} + DESC + color: GREEN, + timestamp: Time.now.iso8601, + }) + end + + def send_deleted + execute({ + title: "Short URL Deleted", + description: <<~DESC, + ID: **#{id}** + Code: **#{code}** + URL: #{url} + Blame: #{blame} + DESC + color: RED, + timestamp: Time.now.iso8601, + }) + end + + def check_change(attr, changes) + changes << "#{attr.to_s.titleize}: **#{send("#{attr}_before_last_save")}** -> **#{send(attr)}**" if send("saved_change_to_#{attr}?") + end + + def send_updated + changes = [] + check_change(:url, changes) + check_change(:creator_name, changes) + + return if changes.empty? + + changes << "Blame: #{blame}" + execute({ + title: "Short URL Updated", + description: changes.join("\n"), + color: YELLOW, + timestamp: Time.now.iso8601, + }) + end + + def blame + if CurrentUser.user.present? + "<@#{CurrentUser.id}> (`#{CurrentUser.name}`)" + elsif Rails.const_defined?("Console") + "**Console**" + elsif CurrentUser.ip_addr + "**#{CurrentUser.ip_addr}" + else + "**Unknown**" + end + end + end + + include WebhookMethods +end diff --git a/app/views/application/_browserconfig.xml.erb b/app/views/application/_browserconfig.xml.erb new file mode 100644 index 0000000..a4f12d1 --- /dev/null +++ b/app/views/application/_browserconfig.xml.erb @@ -0,0 +1,11 @@ + + + + + "/> + "/> + "/> + <%= color %> + + + diff --git a/app/views/application/_dublin_core.html.erb b/app/views/application/_dublin_core.html.erb new file mode 100644 index 0000000..e4ca320 --- /dev/null +++ b/app/views/application/_dublin_core.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/views/application/_head_images.html.erb b/app/views/application/_head_images.html.erb new file mode 100644 index 0000000..6a8e1e3 --- /dev/null +++ b/app/views/application/_head_images.html.erb @@ -0,0 +1,18 @@ +"> +"> +"> +"> +"> +"> +"> +"> +"> +"> +"> +"> +"> + + +"> + +<%= favicon_link_tag image_path("#{assets_path}/favicon.ico") %> diff --git a/app/views/application/_manifest.json.erb b/app/views/application/_manifest.json.erb new file mode 100644 index 0000000..162b092 --- /dev/null +++ b/app/views/application/_manifest.json.erb @@ -0,0 +1,41 @@ +{ + "name": "<%= page_title %>", + "icons": [ + { + "src": "<%= image_path("#{assets_path}/android-icon-36x36.png") %>", + "sizes": "36x36", + "type": "image/png", + "density": "0.75" + }, + { + "src": "<%= image_path("#{assets_path}/android-icon-48x48.png") %>", + "sizes": "48x48", + "type": "image/png", + "density": "1.0" + }, + { + "src": "<%= image_path("#{assets_path}/android-icon-72x72.png") %>", + "sizes": "72x72", + "type": "image/png", + "density": "1.5" + }, + { + "src": "<%= image_path("#{assets_path}/android-icon-96x96.png") %>", + "sizes": "96x96", + "type": "image/png", + "density": "2.0" + }, + { + "src": "<%= image_path("#{assets_path}/android-icon-144x144.png") %>", + "sizes": "144x144", + "type": "image/png", + "density": "3.0" + }, + { + "src": "<%= image_path("#{assets_path}/android-icon-192x192.png") %>", + "sizes": "192x192", + "type": "image/png", + "density": "4.0" + } + ] +} diff --git a/app/views/application/_open_graph.html.erb b/app/views/application/_open_graph.html.erb new file mode 100644 index 0000000..1663ccf --- /dev/null +++ b/app/views/application/_open_graph.html.erb @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/views/application/_twitter_card.html.erb b/app/views/application/_twitter_card.html.erb new file mode 100644 index 0000000..ca229e2 --- /dev/null +++ b/app/views/application/_twitter_card.html.erb @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/views/butts_are_cool/home/custom.html.erb b/app/views/butts_are_cool/home/custom.html.erb new file mode 100644 index 0000000..6861426 --- /dev/null +++ b/app/views/butts_are_cool/home/custom.html.erb @@ -0,0 +1,40 @@ +<% ctype = type.split(/[-_ ]/).map(&:capitalize).join(" ") %> +<% content_for(:html_head) do %> + <% description = "#{ctype} are cool." %> + <%= render "head_images", + manifest_path: send("butts_are_cool_#{type}_manifest_path", :json) + %> + <%= render "twitter_card", + card_creator: "@Donovan_DMC", + card_description: description, + card_image: image_path("#{assets_path}/image.png"), + card_image_alt: "An image of #{type}." + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/image.png"), + og_image_type: "image/png", + og_image_width: 700, + og_image_height: 700, + og_image_alt: "An image of #{type}." + %> +<% end %> + +

+ <%= ctype %> are cool +

+ + <%= image_tag(image_path("#{assets_path}/full.#{file_ext}"), style: "max-width: 80vw; max-height: 70vh;") %> + +
+
+
+

+ Site by Donovan_DMC +

diff --git a/app/views/butts_are_cool/home/index.html.erb b/app/views/butts_are_cool/home/index.html.erb new file mode 100644 index 0000000..fa0344e --- /dev/null +++ b/app/views/butts_are_cool/home/index.html.erb @@ -0,0 +1,39 @@ +<% content_for(:html_head) do %> + <% description = "Butts are cool, as explained by domics." %> + <%= render "head_images", + manifest_path: butts_are_cool_home_manifest_path(:json) + %> + <%= render "twitter_card", + card_creator: "@Donovan_DMC", + card_description: description, + card_image: image_path("#{assets_path}/image.png"), + card_image_alt: "Screenshot From Domic's Video" + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/image.png"), + og_image_type: "image/png", + og_image_width: 700, + og_image_height: 700, + og_image_alt: "Screenshot From Domics' Video" + %> +<% end %> + +

+ Butts Are Cool, As Explained By Domics. +

+ +
+
+
+

+ Site by Donovan_DMC +

diff --git a/app/views/e621_ws/status/_error.html.erb b/app/views/e621_ws/status/_error.html.erb new file mode 100644 index 0000000..797b8a0 --- /dev/null +++ b/app/views/e621_ws/status/_error.html.erb @@ -0,0 +1,7 @@ +

+ We had an issue contacting e621 for a status check. +

+ +

+ Issue first seen: <%= @current.created_at.iso8601 %>. +

diff --git a/app/views/e621_ws/status/_maintenance.html.erb b/app/views/e621_ws/status/_maintenance.html.erb new file mode 100644 index 0000000..10e5fd8 --- /dev/null +++ b/app/views/e621_ws/status/_maintenance.html.erb @@ -0,0 +1,11 @@ +

+ E621 is currently in maintenance mode. +

+ +

+ API Status: Not Available +

+ +

+ We've seen a 503 Service Unavailable (maintenance) since <%= @current.created_at.iso8601 %>. +

diff --git a/app/views/e621_ws/status/_no_status.html.erb b/app/views/e621_ws/status/_no_status.html.erb new file mode 100644 index 0000000..e103be6 --- /dev/null +++ b/app/views/e621_ws/status/_no_status.html.erb @@ -0,0 +1,3 @@ +

+ We currently don't have any status information for e621. +

diff --git a/app/views/e621_ws/status/_primary.erb b/app/views/e621_ws/status/_primary.erb new file mode 100644 index 0000000..b1a245f --- /dev/null +++ b/app/views/e621_ws/status/_primary.erb @@ -0,0 +1,11 @@ +

+ E621 is currently <%= @current.state %>. +

+ +

+ API Status: "><%= @current.available ? "Available" : "Not Available" %> +

+ +

+ We've seen a "><%= @current.status %> since <%= @current.created_at.iso8601 %>. +

diff --git a/app/views/e621_ws/status/index.html.erb b/app/views/e621_ws/status/index.html.erb new file mode 100644 index 0000000..031c306 --- /dev/null +++ b/app/views/e621_ws/status/index.html.erb @@ -0,0 +1,55 @@ +<% content_for(:html_head) do %> + <% description = "A status checker for e621." %> + <%= render "head_images", + manifest_path: e621_ws_status_manifest_path(:json) + %> + <%= render "twitter_card", + card_site: "@e621dotnet", + card_creator: "@Donovan_DMC", + card_description: description, + card_image: image_path("#{assets_path}/icon.png"), + card_image_alt: "E621 Icon" + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/icon.png"), + og_image_type: "image/png", + og_image_width: 300, + og_image_height: 300, + og_image_alt: "E621 Icon" + %> +<% end %> + +<%= @current.created_at.iso8601 %> + +<% case @current.status %> +<% when 0 %> + <%= render "error" %> +<% when 1 %> + <%= render "maintenance" %> +<% when 2 %> + <%= render "no_status" %> +<% else %> + <%= render "primary" %> +<% end %> + +

+ Looking to access this data automatically, or see the history? We have a JSON endpoint.
+ We also provide a Discord Webhook service. +

+ +<% if @current.note %> +

+ <%= @current.note %> +

+<% end %> + +

+ Note: This is not an official status page. It is ran by Donovan_DMC as a communal utility. +

diff --git a/app/views/e621_ws/status/schema/combined.json b/app/views/e621_ws/status/schema/combined.json new file mode 100644 index 0000000..4534b9b --- /dev/null +++ b/app/views/e621_ws/status/schema/combined.json @@ -0,0 +1,91 @@ +{ + "type": "object", + "properties": { + "current": { + "type": "object", + "properties": { + "available": { + "type": "boolean" + }, + "state": { + "enum": [ + "up", + "down", + "partially-down", + "maintenance", + "error" + ], + "type": "string" + }, + "status": { + "type": "number" + }, + "statusMessage": { + "type": "string" + }, + "since": { + "type": "string" + } + }, + "required": [ + "available", + "state", + "status", + "statusMessage", + "since" + ] + }, + "history": { + "type": "array", + "items": { + "type": "object", + "properties": { + "available": { + "type": "boolean" + }, + "state": { + "enum": [ + "up", + "down", + "partially-down", + "maintenance", + "error" + ], + "type": "string" + }, + "status": { + "type": "number" + }, + "statusMessage": { + "type": "string" + }, + "since": { + "type": "string" + }, + "note": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + } + }, + "required": [ + "available", + "state", + "status", + "statusMessage", + "since", + "note" + ] + } + } + }, + "required": [ + "current", + "history" + ] +} diff --git a/app/views/e621_ws/status/schema/current.json b/app/views/e621_ws/status/schema/current.json new file mode 100644 index 0000000..33b2126 --- /dev/null +++ b/app/views/e621_ws/status/schema/current.json @@ -0,0 +1,34 @@ +{ + "type": "object", + "properties": { + "available": { + "type": "boolean" + }, + "state": { + "enum": [ + "up", + "down", + "partially-down", + "maintenance", + "error" + ], + "type": "string" + }, + "status": { + "type": "number" + }, + "statusMessage": { + "type": "string" + }, + "since": { + "type": "string" + } + }, + "required": [ + "available", + "state", + "status", + "statusMessage", + "since" + ] +} diff --git a/app/views/e621_ws/status/schema/history.json b/app/views/e621_ws/status/schema/history.json new file mode 100644 index 0000000..25e4754 --- /dev/null +++ b/app/views/e621_ws/status/schema/history.json @@ -0,0 +1,45 @@ +{ + "type": "object", + "properties": { + "available": { + "type": "boolean" + }, + "state": { + "enum": [ + "up", + "down", + "partially-down", + "maintenance", + "error" + ], + "type": "string" + }, + "status": { + "type": "number" + }, + "statusMessage": { + "type": "string" + }, + "since": { + "type": "string" + }, + "note": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + } + }, + "required": [ + "available", + "state", + "status", + "statusMessage", + "since", + "note" + ] +} diff --git a/app/views/e621_ws/status/webhooks/index.html.erb b/app/views/e621_ws/status/webhooks/index.html.erb new file mode 100644 index 0000000..1966ba4 --- /dev/null +++ b/app/views/e621_ws/status/webhooks/index.html.erb @@ -0,0 +1,60 @@ +<% content_for(:html_head) do %> + + + +<% end %> + +<% content_for(:page_title) do %> + E621 Status | Webhook Prerequisites +<% end %> + +<%# TODO: code links %> + +

+ Webhook Prerequisites +

+

+ A Discord webhook is a good way to get status updates right inside of Discord, the minute we know about changes.
+ The link at the bottom of the page will take you to an OAuth authorization page, which will create a webhook and give the information to us.
+ We do not accept previously create webhooks, this OAuth flow must be followed. +

+

+ Don't trust me? That's fine, I wouldn't trust an online stranger either.
+ You can view the code for this site here.
+ The code for handling setting up webhooks is specifically in TODO. +

+

+ There's a hard limit of one webhook per channel, and 5 webhooks per server.
+ The latter may be negotiable, contact Donovan_DMC if needs be.
+ To disable a status check, simply delete the webhook.
+ The webhook will be marked as "managed" by us, so it serves no other use. +

+

+ Some other miscellaneous information.
+ Webhook details are not stored in an encrypted format, but the database they are in is disconnected from the internet.
+ The OAuth authorization does not add a bot or application to your server, just the webhook.
+ You can view an example of some status updates here.
+ Updates are posted as soon as we know about them currently. In the future, updates may be delayed to avoid spam. +

+

+ With all of that out of the way, the buttons. +

+

+ The minimal button will only authorize the webhook.
+ The other button will authorize both the webhook and your user info (which is optional).
+ If authorized, the id of the user who created the webhook will be stored on our servers alongside the webhook information.
+ This is purely for informational purposes.
+

+ +
+ +
diff --git a/app/views/furry_cool/home/index.html.erb b/app/views/furry_cool/home/index.html.erb new file mode 100644 index 0000000..211f7fe --- /dev/null +++ b/app/views/furry_cool/home/index.html.erb @@ -0,0 +1,44 @@ +<% content_for(:html_head) do %> + <% description = "Hi, I'm Donovan! I like to code and do other random things!" %> + <%= render "head_images", + manifest_path: furry_cool_home_manifest_path(:json) + %> + <%= render "twitter_card", + card_creator: "@Donovan_DMC", + card_description: description, + card_image: image_path("#{assets_path}/DonPride.png"), + card_image_alt: "Donovan_DMC's Icon" + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/DonPride.png"), + og_image_type: "image/png", + og_image_width: 4000, + og_image_height: 4000, + og_image_alt: "Donovan_DMC's Icon" + %> +<% end %> + +<%= image_tag("#{assets_path}/DonPrideTransparent.png", class: "logo") %> +

Donovan_DMC

+

+
+
+ Twitter + | + Github + | + Discord Bot +
+ Discord Server + | + Discord Bot Library + | + E621 Library +

diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000..a2ba0d3 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,43 @@ +<% console if Rails.env.development? %> + + + + <%= page_title %> + + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + <% if content_for(:html_head) %> + <%= yield :html_head %> + <% end %> + + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= javascript_include_tag "application", "data-turbo-track": "reload" %> + + + +
+
+
+
+ <% if flash[:notice] %> +
+ <%= flash[:notice] %> + close +
+ <% end %> + + <% if flash[:alert] %> +
+ <%= flash[:alert] %> + close +
+ <% end %> + + <%= yield :layout %> +
+
+
+
+ + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..3aac900 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/maidboye_cafe/home/index.html.erb b/app/views/maidboye_cafe/home/index.html.erb new file mode 100644 index 0000000..79a9624 --- /dev/null +++ b/app/views/maidboye_cafe/home/index.html.erb @@ -0,0 +1,55 @@ +<% content_for(:html_head) do %> + + + <% description = "Come meet the successor of Furry Bot, Maid Boye! I'm a cute little maid bot for keeping your server nice and tidy." %> + <%= render "head_images", + manifest_path: maidboye_cafe_home_manifest_path(:json) + %> + <%= render "twitter_card", + card_creator: "@Donovan_DMC", + card_site: "@MaidBoye", + card_description: description, + card_image: image_path("#{assets_path}/MaidShy.png"), + card_image_alt: "MaidBoye's Icon" + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/MaidShy.png"), + og_image_type: "image/png", + og_image_width: 512, + og_image_height: 512, + og_image_alt: "MaidBoye's Icon" + %> + + +<% end %> + + +

+ Hi, I'm Maid Boye! +

+

+ A cute little maid bot for keeping your server nice and tidy. +

+

+ To invite me to a server, click + <%= link_to "here", maidboye_cafe_invite_path %>, and for support join <%= link_to "this", maidboye_cafe_support_path %> Discord server. +

+
+
+
+ <%= image_tag("#{assets_path}/MaidShy.png") %> +
+ diff --git a/app/views/maidboye_cafe/home/privacy.html.erb b/app/views/maidboye_cafe/home/privacy.html.erb new file mode 100644 index 0000000..afbd0f4 --- /dev/null +++ b/app/views/maidboye_cafe/home/privacy.html.erb @@ -0,0 +1,46 @@ +<% content_for(:html_head) do %> + <%= favicon_link_tag "#{assets_path}/favicon.ico" %> +<% end %> + +<% content_for(:page_title) do %> + Maid Boye - Privacy Policy +<% end %> + +

Information We Collect

+

We collect the following information from users:

+
    +
  • Guild IDs
  • +
  • Channel IDs
  • +
  • Message IDs
  • +
  • User IDs
  • +
  • Custom Guild Settings
  • +
  • Deleted/Updated Message Content (if snipe features are enabled)
  • +
+

Depending on what logging is enabled, we may use (but not store):

+
    +
  • Channels (create, delete, update)
  • +
  • Emojis (create, delete, update)
  • +
  • Members (add, remove, kick, ban add, ban remove)
  • +
  • Roles (create, delete, update)
  • +
  • Guild (update)
  • +
  • Invites (create, delete, use)
  • +
  • Messages (delete, delete bulk*, update)
  • +
  • Threads (create, delete, update, join, leave, member update)
  • +
  • Voice (join, leave, state update)
  • +
+

* Bulk deletions are stored so the contents can be displayed. They are stored in an encrypted format and deleted after 30 days.

+

Why we need this information

+

We need this information to provide basic services, and to enhance the experience for the specific user.

+

If sniping is enabled, we also store deleted and edited message content. These can be disabled on per user, and server wide. All content stored is fully encrypted, and stored for only 6 hours at most.

+

How we use your information

+

We use this information in the following ways:

+
    +
  • Providing the basic services of the bot
  • +
  • Providing extra functionalities if enabled
  • +

    NO INFORMATION IS SHARED WITH ANY THIRD PARTIES

    +
+

Log Files

+

To combat spam we store how many of each command a given user runs. The specifically entered content is not taken into account, nor are the server or channel.

+

Data Removal

+

All message content will be removed after a set time. Snipes are removed after 6 hours, or as soon as they are "sniped". Bulk deletion reports are deleted after 30 days, or upon request.

+

To request your data be removed, please either contact us in our Discord Server, or email hewwo@yiff.rocks with your request. If you want no user data to be collected further, we will blacklist you so you are completely ignored by us. Removal of this requires contacting a developer again.

diff --git a/app/views/oceanic_ws/docs/index.html.erb b/app/views/oceanic_ws/docs/index.html.erb new file mode 100644 index 0000000..120c359 --- /dev/null +++ b/app/views/oceanic_ws/docs/index.html.erb @@ -0,0 +1,42 @@ +<% content_for(:html_head) do %> + <% description = "A NodeJS library for interfacing with Discord." %> + <%= render "head_images", + manifest_path: oceanic_ws_home_manifest_path(:json) + %> + <%= render "twitter_card", + card_creator: "@Donovan_DMC", + card_site: "@OceanicJS", + card_description: description, + card_image: image_path("#{assets_path}/icon.png"), + card_image_alt: "Oceanic Icon" + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/icon.png"), + og_image_type: "image/png", + og_image_width: 363, + og_image_height: 363, + og_image_alt: "Oceanic Icon" + %> +<% end %> + +<% @versions[:branches].each_slice(4) do |row| %> +

+ <% row.each do |version| %> + <%= link_to version, "/#{version}" %>  + <% end %> +

+<% end %> +<% @versions[:tags].each_slice(4) do |row| %> +

+ <% row.each do |version| %> + <%= link_to version, "/#{version}" %>  + <% end %> +

+<% end %> diff --git a/app/views/oceanic_ws/home/index.html.erb b/app/views/oceanic_ws/home/index.html.erb new file mode 100644 index 0000000..60dd020 --- /dev/null +++ b/app/views/oceanic_ws/home/index.html.erb @@ -0,0 +1,38 @@ +<% content_for(:html_head) do %> + <% description = "A NodeJS library for interfacing with Discord." %> + <%= render "head_images", + manifest_path: oceanic_ws_home_manifest_path(:json) + %> + <%= render "twitter_card", + card_creator: "@Donovan_DMC", + card_site: "@OceanicJS", + card_description: description, + card_image: image_path("#{assets_path}/icon.png"), + card_image_alt: "Oceanic Icon" + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/icon.png"), + og_image_type: "image/png", + og_image_width: 363, + og_image_height: 363, + og_image_alt: "Oceanic Icon" + %> +<% end %> + + + +

Oceanic

+

+ Github + | + Documentation + | + NPM +

diff --git a/app/views/static/access_denied.html.erb b/app/views/static/access_denied.html.erb new file mode 100644 index 0000000..30af641 --- /dev/null +++ b/app/views/static/access_denied.html.erb @@ -0,0 +1,6 @@ + +<% content_for(:page_title) do %> + Access Denied +<% end %> + +

<%= @message %>

diff --git a/app/views/static/access_denied.json.erb b/app/views/static/access_denied.json.erb new file mode 100644 index 0000000..ca22b34 --- /dev/null +++ b/app/views/static/access_denied.json.erb @@ -0,0 +1,5 @@ +{ + "success": false, + "message": "<%= @message %>", + "code": <%= raw @code.to_json %> +} diff --git a/app/views/static/error.html.erb b/app/views/static/error.html.erb new file mode 100644 index 0000000..b605baa --- /dev/null +++ b/app/views/static/error.html.erb @@ -0,0 +1,12 @@ +<% content_for(:page_title) do %> + Error +<% end %> + +

<%= @message %>

+<% if Rails.env.production? %> + <% if @log_code.present? %> +

Log Code: <%= @log_code %>

+ <% end %> +<% else %> + <%= @backtrace %> +<% end %> diff --git a/app/views/static/error.json.erb b/app/views/static/error.json.erb new file mode 100644 index 0000000..944bf34 --- /dev/null +++ b/app/views/static/error.json.erb @@ -0,0 +1,5 @@ +{ + "success": false, + "message": <%= raw @message.to_json %>, + "code": <%= raw @log_code.to_json %> +} diff --git a/app/views/static/not_found.html.erb b/app/views/static/not_found.html.erb new file mode 100644 index 0000000..6fa7ac7 --- /dev/null +++ b/app/views/static/not_found.html.erb @@ -0,0 +1,6 @@ + +<% content_for(:page_title) do %> + Not Found +<% end %> + +

Not Found

diff --git a/app/views/static/not_found.json.erb b/app/views/static/not_found.json.erb new file mode 100644 index 0000000..6621783 --- /dev/null +++ b/app/views/static/not_found.json.erb @@ -0,0 +1,5 @@ +{ + "success": false, + "message": "Not found.", + "code": null +} diff --git a/app/views/static/readonly.html.erb b/app/views/static/readonly.html.erb new file mode 100644 index 0000000..d4ee57c --- /dev/null +++ b/app/views/static/readonly.html.erb @@ -0,0 +1,6 @@ +<% content_for(:page_title) do %> + Read Only +<% end %> + +

Read Only

+

This service is currently in read only mode. Try again later.

diff --git a/app/views/yiff_media/home/index.html.erb b/app/views/yiff_media/home/index.html.erb new file mode 100644 index 0000000..b3737c6 --- /dev/null +++ b/app/views/yiff_media/home/index.html.erb @@ -0,0 +1,45 @@ +<% content_for(:html_head) do %> + <% description = "An image api by furries, for furries." %> + <%= render "head_images", + manifest_path: yiff_media_home_manifest_path(:json) + %> + <%= render "twitter_card", + card_creator: "@Donovan_DMC", + card_description: description, + card_image: image_path("#{assets_path}/Blep.png"), + card_image_alt: "YiffyAPI Icon" + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/Blep.png"), + og_image_type: "image/png", + og_image_width: 710, + og_image_height: 710, + og_image_alt: "YiffyAPI Icon" + %> +<% end %> + + +

+
+ This domain serves as a cdn for my projects. There isn't much here for you. +
+

+ +

+
+ Check Out My Things
+
+
+ Twitter
+ Discord Bot
+ Discord Server
+ Yiffy API
+
+

diff --git a/app/views/yiff_media/reports/index.html.erb b/app/views/yiff_media/reports/index.html.erb new file mode 100644 index 0000000..2543274 --- /dev/null +++ b/app/views/yiff_media/reports/index.html.erb @@ -0,0 +1,35 @@ +<% content_for(:html_head) do %> + <% description = "An image api by furries, for furries." %> + <%= render "head_images", + manifest_path: yiff_media_home_manifest_path(:json) + %> + <%= render "twitter_card", + card_creator: "@Donovan_DMC", + card_description: description, + card_image: image_path("#{assets_path}/Blep.png"), + card_image_alt: "YiffyAPI Icon" + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/Blep.png"), + og_image_type: "image/png", + og_image_width: 710, + og_image_height: 710, + og_image_alt: "YiffyAPI Icon" + %> +<% end %> + + +
+

<%= page_title %>

+ +

+ Reports are currently disabled. +

+
diff --git a/app/views/yiff_rest/apikeys/_apikey.html.erb b/app/views/yiff_rest/apikeys/_apikey.html.erb new file mode 100644 index 0000000..9c2faa6 --- /dev/null +++ b/app/views/yiff_rest/apikeys/_apikey.html.erb @@ -0,0 +1,62 @@ +
+
+ <% if CurrentUser.is_manager? %> +
+ <% if key.owner.avatar.attached? %> + <%= image_tag(key.owner.avatar, width: 30, height: 30, class: "rounded-4") %> + <% end %> + <%= key.owner.name %> +
+ <% end %> +
<%= key.application_name %>
+

<%= key.usage %>

+ +
Services
+

<%= key.access_string %>

+ <% if key.disabled? %> +
Disabled
+

<%= key.disabled_reason || "No Reason Provided" %>

+ <% end %> + <% unless key.active? %> +
Inactive
+ <% end %> + +
+ <% if key.can_view?(CurrentUser) %> + + <% end %> + <% if key.can_edit?(CurrentUser) %> + <% if CurrentUser.is_admin? %> + <%= link_to "Edit", edit_yiff_rest_apikey_path(key), class: "btn btn-success" %> + <% else %> + + <% end %> + <% end %> + <% if key.can_delete?(CurrentUser) %> + + <% end %> + + +
+
+
diff --git a/app/views/yiff_rest/apikeys/_delete_modal.html.erb b/app/views/yiff_rest/apikeys/_delete_modal.html.erb new file mode 100644 index 0000000..4e7cd9c --- /dev/null +++ b/app/views/yiff_rest/apikeys/_delete_modal.html.erb @@ -0,0 +1,17 @@ + diff --git a/app/views/yiff_rest/apikeys/_disable_modal.html.erb b/app/views/yiff_rest/apikeys/_disable_modal.html.erb new file mode 100644 index 0000000..089a2f5 --- /dev/null +++ b/app/views/yiff_rest/apikeys/_disable_modal.html.erb @@ -0,0 +1,21 @@ + diff --git a/app/views/yiff_rest/apikeys/_edit_modal.html.erb b/app/views/yiff_rest/apikeys/_edit_modal.html.erb new file mode 100644 index 0000000..ed6692f --- /dev/null +++ b/app/views/yiff_rest/apikeys/_edit_modal.html.erb @@ -0,0 +1,24 @@ + diff --git a/app/views/yiff_rest/apikeys/_nav.html.erb b/app/views/yiff_rest/apikeys/_nav.html.erb new file mode 100644 index 0000000..bb56d22 --- /dev/null +++ b/app/views/yiff_rest/apikeys/_nav.html.erb @@ -0,0 +1,52 @@ + diff --git a/app/views/yiff_rest/apikeys/_view_modal.html.erb b/app/views/yiff_rest/apikeys/_view_modal.html.erb new file mode 100644 index 0000000..5df8c34 --- /dev/null +++ b/app/views/yiff_rest/apikeys/_view_modal.html.erb @@ -0,0 +1,16 @@ + diff --git a/app/views/yiff_rest/apikeys/edit.html.erb b/app/views/yiff_rest/apikeys/edit.html.erb new file mode 100644 index 0000000..57b219b --- /dev/null +++ b/app/views/yiff_rest/apikeys/edit.html.erb @@ -0,0 +1,28 @@ +<% content_for(:page_title) do %> + YiffyAPI - Edit API Key +<% end %> + +<%= render partial: "yiff_rest/home/head" %> +<%= render partial: "nav" %> + +
+
+ <%= simple_form_for(@apikey, url: yiff_rest_apikey_path(@apikey)) do |f| %> + <%= f.input(:application_name, label: "Application Name") %> + <%= f.input(:usage) %> + <%= f.input(:limit_long, input_html: { min: 0 }, label: "Limit (Long)") %> + <%= f.input(:window_long, input_html: { min: 0 }, label: "Window (Long)") %> + <%= f.input(:limit_short, input_html: { min: 0 }, label: "Limit (Short)") %> + <%= f.input(:window_short, input_html: { min: 0 }, label: "Window (Short)") %> + <%= f.input(:bulk_limit, input_html: { min: 0 }, label: "Bulk Limit") %> + <%= f.input(:owner_id, label: "Owner ID") %> +
+ <%= f.input(:flags_images, as: :boolean, label: "Images Access") %> + <%= f.input(:flags_thumbs, as: :boolean, label: "Thumbs Access") %> + <%= f.input(:flags_shortener, as: :boolean, label: "Shortener Access") %> + <%= f.input(:flags_images_bulk, as: :boolean, label: "Images Bulk Access") %> +
+ <%= f.button(:submit, name: nil, label: "Save Changes") %> + <% end %> +
+
diff --git a/app/views/yiff_rest/apikeys/index.html.erb b/app/views/yiff_rest/apikeys/index.html.erb new file mode 100644 index 0000000..f662db2 --- /dev/null +++ b/app/views/yiff_rest/apikeys/index.html.erb @@ -0,0 +1,27 @@ +<%= render partial: "yiff_rest/home/head" %> +<%= render partial: "nav" %> + + <% @apikeys.each_slice(4) do |keys| %> +
+ <% keys.each do |key| %> + <%= render partial: "apikey", locals: { key: key } %> + <% end %> +
+ <% end %> + +<% @apikeys.each do |key| %> +
+ <% if key.can_view?(CurrentUser) %> + <%= render partial: "view_modal", locals: { key: key } %> + <% end %> + <% if key.can_edit?(CurrentUser) %> + <%= render partial: "edit_modal", locals: { key: key } %> + <% end %> + <% if key.can_delete?(CurrentUser) %> + <%= render partial: "delete_modal", locals: { key: key } %> + <% end %> + <% if key.can_disable?(CurrentUser) %> + <%= render partial: "disable_modal", locals: { key: key } %> + <% end %> +
+<% end %> diff --git a/app/views/yiff_rest/apikeys/new.html.erb b/app/views/yiff_rest/apikeys/new.html.erb new file mode 100644 index 0000000..073e6b5 --- /dev/null +++ b/app/views/yiff_rest/apikeys/new.html.erb @@ -0,0 +1,30 @@ +<% content_for(:page_title) do %> + YiffyAPI - Create API Key +<% end %> + +<%= render partial: "yiff_rest/home/head" %> +<%= render partial: "nav" %> + +
+
+ <%= simple_form_for(@apikey, url: yiff_rest_apikeys_path, method: :post) do |f| %> + <%= f.input(:application_name, label: "Application Name", placeholder: "The name of your application.", required: true) %> + <%= f.input(:usage, placeholder: "What you plan to do with your apikey.", required: true) %> + <% if CurrentUser.is_admin? %> + <%= f.input(:limit_long, input_html: { min: 0 }, label: "Limit (Long)") %> + <%= f.input(:window_long, input_html: { min: 0 }, label: "Window (Long)") %> + <%= f.input(:limit_short, input_html: { min: 0 }, label: "Limit (Short)") %> + <%= f.input(:window_short, input_html: { min: 0 }, label: "Window (Short)") %> + <%= f.input(:bulk_limit, input_html: { min: 0 }, label: "Bulk Limit") %> + <%= f.input(:owner_id, label: "Owner ID") %> +
+ <%= f.input(:flags_images, as: :boolean, label: "Images Access Flag") %> + <%= f.input(:flags_thumbs, as: :boolean, label: "Thumbs Access Flag") %> + <%= f.input(:flags_shortener, as: :boolean, label: "Shortener Access Flag") %> + <%= f.input(:flags_images_bulk, as: :boolean, label: "Images Bulk Access Flag") %> +
+ <% end %> + <%= f.button(:submit, name: nil, label: "Create API Key") %> + <% end %> +
+
diff --git a/app/views/yiff_rest/discord/_head.html.erb b/app/views/yiff_rest/discord/_head.html.erb new file mode 100644 index 0000000..e882db7 --- /dev/null +++ b/app/views/yiff_rest/discord/_head.html.erb @@ -0,0 +1,26 @@ +<%= content_for(:html_head) do %> + <% description = "Random Discord utilities." %> + <%= render "head_images", + manifest_path: yiff_rest_home_manifest_path(:json) + %> + <%= render "twitter_card", + card_creator: "@Donovan_DMC", + card_description: description, + card_image: image_path("#{assets_path}/Blep.png"), + card_image_alt: "YiffyAPI Icon" + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/Blep.png"), + og_image_type: "image/png", + og_image_width: 710, + og_image_height: 710, + og_image_alt: "YiffyAPI Icon" + %> +<% end %> diff --git a/app/views/yiff_rest/discord/count_servers.html.erb b/app/views/yiff_rest/discord/count_servers.html.erb new file mode 100644 index 0000000..d3a25c3 --- /dev/null +++ b/app/views/yiff_rest/discord/count_servers.html.erb @@ -0,0 +1,19 @@ +<% content_for(:page_title) do %> + YiffyAPI - Count Servers +<% end %> + +<%= render partial: "head" %> + +Total Servers: <%= @info[:total] %>
+Owner Of: <%= @info[:owner] %>
+Admin Of: <%= @info[:admin] %> (<%= @info[:admin_owner] %>)
+
+Other Links: +
    +
  • + <%= link_to "Flags", yiff_rest_discord_flags_path %> +
  • +
  • + <%= link_to "Refresh Servers", yiff_rest_discord_count_servers_path %> +
  • +
diff --git a/app/views/yiff_rest/discord/flags.html.erb b/app/views/yiff_rest/discord/flags.html.erb new file mode 100644 index 0000000..189ed7c --- /dev/null +++ b/app/views/yiff_rest/discord/flags.html.erb @@ -0,0 +1,27 @@ +<% content_for(:page_title) do %> + YiffyAPI - Flags +<% end %> + +<%= render partial: "head" %> + +Number: <%= @info[:public_number] %> ([A] <%= @info[:all_number] %>)
+
+Flags:
+<% @info[:public_flags].each do |flag| %> + - <%= flag %>
+<% end %> +
+<% unless @info[:all_flags].empty? %> +All Flags:
+ <% @info[:all_flags].each do |flag | %> +- [A] <%= flag %>
+<% end %> +<% end %> +
    +
  • + <%= link_to "Refresh Flags", yiff_rest_discord_flags_path %> +
  • +
  • + <%= link_to "Count Servers", yiff_rest_discord_count_servers_path %> +
  • +
diff --git a/app/views/yiff_rest/discord/index.html.erb b/app/views/yiff_rest/discord/index.html.erb new file mode 100644 index 0000000..c38ec1c --- /dev/null +++ b/app/views/yiff_rest/discord/index.html.erb @@ -0,0 +1,9 @@ +<%= render partial: "head" %> +
    +
  • + <%= link_to "Count Servers", yiff_rest_discord_count_servers_path %> +
  • +
  • + <%= link_to "Flags", yiff_rest_discord_flags_path %> +
  • +
diff --git a/app/views/yiff_rest/home/_head.html.erb b/app/views/yiff_rest/home/_head.html.erb new file mode 100644 index 0000000..32a47e2 --- /dev/null +++ b/app/views/yiff_rest/home/_head.html.erb @@ -0,0 +1,26 @@ +<%= content_for(:html_head) do %> + <% description = "A hand picked image api, for furries." %> + <%= render "head_images", + manifest_path: yiff_rest_home_manifest_path(:json) + %> + <%= render "twitter_card", + card_creator: "@Donovan_DMC", + card_description: description, + card_image: image_path("#{assets_path}/Blep.png"), + card_image_alt: "YiffyAPI Icon" + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/Blep.png"), + og_image_type: "image/png", + og_image_width: 710, + og_image_height: 710, + og_image_alt: "YiffyAPI Icon" + %> +<% end %> diff --git a/app/views/yiff_rest/home/index.html.erb b/app/views/yiff_rest/home/index.html.erb new file mode 100644 index 0000000..30b1bbf --- /dev/null +++ b/app/views/yiff_rest/home/index.html.erb @@ -0,0 +1,43 @@ +<% content_for(:html_head) do %> + + + <%= render partial: "yiff_rest/home/head" %> + + + +<% end %> + + + +

+ +

+ Yiffy API +

+

+ Pardon the blandness, but I cannot design websites to save my life. +

+

+ This is a combination <%= link_to "image api", "https://state.yiff.rest" %>, <%= link_to "url shortener", "https://yiff.rocks" %>, and <%= link_to "e621 video thumbnailer", "https://thumbs.yiff.rest" %>. +

+

+ You can see the documentation <%= link_to "here", "https://docs.yiff.rest" %>, or use our + <%= link_to "npm module", "https://npm.im/yiffy" %>. +

+

+ You can manage your apikeys <%= link_to "here", yiff_rest_apikeys_path %>. +

diff --git a/app/views/yiff_rest/state/index.html.erb b/app/views/yiff_rest/state/index.html.erb new file mode 100644 index 0000000..2d28d15 --- /dev/null +++ b/app/views/yiff_rest/state/index.html.erb @@ -0,0 +1,36 @@ +<% content_for(:html_head) do %> + + + <%= render partial: "yiff_rest/home/head" %> + + + +<% end %> + + + + + + + + + + <% @state.each do |state| %> + + + + + <% end %> + +
Category NameImage Count
<%= state[:name] %><%= state[:count] %>
diff --git a/app/views/yiff_rocks/home/index.html.erb b/app/views/yiff_rocks/home/index.html.erb new file mode 100644 index 0000000..b37fbdb --- /dev/null +++ b/app/views/yiff_rocks/home/index.html.erb @@ -0,0 +1,52 @@ +<% content_for(:html_head) do %> + + + <% description = "A url shortener made by furries, for furries." %> + <%= render "head_images", + manifest_path: yiff_rocks_home_manifest_path(:json) + %> + <%= render "twitter_card", + card_creator: "@Donovan_DMC", + card_description: description, + card_image: image_path("#{assets_path}/Blep.png"), + card_image_alt: "YiffyAPI Icon" + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/Blep.png"), + og_image_type: "image/png", + og_image_width: 710, + og_image_height: 710, + og_image_alt: "YiffyAPI Icon" + %> +<% end %> + + + +

+ +

+ Yiff Rocks +

+

+ Pardon the blandness, but I cannot design websites to save my life. +

+

+ This is a url shortener. +

+

+ You can see the documentation here, or use our + npm module. +

+ +

+ Using this service requires an apikey, which you can get <%= link_to "here", "https://yiff.rest/apikeys" %> +

diff --git a/app/views/yiff_rocks/home/preview.html.erb b/app/views/yiff_rocks/home/preview.html.erb new file mode 100644 index 0000000..c58492f --- /dev/null +++ b/app/views/yiff_rocks/home/preview.html.erb @@ -0,0 +1,32 @@ +<% content_for(:html_head) do %> + + + <% description = "A shortened url: #{@short.url}" %> + <%= render "head_images", + manifest_path: yiff_rocks_home_manifest_path(:json) + %> + <%= render "twitter_card", + card_creator: "@Donovan_DMC", + card_description: description, + card_image: image_path("#{assets_path}/Blep.png"), + card_image_alt: "YiffyAPI Icon" + %> + + <%= render "dublin_core", + dc_creator: "Donovan_DMC", + dc_description: description + %> + + <%= render "open_graph", + og_description: description, + og_image: image_path("#{assets_path}/Blep.png"), + og_image_type: "image/png", + og_image_width: 710, + og_image_height: 710, + og_image_alt: "YiffyAPI Icon" + %> +<% end %> + +

+ This short url redirects to <%= link_to @short.url, @short.shorturl %> +

diff --git a/bin/assets b/bin/assets new file mode 100755 index 0000000..d75a351 --- /dev/null +++ b/bin/assets @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +docker compose -f docker-compose.prod.yml run --rm -e RAILS_ENV=production websites rake assets:precompile diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000..42c7fd7 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000..a4e05fa --- /dev/null +++ b/bin/dev @@ -0,0 +1,11 @@ +#!/usr/bin/env sh + +if ! gem list foreman -i --silent; then + echo "Installing foreman..." + gem install foreman +fi + +# Default to port 3000 if not specified +export PORT="${PORT:-3000}" + +exec foreman start -f Procfile.dev "$@" diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000..67ef493 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,8 @@ +#!/bin/bash -e + +# If running the rails server then create or migrate existing database +if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..efc0377 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..4fbf10b --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..ad1e8bd --- /dev/null +++ b/bin/setup @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path("..", __dir__) +RAILS_ENV = ENV.fetch("RAILS_ENV", "development") + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "\n== Copying sample files ==" + unless File.exist?('config/local_config.rb') + FileUtils.cp 'docker/local_config.rb', 'config/local_config.rb' + end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..2e03084 --- /dev/null +++ b/config.ru @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..ec56eaa --- /dev/null +++ b/config/application.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require_relative "boot" +require_relative "../lib/custom_static_middleware" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +require_relative "default_config" +require_relative "local_config" + +module Websites + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults(7.1) + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + # + config.autoload_paths += Dir[Rails.root.join(Rails.root.join("config/routes/**/"))] + + config.action_controller.action_on_unpermitted_parameters = :raise + config.action_dispatch.default_headers.clear + + config.middleware.insert_before(ActionDispatch::Static, CustomStaticMiddleware, { + /^i\.furry\.cool/ => "/furry.cool/images", + /^i\.maidboye\.cafe/ => "/maidboye.cafe/images", + %r{^maidboye\.cafe/screenshots} => "/maidboye.cafe", + /^assets\.maidboye\.cafe/ => "/maidboye.cafe/assets", + /^i\.oceanic\.ws/ => "/oceanic.ws/images", + %r{^yiff\.rest/Blep\.png$} => "/yiff.rest", + %r{^yiff\.rocks/mascots} => "/yiff.rocks", + }) + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..aef6d03 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..9b0dae9 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: websites_production diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000..8ad7e4e --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +Xs3xSEBpsdZ5+I1Ed3xgivx9nX/+ohIovPpfw5kq1cDmGU6JlLc+niQEIDOpHXro0I6BdR1qfEtTumtTPR//7g1IR0T5l33ldHFAt9qeGq2bH/guFQuH8YRu60MUcGShUGz8Suby8W3vprs3iwn1LG/so3Wio3E4Ez5+InVp3anPMluGoJRJ7KZ0lKa7H/eCIj64zTgKw/vjnLo36WeRZ51o93UEXZ9/whhVk1XlHBTNaH8ECPAv9HARG+tTG/k/4mO7LB5dJMNroqtm7WwvVFaFrZRIPWI33l1vK+Ce0e3U6lVyimLP4I88ykLPqJkush5UXdXjGZBU5JJHFZzzK29F9whEYi6ILJKoA/uZ929uCTxYfL1fmHVm+K4jSsZdoFCgUW8dM93sINgpv0aCU/ahWaK4--SJE9TIVifEifw5wK--BIlVH0Fbagdb26v4KloPMw== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..f737e1d --- /dev/null +++ b/config/database.yml @@ -0,0 +1,85 @@ +# PostgreSQL. Versions 9.3 and up are supported. +# +# Install the pg driver: +# gem install pg +# On macOS with Homebrew: +# gem install pg -- --with-pg-config=/usr/local/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem "pg" +# +default: &default + adapter: postgresql + encoding: unicode + # For details on connection pooling, see Rails configuration guide + # https://guides.rubyonrails.org/configuring.html#database-pooling + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + host: postgres.websites4.containers.local + user: websites + +development: + <<: *default + database: websites_development + + # The specified database role being used to connect to PostgreSQL. + # To create additional roles in PostgreSQL see `$ createuser --help`. + # When left blank, PostgreSQL will use the default role. This is + # the same name as the operating system user running Rails. + #username: websites + + # The password associated with the PostgreSQL role (username). + #password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # Defaults to warning. + #min_messages: notice + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: websites_test + +# As with config/credentials.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password or a full connection URL as an environment +# variable when you boot the app. For example: +# +# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" +# +# If the connection URL is provided in the special DATABASE_URL environment +# variable, Rails will automatically merge its configuration values on top of +# the values provided in this file. Alternatively, you can specify a connection +# URL environment variable explicitly: +# +# production: +# url: <%= ENV["MY_APP_DATABASE_URL"] %> +# +# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full overview on how database connection configuration can be specified. +# +production: + <<: *default + database: websites_production + password: <%= ENV["WEBSITES_DATABASE_PASSWORD"] %> diff --git a/config/default_config.rb b/config/default_config.rb new file mode 100644 index 0000000..7d23161 --- /dev/null +++ b/config/default_config.rb @@ -0,0 +1,242 @@ +# frozen_string_literal: true + +module Websites + class Configuration + def version + GitHelper.short_hash + end + + def version_link + GitHelper.commit_url(GitHelper.short_hash) + end + + def readonly? + false + end + + def source_code_url + "https://github.com/DonovanDMC/Websites" + end + + def e621_status_check_discord_id + end + + def e621_status_check_discord_secret + end + + def e621_status_check_discord_redirect + return "http://websites4.containers.local:3000/webhook/discord/cb?domain=status.e621.ws" if Rails.env.development? + "https://status.e621.ws/webhook/discord/cb" + end + + def e621_status_check_discord_scopes + { + min: %w[webhook.incoming], + all: %w[identify webhook.incoming], + } + end + + def e621_status_check_discord_icon + Base64.encode64(Rails.root.join("app/assets/images/e621.ws/icon.png").read) + end + + def e621_status_check_logs_webhook + # Requests::DiscordWebhook.new(id: "", token: "") + end + + def redis_url + "redis://redis.websites4.containers.local/0" + end + + def github_webhook_secret + end + + def blocked_ip_addresses + [] + end + + def yiffyapi_usage_webhook + # Requests::DiscordWebhook.new(id: "", token: "") + end + + def yiffyapi_ratelimit_webhook + # Requests::DiscordWebhook.new(id: "", token: "") + end + + def yiffyapi_discord_redirect(type) + return "http://websites4.containers.local:3000/#{type}?domain=discord.yiff.rest" if Rails.env.development? + "https://discord.yiff.rest/#{type}" + end + + def yiffyapi_discord_scopes(type) + case type + when "count_servers" + %w[identify guilds] + else + %w[identify] + end + end + + def yiffyapi_apikey_logs_webhook + # Requests::DiscordWebhook.new(id: "", token: "") + end + + def yiffyapi_apikey_shortener_webhook + # Requests::DiscordWebhook.new(id: "", token: "") + end + + def yiffyapi_administrators + %w[242843345402069002] + end + + def yiffyapi_public_key + end + + def yiffyapi_discord_id + end + + def yiffyapi_discord_secret + end + + def yiffyapi_discord_guild + end + + def e621_thumbnails_access_key + end + + def e621_thumbnails_storage_zone_name + end + + def e621_thumbnails_base_url + "https://thumbs.yiff.media" + end + + def e621_thumbnails_base_path + "/data/e621-thumbnails" + end + + def e621_thumbnails_webhook + # Requests::DiscordWebhook.new(id: "", token: "") + end + + # gifs are unpredictable, prone to timing out and HUGE file sizes -for instance, https://e621.net/posts/4693107 took ~300 seconds + # to fully generate and be optimized, and produced a 270MB file which is completely unusable + def e621_thumbnails_disable_gif + true + end + + def pastebin_dev_key + end + + def pastebin_user_key + end + + def pastebin_folder + end + + def common_headers(domain) + { + "Report-To": { + group: "default", + max_age: 31_536_000, + endpoints: [ + { url: "https://yiff.report-uri.com/a/d/g" }, + ], + include_subdomains: true, + }.to_json, + "NEL": { + report_to: "default", + max_age: 31_536_000, + include_subdomains: true, + }.to_json, + "Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload", + "Expect-CT": "max-age=63072000, enforce, report-uri=\"https://yiff.report-uri.com/r/d/ct/enforce\"", + "Upgrade-Insecure-Requests": "1", + "Referrer-Policy": "strict-origin-when-cross-origin", + "X-XSS-Protection": "0", + "X-Permitted-Cross-Domain-Policies": "none", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS", + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "X-Feature-Policy": [ + "accelerometer 'none'", + "ambient-light-sensor 'none'", + "autoplay 'none'", + "battery 'none'", + "camera 'none'", + "display-capture 'none'", + "document-domain 'none'", + "encrypted-media 'none'", + "execution-while-not-rendered 'none'", + "execution-while-out-of-viewport 'none'", + "fullscreen 'none'", + "gamepad 'none'", + "geolocation 'none'", + "gyroscope 'none'", + "layout-animations 'none'", + "legacy-image-formats 'none'", + "magnetometer 'none'", + "microphone 'none'", + "midi 'none'", + "navigation-override 'none'", + "oversied-images 'none'", + "payment 'none'", + "picture-in-picture 'none'", + "publickey-credentials-get 'none'", + "speaker-selection 'none'", + "sync-xhr 'none'", + "unoptimized-images 'none'", + "unsized-media 'none'", + "usb 'none'", + "vr 'none'", + "vibrate 'none'", + "screen-wake-lock 'none'", + "xr-spatial-tracking 'none'", + ].join("; "), + "Content-Security-Policy": [ + "default-src 'self' #{domain} *.#{domain}", + "script-src 'self' 'unsafe-inline' #{domain} *.#{domain} https://cdnjs.cloudflare.com https://static.cloudflareinsights.com", + "style-src 'self' 'unsafe-inline' #{domain} *.#{domain} https://cdnjs.cloudflare.com https://fonts.googleapis.com", + "img-src 'self' https: data:", + "font-src 'self' https: data:", + "report-uri https://yiff.report-uri.com/r/d/csp/enforce", + "report-to default", + "upgrade-insecure-requests", + "block-all-mixed-content", + "require-sri-for script style", + ].join("; "), + } + end + end + + class EnvironmentConfiguration + def custom_configuration + @custom_configuration ||= CustomConfiguration.new + end + + def env_to_boolean(method, var) + is_boolean = method.to_s.end_with?("?") + return true if is_boolean && var.truthy? + return false if is_boolean && var.falsy? + var + end + + def method_missing(method, *) + var = ENV.fetch("WEBSITES_#{method.to_s.upcase.chomp('?')}", nil) + + if var.present? + env_to_boolean(method, var) + else + custom_configuration.send(method, *) + end + end + end + + def config + @config ||= EnvironmentConfiguration.new + end + + module_function :config +end diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..7df99e8 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..8f48e4e --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing + config.server_timing = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{2.days.to_i}", + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true + + config.hosts << "websites4.containers.local" + config.hosts << /.*\.ngrok-free\.app/ + + %w[butts-are.cool e621.ws furry.cool maidboye.cafe oceanic.ws yiff.media yiff.rest yiff.rocks].each do |host| + config.hosts << /(.*\.)?#{host}/ + end + + config.web_console.whitelisted_ips = "172.0.0.0/8" +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..21932f2 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment + # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Enable static file serving from the `/public` folder (turn off if using NGINX/Apache for it). + config.public_file_server.enabled = true + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Log to STDOUT by default + config.logger = ActiveSupport::Logger.new($stdout) + .tap { |logger| logger.formatter = Logger::Formatter.new } + .then { |logger| ActiveSupport::TaggedLogging.new(logger) } + + # Prepend all log lines with the following tags. + config.log_tags = [:request_id] + + # Info include generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "websites_production" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } + config.exceptions_app = ->(env) { ApplicationController.action(:handle_error).call(env) } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..2865fbc --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{1.hour.to_i}", + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000..101a290 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..af395e4 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..c416e6a --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += %i[ + passw secret token _key crypt salt certificate otp ssn +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..3be7a7d --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym("YiffyAPI") + inflect.acronym("API") +end diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb new file mode 100644 index 0000000..b635b52 --- /dev/null +++ b/config/initializers/permissions_policy.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +# Be sure to restart your server when you modify this file. + +# Define an application-wide HTTP permissions policy. For further +# information see: https://developers.google.com/web/updates/2018/06/feature-policy + +# Rails.application.config.permissions_policy do |policy| +# policy.camera :none +# policy.gyroscope :none +# policy.microphone :none +# policy.usb :none +# policy.fullscreen :self +# policy.payment :self, "https://secure.example.com" +# end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb new file mode 100644 index 0000000..15d8a55 --- /dev/null +++ b/config/initializers/session_store.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +if Rails.env.production? + Rails.application.config.session_store(:cookie_store, key: "_websites_session", domain: :all) +else + Rails.application.config.session_store(:cookie_store, key: "_websites_session") +end diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb new file mode 100644 index 0000000..98dda89 --- /dev/null +++ b/config/initializers/simple_form.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +# +# Uncomment this and change the path if necessary to include your own +# components. +# See https://github.com/heartcombo/simple_form#custom-components to know +# more about custom components. +# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } +# +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + # Wrappers are used by the form builder to generate a + # complete input. You can remove any component from the + # wrapper, change the order or even add your own to the + # stack. The options given below are used to wrap the + # whole input. + config.wrappers(:default, class: :input, + hint_class: :field_with_hint, error_class: :field_with_errors, valid_class: :field_without_errors) do |b| + ## Extensions enabled by default + # Any of these extensions can be disabled for a + # given input by passing: `f.input EXTENSION_NAME => false`. + # You can make any of these extensions optional by + # renaming `b.use` to `b.optional`. + + # Determines whether to use HTML5 (:email, :url, ...) + # and required attributes + b.use(:html5) + + # Calculates placeholders automatically from I18n + # You can also pass a string as f.input placeholder: "Placeholder" + b.use(:placeholder) + + ## Optional extensions + # They are disabled unless you pass `f.input EXTENSION_NAME => true` + # to the input. If so, they will retrieve the values from the model + # if any exists. If you want to enable any of those + # extensions by default, you can change `b.optional` to `b.use`. + + # Calculates maxlength from length validations for string inputs + # and/or database column lengths + b.optional(:maxlength) + + # Calculate minlength from length validations for string inputs + b.optional(:minlength) + + # Calculates pattern from format validations for string inputs + b.optional(:pattern) + + # Calculates min and max from length validations for numeric inputs + b.optional(:min_max) + + # Calculates readonly automatically from readonly attributes + b.optional(:readonly) + + ## Inputs + # b.use :input, class: 'input', error_class: 'is-invalid', valid_class: 'is-valid' + b.use(:label_input) + b.use(:hint, wrap_with: { tag: :span, class: :hint }) + b.use(:error, wrap_with: { tag: :span, class: :error }) + + ## full_messages_for + # If you want to display the full error message for the attribute, you can + # use the component :full_error, like: + # + # b.use :full_error, wrap_with: { tag: :span, class: :error } + end + + # The default wrapper to be used by the FormBuilder. + config.default_wrapper = :default + + # Define the way to render check boxes / radio buttons with labels. + # Defaults to :nested for bootstrap config. + # inline: input + label + # nested: label > input + config.boolean_style = :nested + + # Default class for buttons + config.button_class = "btn" + + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # Use :to_sentence to list all errors for each field. + # config.error_method = :first + + # Default tag used for error notification helper. + config.error_notification_tag = :div + + # CSS class to add for error notification helper. + config.error_notification_class = "error_notification" + + # Series of attempts to detect a default label method for collection. + # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] + + # Series of attempts to detect a default value method for collection. + # config.collection_value_methods = [ :id, :to_s ] + + # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. + # config.collection_wrapper_tag = nil + + # You can define the class to use on all collection wrappers. Defaulting to none. + # config.collection_wrapper_class = nil + + # You can wrap each item in a collection of radio/check boxes with a tag, + # defaulting to :span. + # config.item_wrapper_tag = :span + + # You can define a class to use in all item wrappers. Defaulting to none. + # config.item_wrapper_class = nil + + # How the label text should be generated altogether with the required text. + # config.label_text = lambda { |label, required, explicit_label| "#{required} #{label}" } + + # You can define the class to use on all labels. Default is nil. + # config.label_class = nil + + # You can define the default class to be used on forms. Can be overridden + # with `html: { :class }`. Defaulting to none. + # config.default_form_class = nil + + # You can define which elements should obtain additional classes + # config.generate_additional_classes_for = [:wrapper, :label, :input] + + # Whether attributes are required by default (or not). Default is true. + # config.required_by_default = true + + # Tell browsers whether to use the native HTML5 validations (novalidate form option). + # These validations are enabled in SimpleForm's internal config but disabled by default + # in this configuration, which is recommended due to some quirks from different browsers. + # To stop SimpleForm from generating the novalidate option, enabling the HTML5 validations, + # change this configuration to true. + config.browser_validations = false + + # Custom mappings for input types. This should be a hash containing a regexp + # to match as key, and the input type that will be used when the field name + # matches the regexp as value. + # config.input_mappings = { /count/ => :integer } + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. + # config.wrapper_mappings = { string: :prepend } + + # Namespaces where SimpleForm should look for custom input classes that + # override default inputs. + # config.custom_inputs_namespaces << "CustomInputs" + + # Default priority for time_zone inputs. + # config.time_zone_priority = nil + + # Default priority for country inputs. + # config.country_priority = nil + + # When false, do not use translations for labels. + # config.translate_labels = true + + # Automatically discover new inputs in Rails' autoload path. + # config.inputs_discovery = true + + # Cache SimpleForm inputs discovery + # config.cache_discovery = !Rails.env.development? + + # Default class for inputs + # config.input_class = nil + + # Define the default class of the input wrapper of the boolean input. + config.boolean_label_class = "checkbox" + + # Defines if the default input wrapper class should be included in radio + # collection wrappers. + # config.include_default_input_wrapper_class = true + + # Defines which i18n scope will be used in Simple Form. + # config.i18n_scope = 'simple_form' + + # Defines validation classes to the input_field. By default it's nil. + # config.input_field_valid_class = 'is-valid' + # config.input_field_error_class = 'is-invalid' +end diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb new file mode 100644 index 0000000..3d6f71e --- /dev/null +++ b/config/initializers/simple_form_bootstrap.rb @@ -0,0 +1,365 @@ +# frozen_string_literal: true + +# These defaults are defined and maintained by the community at +# https://github.com/heartcombo/simple_form-bootstrap +# Please submit feedback, changes and tests only there. + +# Uncomment this and change the path if necessary to include your own +# components. +# See https://github.com/heartcombo/simple_form#custom-components +# to know more about custom components. +# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } + +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + # Default class for buttons + config.button_class = "btn btn-secondary" + + # Define the default class of the input wrapper of the boolean input. + config.boolean_label_class = "form-check-label" + + # How the label text should be generated altogether with the required text. + config.label_text = ->(label, required, _explicit_label) { "#{label} #{required}" } + + # Define the way to render check boxes / radio buttons with labels. + config.boolean_style = :inline + + # You can wrap each item in a collection of radio/check boxes with a tag + config.item_wrapper_tag = :div + + # Defines if the default input wrapper class should be included in radio + # collection wrappers. + config.include_default_input_wrapper_class = false + + # CSS class to add for error notification helper. + config.error_notification_class = "alert alert-danger" + + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # :to_sentence to list all errors for each field. + config.error_method = :to_sentence + + # add validation classes to `input_field` + config.input_field_error_class = "is-invalid" + config.input_field_valid_class = "is-valid" + + # vertical forms + # + # vertical default_wrapper + config.wrappers(:vertical_form, class: "mb-2") do |b| + b.use(:html5) + b.use(:placeholder) + b.optional(:maxlength) + b.optional(:minlength) + b.optional(:pattern) + b.optional(:min_max) + b.optional(:readonly) + b.use(:label, class: "form-label") + b.use(:input, class: "form-control", error_class: "is-invalid", valid_class: "is-valid") + b.use(:full_error, wrap_with: { class: "invalid-feedback" }) + b.use(:hint, wrap_with: { class: "form-text" }) + end + + # vertical input for boolean + config.wrappers(:vertical_boolean, tag: "fieldset", class: "mb-2") do |b| + b.use(:html5) + b.optional(:readonly) + b.wrapper(:form_check_wrapper, class: "form-check") do |bb| + bb.use(:input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid") + bb.use(:label, class: "form-check-label") + bb.use(:full_error, wrap_with: { class: "invalid-feedback" }) + bb.use(:hint, wrap_with: { class: "form-text" }) + end + end + + # vertical input for radio buttons and check boxes + config.wrappers(:vertical_collection, item_wrapper_class: "form-check", item_label_class: "form-check-label", tag: "fieldset", class: "mb-2") do |b| + b.use(:html5) + b.optional(:readonly) + b.wrapper(:legend_tag, tag: "legend", class: "col-form-label pt-0") do |ba| + ba.use(:label_text) + end + b.use(:input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid") + b.use(:full_error, wrap_with: { class: "invalid-feedback d-block" }) + b.use(:hint, wrap_with: { class: "form-text" }) + end + + # vertical input for inline radio buttons and check boxes + config.wrappers(:vertical_collection_inline, item_wrapper_class: "form-check form-check-inline", item_label_class: "form-check-label", tag: "fieldset", class: "mb-2") do |b| + b.use(:html5) + b.optional(:readonly) + b.wrapper(:legend_tag, tag: "legend", class: "col-form-label pt-0") do |ba| + ba.use(:label_text) + end + b.use(:input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid") + b.use(:full_error, wrap_with: { class: "invalid-feedback d-block" }) + b.use(:hint, wrap_with: { class: "form-text" }) + end + + # vertical file input + config.wrappers(:vertical_file, class: "mb-2") do |b| + b.use(:html5) + b.use(:placeholder) + b.optional(:maxlength) + b.optional(:minlength) + b.optional(:readonly) + b.use(:label, class: "form-label") + b.use(:input, class: "form-control", error_class: "is-invalid", valid_class: "is-valid") + b.use(:full_error, wrap_with: { class: "invalid-feedback" }) + b.use(:hint, wrap_with: { class: "form-text" }) + end + + # vertical select input + config.wrappers(:vertical_select, class: "mb-2") do |b| + b.use(:html5) + b.optional(:readonly) + b.use(:label, class: "form-label") + b.use(:input, class: "form-select", error_class: "is-invalid", valid_class: "is-valid") + b.use(:full_error, wrap_with: { class: "invalid-feedback" }) + b.use(:hint, wrap_with: { class: "form-text" }) + end + + # vertical multi select + config.wrappers(:vertical_multi_select, class: "mb-2") do |b| + b.use(:html5) + b.optional(:readonly) + b.use(:label, class: "form-label") + b.wrapper(class: "d-flex flex-row justify-content-between align-items-center") do |ba| + ba.use(:input, class: "form-select mx-1", error_class: "is-invalid", valid_class: "is-valid") + end + b.use(:full_error, wrap_with: { class: "invalid-feedback d-block" }) + b.use(:hint, wrap_with: { class: "form-text" }) + end + + # vertical range input + config.wrappers(:vertical_range, class: "mb-2") do |b| + b.use(:html5) + b.use(:placeholder) + b.optional(:readonly) + b.optional(:step) + b.use(:label, class: "form-label") + b.use(:input, class: "form-range", error_class: "is-invalid", valid_class: "is-valid") + b.use(:full_error, wrap_with: { class: "invalid-feedback" }) + b.use(:hint, wrap_with: { class: "form-text" }) + end + + # horizontal forms + # + # horizontal default_wrapper + config.wrappers(:horizontal_form, class: "row mb-2") do |b| + b.use(:html5) + b.use(:placeholder) + b.optional(:maxlength) + b.optional(:minlength) + b.optional(:pattern) + b.optional(:min_max) + b.optional(:readonly) + b.use(:label, class: "col-sm-3 col-form-label") + b.wrapper(:grid_wrapper, class: "col-sm-9") do |ba| + ba.use(:input, class: "form-control", error_class: "is-invalid", valid_class: "is-valid") + ba.use(:full_error, wrap_with: { class: "invalid-feedback" }) + ba.use(:hint, wrap_with: { class: "form-text" }) + end + end + + # horizontal input for boolean + config.wrappers(:horizontal_boolean, class: "row mb-2") do |b| + b.use(:html5) + b.optional(:readonly) + b.wrapper(:grid_wrapper, class: "col-sm-9 offset-sm-3") do |wr| + wr.wrapper(:form_check_wrapper, class: "form-check") do |bb| + bb.use(:input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid") + bb.use(:label, class: "form-check-label") + bb.use(:full_error, wrap_with: { class: "invalid-feedback" }) + bb.use(:hint, wrap_with: { class: "form-text" }) + end + end + end + + # horizontal input for radio buttons and check boxes + config.wrappers(:horizontal_collection, item_wrapper_class: "form-check", item_label_class: "form-check-label", class: "row mb-2") do |b| + b.use(:html5) + b.optional(:readonly) + b.use(:label, class: "col-sm-3 col-form-label pt-0") + b.wrapper(:grid_wrapper, class: "col-sm-9") do |ba| + ba.use(:input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid") + ba.use(:full_error, wrap_with: { class: "invalid-feedback d-block" }) + ba.use(:hint, wrap_with: { class: "form-text" }) + end + end + + # horizontal input for inline radio buttons and check boxes + config.wrappers(:horizontal_collection_inline, item_wrapper_class: "form-check form-check-inline", item_label_class: "form-check-label", class: "row mb-2") do |b| + b.use(:html5) + b.optional(:readonly) + b.use(:label, class: "col-sm-3 col-form-label pt-0") + b.wrapper(:grid_wrapper, class: "col-sm-9") do |ba| + ba.use(:input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid") + ba.use(:full_error, wrap_with: { class: "invalid-feedback d-block" }) + ba.use(:hint, wrap_with: { class: "form-text" }) + end + end + + # horizontal file input + config.wrappers(:horizontal_file, class: "row mb-2") do |b| + b.use(:html5) + b.use(:placeholder) + b.optional(:maxlength) + b.optional(:minlength) + b.optional(:readonly) + b.use(:label, class: "col-sm-3 col-form-label") + b.wrapper(:grid_wrapper, class: "col-sm-9") do |ba| + ba.use(:input, class: "form-control", error_class: "is-invalid", valid_class: "is-valid") + ba.use(:full_error, wrap_with: { class: "invalid-feedback" }) + ba.use(:hint, wrap_with: { class: "form-text" }) + end + end + + # horizontal select input + config.wrappers(:horizontal_select, class: "row mb-2") do |b| + b.use(:html5) + b.optional(:readonly) + b.use(:label, class: "col-sm-3 col-form-label") + b.wrapper(:grid_wrapper, class: "col-sm-9") do |ba| + ba.use(:input, class: "form-select", error_class: "is-invalid", valid_class: "is-valid") + ba.use(:full_error, wrap_with: { class: "invalid-feedback" }) + ba.use(:hint, wrap_with: { class: "form-text" }) + end + end + + # horizontal multi select + config.wrappers(:horizontal_multi_select, class: "row mb-2") do |b| + b.use(:html5) + b.optional(:readonly) + b.use(:label, class: "col-sm-3 col-form-label") + b.wrapper(:grid_wrapper, class: "col-sm-9") do |ba| + ba.wrapper(class: "d-flex flex-row justify-content-between align-items-center") do |bb| + bb.use(:input, class: "form-select mx-1", error_class: "is-invalid", valid_class: "is-valid") + end + ba.use(:full_error, wrap_with: { class: "invalid-feedback d-block" }) + ba.use(:hint, wrap_with: { class: "form-text" }) + end + end + + # horizontal range input + config.wrappers(:horizontal_range, class: "row mb-2") do |b| + b.use(:html5) + b.use(:placeholder) + b.optional(:readonly) + b.optional(:step) + b.use(:label, class: "col-sm-3 col-form-label pt-0") + b.wrapper(:grid_wrapper, class: "col-sm-9") do |ba| + ba.use(:input, class: "form-range", error_class: "is-invalid", valid_class: "is-valid") + ba.use(:full_error, wrap_with: { class: "invalid-feedback" }) + ba.use(:hint, wrap_with: { class: "form-text" }) + end + end + + # inline forms + # + # inline default_wrapper + config.wrappers(:inline_form, class: "col-12") do |b| + b.use(:html5) + b.use(:placeholder) + b.optional(:maxlength) + b.optional(:minlength) + b.optional(:pattern) + b.optional(:min_max) + b.optional(:readonly) + b.use(:label, class: "visually-hidden") + + b.use(:input, class: "form-control", error_class: "is-invalid", valid_class: "is-valid") + b.use(:error, wrap_with: { class: "invalid-feedback" }) + b.optional(:hint, wrap_with: { class: "form-text" }) + end + + # inline input for boolean + config.wrappers(:inline_boolean, class: "col-12") do |b| + b.use(:html5) + b.optional(:readonly) + b.wrapper(:form_check_wrapper, class: "form-check") do |bb| + bb.use(:input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid") + bb.use(:label, class: "form-check-label") + bb.use(:error, wrap_with: { class: "invalid-feedback" }) + bb.optional(:hint, wrap_with: { class: "form-text" }) + end + end + + # bootstrap custom forms + # + # custom input switch for boolean + config.wrappers(:custom_boolean_switch, class: "mb-2") do |b| + b.use(:html5) + b.optional(:readonly) + b.wrapper(:form_check_wrapper, tag: "div", class: "form-check form-switch") do |bb| + bb.use(:input, class: "form-check-input", error_class: "is-invalid", valid_class: "is-valid") + bb.use(:label, class: "form-check-label") + bb.use(:full_error, wrap_with: { tag: "div", class: "invalid-feedback" }) + bb.use(:hint, wrap_with: { class: "form-text" }) + end + end + + # Input Group - custom component + # see example app and config at https://github.com/heartcombo/simple_form-bootstrap + config.wrappers(:input_group, class: "mb-2") do |b| + b.use(:html5) + b.use(:placeholder) + b.optional(:maxlength) + b.optional(:minlength) + b.optional(:pattern) + b.optional(:min_max) + b.optional(:readonly) + b.use(:label, class: "form-label") + b.wrapper(:input_group_tag, class: "input-group") do |ba| + ba.optional(:prepend) + ba.use(:input, class: "form-control", error_class: "is-invalid", valid_class: "is-valid") + ba.optional(:append) + ba.use(:full_error, wrap_with: { class: "invalid-feedback" }) + end + b.use(:hint, wrap_with: { class: "form-text" }) + end + + # Floating Labels form + # + # floating labels default_wrapper + config.wrappers(:floating_labels_form, class: "form-floating mb-2") do |b| + b.use(:html5) + b.use(:placeholder) + b.optional(:maxlength) + b.optional(:minlength) + b.optional(:pattern) + b.optional(:min_max) + b.optional(:readonly) + b.use(:input, class: "form-control", error_class: "is-invalid", valid_class: "is-valid") + b.use(:label) + b.use(:full_error, wrap_with: { class: "invalid-feedback" }) + b.use(:hint, wrap_with: { class: "form-text" }) + end + + # custom multi select + config.wrappers(:floating_labels_select, class: "form-floating mb-2") do |b| + b.use(:html5) + b.optional(:readonly) + b.use(:input, class: "form-select", error_class: "is-invalid", valid_class: "is-valid") + b.use(:label) + b.use(:full_error, wrap_with: { class: "invalid-feedback" }) + b.use(:hint, wrap_with: { class: "form-text" }) + end + + # The default wrapper to be used by the FormBuilder. + config.default_wrapper = :vertical_form + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. + config.wrapper_mappings = { + boolean: :vertical_boolean, + check_boxes: :vertical_collection, + date: :vertical_multi_select, + datetime: :vertical_multi_select, + file: :vertical_file, + radio_buttons: :vertical_collection, + range: :vertical_range, + time: :vertical_multi_select, + select: :vertical_select, + } +end diff --git a/config/initializers/string_extension.rb b/config/initializers/string_extension.rb new file mode 100644 index 0000000..2c307d6 --- /dev/null +++ b/config/initializers/string_extension.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class String + def truthy? + match?(/\A(true|t|yes|y|on|1)\z/i) + end + + def falsy? + match?(/\A(false|f|no|n|off|0)\z/i) + end +end diff --git a/config/initializers/uptime.rb b/config/initializers/uptime.rb new file mode 100644 index 0000000..8177c6c --- /dev/null +++ b/config/initializers/uptime.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +Websites::STARTED_AT = Time.now diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..6c349ae --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml new file mode 100644 index 0000000..2374383 --- /dev/null +++ b/config/locales/simple_form.en.yml @@ -0,0 +1,31 @@ +en: + simple_form: + "yes": 'Yes' + "no": 'No' + required: + text: 'required' + mark: '*' + # You can uncomment the line below if you need to overwrite the whole required html. + # When using html, text and mark won't be used. + # html: '*' + error_notification: + default_message: "Please review the problems below:" + # Examples + # labels: + # defaults: + # password: 'Password' + # user: + # new: + # email: 'E-mail to sign in.' + # edit: + # email: 'E-mail.' + # hints: + # defaults: + # username: 'User name to sign in.' + # password: 'No special characters, please.' + # include_blanks: + # defaults: + # age: 'Rather not say' + # prompts: + # defaults: + # age: 'Select your age' diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..600872c --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. + +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies that the worker count should equal the number of processors in production. +if ENV["RAILS_ENV"] == "production" + require "concurrent-ruby" + worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) + workers worker_count if worker_count > 1 +end + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Specifies the `environment` that Puma will run in. +environment ENV.fetch("RAILS_ENV", "development") + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE", "tmp/pids/server.pid") + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..86480ac --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +Rails.application.routes.draw do + extend ButtsAreCoolRoutes + extend E621WsRoutes + extend FurryCoolRoutes + extend MaidboyeCafeRoutes + extend OceanicWsRoutes + extend YiffMediaRoutes + extend YiffRestRoutes + extend YiffRocksRoutes + get "up" => "rails/health#show", as: :rails_health_check + + get "/online", to: "application#online" + root to: "application#access_denied" + match "*other", to: "application#not_found", via: :all, constraints: ->(req) { !req.path.start_with?("/rails/active_storage") } +end diff --git a/config/routes/butts_are_cool_routes.rb b/config/routes/butts_are_cool_routes.rb new file mode 100644 index 0000000..ddac558 --- /dev/null +++ b/config/routes/butts_are_cool_routes.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module ButtsAreCoolRoutes + DOMAIN = "butts-are.cool" + BALLS = "balls" + BALLS_DOMAIN = "#{BALLS}.#{DOMAIN}".freeze + COCKS = "cocks" + COCKS_DOMAIN = "#{COCKS}.#{DOMAIN}".freeze + KNOTS = "knots" + KNOTS_DOMAIN = "#{KNOTS}.#{DOMAIN}".freeze + SHEATHS = "sheaths" + SHEATHS_DOMAIN = "#{SHEATHS}.#{DOMAIN}".freeze + + def self.extended(router) + router.instance_exec do + namespace(:butts_are_cool, path: "") do + constraints(DomainConstraint.new(DOMAIN)) do + namespace(:home, path: "") do + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + end + + root(to: "home#index") + end + constraints(DomainConstraint.new(DOMAIN, BALLS)) do + namespace(:balls, path: "") do + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + root(action: :index) + end + end + constraints(DomainConstraint.new(DOMAIN, COCKS)) do + namespace(:cocks, path: "") do + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + root(action: :index) + end + end + constraints(DomainConstraint.new(DOMAIN, KNOTS)) do + namespace(:knots, path: "") do + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + root(action: :index) + end + end + constraints(DomainConstraint.new(DOMAIN, SHEATHS)) do + namespace(:sheaths, path: "") do + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + root(action: :index) + end + end + end + end + end +end diff --git a/config/routes/domain_constraint.rb b/config/routes/domain_constraint.rb new file mode 100644 index 0000000..8f8ad0a --- /dev/null +++ b/config/routes/domain_constraint.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class DomainConstraint + attr_reader :domain, :subdomain + + DEFAULT_DOMAIN = nil # "yiff.rest".freeze + USE_DEV_HOST = Rails.env.development? + + def dev_host + "#{domain.tr('.', '-')}.websites.containers.local" + end + + def initialize(domain, subdomain = nil) + @domain = domain + @subdomain = subdomain + end + + def resolve_domains(current_host, level = 1) + current_subdomains = nil + if current_host.scan(".").length > level + parts = current_host.split(".") + current_host = parts.pop(level + 1).join(".") + current_subdomains = parts.join(".") if parts.length >= 1 + end + + [current_host, current_subdomains] + end + + def matches?(request) + host = domain + parse = request.domain + parse = "#{request.subdomains.join('.')}.#{request.domain}" unless request.subdomains.empty? + current_host, current_subdomains = resolve_domains(parse) + if Rails.env.development? && USE_DEV_HOST + if DEFAULT_DOMAIN.present? + current_host, current_subdomains = resolve_domains(DEFAULT_DOMAIN) + elsif request.query_parameters[:domain].present? + current_host, current_subdomains = resolve_domains(request.query_parameters[:domain]) + elsif current_host.scan(".").length > 3 + current_host, current_subdomains = resolve_domains(current_host, 3) + end + + current_host = dev_host if current_host == domain + host = dev_host + end + Rails.logger.info("Host: #{host}; Subdomain: #{subdomain}; Current Host: #{current_host}; Current Subdomain: #{current_subdomains}; Matches (Domain): #{current_host == host}; Matches (Subdomain): #{current_subdomains == subdomain || subdomain&.to_sym == :any}") if Rails.env.development? + current_host == host && (current_subdomains == subdomain || subdomain&.to_sym == :any) + end +end diff --git a/config/routes/e621_ws_routes.rb b/config/routes/e621_ws_routes.rb new file mode 100644 index 0000000..48cb97c --- /dev/null +++ b/config/routes/e621_ws_routes.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module E621WsRoutes + DOMAIN = "e621.ws" + STATUS = "status" + STATUS_DOMAIN = "#{STATUS}.#{DOMAIN}".freeze + + def self.extended(router) + router.instance_exec do + namespace(:e621_ws, path: "") do + constraints(DomainConstraint.new(DOMAIN, STATUS)) do + resource(:json, only: %i[index current history]) do + get(:index, controller: :status, defaults: { format: :json }) + get(:current, controller: :status, defaults: { format: :json }) + get(:history, controller: :status, defaults: { format: :json }) + end + + namespace(:status, path: "") do + resource(:schema, only: %i[combined current history index]) do + get(:index, controller: :schema, defaults: { format: :json }) + get(:combined, controller: :schema, defaults: { format: :json }) + get(:current, controller: :schema, defaults: { format: :json }) + get(:history, controller: :schema, defaults: { format: :json }) + end + + resource(:webhook) do + get(:index) + get(:discord) + get("/discord/cb", action: :discord_callback) + end + + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + end + + root(to: "status#index") + end + end + end + end +end diff --git a/config/routes/furry_cool_routes.rb b/config/routes/furry_cool_routes.rb new file mode 100644 index 0000000..d841e53 --- /dev/null +++ b/config/routes/furry_cool_routes.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module FurryCoolRoutes + DOMAIN = "furry.cool" + + def self.extended(router) + router.instance_exec do + namespace(:furry_cool, path: "") do + constraints(DomainConstraint.new(DOMAIN)) do + namespace(:home, path: "") do + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + end + + root(to: "home#index") + end + end + end + end +end diff --git a/config/routes/maidboye_cafe_routes.rb b/config/routes/maidboye_cafe_routes.rb new file mode 100644 index 0000000..e810c02 --- /dev/null +++ b/config/routes/maidboye_cafe_routes.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module MaidboyeCafeRoutes + DOMAIN = "maidboye.cafe" + + def self.extended(router) + router.instance_exec do + namespace(:maidboye_cafe, path: "") do + constraints(DomainConstraint.new(DOMAIN)) do + namespace(:home, path: "") do + get(:privacy) + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + end + + get("/support", to: redirect("https://api.maidboye.cafe/links/support?source=website"), as: :support) + get("/invite", to: redirect("https://api.maidboye.cafe/links/invite?source=website"), as: :invite) + get("/inv", to: redirect("https://api.maidboye.cafe/links/invite?source=website")) + root(to: "home#index") + end + end + end + end +end diff --git a/config/routes/oceanic_ws_routes.rb b/config/routes/oceanic_ws_routes.rb new file mode 100644 index 0000000..3c07721 --- /dev/null +++ b/config/routes/oceanic_ws_routes.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module OceanicWsRoutes + DOMAIN = "oceanic.ws" + DOCS = "docs" + DOCS_DOMAIN = "#{DOCS}.#{DOMAIN}".freeze + GITHUB = "github" + GITHUB_DOMAIN = "#{GITHUB}.#{DOMAIN}".freeze + + def self.extended(router) + router.instance_exec do + namespace(:oceanic_ws, path: "") do + constraints(DomainConstraint.new(DOMAIN)) do + namespace(:home, path: "") do + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + end + + root(to: "home#index") + end + + constraints(DomainConstraint.new(DOMAIN, DOCS)) do + namespace(:docs, path: "") do + get("/latest(*other)", action: :latest, as: :latest) + get(:json, constraints: { format: "json" }) + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + end + + root(to: "docs#index", as: :docs_root) + end + + constraints(DomainConstraint.new(DOMAIN, GITHUB)) do + resource(:github_webhooks, path: "", only: :create, defaults: { format: "json" }) + end + end + end + end +end diff --git a/config/routes/yiff_media_routes.rb b/config/routes/yiff_media_routes.rb new file mode 100644 index 0000000..4052053 --- /dev/null +++ b/config/routes/yiff_media_routes.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module YiffMediaRoutes + DOMAIN = "yiff.media" + REPORT = "report" + REPORT_DOMAIN = "#{REPORT}.#{DOMAIN}".freeze + + def self.extended(router) + router.instance_exec do + namespace(:yiff_media, path: "") do + constraints(DomainConstraint.new(DOMAIN)) do + namespace(:home, path: "") do + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + end + + root(to: "home#index") + end + + constraints(DomainConstraint.new(DOMAIN, REPORT)) do + namespace(:reports, path: "") do + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + end + + root(to: "reports#index", as: :reports_root) + end + end + end + end +end diff --git a/config/routes/yiff_rest_routes.rb b/config/routes/yiff_rest_routes.rb new file mode 100644 index 0000000..c598f86 --- /dev/null +++ b/config/routes/yiff_rest_routes.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module YiffRestRoutes + DOMAIN = "yiff.rest" + V2 = "v2" + V2_DOMAIN = "#{V2}.#{DOMAIN}".freeze + DISCORD = "discord" + DISCORD_DOMAIN = "#{DISCORD}.#{DOMAIN}".freeze + THUMBS = "thumbs" + THUMBS_DOMAIN = "#{THUMBS}.#{DOMAIN}".freeze + STATE = "state" + STATE_DOMAIN = "#{STATE}.#{DOMAIN}".freeze + + def self.extended(router) + router.instance_exec do + namespace(:yiff_rest, path: "") do + constraints(DomainConstraint.new(DOMAIN)) do + resources(:apikeys, only: %i[index new create destroy edit update]) do + get(:logout, on: :collection) + member do + post(:disable) + put(:enable) + put(:deactivate) + put(:reactivate) + put(:regenerate) + end + end + + namespace(:home, path: "") do + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + end + + get("/V2/(*category)", action: :index, controller: "api_v2") + root(to: "home#index") + end + + constraints(DomainConstraint.new(DOMAIN, STATE)) do + namespace(:state, path: "") do + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + end + + root(to: "state#index", as: :state_index) + end + + constraints(DomainConstraint.new(DOMAIN, V2)) do + namespace(:api_v2, path: "") do + get("/", to: redirect("https://yiff.rest")) + get(:robots, constraints: { format: "txt" }) + get(:state, to: redirect("https://state.yiff.rest")) + get(:online) + get(:stats) + get(:categories) + get("/categories/(*category)", action: :category, as: :api_v2_category, constraints: { category: /[a-z0-9\-.]+/i }) + get("/images/:id", action: :image, as: :api_v2_image, constraints: { id: /[a-f0-9]{32}/ }) + post(:bulk, constraints: { format: "json" }, defaults: { format: :json }) + get("/(*category)", action: :index, as: :api_v2_images) + end + end + + constraints(DomainConstraint.new(DOMAIN, DISCORD)) do + namespace(:discord, path: "") do + get(:count_servers) + get(:flags) + get(:apikey) + post(:interactions) + end + + root(to: "discord#index", as: :discord_root) + end + + constraints(DomainConstraint.new(DOMAIN, THUMBS)) do + namespace(:thumbs, path: "") do + get("/:id", action: :show, as: :thumbs_show, constraints: { id: /[a-f0-9]{32}|\d+/ }) + put("/:id/:type", action: :create, as: :thumbs_create, constraints: { id: /[a-f0-9]{32}|\d+/, type: /gif|png/ }) + get("/check/:id/:type", action: :check, as: :thumbs_check, constraints: { md5: /[a-f0-9]{32}/, type: /gif|png/ }) + end + + root(to: redirect("https://docs.yiff.rest/thumbnails"), as: :thumbs_root) + end + end + end + end +end diff --git a/config/routes/yiff_rocks_routes.rb b/config/routes/yiff_rocks_routes.rb new file mode 100644 index 0000000..754139c --- /dev/null +++ b/config/routes/yiff_rocks_routes.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module YiffRocksRoutes + DOMAIN = "yiff.rocks" + + def self.extended(router) + router.instance_exec do + namespace(:yiff_rocks, path: "") do + constraints(DomainConstraint.new(DOMAIN)) do + namespace(:home, path: "") do + get(:manifest, constraints: { format: "json" }) + get(:browserconfig, constraints: { format: "xml" }) + + get("/:code", action: :show) + post(:create, constraints: { format: "json" }, defaults: { format: :json }) + delete("/:code", action: :destroy, constraints: { format: "json" }, defaults: { format: :json }) + patch("/:code", action: :update, constraints: { format: "json" }, defaults: { format: :json }) + end + + root(to: "home#index") + end + end + end + end +end diff --git a/config/schedule.rb b/config/schedule.rb new file mode 100644 index 0000000..349a943 --- /dev/null +++ b/config/schedule.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Use this file to easily define all of your cron jobs. +# +# It's helpful, but not entirely necessary to understand cron before proceeding. +# http://en.wikipedia.org/wiki/Cron + +# Example: +# +# set :output, "/path/to/my/cron_log.log" +# +# every 2.hours do +# command "/usr/bin/some_great_command" +# runner "MyModel.some_method" +# rake "some:great:rake:task" +# end +# +# every 4.days do +# runner "AnotherModel.prune_old_records" +# end + +# Learn more: http://github.com/javan/whenever + +set :job_template, "/bin/sh -c ':job'" +set :output, "log/cron.log" + +# Perform an e621 status update every minute +every 1.minute do + rake "e621:status_update" +end diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000..4942ab6 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/db/migrate/20231107120858_create_active_storage_tables.active_storage.rb b/db/migrate/20231107120858_create_active_storage_tables.active_storage.rb new file mode 100644 index 0000000..69c6754 --- /dev/null +++ b/db/migrate/20231107120858_create_active_storage_tables.active_storage.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[7.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table(:active_storage_blobs, id: primary_key_type) do |t| + t.string(:key, null: false) + t.string(:filename, null: false) + t.string(:content_type) + t.text(:metadata) + t.string(:service_name, null: false) + t.bigint(:byte_size, null: false) + t.string(:checksum) + + if connection.supports_datetime_with_precision? + t.datetime(:created_at, precision: 6, null: false) + else + t.datetime(:created_at, null: false) + end + + t.index([:key], unique: true) + end + + create_table(:active_storage_attachments, id: primary_key_type) do |t| + t.string(:name, null: false) + t.references(:record, null: false, polymorphic: true, index: false, type: foreign_key_type) + t.references(:blob, null: false, type: foreign_key_type) + + if connection.supports_datetime_with_precision? + t.datetime(:created_at, precision: 6, null: false) + else + t.datetime(:created_at, null: false) + end + + t.index(%i[record_type record_id name blob_id], name: :index_active_storage_attachments_uniqueness, unique: true) + t.foreign_key(:active_storage_blobs, column: :blob_id) + end + + create_table(:active_storage_variant_records, id: primary_key_type) do |t| + t.belongs_to(:blob, null: false, index: false, type: foreign_key_type) + t.string(:variation_digest, null: false) + + t.index(%i[blob_id variation_digest], name: :index_active_storage_variant_records_uniqueness, unique: true) + t.foreign_key(:active_storage_blobs, column: :blob_id) + end + end + + private + + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/migrate/20231108110858_create_e621_statuses.rb b/db/migrate/20231108110858_create_e621_statuses.rb new file mode 100644 index 0000000..e85f7ec --- /dev/null +++ b/db/migrate/20231108110858_create_e621_statuses.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class CreateE621Statuses < ActiveRecord::Migration[7.1] + def change + create_table(:e621_statuses) do |t| + t.integer(:status, null: false) + t.datetime(:created_at, precision: 3, null: false) + end + end +end diff --git a/db/migrate/20231108214419_create_e621_webhooks.rb b/db/migrate/20231108214419_create_e621_webhooks.rb new file mode 100644 index 0000000..02999b4 --- /dev/null +++ b/db/migrate/20231108214419_create_e621_webhooks.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateE621Webhooks < ActiveRecord::Migration[7.1] + def change + create_table(:e621_webhooks) do |t| + t.string(:channel_id, null: false) + t.string(:creator_id) + t.string(:guild_id, null: false) + t.string(:webhook_id, null: false) + t.string(:webhook_token, null: false) + t.datetime(:created_at, null: false) + end + end +end diff --git a/db/migrate/20231113021606_create_api_users.rb b/db/migrate/20231113021606_create_api_users.rb new file mode 100644 index 0000000..49626d3 --- /dev/null +++ b/db/migrate/20231113021606_create_api_users.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CreateAPIUsers < ActiveRecord::Migration[7.1] + def change + create_table(:api_users) do |t| + t.integer(:level, null: false) + t.string(:name, null: false) + t.string(:last_avatar_hash) + t.timestamps + end + end +end diff --git a/db/migrate/20231114021606_create_api_keys.rb b/db/migrate/20231114021606_create_api_keys.rb new file mode 100644 index 0000000..409dd62 --- /dev/null +++ b/db/migrate/20231114021606_create_api_keys.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class CreateAPIKeys < ActiveRecord::Migration[7.1] + def change + create_table(:api_keys) do |t| + t.references(:owner, foreign_key: { to_table: :api_users }, null: false) + t.boolean(:active, default: true, null: false) + t.boolean(:disabled, default: false, null: false) + t.boolean(:super, default: false, null: false) + t.boolean(:unlimited, default: false, null: false) + t.integer(:bulk_limit, default: 100, null: false) + t.integer(:flags, default: APIKey::Flags.default, null: false) + t.integer(:limit_long, default: 12, null: false) + t.integer(:limit_short, default: 4, null: false) + t.integer(:window_long, default: 10_000, null: false) + t.integer(:window_short, default: 2000, null: false) + t.string(:application_name, null: false) + t.string(:usage, null: false, default: "") + t.string(:disabled_reason) + t.string(:key, null: false) + t.timestamps + end + end +end diff --git a/db/migrate/20231114130956_create_api_images.rb b/db/migrate/20231114130956_create_api_images.rb new file mode 100644 index 0000000..f516d58 --- /dev/null +++ b/db/migrate/20231114130956_create_api_images.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class CreateAPIImages < ActiveRecord::Migration[7.1] + def change + create_table(:api_images, id: :uuid) do |t| + t.references(:creator, foreign_key: { to_table: :api_users }, null: false) + t.inet(:creator_ip_addr, null: false) + t.string(:artists, array: true, default: []) + t.string(:sources, array: true, default: []) + t.integer(:width, null: false) + t.integer(:height, null: false) + t.string(:mime_type, null: false) + t.string(:category, null: false) + t.string(:created_by, null: false) + t.string(:original_url) + t.string(:file_ext, null: false) + t.integer(:file_size, null: false) + t.timestamps + end + end +end diff --git a/db/migrate/20231114133133_create_short_urls.rb b/db/migrate/20231114133133_create_short_urls.rb new file mode 100644 index 0000000..dc3c5e2 --- /dev/null +++ b/db/migrate/20231114133133_create_short_urls.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateShortUrls < ActiveRecord::Migration[7.1] + def change + create_table(:short_urls) do |t| + t.references(:creator, foreign_key: { to_table: :api_users }, null: false) + t.references(:api_key) + t.inet(:creator_ip_addr, null: false) + t.string(:code, null: false, index: { unique: true }) + t.string(:url, null: false) + t.integer(:hits, null: false, default: 0) + t.string(:creator_name, null: false) + t.string(:creator_ua, null: false) + t.string(:management_code) + t.timestamps + end + end +end diff --git a/db/migrate/20231116040717_create_e621_thumbnails.rb b/db/migrate/20231116040717_create_e621_thumbnails.rb new file mode 100644 index 0000000..d1fc69f --- /dev/null +++ b/db/migrate/20231116040717_create_e621_thumbnails.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateE621Thumbnails < ActiveRecord::Migration[7.1] + def change + create_table(:e621_thumbnails) do |t| + t.references(:creator, foreign_key: { to_table: :api_users }, null: false) + t.references(:api_key) + t.inet(:creator_ip_addr, null: false) + t.integer(:post_id, null: false) + t.uuid(:md5, null: false) + t.string(:status, null: false, default: "pending") + t.string(:filetype, null: false) + t.string(:error_code) + t.datetime(:expires_at) + t.timestamps + end + end +end diff --git a/db/migrate/20231231020215_create_exception_logs.rb b/db/migrate/20231231020215_create_exception_logs.rb new file mode 100644 index 0000000..4f2ff3a --- /dev/null +++ b/db/migrate/20231231020215_create_exception_logs.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateExceptionLogs < ActiveRecord::Migration[7.1] + def change + create_table(:exception_logs) do |t| + t.string(:class_name) + t.inet(:ip_addr) + t.string(:version) + t.text(:extra_params) + t.text(:message) + t.text(:trace) + t.uuid(:code) + t.timestamps + end + end +end diff --git a/db/migrate/20240102073042_create_api_usages.rb b/db/migrate/20240102073042_create_api_usages.rb new file mode 100644 index 0000000..043fdbe --- /dev/null +++ b/db/migrate/20240102073042_create_api_usages.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateAPIUsages < ActiveRecord::Migration[7.1] + def change + create_table(:api_usages) do |t| + t.references(:user, foreign_key: { to_table: :api_users }) + t.references(:api_key) + t.text(:user_agent, null: false, default: "") + t.text(:method, null: false, default: "GET") + t.text(:path, null: false, default: "/") + t.jsonb(:params, null: false, default: "{}") + t.text(:service, null: false) + t.inet(:ip_addr, null: false) + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..29c4269 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,173 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.1].define(version: 2024_01_02_073042) do + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "active_storage_variant_records", force: :cascade do |t| + t.bigint "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + + create_table "api_images", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.bigint "creator_id", null: false + t.inet "creator_ip_addr", null: false + t.string "artists", default: [], array: true + t.string "sources", default: [], array: true + t.integer "width", null: false + t.integer "height", null: false + t.string "mime_type", null: false + t.string "category", null: false + t.string "created_by", null: false + t.string "original_url" + t.string "file_ext", null: false + t.integer "file_size", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["creator_id"], name: "index_api_images_on_creator_id" + end + + create_table "api_keys", force: :cascade do |t| + t.bigint "owner_id", null: false + t.boolean "active", default: true, null: false + t.boolean "disabled", default: false, null: false + t.boolean "super", default: false, null: false + t.boolean "unlimited", default: false, null: false + t.integer "bulk_limit", default: 100, null: false + t.integer "flags", default: 7, null: false + t.integer "limit_long", default: 12, null: false + t.integer "limit_short", default: 4, null: false + t.integer "window_long", default: 10000, null: false + t.integer "window_short", default: 2000, null: false + t.string "application_name", null: false + t.string "usage", default: "", null: false + t.string "disabled_reason" + t.string "key", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["owner_id"], name: "index_api_keys_on_owner_id" + end + + create_table "api_usages", force: :cascade do |t| + t.bigint "user_id" + t.bigint "api_key_id" + t.text "user_agent", default: "", null: false + t.text "method", default: "GET", null: false + t.text "path", default: "/", null: false + t.jsonb "params", default: "{}", null: false + t.text "service", null: false + t.inet "ip_addr", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["api_key_id"], name: "index_api_usages_on_api_key_id" + t.index ["user_id"], name: "index_api_usages_on_user_id" + end + + create_table "api_users", force: :cascade do |t| + t.integer "level", null: false + t.string "name", null: false + t.string "last_avatar_hash" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "e621_statuses", force: :cascade do |t| + t.integer "status", null: false + t.datetime "created_at", precision: 3, null: false + end + + create_table "e621_thumbnails", force: :cascade do |t| + t.bigint "creator_id", null: false + t.bigint "api_key_id" + t.inet "creator_ip_addr", null: false + t.integer "post_id", null: false + t.uuid "md5", null: false + t.string "status", default: "pending", null: false + t.string "filetype", null: false + t.string "error_code" + t.datetime "expires_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["api_key_id"], name: "index_e621_thumbnails_on_api_key_id" + t.index ["creator_id"], name: "index_e621_thumbnails_on_creator_id" + end + + create_table "e621_webhooks", force: :cascade do |t| + t.string "channel_id", null: false + t.string "creator_id" + t.string "guild_id", null: false + t.string "webhook_id", null: false + t.string "webhook_token", null: false + t.datetime "created_at", null: false + end + + create_table "exception_logs", force: :cascade do |t| + t.string "class_name" + t.inet "ip_addr" + t.string "version" + t.text "extra_params" + t.text "message" + t.text "trace" + t.uuid "code" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "short_urls", force: :cascade do |t| + t.bigint "creator_id", null: false + t.bigint "api_key_id" + t.inet "creator_ip_addr", null: false + t.string "code", null: false + t.string "url", null: false + t.integer "hits", default: 0, null: false + t.string "creator_name", null: false + t.string "creator_ua", null: false + t.string "management_code" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["api_key_id"], name: "index_short_urls_on_api_key_id" + t.index ["code"], name: "index_short_urls_on_code", unique: true + t.index ["creator_id"], name: "index_short_urls_on_creator_id" + end + + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "api_images", "api_users", column: "creator_id" + add_foreign_key "api_keys", "api_users", column: "owner_id" + add_foreign_key "api_usages", "api_users", column: "user_id" + add_foreign_key "e621_thumbnails", "api_users", column: "creator_id" + add_foreign_key "short_urls", "api_users", column: "creator_id" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..07b11e8 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +# This file should ensure the existence of records required to run the application in every environment (production, +# development, test). The code here should be idempotent so that it can be executed at any point in every environment. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Example: +# +# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| +# MovieGenre.find_or_create_by!(name: genre_name) +# end diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..6e1be8b --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,77 @@ +version: "3" + +x-environment: &common-env + WEBSITES_READONLY: "${READONLY:-0}" + +services: + websites: + build: + dockerfile: Dockerfile + context: . + image: websites4 + command: foreman start -f Procfile + volumes: + - .:/app + - /var/www/e621-thumbnails:/data/e621-thumbnails + - /var/www/oceanic-docs:/data/oceanic-docs + tmpfs: + - /app/tmp/pids + environment: + <<: *common-env + RAILS_ENV: production + depends_on: + postgres: + condition: service_started + redis: + condition: service_started + labels: + - "hostname=websites4.containers.local" + tty: true + + postgres: + image: postgres:14-alpine + volumes: + - db_data:/var/lib/postgresql/data + restart: unless-stopped + environment: + - POSTGRES_USER=websites + - POSTGRES_DB=websites + - POSTGRES_HOST_AUTH_METHOD=trust + healthcheck: + interval: 5s + timeout: 2s + test: pg_isready -U websites + hostname: postgres.websites4.containers.local + labels: + - "hostname=postgres.websites4.containers.local" + networks: + - default + + redis: + image: redis:alpine + command: redis-server --save 10 1 --loglevel warning + volumes: + - redis_data:/data + restart: unless-stopped + healthcheck: + test: redis-cli ping + interval: 10s + timeout: 5s + hostname: redis.websites4.containers.local + labels: + - "hostname=redis.websites4.containers.local" + networks: + - default + +networks: + default: + name: websites4 + driver: bridge + ipam: + driver: default + config: + - subnet: 172.19.3.64/27 + +volumes: + db_data: + redis_data: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7122964 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,76 @@ +version: "3" + +x-environment: &common-env + WEBSITES_READONLY: "${READONLY:-0}" + +services: + websites: + build: . + image: websites4 + volumes: + - .:/app + - e621_thumbnail_data:/data/e621-thumbnails + - oceanic_docs_data:/data/oceanic-docs + tmpfs: + - /app/tmp/pids + environment: + <<: *common-env + RAILS_ENV: development + depends_on: + postgres: + condition: service_started + redis: + condition: service_started + labels: + - "hostname=websites4.containers.local" + tty: true + + postgres: + image: postgres:14-alpine + volumes: + - db_data:/var/lib/postgresql/data + restart: unless-stopped + environment: + - POSTGRES_USER=websites + - POSTGRES_DB=websites + - POSTGRES_HOST_AUTH_METHOD=trust + healthcheck: + interval: 5s + timeout: 2s + test: pg_isready -U websites + hostname: postgres.websites4.containers.local + labels: + - "hostname=postgres.websites4.containers.local" + networks: + - default + + redis: + image: redis:alpine + command: redis-server --save 10 1 --loglevel warning + volumes: + - redis_data:/data + restart: unless-stopped + healthcheck: + test: redis-cli ping + interval: 10s + timeout: 5s + hostname: redis.websites4.containers.local + labels: + - "hostname=redis.websites4.containers.local" + networks: + - default + +networks: + default: + name: websites4 + driver: bridge + ipam: + driver: default + config: + - subnet: 172.19.3.64/27 + +volumes: + db_data: + redis_data: + e621_thumbnail_data: + oceanic_docs_data: diff --git a/docker/local_config.rb b/docker/local_config.rb new file mode 100644 index 0000000..cea5c9d --- /dev/null +++ b/docker/local_config.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Websites + class CustomConfiguration < Configuration + end +end diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..a68bfb3 --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,34 @@ +import { sassPlugin } from "esbuild-sass-plugin"; +import esbuild from "esbuild"; +import chokidar from "chokidar"; + +/** @type import("esbuild").BuildOptions */ +const options = { + entryPoints: ["./app/javascript/application.ts"], + outdir: "./app/assets/builds", + bundle: true, + allowOverwrite: true, + plugins: [ + sassPlugin({ + loadPaths: ["./node_modules"], + }) + ], + sourcemap: true, + publicPath: "assets" +}; + +const watch = process.argv.includes("--watch"); + +if(watch) { + const watcher = chokidar.watch("./app/javascript/**/*.{ts,scss}"); + watcher.on("change", async () => { + console.log("👀 Change detected, rebuilding..."); + await esbuild.build(options) + .then(() => console.log("⚡ Done")) + .catch(() => console.log("🚨 Failed")); + }); +} + +await esbuild.build(options) + .then(() => console.log("⚡ Done")) + .catch(() => console.log("🚨 Failed")); diff --git a/lib/custom_static_middleware.rb b/lib/custom_static_middleware.rb new file mode 100644 index 0000000..1438f8a --- /dev/null +++ b/lib/custom_static_middleware.rb @@ -0,0 +1,28 @@ +class CustomStaticMiddleware + def initialize(app, domain_map) + @app = app + @domain_map = domain_map + end + + def call(env) + request = Rack::Request.new(env) + host = request.host + + if (target_path = find_mapped_path(host, request.path)) + env["PATH_INFO"] = target_path + end + + @app.call(env) + end + + private + + def find_mapped_path(host, path) + @domain_map.each do |domain_pattern, subdirectory| + matcher = "#{host}#{path}" + return "#{subdirectory}#{path}" if matcher.match?(domain_pattern) + end + + nil + end +end diff --git a/lib/tasks/e621.rake b/lib/tasks/e621.rake new file mode 100644 index 0000000..220b76c --- /dev/null +++ b/lib/tasks/e621.rake @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +namespace :e621 do + desc "Tasks related to e621" + task status_update: :environment do + E621StatusUpdater.run + end + + task remove_timeouts: :environment do + E621Thumbnail.where(status: "timeout", created_at: ..10.minutes.ago).find_each do |entry| + puts "Removing timed out generation for post #{entry.post_id} (#{entry.stripped_md5}" + entry.destroy + end + end +end diff --git a/lib/templates/erb/scaffold/_form.html.erb b/lib/templates/erb/scaffold/_form.html.erb new file mode 100644 index 0000000..106b71e --- /dev/null +++ b/lib/templates/erb/scaffold/_form.html.erb @@ -0,0 +1,15 @@ +<%# frozen_string_literal: true %> +<%%= simple_form_for(@<%= singular_table_name %>) do |f| %> + <%%= f.error_notification %> + <%%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %> + +
+ <%- attributes.each do |attribute| -%> + <%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %> + <%- end -%> +
+ +
+ <%%= f.button :submit %> +
+<%% end %> diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..8592b08 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "app", + "packageManager": "yarn@1.22.19", + "dependencies": { + "@hotwired/stimulus": "^3.2.2", + "@hotwired/turbo-rails": "^7.3.0", + "@popperjs/core": "^2.11.8", + "@rails/ujs": "^7.1.2", + "bootstrap": "^5.3.2", + "esbuild": "^0.19.5", + "jquery": "^3.7.1", + "jquery-ujs": "^1.2.3" + }, + "scripts": { + "build": "node esbuild.config.mjs" + }, + "devDependencies": { + "@types/bootstrap": "^5.2.10", + "@types/jquery": "^3.5.29", + "@types/jqueryui": "^1.12.20", + "@types/rails__ujs": "^6.0.4", + "@uwu-codes/eslint-config": "^1.1.26", + "chokidar": "^3.5.3", + "esbuild-sass-plugin": "^2.16.0", + "eslint": "^8.53.0", + "prettier": "^3.0.3", + "typescript": "^5.2.2" + }, + "license": "MIT" +} diff --git a/public/furry.cool/images/Don.gif b/public/furry.cool/images/Don.gif new file mode 100644 index 0000000..3adf1e7 Binary files /dev/null and b/public/furry.cool/images/Don.gif differ diff --git a/public/furry.cool/images/Don.jpeg b/public/furry.cool/images/Don.jpeg new file mode 100644 index 0000000..a77bc32 Binary files /dev/null and b/public/furry.cool/images/Don.jpeg differ diff --git a/public/furry.cool/images/Don2.png b/public/furry.cool/images/Don2.png new file mode 100644 index 0000000..a472d28 Binary files /dev/null and b/public/furry.cool/images/Don2.png differ diff --git a/public/furry.cool/images/DonAndParker.png b/public/furry.cool/images/DonAndParker.png new file mode 100644 index 0000000..cacf696 Binary files /dev/null and b/public/furry.cool/images/DonAndParker.png differ diff --git a/public/furry.cool/images/DonAndSkull.jpg b/public/furry.cool/images/DonAndSkull.jpg new file mode 100644 index 0000000..795992a Binary files /dev/null and b/public/furry.cool/images/DonAndSkull.jpg differ diff --git a/public/furry.cool/images/DonBG.jpg b/public/furry.cool/images/DonBG.jpg new file mode 100644 index 0000000..1773984 Binary files /dev/null and b/public/furry.cool/images/DonBG.jpg differ diff --git a/public/furry.cool/images/DonBlep.png b/public/furry.cool/images/DonBlep.png new file mode 100644 index 0000000..48ebf81 Binary files /dev/null and b/public/furry.cool/images/DonBlep.png differ diff --git a/public/furry.cool/images/DonBlushy.png b/public/furry.cool/images/DonBlushy.png new file mode 100644 index 0000000..d81ff5d Binary files /dev/null and b/public/furry.cool/images/DonBlushy.png differ diff --git a/public/furry.cool/images/DonBongo.gif b/public/furry.cool/images/DonBongo.gif new file mode 100644 index 0000000..526e88f Binary files /dev/null and b/public/furry.cool/images/DonBongo.gif differ diff --git a/public/furry.cool/images/DonBulge.png b/public/furry.cool/images/DonBulge.png new file mode 100644 index 0000000..1b19078 Binary files /dev/null and b/public/furry.cool/images/DonBulge.png differ diff --git a/public/furry.cool/images/DonButt.png b/public/furry.cool/images/DonButt.png new file mode 100644 index 0000000..7a021ac Binary files /dev/null and b/public/furry.cool/images/DonButt.png differ diff --git a/public/furry.cool/images/DonChristmas.png b/public/furry.cool/images/DonChristmas.png new file mode 100644 index 0000000..324951b Binary files /dev/null and b/public/furry.cool/images/DonChristmas.png differ diff --git a/public/furry.cool/images/DonCoffee.png b/public/furry.cool/images/DonCoffee.png new file mode 100644 index 0000000..d837dfe Binary files /dev/null and b/public/furry.cool/images/DonCoffee.png differ diff --git a/public/furry.cool/images/DonCute.png b/public/furry.cool/images/DonCute.png new file mode 100644 index 0000000..cacf696 Binary files /dev/null and b/public/furry.cool/images/DonCute.png differ diff --git a/public/furry.cool/images/DonDusting.jpg b/public/furry.cool/images/DonDusting.jpg new file mode 100644 index 0000000..7589078 Binary files /dev/null and b/public/furry.cool/images/DonDusting.jpg differ diff --git a/public/furry.cool/images/DonDustingTransparent.png b/public/furry.cool/images/DonDustingTransparent.png new file mode 100644 index 0000000..adf1de8 Binary files /dev/null and b/public/furry.cool/images/DonDustingTransparent.png differ diff --git a/public/furry.cool/images/DonLying.png b/public/furry.cool/images/DonLying.png new file mode 100644 index 0000000..d95cb22 Binary files /dev/null and b/public/furry.cool/images/DonLying.png differ diff --git a/public/furry.cool/images/DonLyingTransparent.png b/public/furry.cool/images/DonLyingTransparent.png new file mode 100644 index 0000000..2369f7e Binary files /dev/null and b/public/furry.cool/images/DonLyingTransparent.png differ diff --git a/public/furry.cool/images/DonMaid.png b/public/furry.cool/images/DonMaid.png new file mode 100644 index 0000000..5cb3d35 Binary files /dev/null and b/public/furry.cool/images/DonMaid.png differ diff --git a/public/furry.cool/images/DonMaid2.png b/public/furry.cool/images/DonMaid2.png new file mode 100644 index 0000000..c1f9d98 Binary files /dev/null and b/public/furry.cool/images/DonMaid2.png differ diff --git a/public/furry.cool/images/DonMaidCrop.png b/public/furry.cool/images/DonMaidCrop.png new file mode 100644 index 0000000..7ddb333 Binary files /dev/null and b/public/furry.cool/images/DonMaidCrop.png differ diff --git a/public/furry.cool/images/DonMaidTransparent.png b/public/furry.cool/images/DonMaidTransparent.png new file mode 100644 index 0000000..b9de6b1 Binary files /dev/null and b/public/furry.cool/images/DonMaidTransparent.png differ diff --git a/public/furry.cool/images/DonOld.png b/public/furry.cool/images/DonOld.png new file mode 100644 index 0000000..1e30e18 Binary files /dev/null and b/public/furry.cool/images/DonOld.png differ diff --git a/public/furry.cool/images/DonOld2.gif b/public/furry.cool/images/DonOld2.gif new file mode 100644 index 0000000..3adf1e7 Binary files /dev/null and b/public/furry.cool/images/DonOld2.gif differ diff --git a/public/furry.cool/images/DonPaws.jpg b/public/furry.cool/images/DonPaws.jpg new file mode 100644 index 0000000..4ff9c2b Binary files /dev/null and b/public/furry.cool/images/DonPaws.jpg differ diff --git a/public/furry.cool/images/DonPeek.png b/public/furry.cool/images/DonPeek.png new file mode 100644 index 0000000..6d1e78f Binary files /dev/null and b/public/furry.cool/images/DonPeek.png differ diff --git a/public/furry.cool/images/DonPeekCirc.png b/public/furry.cool/images/DonPeekCirc.png new file mode 100644 index 0000000..55a1a9b Binary files /dev/null and b/public/furry.cool/images/DonPeekCirc.png differ diff --git a/public/furry.cool/images/DonPride.png b/public/furry.cool/images/DonPride.png new file mode 100644 index 0000000..338093e Binary files /dev/null and b/public/furry.cool/images/DonPride.png differ diff --git a/public/furry.cool/images/DonPrideBerube.png b/public/furry.cool/images/DonPrideBerube.png new file mode 100644 index 0000000..e6e6a37 Binary files /dev/null and b/public/furry.cool/images/DonPrideBerube.png differ diff --git a/public/furry.cool/images/DonPrideTransparent.png b/public/furry.cool/images/DonPrideTransparent.png new file mode 100644 index 0000000..e4cbdc1 Binary files /dev/null and b/public/furry.cool/images/DonPrideTransparent.png differ diff --git a/public/furry.cool/images/DonPrigeBerube.original.png b/public/furry.cool/images/DonPrigeBerube.original.png new file mode 100644 index 0000000..07ef7c6 Binary files /dev/null and b/public/furry.cool/images/DonPrigeBerube.original.png differ diff --git a/public/furry.cool/images/DonPrigeOriginal.png b/public/furry.cool/images/DonPrigeOriginal.png new file mode 100644 index 0000000..07ef7c6 Binary files /dev/null and b/public/furry.cool/images/DonPrigeOriginal.png differ diff --git a/public/furry.cool/images/DonRay.png b/public/furry.cool/images/DonRay.png new file mode 100644 index 0000000..4e72dab Binary files /dev/null and b/public/furry.cool/images/DonRay.png differ diff --git a/public/furry.cool/images/DonRef.png b/public/furry.cool/images/DonRef.png new file mode 100644 index 0000000..c87529e Binary files /dev/null and b/public/furry.cool/images/DonRef.png differ diff --git a/public/furry.cool/images/DonShy.png b/public/furry.cool/images/DonShy.png new file mode 100644 index 0000000..1b19078 Binary files /dev/null and b/public/furry.cool/images/DonShy.png differ diff --git a/public/furry.cool/images/DonShyTransparent.png b/public/furry.cool/images/DonShyTransparent.png new file mode 100644 index 0000000..247ac1f Binary files /dev/null and b/public/furry.cool/images/DonShyTransparent.png differ diff --git a/public/furry.cool/images/DonSpooky.png b/public/furry.cool/images/DonSpooky.png new file mode 100644 index 0000000..844878a Binary files /dev/null and b/public/furry.cool/images/DonSpooky.png differ diff --git a/public/furry.cool/images/DonYoshi.jpg b/public/furry.cool/images/DonYoshi.jpg new file mode 100644 index 0000000..8ba2921 Binary files /dev/null and b/public/furry.cool/images/DonYoshi.jpg differ diff --git a/public/furry.cool/images/FoxyBlep.png b/public/furry.cool/images/FoxyBlep.png new file mode 100644 index 0000000..8e6a626 Binary files /dev/null and b/public/furry.cool/images/FoxyBlep.png differ diff --git a/public/furry.cool/images/FoxyRefNSFW.png b/public/furry.cool/images/FoxyRefNSFW.png new file mode 100644 index 0000000..889926b Binary files /dev/null and b/public/furry.cool/images/FoxyRefNSFW.png differ diff --git a/public/furry.cool/images/FoxyRefSFW.png b/public/furry.cool/images/FoxyRefSFW.png new file mode 100644 index 0000000..b3dac46 Binary files /dev/null and b/public/furry.cool/images/FoxyRefSFW.png differ diff --git a/public/furry.cool/images/FoxyRefSheath.png b/public/furry.cool/images/FoxyRefSheath.png new file mode 100644 index 0000000..b705b56 Binary files /dev/null and b/public/furry.cool/images/FoxyRefSheath.png differ diff --git a/public/furry.cool/images/GaoNSFWErect.png b/public/furry.cool/images/GaoNSFWErect.png new file mode 100644 index 0000000..91be10d Binary files /dev/null and b/public/furry.cool/images/GaoNSFWErect.png differ diff --git a/public/furry.cool/images/GaoNSFWErectTransparent.png b/public/furry.cool/images/GaoNSFWErectTransparent.png new file mode 100644 index 0000000..e9c3e31 Binary files /dev/null and b/public/furry.cool/images/GaoNSFWErectTransparent.png differ diff --git a/public/furry.cool/images/GaoNSFWMaidErect.png b/public/furry.cool/images/GaoNSFWMaidErect.png new file mode 100644 index 0000000..2fe589b Binary files /dev/null and b/public/furry.cool/images/GaoNSFWMaidErect.png differ diff --git a/public/furry.cool/images/GaoNSFWMaidErectTransparent.png b/public/furry.cool/images/GaoNSFWMaidErectTransparent.png new file mode 100644 index 0000000..804f515 Binary files /dev/null and b/public/furry.cool/images/GaoNSFWMaidErectTransparent.png differ diff --git a/public/furry.cool/images/GaoNSFWMaidSheath.png b/public/furry.cool/images/GaoNSFWMaidSheath.png new file mode 100644 index 0000000..c11e4cf Binary files /dev/null and b/public/furry.cool/images/GaoNSFWMaidSheath.png differ diff --git a/public/furry.cool/images/GaoNSFWMaidSheathTransparent.png b/public/furry.cool/images/GaoNSFWMaidSheathTransparent.png new file mode 100644 index 0000000..0b3a387 Binary files /dev/null and b/public/furry.cool/images/GaoNSFWMaidSheathTransparent.png differ diff --git a/public/furry.cool/images/GaoNSFWSheath.png b/public/furry.cool/images/GaoNSFWSheath.png new file mode 100644 index 0000000..ae2bf9d Binary files /dev/null and b/public/furry.cool/images/GaoNSFWSheath.png differ diff --git a/public/furry.cool/images/GaoNSFWSheathTransparent.png b/public/furry.cool/images/GaoNSFWSheathTransparent.png new file mode 100644 index 0000000..1c70cdb Binary files /dev/null and b/public/furry.cool/images/GaoNSFWSheathTransparent.png differ diff --git a/public/furry.cool/images/GaoYCH.png b/public/furry.cool/images/GaoYCH.png new file mode 100644 index 0000000..a7f7a8c Binary files /dev/null and b/public/furry.cool/images/GaoYCH.png differ diff --git a/public/furry.cool/images/Jessi.png b/public/furry.cool/images/Jessi.png new file mode 100644 index 0000000..d5f8516 Binary files /dev/null and b/public/furry.cool/images/Jessi.png differ diff --git a/public/furry.cool/images/Joel.png b/public/furry.cool/images/Joel.png new file mode 100644 index 0000000..b0b0548 Binary files /dev/null and b/public/furry.cool/images/Joel.png differ diff --git a/public/furry.cool/images/Maid.png b/public/furry.cool/images/Maid.png new file mode 100644 index 0000000..a2f08ea Binary files /dev/null and b/public/furry.cool/images/Maid.png differ diff --git a/public/furry.cool/images/MaidBlep.png b/public/furry.cool/images/MaidBlep.png new file mode 100644 index 0000000..25162a8 Binary files /dev/null and b/public/furry.cool/images/MaidBlep.png differ diff --git a/public/furry.cool/images/MaidBoye.png b/public/furry.cool/images/MaidBoye.png new file mode 100644 index 0000000..a2f08ea Binary files /dev/null and b/public/furry.cool/images/MaidBoye.png differ diff --git a/public/furry.cool/images/MaidFull.png b/public/furry.cool/images/MaidFull.png new file mode 100644 index 0000000..41ed4a8 Binary files /dev/null and b/public/furry.cool/images/MaidFull.png differ diff --git a/public/furry.cool/images/Security.png b/public/furry.cool/images/Security.png new file mode 100644 index 0000000..18439bc Binary files /dev/null and b/public/furry.cool/images/Security.png differ diff --git a/public/furry.cool/images/Strawberry.png b/public/furry.cool/images/Strawberry.png new file mode 100644 index 0000000..9778468 Binary files /dev/null and b/public/furry.cool/images/Strawberry.png differ diff --git a/public/furry.cool/images/home-bg.jpg b/public/furry.cool/images/home-bg.jpg new file mode 100644 index 0000000..800b185 Binary files /dev/null and b/public/furry.cool/images/home-bg.jpg differ diff --git a/public/furry.cool/images/hypesquad-bravery-black.png b/public/furry.cool/images/hypesquad-bravery-black.png new file mode 100644 index 0000000..50380ec Binary files /dev/null and b/public/furry.cool/images/hypesquad-bravery-black.png differ diff --git a/public/furry.cool/images/hypesquad-bravery-white.png b/public/furry.cool/images/hypesquad-bravery-white.png new file mode 100644 index 0000000..2c62bb8 Binary files /dev/null and b/public/furry.cool/images/hypesquad-bravery-white.png differ diff --git a/public/furry.cool/images/noicon.png b/public/furry.cool/images/noicon.png new file mode 100644 index 0000000..4694873 Binary files /dev/null and b/public/furry.cool/images/noicon.png differ diff --git a/public/furry.cool/images/npm.png b/public/furry.cool/images/npm.png new file mode 100644 index 0000000..a7d49a0 Binary files /dev/null and b/public/furry.cool/images/npm.png differ diff --git a/public/furry.cool/images/yap.gif b/public/furry.cool/images/yap.gif new file mode 100644 index 0000000..ee638af Binary files /dev/null and b/public/furry.cool/images/yap.gif differ diff --git a/public/maidboye.cafe/assets/8Ball/Negative1.png b/public/maidboye.cafe/assets/8Ball/Negative1.png new file mode 100644 index 0000000..ce2b890 Binary files /dev/null and b/public/maidboye.cafe/assets/8Ball/Negative1.png differ diff --git a/public/maidboye.cafe/assets/8Ball/Negative2.png b/public/maidboye.cafe/assets/8Ball/Negative2.png new file mode 100644 index 0000000..d4a2143 Binary files /dev/null and b/public/maidboye.cafe/assets/8Ball/Negative2.png differ diff --git a/public/maidboye.cafe/assets/8Ball/Negative3.png b/public/maidboye.cafe/assets/8Ball/Negative3.png new file mode 100644 index 0000000..1b4680d Binary files /dev/null and b/public/maidboye.cafe/assets/8Ball/Negative3.png differ diff --git a/public/maidboye.cafe/assets/8Ball/Neutral1.png b/public/maidboye.cafe/assets/8Ball/Neutral1.png new file mode 100644 index 0000000..94a3bc5 Binary files /dev/null and b/public/maidboye.cafe/assets/8Ball/Neutral1.png differ diff --git a/public/maidboye.cafe/assets/8Ball/Neutral2.png b/public/maidboye.cafe/assets/8Ball/Neutral2.png new file mode 100644 index 0000000..3f2d78c Binary files /dev/null and b/public/maidboye.cafe/assets/8Ball/Neutral2.png differ diff --git a/public/maidboye.cafe/assets/8Ball/Neutral3.png b/public/maidboye.cafe/assets/8Ball/Neutral3.png new file mode 100644 index 0000000..0a59c43 Binary files /dev/null and b/public/maidboye.cafe/assets/8Ball/Neutral3.png differ diff --git a/public/maidboye.cafe/assets/8Ball/Positive1.png b/public/maidboye.cafe/assets/8Ball/Positive1.png new file mode 100644 index 0000000..a245db5 Binary files /dev/null and b/public/maidboye.cafe/assets/8Ball/Positive1.png differ diff --git a/public/maidboye.cafe/assets/8Ball/Positive2.png b/public/maidboye.cafe/assets/8Ball/Positive2.png new file mode 100644 index 0000000..caa9550 Binary files /dev/null and b/public/maidboye.cafe/assets/8Ball/Positive2.png differ diff --git a/public/maidboye.cafe/assets/8Ball/Positive3.png b/public/maidboye.cafe/assets/8Ball/Positive3.png new file mode 100644 index 0000000..4d89419 Binary files /dev/null and b/public/maidboye.cafe/assets/8Ball/Positive3.png differ diff --git a/public/maidboye.cafe/assets/Gay.png b/public/maidboye.cafe/assets/Gay.png new file mode 100644 index 0000000..4bfd969 Binary files /dev/null and b/public/maidboye.cafe/assets/Gay.png differ diff --git a/public/maidboye.cafe/assets/PoweredByGiphy.png b/public/maidboye.cafe/assets/PoweredByGiphy.png new file mode 100644 index 0000000..2387823 Binary files /dev/null and b/public/maidboye.cafe/assets/PoweredByGiphy.png differ diff --git a/public/maidboye.cafe/assets/bap.gif b/public/maidboye.cafe/assets/bap.gif new file mode 100644 index 0000000..9dab615 Binary files /dev/null and b/public/maidboye.cafe/assets/bap.gif differ diff --git a/public/maidboye.cafe/assets/bellyrub.gif b/public/maidboye.cafe/assets/bellyrub.gif new file mode 100644 index 0000000..9fcc47a Binary files /dev/null and b/public/maidboye.cafe/assets/bellyrub.gif differ diff --git a/public/maidboye.cafe/assets/dadjoke.png b/public/maidboye.cafe/assets/dadjoke.png new file mode 100644 index 0000000..e41f4c3 Binary files /dev/null and b/public/maidboye.cafe/assets/dadjoke.png differ diff --git a/public/maidboye.cafe/assets/loading.gif b/public/maidboye.cafe/assets/loading.gif new file mode 100644 index 0000000..faadfe5 Binary files /dev/null and b/public/maidboye.cafe/assets/loading.gif differ diff --git a/public/maidboye.cafe/assets/noicon.png b/public/maidboye.cafe/assets/noicon.png new file mode 100644 index 0000000..4694873 Binary files /dev/null and b/public/maidboye.cafe/assets/noicon.png differ diff --git a/public/maidboye.cafe/assets/ship/1-percent.png b/public/maidboye.cafe/assets/ship/1-percent.png new file mode 100644 index 0000000..71649fe Binary files /dev/null and b/public/maidboye.cafe/assets/ship/1-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/1-percent.svg b/public/maidboye.cafe/assets/ship/1-percent.svg new file mode 100644 index 0000000..420d0a9 --- /dev/null +++ b/public/maidboye.cafe/assets/ship/1-percent.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/maidboye.cafe/assets/ship/100-percent.png b/public/maidboye.cafe/assets/ship/100-percent.png new file mode 100644 index 0000000..a8c01d6 Binary files /dev/null and b/public/maidboye.cafe/assets/ship/100-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/100-percent.svg b/public/maidboye.cafe/assets/ship/100-percent.svg new file mode 100644 index 0000000..68d115a --- /dev/null +++ b/public/maidboye.cafe/assets/ship/100-percent.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/maidboye.cafe/assets/ship/2-19-percent.png b/public/maidboye.cafe/assets/ship/2-19-percent.png new file mode 100644 index 0000000..ebd903b Binary files /dev/null and b/public/maidboye.cafe/assets/ship/2-19-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/2-19-percent.svg b/public/maidboye.cafe/assets/ship/2-19-percent.svg new file mode 100644 index 0000000..1c295ea --- /dev/null +++ b/public/maidboye.cafe/assets/ship/2-19-percent.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/maidboye.cafe/assets/ship/20-39-percent.png b/public/maidboye.cafe/assets/ship/20-39-percent.png new file mode 100644 index 0000000..2e13f37 Binary files /dev/null and b/public/maidboye.cafe/assets/ship/20-39-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/20-39-percent.svg b/public/maidboye.cafe/assets/ship/20-39-percent.svg new file mode 100644 index 0000000..3caf9de --- /dev/null +++ b/public/maidboye.cafe/assets/ship/20-39-percent.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/maidboye.cafe/assets/ship/40-59-percent.png b/public/maidboye.cafe/assets/ship/40-59-percent.png new file mode 100644 index 0000000..d7d6aae Binary files /dev/null and b/public/maidboye.cafe/assets/ship/40-59-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/40-59-percent.svg b/public/maidboye.cafe/assets/ship/40-59-percent.svg new file mode 100644 index 0000000..d625f22 --- /dev/null +++ b/public/maidboye.cafe/assets/ship/40-59-percent.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/maidboye.cafe/assets/ship/60-79-percent.png b/public/maidboye.cafe/assets/ship/60-79-percent.png new file mode 100644 index 0000000..e5dadeb Binary files /dev/null and b/public/maidboye.cafe/assets/ship/60-79-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/60-79-percent.svg b/public/maidboye.cafe/assets/ship/60-79-percent.svg new file mode 100644 index 0000000..93d5947 --- /dev/null +++ b/public/maidboye.cafe/assets/ship/60-79-percent.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/maidboye.cafe/assets/ship/80-99-percent.png b/public/maidboye.cafe/assets/ship/80-99-percent.png new file mode 100644 index 0000000..a974187 Binary files /dev/null and b/public/maidboye.cafe/assets/ship/80-99-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/80-99-percent.svg b/public/maidboye.cafe/assets/ship/80-99-percent.svg new file mode 100644 index 0000000..9cf3dba --- /dev/null +++ b/public/maidboye.cafe/assets/ship/80-99-percent.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/maidboye.cafe/assets/ship/old/1-percent.png b/public/maidboye.cafe/assets/ship/old/1-percent.png new file mode 100644 index 0000000..02b090c Binary files /dev/null and b/public/maidboye.cafe/assets/ship/old/1-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/old/1-percent.svg b/public/maidboye.cafe/assets/ship/old/1-percent.svg new file mode 100644 index 0000000..e6866bf --- /dev/null +++ b/public/maidboye.cafe/assets/ship/old/1-percent.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/maidboye.cafe/assets/ship/old/100-percent.png b/public/maidboye.cafe/assets/ship/old/100-percent.png new file mode 100644 index 0000000..11c598c Binary files /dev/null and b/public/maidboye.cafe/assets/ship/old/100-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/old/100-percent.svg b/public/maidboye.cafe/assets/ship/old/100-percent.svg new file mode 100644 index 0000000..d25ef6b --- /dev/null +++ b/public/maidboye.cafe/assets/ship/old/100-percent.svg @@ -0,0 +1 @@ +image/svg+xml diff --git a/public/maidboye.cafe/assets/ship/old/2-19-percent.png b/public/maidboye.cafe/assets/ship/old/2-19-percent.png new file mode 100644 index 0000000..9aadce6 Binary files /dev/null and b/public/maidboye.cafe/assets/ship/old/2-19-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/old/2-19-percent.svg b/public/maidboye.cafe/assets/ship/old/2-19-percent.svg new file mode 100644 index 0000000..5ef9749 --- /dev/null +++ b/public/maidboye.cafe/assets/ship/old/2-19-percent.svg @@ -0,0 +1,30 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + + diff --git a/public/maidboye.cafe/assets/ship/old/20-39-percent.png b/public/maidboye.cafe/assets/ship/old/20-39-percent.png new file mode 100644 index 0000000..d63c526 Binary files /dev/null and b/public/maidboye.cafe/assets/ship/old/20-39-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/old/20-39-percent.svg b/public/maidboye.cafe/assets/ship/old/20-39-percent.svg new file mode 100644 index 0000000..143213d --- /dev/null +++ b/public/maidboye.cafe/assets/ship/old/20-39-percent.svg @@ -0,0 +1,6 @@ + + + + diff --git a/public/maidboye.cafe/assets/ship/old/40-59-percent.png b/public/maidboye.cafe/assets/ship/old/40-59-percent.png new file mode 100644 index 0000000..966b932 Binary files /dev/null and b/public/maidboye.cafe/assets/ship/old/40-59-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/old/40-59-percent.svg b/public/maidboye.cafe/assets/ship/old/40-59-percent.svg new file mode 100644 index 0000000..6dc97e2 --- /dev/null +++ b/public/maidboye.cafe/assets/ship/old/40-59-percent.svg @@ -0,0 +1 @@ +image/svg+xml diff --git a/public/maidboye.cafe/assets/ship/old/60-79-percent.png b/public/maidboye.cafe/assets/ship/old/60-79-percent.png new file mode 100644 index 0000000..fa86906 Binary files /dev/null and b/public/maidboye.cafe/assets/ship/old/60-79-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/old/60-79-percent.svg b/public/maidboye.cafe/assets/ship/old/60-79-percent.svg new file mode 100644 index 0000000..1f586cd --- /dev/null +++ b/public/maidboye.cafe/assets/ship/old/60-79-percent.svg @@ -0,0 +1 @@ +image/svg+xml diff --git a/public/maidboye.cafe/assets/ship/old/80-99-percent.png b/public/maidboye.cafe/assets/ship/old/80-99-percent.png new file mode 100644 index 0000000..292fbf2 Binary files /dev/null and b/public/maidboye.cafe/assets/ship/old/80-99-percent.png differ diff --git a/public/maidboye.cafe/assets/ship/old/80-99-percent.svg b/public/maidboye.cafe/assets/ship/old/80-99-percent.svg new file mode 100644 index 0000000..b8e0e29 --- /dev/null +++ b/public/maidboye.cafe/assets/ship/old/80-99-percent.svg @@ -0,0 +1 @@ +image/svg+xml diff --git a/public/maidboye.cafe/images/MaidAngry.png b/public/maidboye.cafe/images/MaidAngry.png new file mode 100644 index 0000000..d796c7b Binary files /dev/null and b/public/maidboye.cafe/images/MaidAngry.png differ diff --git a/public/maidboye.cafe/images/MaidBlep.png b/public/maidboye.cafe/images/MaidBlep.png new file mode 100644 index 0000000..25162a8 Binary files /dev/null and b/public/maidboye.cafe/images/MaidBlep.png differ diff --git a/public/maidboye.cafe/images/MaidBoye.png b/public/maidboye.cafe/images/MaidBoye.png new file mode 100644 index 0000000..a2f08ea Binary files /dev/null and b/public/maidboye.cafe/images/MaidBoye.png differ diff --git a/public/maidboye.cafe/images/MaidBulge.png b/public/maidboye.cafe/images/MaidBulge.png new file mode 100644 index 0000000..41ed4a8 Binary files /dev/null and b/public/maidboye.cafe/images/MaidBulge.png differ diff --git a/public/maidboye.cafe/images/MaidFullTransparent.png b/public/maidboye.cafe/images/MaidFullTransparent.png new file mode 100644 index 0000000..44756a7 Binary files /dev/null and b/public/maidboye.cafe/images/MaidFullTransparent.png differ diff --git a/public/maidboye.cafe/images/MaidHappy.png b/public/maidboye.cafe/images/MaidHappy.png new file mode 100644 index 0000000..6ca7957 Binary files /dev/null and b/public/maidboye.cafe/images/MaidHappy.png differ diff --git a/public/maidboye.cafe/images/MaidLite.png b/public/maidboye.cafe/images/MaidLite.png new file mode 100644 index 0000000..d11df9b Binary files /dev/null and b/public/maidboye.cafe/images/MaidLite.png differ diff --git a/public/maidboye.cafe/images/MaidMascot.png b/public/maidboye.cafe/images/MaidMascot.png new file mode 100644 index 0000000..fe6fcbe Binary files /dev/null and b/public/maidboye.cafe/images/MaidMascot.png differ diff --git a/public/maidboye.cafe/images/MaidMascot_blur.png b/public/maidboye.cafe/images/MaidMascot_blur.png new file mode 100644 index 0000000..c66f344 Binary files /dev/null and b/public/maidboye.cafe/images/MaidMascot_blur.png differ diff --git a/public/maidboye.cafe/images/MaidRef.png b/public/maidboye.cafe/images/MaidRef.png new file mode 100644 index 0000000..9a990f3 Binary files /dev/null and b/public/maidboye.cafe/images/MaidRef.png differ diff --git a/public/maidboye.cafe/images/MaidShy.png b/public/maidboye.cafe/images/MaidShy.png new file mode 100644 index 0000000..28b645f Binary files /dev/null and b/public/maidboye.cafe/images/MaidShy.png differ diff --git a/public/maidboye.cafe/images/blep.png b/public/maidboye.cafe/images/blep.png new file mode 100644 index 0000000..25162a8 Binary files /dev/null and b/public/maidboye.cafe/images/blep.png differ diff --git a/public/maidboye.cafe/images/full.png b/public/maidboye.cafe/images/full.png new file mode 100644 index 0000000..41ed4a8 Binary files /dev/null and b/public/maidboye.cafe/images/full.png differ diff --git a/public/maidboye.cafe/images/lite.png b/public/maidboye.cafe/images/lite.png new file mode 100644 index 0000000..25162a8 Binary files /dev/null and b/public/maidboye.cafe/images/lite.png differ diff --git a/public/maidboye.cafe/images/ref.png b/public/maidboye.cafe/images/ref.png new file mode 100644 index 0000000..9a990f3 Binary files /dev/null and b/public/maidboye.cafe/images/ref.png differ diff --git a/public/maidboye.cafe/screenshots/bulkdelete-1.png b/public/maidboye.cafe/screenshots/bulkdelete-1.png new file mode 100644 index 0000000..5fd612c Binary files /dev/null and b/public/maidboye.cafe/screenshots/bulkdelete-1.png differ diff --git a/public/maidboye.cafe/screenshots/bulkdelete-2.png b/public/maidboye.cafe/screenshots/bulkdelete-2.png new file mode 100644 index 0000000..2e59b7b Binary files /dev/null and b/public/maidboye.cafe/screenshots/bulkdelete-2.png differ diff --git a/public/maidboye.cafe/screenshots/delete.png b/public/maidboye.cafe/screenshots/delete.png new file mode 100644 index 0000000..a1285c0 Binary files /dev/null and b/public/maidboye.cafe/screenshots/delete.png differ diff --git a/public/maidboye.cafe/screenshots/edit-crossposted.png b/public/maidboye.cafe/screenshots/edit-crossposted.png new file mode 100644 index 0000000..6e5c604 Binary files /dev/null and b/public/maidboye.cafe/screenshots/edit-crossposted.png differ diff --git a/public/maidboye.cafe/screenshots/edit-suppress-embeds.png b/public/maidboye.cafe/screenshots/edit-suppress-embeds.png new file mode 100644 index 0000000..442541a Binary files /dev/null and b/public/maidboye.cafe/screenshots/edit-suppress-embeds.png differ diff --git a/public/maidboye.cafe/screenshots/edit-thread-creation.png b/public/maidboye.cafe/screenshots/edit-thread-creation.png new file mode 100644 index 0000000..87ae974 Binary files /dev/null and b/public/maidboye.cafe/screenshots/edit-thread-creation.png differ diff --git a/public/maidboye.cafe/screenshots/edit.png b/public/maidboye.cafe/screenshots/edit.png new file mode 100644 index 0000000..ebdfb1c Binary files /dev/null and b/public/maidboye.cafe/screenshots/edit.png differ diff --git a/public/maidboye.cafe/screenshots/snipe-delete-ref.png b/public/maidboye.cafe/screenshots/snipe-delete-ref.png new file mode 100644 index 0000000..60d5943 Binary files /dev/null and b/public/maidboye.cafe/screenshots/snipe-delete-ref.png differ diff --git a/public/maidboye.cafe/screenshots/snipe-delete.png b/public/maidboye.cafe/screenshots/snipe-delete.png new file mode 100644 index 0000000..94ab3eb Binary files /dev/null and b/public/maidboye.cafe/screenshots/snipe-delete.png differ diff --git a/public/maidboye.cafe/screenshots/snipe-edit.png b/public/maidboye.cafe/screenshots/snipe-edit.png new file mode 100644 index 0000000..6027533 Binary files /dev/null and b/public/maidboye.cafe/screenshots/snipe-edit.png differ diff --git a/public/maidboye.cafe/screenshots/snipe-keydb.png b/public/maidboye.cafe/screenshots/snipe-keydb.png new file mode 100644 index 0000000..9f70844 Binary files /dev/null and b/public/maidboye.cafe/screenshots/snipe-keydb.png differ diff --git a/public/oceanic.ws/images/icon.png b/public/oceanic.ws/images/icon.png new file mode 100644 index 0000000..2fff148 Binary files /dev/null and b/public/oceanic.ws/images/icon.png differ diff --git a/public/skye_tableflip.gif b/public/skye_tableflip.gif new file mode 100644 index 0000000..2efb07a Binary files /dev/null and b/public/skye_tableflip.gif differ diff --git a/public/yiff.rest/Blep.png b/public/yiff.rest/Blep.png new file mode 100644 index 0000000..8e6a626 Binary files /dev/null and b/public/yiff.rest/Blep.png differ diff --git a/public/yiff.rocks/mascots/DonovanLying.png b/public/yiff.rocks/mascots/DonovanLying.png new file mode 100644 index 0000000..fca5119 Binary files /dev/null and b/public/yiff.rocks/mascots/DonovanLying.png differ diff --git a/public/yiff.rocks/mascots/DonovanMaid.png b/public/yiff.rocks/mascots/DonovanMaid.png new file mode 100644 index 0000000..cb8c69a Binary files /dev/null and b/public/yiff.rocks/mascots/DonovanMaid.png differ diff --git a/public/yiff.rocks/mascots/DonovanSheath.png b/public/yiff.rocks/mascots/DonovanSheath.png new file mode 100644 index 0000000..ad7eda0 Binary files /dev/null and b/public/yiff.rocks/mascots/DonovanSheath.png differ diff --git a/public/yiff.rocks/mascots/DonovanShy.png b/public/yiff.rocks/mascots/DonovanShy.png new file mode 100644 index 0000000..fdfa8e2 Binary files /dev/null and b/public/yiff.rocks/mascots/DonovanShy.png differ diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..28b9ca8 --- /dev/null +++ b/start.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +docker compose run --rm websites rake assets:precompile +docker compose -f docker-compose.prod.yml up -d diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/storage/.keep b/tmp/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..33459bd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,139 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "compilerOptions": { + // Type Checking + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "alwaysStrict": true, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strict": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "useUnknownInCatchVariables": true, + + // Modules + "allowArbitraryExtensions": false, + "allowImportingTsExtensions": false, + "allowUmdGlobalAccess": true, + // baseUrl + // customConditions + "module": "ES2015", + "moduleResolution": "Node", + // moduleSuffixes + // noResolve + // paths + "resolveJsonModule": false, + // resolvePackageJsonExports + // resolvePackageJsonImports + // rootDir + // rootDirs + "typeRoots": ["node_modules/@types", "app/javascript/@types"], + // types + + // Emit + "declaration": false, + // declarationDir + // declarationMap + "downlevelIteration": true, + "emitBOM": false, + // emitDeclarationOnly + "importHelpers": false, + // importsNotUsedAsValues + "inlineSourceMap": false, + // inlineSources + // mapRoot + "newLine": "LF", + // noEmit + // noEmitHelpers + "noEmitOnError": true, + "outDir": "./dist", + // outFile + "preserveConstEnums": false, + // preserveValueImports + // removeComments + // sourceMap, + // sourceRoot + // stripInternal + + // JavaScript Support + "allowJs": true, + "checkJs": true, + // maxNodeModuleJsDepth + + // Editor Support + // disableSizeLimit + // plugins + + // Interop Constraints + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + // isolatedModules + // preserveSymlinks + // verbatimModuleSyntax + + // Backward Compatibility + // charset + // keyofStringsOnly + // noImplicitUseStrict + // noStrictGenericChecks + // out + // suppressExcessPropertyErrors + // suppressImplicitAnyIndexErrors + + // Language and Environment + // emitDecoratorMetadata + // experimentalDecorators + "jsx": "preserve", + // jsxFactory + // jsxFragmentFactory + // jsxImportSource + "lib": ["ES2022", "DOM"], + "moduleDetection": "force", + // noLib + // reactNamespace + "target": "ES2022", + "useDefineForClassFields": true, + + // Compiler Diagnostics + // diagnostics + // explainFiles + // extendedDiagnostics + // generateCpuProfile + // listEmittedFiles + // listFiles + // traceResolution + + // Projects + // composite + // disableReferencedProjectLoad + // disableSolutionSearching + // disableSourceOfProjectReferenceRedirect + // incremental + // tsBuildInfoFile + + // Output Formatting + "noErrorTruncation": true, + // preserveWatchOutput + // pretty + + // Completeness + // skipDefaultLibCheck + "skipLibCheck": true + + // Watch Options + // assumeChangesOnlyAffectDirectDependencies + }, + "include": ["app/javascript/**/*"] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..3a7cdb6 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1936 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + +"@babel/code-frame@^7.0.0": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + +"@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.22.5": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@es-joy/jsdoccomment@~0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz#4a2f7db42209c0425c71a1476ef1bdb6dcd836f6" + integrity sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw== + dependencies: + comment-parser "1.4.1" + esquery "^1.5.0" + jsdoc-type-pratt-parser "~4.0.0" + +"@esbuild/android-arm64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz#276c5f99604054d3dbb733577e09adae944baa90" + integrity sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ== + +"@esbuild/android-arm@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.5.tgz#4a3cbf14758166abaae8ba9c01a80e68342a4eec" + integrity sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA== + +"@esbuild/android-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.5.tgz#21a3d11cd4613d2d3c5ccb9e746c254eb9265b0a" + integrity sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA== + +"@esbuild/darwin-arm64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz#714cb839f467d6a67b151ee8255886498e2b9bf6" + integrity sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw== + +"@esbuild/darwin-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz#2c553e97a6d2b4ae76a884e35e6cbab85a990bbf" + integrity sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA== + +"@esbuild/freebsd-arm64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz#d554f556718adb31917a0da24277bf84b6ee87f3" + integrity sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ== + +"@esbuild/freebsd-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz#288f7358a3bb15d99e73c65c9adaa3dabb497432" + integrity sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ== + +"@esbuild/linux-arm64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz#95933ae86325c93cb6b5e8333d22120ecfdc901b" + integrity sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA== + +"@esbuild/linux-arm@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz#0acef93aa3e0579e46d33b666627bddb06636664" + integrity sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ== + +"@esbuild/linux-ia32@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz#b6e5c9e80b42131cbd6b1ddaa48c92835f1ed67f" + integrity sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ== + +"@esbuild/linux-loong64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz#e5f0cf95a180158b01ff5f417da796a1c09dfbea" + integrity sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw== + +"@esbuild/linux-mips64el@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz#ae36fb86c7d5f641f3a0c8472e83dcb6ea36a408" + integrity sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg== + +"@esbuild/linux-ppc64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz#7960cb1666f0340ddd9eef7b26dcea3835d472d0" + integrity sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q== + +"@esbuild/linux-riscv64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz#32207df26af60a3a9feea1783fc21b9817bade19" + integrity sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag== + +"@esbuild/linux-s390x@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz#b38d5681db89a3723862dfa792812397b1510a7d" + integrity sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw== + +"@esbuild/linux-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz#46feba2ad041a241379d150f415b472fe3885075" + integrity sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A== + +"@esbuild/netbsd-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz#3b5c1fb068f26bfc681d31f682adf1bea4ef0702" + integrity sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g== + +"@esbuild/openbsd-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz#ca6830316ca68056c5c88a875f103ad3235e00db" + integrity sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA== + +"@esbuild/sunos-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz#9efc4eb9539a7be7d5a05ada52ee43cda0d8e2dd" + integrity sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg== + +"@esbuild/win32-arm64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz#29f8184afa7a02a956ebda4ed638099f4b8ff198" + integrity sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg== + +"@esbuild/win32-ia32@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz#f3de07afb292ecad651ae4bb8727789de2d95b05" + integrity sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw== + +"@esbuild/win32-x64@0.19.5": + version "0.19.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz#faad84c41ba12e3a0acb52571df9bff37bee75f6" + integrity sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw== + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + +"@eslint/eslintrc@^2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.3.tgz#797470a75fe0fbd5a53350ee715e85e87baff22d" + integrity sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.53.0": + version "8.53.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.53.0.tgz#bea56f2ed2b5baea164348ff4d5a879f6f81f20d" + integrity sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w== + +"@hotwired/stimulus@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.2.tgz#071aab59c600fed95b97939e605ff261a4251608" + integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A== + +"@hotwired/turbo-rails@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-7.3.0.tgz#422c21752509f3edcd6c7b2725bbe9e157815f51" + integrity sha512-fvhO64vp/a2UVQ3jue9WTc2JisMv9XilIC7ViZmXAREVwiQ2S4UC7Go8f9A1j4Xu7DBI6SbFdqILk5ImqVoqyA== + dependencies: + "@hotwired/turbo" "^7.3.0" + "@rails/actioncable" "^7.0" + +"@hotwired/turbo@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.3.0.tgz#2226000fff1aabda9fd9587474565c9929dbf15d" + integrity sha512-Dcu+NaSvHLT7EjrDrkEmH4qET2ZJZ5IcCWmNXxNQTBwlnE5tBZfN6WxZ842n5cHV52DH/AKNirbPBtcEXDLW4g== + +"@humanwhocodes/config-array@^0.11.13": + version "0.11.13" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" + integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ== + dependencies: + "@humanwhocodes/object-schema" "^2.0.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" + integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@popperjs/core@^2.11.8", "@popperjs/core@^2.9.2": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + +"@rails/actioncable@^7.0": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.1.1.tgz#e8c49769d41f35a4473133c259cc98adc04dddf8" + integrity sha512-ZRJ9rdwFQQjRbtgJnweY0/4UQyxN6ojEGRdib0JkjnuIciv+4ok/aAeZmBJqNreTMaBqS0eHyA9hCArwN58opg== + +"@rails/ujs@^7.1.2": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.1.2.tgz#ea903bcc0224e17156015d995b6f1b83e27d64b2" + integrity sha512-c5x02djEKEVVE4qfN4XgElJS4biM0xxtIVpcJ0ZHLK116U19rowTtmD0AJ/RCb3Xaewa4GPIWLlwgeC0dCQqzw== + +"@swc/core-darwin-arm64@1.3.96": + version "1.3.96" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.96.tgz#7c1c4245ce3f160a5b36a48ed071e3061a839e1d" + integrity sha512-8hzgXYVd85hfPh6mJ9yrG26rhgzCmcLO0h1TIl8U31hwmTbfZLzRitFQ/kqMJNbIBCwmNH1RU2QcJnL3d7f69A== + +"@swc/core-darwin-x64@1.3.96": + version "1.3.96" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.3.96.tgz#4720ff897ca3f22fe77d0be688968161480c80f0" + integrity sha512-mFp9GFfuPg+43vlAdQZl0WZpZSE8sEzqL7sr/7Reul5McUHP0BaLsEzwjvD035ESfkY8GBZdLpMinblIbFNljQ== + +"@swc/core-linux-arm-gnueabihf@1.3.96": + version "1.3.96" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.96.tgz#2c238ae00b13918ac058b132a31dc57dbcf94e39" + integrity sha512-8UEKkYJP4c8YzYIY/LlbSo8z5Obj4hqcv/fUTHiEePiGsOddgGf7AWjh56u7IoN/0uEmEro59nc1ChFXqXSGyg== + +"@swc/core-linux-arm64-gnu@1.3.96": + version "1.3.96" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.96.tgz#be2e84506b9761b561fb9a341e587f8594a8e55d" + integrity sha512-c/IiJ0s1y3Ymm2BTpyC/xr6gOvoqAVETrivVXHq68xgNms95luSpbYQ28rqaZC8bQC8M5zdXpSc0T8DJu8RJGw== + +"@swc/core-linux-arm64-musl@1.3.96": + version "1.3.96" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.96.tgz#22c9ce17bd923ae358760e668ca33c90210c2ae5" + integrity sha512-i5/UTUwmJLri7zhtF6SAo/4QDQJDH2fhYJaBIUhrICmIkRO/ltURmpejqxsM/ye9Jqv5zG7VszMC0v/GYn/7BQ== + +"@swc/core-linux-x64-gnu@1.3.96": + version "1.3.96" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.96.tgz#c17c072e338341c0ac3507a31ab2a36d16d79c98" + integrity sha512-USdaZu8lTIkm4Yf9cogct/j5eqtdZqTgcTib4I+NloUW0E/hySou3eSyp3V2UAA1qyuC72ld1otXuyKBna0YKQ== + +"@swc/core-linux-x64-musl@1.3.96": + version "1.3.96" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.96.tgz#eb74594a48b4e9cabdce7f5525b3b946f8d6dd16" + integrity sha512-QYErutd+G2SNaCinUVobfL7jWWjGTI0QEoQ6hqTp7PxCJS/dmKmj3C5ZkvxRYcq7XcZt7ovrYCTwPTHzt6lZBg== + +"@swc/core-win32-arm64-msvc@1.3.96": + version "1.3.96" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.96.tgz#6f7c0d20d80534b0676dc6761904288c16e93857" + integrity sha512-hjGvvAduA3Un2cZ9iNP4xvTXOO4jL3G9iakhFsgVhpkU73SGmK7+LN8ZVBEu4oq2SUcHO6caWvnZ881cxGuSpg== + +"@swc/core-win32-ia32-msvc@1.3.96": + version "1.3.96" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.96.tgz#47bb24ef2e4c81407a6786649246983cc69e7854" + integrity sha512-Far2hVFiwr+7VPCM2GxSmbh3ikTpM3pDombE+d69hkedvYHYZxtTF+2LTKl/sXtpbUnsoq7yV/32c9R/xaaWfw== + +"@swc/core-win32-x64-msvc@1.3.96": + version "1.3.96" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.96.tgz#c796e3df7afe2875d227c74add16a7d09c77d8bd" + integrity sha512-4VbSAniIu0ikLf5mBX81FsljnfqjoVGleEkCQv4+zRlyZtO3FHoDPkeLVoy6WRlj7tyrRcfUJ4mDdPkbfTO14g== + +"@swc/core@*": + version "1.3.96" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.3.96.tgz#f04d58b227ceed2fee6617ce2cdddf21d0803f96" + integrity sha512-zwE3TLgoZwJfQygdv2SdCK9mRLYluwDOM53I+dT6Z5ZvrgVENmY3txvWDvduzkV+/8IuvrRbVezMpxcojadRdQ== + dependencies: + "@swc/counter" "^0.1.1" + "@swc/types" "^0.1.5" + optionalDependencies: + "@swc/core-darwin-arm64" "1.3.96" + "@swc/core-darwin-x64" "1.3.96" + "@swc/core-linux-arm-gnueabihf" "1.3.96" + "@swc/core-linux-arm64-gnu" "1.3.96" + "@swc/core-linux-arm64-musl" "1.3.96" + "@swc/core-linux-x64-gnu" "1.3.96" + "@swc/core-linux-x64-musl" "1.3.96" + "@swc/core-win32-arm64-msvc" "1.3.96" + "@swc/core-win32-ia32-msvc" "1.3.96" + "@swc/core-win32-x64-msvc" "1.3.96" + +"@swc/counter@^0.1.1": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.2.tgz#bf06d0770e47c6f1102270b744e17b934586985e" + integrity sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw== + +"@swc/helpers@*": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.3.tgz#98c6da1e196f5f08f977658b80d6bd941b5f294f" + integrity sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A== + dependencies: + tslib "^2.4.0" + +"@swc/types@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.5.tgz#043b731d4f56a79b4897a3de1af35e75d56bc63a" + integrity sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw== + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/bootstrap@^5.2.10": + version "5.2.10" + resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.10.tgz#58506463bccc6602bc051487ad8d3a6458f94c6c" + integrity sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g== + dependencies: + "@popperjs/core" "^2.9.2" + +"@types/jquery@*": + version "3.5.27" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.27.tgz#d9d67a003d0292a36fe35868a618c82f8fd12b19" + integrity sha512-TR28Y8ezIGgfyA02UOh9x+Fy16/1qWYAnvtRd2gTBJuccX/vmddyti0MezLkTv7f+OLofVc2T961VPyKv1tXJQ== + dependencies: + "@types/sizzle" "*" + +"@types/jquery@^3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.29.tgz#3c06a1f519cd5fc3a7a108971436c00685b5dcea" + integrity sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg== + dependencies: + "@types/sizzle" "*" + +"@types/jqueryui@^1.12.20": + version "1.12.20" + resolved "https://registry.yarnpkg.com/@types/jqueryui/-/jqueryui-1.12.20.tgz#c726cba62b3f32134275e85f79aeff4ffff16c90" + integrity sha512-kHjvlPOHy+8+3SBYFA4g+n144SBZ/RFEq6oYr5w1I7PfrrGJ3RXNc13UlGA2ohkx2EjGD93QB97or63IJWB+Lw== + dependencies: + "@types/jquery" "*" + +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/normalize-package-data@^2.4.0": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== + +"@types/rails__ujs@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/rails__ujs/-/rails__ujs-6.0.4.tgz#c5fc210e1a1fe186d9a4386280490f3ceb17d0e9" + integrity sha512-4A8+Xp+2bd4E/mLjfwldc46katQQJfLfwTRbhDBoQyj4timlpWfJ6uHqwyre/qwcapgjKtVd365pt9ovD/3tRw== + +"@types/semver@^7.5.0": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.5.tgz#deed5ab7019756c9c90ea86139106b0346223f35" + integrity sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg== + +"@types/sizzle@*": + version "2.3.6" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.6.tgz#e39b7123dac4631001939bd4c2a26d46010f2275" + integrity sha512-m04Om5Gz6kbjUwAQ7XJJQ30OdEFsSmAVsvn4NYwcTRyMVpKKa1aPuESw1n2CxS5fYkOQv3nHgDKeNa8e76fUkw== + +"@typescript-eslint/eslint-plugin@^6.3.0": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.10.0.tgz#cfe2bd34e26d2289212946b96ab19dcad64b661a" + integrity sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg== + dependencies: + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.10.0" + "@typescript-eslint/type-utils" "6.10.0" + "@typescript-eslint/utils" "6.10.0" + "@typescript-eslint/visitor-keys" "6.10.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/parser@^6.3.0": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.10.0.tgz#578af79ae7273193b0b6b61a742a2bc8e02f875a" + integrity sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog== + dependencies: + "@typescript-eslint/scope-manager" "6.10.0" + "@typescript-eslint/types" "6.10.0" + "@typescript-eslint/typescript-estree" "6.10.0" + "@typescript-eslint/visitor-keys" "6.10.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@6.10.0": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz#b0276118b13d16f72809e3cecc86a72c93708540" + integrity sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg== + dependencies: + "@typescript-eslint/types" "6.10.0" + "@typescript-eslint/visitor-keys" "6.10.0" + +"@typescript-eslint/type-utils@6.10.0": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.10.0.tgz#1007faede067c78bdbcef2e8abb31437e163e2e1" + integrity sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg== + dependencies: + "@typescript-eslint/typescript-estree" "6.10.0" + "@typescript-eslint/utils" "6.10.0" + debug "^4.3.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/types@6.10.0": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.10.0.tgz#f4f0a84aeb2ac546f21a66c6e0da92420e921367" + integrity sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg== + +"@typescript-eslint/typescript-estree@6.10.0": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz#667381eed6f723a1a8ad7590a31f312e31e07697" + integrity sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg== + dependencies: + "@typescript-eslint/types" "6.10.0" + "@typescript-eslint/visitor-keys" "6.10.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.10.0": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.10.0.tgz#4d76062d94413c30e402c9b0df8c14aef8d77336" + integrity sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.10.0" + "@typescript-eslint/types" "6.10.0" + "@typescript-eslint/typescript-estree" "6.10.0" + semver "^7.5.4" + +"@typescript-eslint/visitor-keys@6.10.0": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz#b9eaf855a1ac7e95633ae1073af43d451e8f84e3" + integrity sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg== + dependencies: + "@typescript-eslint/types" "6.10.0" + eslint-visitor-keys "^3.4.1" + +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +"@uwu-codes/eslint-config@^1.1.26": + version "1.1.26" + resolved "https://registry.yarnpkg.com/@uwu-codes/eslint-config/-/eslint-config-1.1.26.tgz#1e01ca56f7021017b1a4e9111cfa5d5ffa3809a1" + integrity sha512-HDUe7tMvOBaD6mqltI754e959sz3h+qbsU4AENlDBxcrVVLamxm0wUStF/DSeBwyNWsFEThfEEYygmSsmGBOAQ== + dependencies: + "@typescript-eslint/eslint-plugin" "^6.3.0" + "@typescript-eslint/parser" "^6.3.0" + "@uwu-codes/tsconfig" "^1.0.7" + eslint "^8.46.0" + eslint-plugin-import "npm:eslint-plugin-i@^2.28.0" + eslint-plugin-import-newlines "^1.3.4" + eslint-plugin-jsdoc "^46.4.6" + eslint-plugin-json "^3.1.0" + eslint-plugin-prefer-arrow "^1.2.3" + eslint-plugin-unicorn "^48.0.1" + eslint-plugin-unused-imports "^3.0.0" + +"@uwu-codes/tsconfig@^1.0.7": + version "1.0.12" + resolved "https://registry.yarnpkg.com/@uwu-codes/tsconfig/-/tsconfig-1.0.12.tgz#d1a5b1d39ade6a95683f56a8f9273ec1d71bfdee" + integrity sha512-AlESNcCgf3juVddicrfIkI3vQe/TPB488TyzpOul2CAflmaL7eecdhMCOjVKdlm7qy0/5WKEbAlDvknXAZYBHg== + dependencies: + typescript "^5.1.6" + optionalDependencies: + "@swc/core" "*" + "@swc/helpers" "*" + ts-node "*" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^8.1.1: + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" + integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== + +acorn@^8.4.1, acorn@^8.9.0: + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +are-docs-informative@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/are-docs-informative/-/are-docs-informative-0.0.2.tgz#387f0e93f5d45280373d387a59d34c96db321963" + integrity sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bootstrap@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.2.tgz#97226583f27aae93b2b28ab23f4c114757ff16ae" + integrity sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +ci-info@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +clean-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clean-regexp/-/clean-regexp-1.0.0.tgz#8df7c7aae51fd36874e8f8d05b9180bc11a3fed7" + integrity sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw== + dependencies: + escape-string-regexp "^1.0.5" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +comment-parser@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.4.1.tgz#bdafead37961ac079be11eb7ec65c4d021eaf9cc" + integrity sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +esbuild-sass-plugin@^2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.0.tgz#2908ab5e104cfc980118c46d0b409cbab8aa32dd" + integrity sha512-mGCe9MxNYvZ+j77Q/QFO+rwUGA36mojDXkOhtVmoyz1zwYbMaNrtVrmXwwYDleS/UMKTNU3kXuiTtPiAD3K+Pw== + dependencies: + resolve "^1.22.6" + sass "^1.7.3" + +esbuild@^0.19.5: + version "0.19.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.5.tgz#53a0e19dfbf61ba6c827d51a80813cf071239a8c" + integrity sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ== + optionalDependencies: + "@esbuild/android-arm" "0.19.5" + "@esbuild/android-arm64" "0.19.5" + "@esbuild/android-x64" "0.19.5" + "@esbuild/darwin-arm64" "0.19.5" + "@esbuild/darwin-x64" "0.19.5" + "@esbuild/freebsd-arm64" "0.19.5" + "@esbuild/freebsd-x64" "0.19.5" + "@esbuild/linux-arm" "0.19.5" + "@esbuild/linux-arm64" "0.19.5" + "@esbuild/linux-ia32" "0.19.5" + "@esbuild/linux-loong64" "0.19.5" + "@esbuild/linux-mips64el" "0.19.5" + "@esbuild/linux-ppc64" "0.19.5" + "@esbuild/linux-riscv64" "0.19.5" + "@esbuild/linux-s390x" "0.19.5" + "@esbuild/linux-x64" "0.19.5" + "@esbuild/netbsd-x64" "0.19.5" + "@esbuild/openbsd-x64" "0.19.5" + "@esbuild/sunos-x64" "0.19.5" + "@esbuild/win32-arm64" "0.19.5" + "@esbuild/win32-ia32" "0.19.5" + "@esbuild/win32-x64" "0.19.5" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-module-utils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" + integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== + dependencies: + debug "^3.2.7" + +eslint-plugin-import-newlines@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-import-newlines/-/eslint-plugin-import-newlines-1.3.4.tgz#c3917ae478b1dcce2a920637eaa8af001b8e1477" + integrity sha512-Lmf/BbK+EQKUfjKPcZpslE/KTGYlgaI8ZJ/sYzdbb3BVTg5+GmLBLHBjsUKNEVRM1SEhDTF/didtOSYKi4tSnQ== + +"eslint-plugin-import@npm:eslint-plugin-i@^2.28.0": + version "2.29.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-i/-/eslint-plugin-i-2.29.0.tgz#6b7d407e7a9a23d84c5c330b69aeea198f2f3d10" + integrity sha512-slGeTS3GQzx9267wLJnNYNO8X9EHGsc75AKIAFvnvMYEcTJKotPKL1Ru5PIGVHIVet+2DsugePWp8Oxpx8G22w== + dependencies: + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.8.0" + get-tsconfig "^4.6.2" + is-glob "^4.0.3" + minimatch "^3.1.2" + resolve "^1.22.3" + semver "^7.5.3" + +eslint-plugin-jsdoc@^46.4.6: + version "46.9.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.9.0.tgz#9887569dbeef0a008a2770bfc5d0f7fc39f21f2b" + integrity sha512-UQuEtbqLNkPf5Nr/6PPRCtr9xypXY+g8y/Q7gPa0YK7eDhh0y2lWprXRnaYbW7ACgIUvpDKy9X2bZqxtGzBG9Q== + dependencies: + "@es-joy/jsdoccomment" "~0.41.0" + are-docs-informative "^0.0.2" + comment-parser "1.4.1" + debug "^4.3.4" + escape-string-regexp "^4.0.0" + esquery "^1.5.0" + is-builtin-module "^3.2.1" + semver "^7.5.4" + spdx-expression-parse "^3.0.1" + +eslint-plugin-json@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-json/-/eslint-plugin-json-3.1.0.tgz#251108ba1681c332e0a442ef9513bd293619de67" + integrity sha512-MrlG2ynFEHe7wDGwbUuFPsaT2b1uhuEFhJ+W1f1u+1C2EkXmTYJp4B1aAdQQ8M+CC3t//N/oRKiIVw14L2HR1g== + dependencies: + lodash "^4.17.21" + vscode-json-languageservice "^4.1.6" + +eslint-plugin-prefer-arrow@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.3.tgz#e7fbb3fa4cd84ff1015b9c51ad86550e55041041" + integrity sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ== + +eslint-plugin-unicorn@^48.0.1: + version "48.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-48.0.1.tgz#a6573bc1687ae8db7121fdd8f92394b6549a6959" + integrity sha512-FW+4r20myG/DqFcCSzoumaddKBicIPeFnTrifon2mWIzlfyvzwyqZjqVP7m4Cqr/ZYisS2aiLghkUWaPg6vtCw== + dependencies: + "@babel/helper-validator-identifier" "^7.22.5" + "@eslint-community/eslint-utils" "^4.4.0" + ci-info "^3.8.0" + clean-regexp "^1.0.0" + esquery "^1.5.0" + indent-string "^4.0.0" + is-builtin-module "^3.2.1" + jsesc "^3.0.2" + lodash "^4.17.21" + pluralize "^8.0.0" + read-pkg-up "^7.0.1" + regexp-tree "^0.1.27" + regjsparser "^0.10.0" + semver "^7.5.4" + strip-indent "^3.0.0" + +eslint-plugin-unused-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.0.0.tgz#d25175b0072ff16a91892c3aa72a09ca3a9e69e7" + integrity sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw== + dependencies: + eslint-rule-composer "^0.3.0" + +eslint-rule-composer@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" + integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.46.0, eslint@^8.53.0: + version "8.53.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.53.0.tgz#14f2c8244298fcae1f46945459577413ba2697ce" + integrity sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.3" + "@eslint/js" "8.53.0" + "@humanwhocodes/config-array" "^0.11.13" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esquery@^1.4.2, esquery@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.1.tgz#a02a15fdec25a8f844ff7cc658f03dd99eb4609b" + integrity sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatted@^3.2.9: + version "3.2.9" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" + integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-tsconfig@^4.6.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.2.tgz#0dcd6fb330391d46332f4c6c1bf89a6514c2ddce" + integrity sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A== + dependencies: + resolve-pkg-maps "^1.0.0" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^13.19.0: + version "13.23.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.23.0.tgz#ef31673c926a0976e1f61dab4dca57e0c0a8af02" + integrity sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA== + dependencies: + type-fest "^0.20.2" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + +ignore@^5.2.0, ignore@^5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +immutable@^4.0.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" + integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-builtin-module@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== + dependencies: + builtin-modules "^3.3.0" + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +jquery-ujs@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.3.tgz#dcac6026ab7268e5ee41faf9d31c997cd4ddd603" + integrity sha512-59wvfx5vcCTHMeQT1/OwFiAj+UffLIwjRIoXdpO7Z7BCFGepzq9T9oLVeoItjTqjoXfUrHJvV7QU6pUR+UzOoA== + +jquery@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de" + integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsdoc-type-pratt-parser@~4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz#136f0571a99c184d84ec84662c45c29ceff71114" + integrity sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ== + +jsesc@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +jsonc-parser@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" + integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regexp-tree@^0.1.27: + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + +regjsparser@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.10.0.tgz#b1ed26051736b436f22fdec1c8f72635f9f44892" + integrity sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA== + dependencies: + jsesc "~0.5.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + +resolve@^1.10.0, resolve@^1.22.3, resolve@^1.22.4, resolve@^1.22.6: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +sass@^1.7.3: + version "1.69.5" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.5.tgz#23e18d1c757a35f2e52cc81871060b9ad653dfde" + integrity sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +"semver@2 || 3 || 4 || 5": + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^7.5.3, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +"source-map-js@>=0.6.2 <2.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +spdx-correct@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" + integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0, spdx-expression-parse@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.16" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f" + integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw== + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-api-utils@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" + integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== + +ts-node@*: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tslib@^2.4.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +typescript@^5.1.6, typescript@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vscode-json-languageservice@^4.1.6: + version "4.2.1" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.2.1.tgz#94b6f471ece193bf4a1ef37f6ab5cce86d50a8b4" + integrity sha512-xGmv9QIWs2H8obGbWg+sIPI/3/pFgj/5OWBhNzs00BkYQ9UaB2F6JJaGB/2/YOZJ3BvLXQTC4Q7muqU25QgAhA== + dependencies: + jsonc-parser "^3.0.0" + vscode-languageserver-textdocument "^1.0.3" + vscode-languageserver-types "^3.16.0" + vscode-nls "^5.0.0" + vscode-uri "^3.0.3" + +vscode-languageserver-textdocument@^1.0.3: + version "1.0.11" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" + integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== + +vscode-languageserver-types@^3.16.0: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + +vscode-nls@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.2.0.tgz#3cb6893dd9bd695244d8a024bdf746eea665cc3f" + integrity sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng== + +vscode-uri@^3.0.3: + version "3.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==