diff --git a/.dockerignore b/.dockerignore
index 9e95e1ed..f0539e4a 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,16 +1,25 @@
.git
-.buildkite
+.gitignore
+.env
+.prettierignore
+.stylelintrc
+.editorconfig
.vscode
env
mounts
*.pyc
__pycache__/
-settings.py
-celeryconfig.py
+web/settings.py
+web/settings.*.py
+web/celeryconfig.py
+web/celeryconfig.*.py
.eggs/
-build/
-html/
pip-log.txt
.DS_Store
*.swp
.deploy.env
+README.md
+LICENSE
+web/yoyo.*.ini
+web/yoyo.ini
+Makefile
diff --git a/.github/workflows/test-deploy.yml b/.github/workflows/test-deploy.yml
index 3ac3adc4..5406db43 100644
--- a/.github/workflows/test-deploy.yml
+++ b/.github/workflows/test-deploy.yml
@@ -32,7 +32,7 @@ jobs:
- name: Build and push
uses: docker/build-push-action@v6
with:
- file: ./Dockerfile
+ file: ./setup/web/Dockerfile
context: .
push: true
tags: mltshp/mltshp-web:build-${{ env.build_number }}
@@ -56,7 +56,7 @@ jobs:
- name: Build and push
uses: docker/build-push-action@v6
with:
- file: ./Dockerfile.worker
+ file: ./setup/worker/Dockerfile
context: .
push: true
tags: mltshp/mltshp-worker:build-${{ env.build_number }}
@@ -69,22 +69,22 @@ jobs:
uses: actions/checkout@v4
- name: Start services with Docker Compose
- run: docker compose -f release/docker-compose.yml up -d
+ run: docker compose -f setup/test/docker-compose.yml --project-directory=. up -d
- name: Wait for services to be ready
run: sleep 10
- name: Run tests
- run: docker exec -t release-mltshp-1 ./run-tests.sh
+ run: docker exec -t mltshp-mltshp-1 ./test/run-tests.sh
- name: Run Coveralls report
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
- run: docker exec -t -e COVERALLS_REPO_TOKEN release-mltshp-1 ./coveralls-report.sh
+ run: docker exec -t -e COVERALLS_REPO_TOKEN mltshp-mltshp-1 ./test/coveralls-report.sh
- name: Stop services and clean up
run: |
- docker compose -f release/docker-compose.yml down
+ docker compose -f setup/test/docker-compose.yml --project-directory=. down
docker container prune -f
staging:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7be9894e..8da9d4de 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -15,15 +15,15 @@ jobs:
uses: actions/checkout@v4
- name: Build and start services with Docker Compose
- run: docker compose -f release/docker-compose.yml up -d --build
+ run: docker compose -f setup/test/docker-compose.yml --project-directory=. up -d --build
- name: Wait for services to be ready
run: sleep 10
- name: Run tests
- run: docker exec -t release-mltshp-1 ./run-tests.sh
+ run: docker exec -t mltshp-mltshp-1 ./test/run-tests.sh
- name: Stop services and clean up
run: |
- docker compose -f release/docker-compose.yml down
+ docker compose -f setup/test/docker-compose.yml --project-directory=. down
docker container prune -f
diff --git a/.gitignore b/.gitignore
index 1934add2..0602e33b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,10 @@
*.pyc
-/settings.py
-celeryconfig.py
+web/settings.py
+web/settings.*.py
+web/yoyo.ini
+web/yoyo.*.ini
+web/celeryconfig.py
.eggs/
-build/
-html/
.figleaf
pip-log.txt
.DS_Store
@@ -11,7 +12,7 @@ pip-log.txt
env
mounts
.env
-.deploy.env
+.*.env
.coverage
coverage.xml
.vscode/
diff --git a/.stylelintrc b/.stylelintrc
index 839eae9a..52b69940 100644
--- a/.stylelintrc
+++ b/.stylelintrc
@@ -40,7 +40,7 @@
"declaration-colon-space-before": "never",
"declaration-empty-line-before": "never",
"declaration-block-no-duplicate-properties": [ true, {
- ignore: ["consecutive-duplicates-with-different-values"],
+ "ignore": ["consecutive-duplicates-with-different-values"],
} ],
"declaration-block-no-ignored-properties": true,
"declaration-block-no-redundant-longhand-properties": true,
@@ -82,10 +82,10 @@
"selector-list-comma-newline-after": "always",
"selector-list-comma-space-before": "never",
"rule-nested-empty-line-before": [ "always-multi-line", {
- ignore: ["after-comment"],
+ "ignore": ["after-comment"],
} ],
"rule-non-nested-empty-line-before": [ "always-multi-line", {
- ignore: ["after-comment"],
+ "ignore": ["after-comment"],
} ],
"media-feature-colon-space-after": "always",
"media-feature-colon-space-before": "never",
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 0f895a7a..00000000
--- a/Dockerfile
+++ /dev/null
@@ -1,72 +0,0 @@
-FROM ubuntu:24.04
-LABEL maintainer="brad@bradchoate.com"
-ENV PYTHONUNBUFFERED=1
-
-# Installs the base system dependencies for running the site.
-# None of this will change with the codebase itself, so this
-# whole layer and steps to build it should be cached.
-RUN apt-get -y update && \
- apt-get install -y \
- supervisor \
- build-essential \
- pkg-config \
- python3-dev \
- python3-full \
- python3-pip \
- libmysqlclient-dev \
- mysql-client \
- libjpeg-dev \
- libcurl4-openssl-dev \
- curl \
- wget \
- libpcre3 \
- libpcre3-dev \
- libssl-dev \
- libffi-dev && \
- rm -rf /var/lib/apt/lists/* && \
- # install nginx + upload module
- mkdir -p /tmp/install && \
- cd /tmp/install && \
- wget http://nginx.org/download/nginx-1.25.3.tar.gz && tar zxf nginx-1.25.3.tar.gz && \
- wget https://github.com/fdintino/nginx-upload-module/archive/2.3.0.tar.gz && tar zxf 2.3.0.tar.gz && \
- cd /tmp/install/nginx-1.25.3 && \
- ./configure \
- --with-http_ssl_module \
- --with-http_stub_status_module \
- --with-pcre \
- --sbin-path=/usr/sbin/nginx \
- --pid-path=/run/nginx.pid \
- --conf-path=/etc/nginx/nginx.conf \
- --error-log-path=/srv/mltshp.com/nginx-error.log \
- --http-log-path=/srv/mltshp.com/nginx-access.log \
- --add-module=/tmp/install/nginx-upload-module-2.3.0 && \
- make && make install && \
- mkdir -p /etc/nginx && \
- rm -rf /tmp/install && \
- groupadd mltshp --gid=1010 && \
- useradd mltshp --create-home --home-dir=/home/mltshp \
- --uid=1010 --gid=1010 && \
- mkdir -p /mnt/tmpuploads/0 /mnt/tmpuploads/1 /mnt/tmpuploads/2 \
- /mnt/tmpuploads/3 /mnt/tmpuploads/4 /mnt/tmpuploads/5 \
- /mnt/tmpuploads/6 /mnt/tmpuploads/7 /mnt/tmpuploads/8 \
- /mnt/tmpuploads/9 && \
- chmod 777 /mnt/tmpuploads/* && \
- mkdir -p /srv/mltshp.com/uploaded /srv/mltshp.com/logs && \
- chown -R mltshp:mltshp /srv/mltshp.com
-
-# Install python dependencies which will be cached on the
-# contents of requirements.txt:
-COPY requirements.txt /tmp
-# It's okay to install to the system packages; we're in a container
-RUN pip install --break-system-packages -r /tmp/requirements.txt && rm /tmp/requirements.txt
-
-# Copy configuration settings into place
-COPY setup/production/supervisord-web.conf /etc/supervisor/conf.d/mltshp.conf
-COPY setup/production/nginx.conf /etc/nginx/nginx.conf
-
-# Add "." for the app code itself (also allows for local dev)
-ADD . /srv/mltshp.com/mltshp
-WORKDIR /srv/mltshp.com/mltshp
-
-EXPOSE 80
-CMD ["/usr/bin/supervisord"]
diff --git a/Dockerfile.worker b/Dockerfile.worker
deleted file mode 100644
index cc8804cc..00000000
--- a/Dockerfile.worker
+++ /dev/null
@@ -1,45 +0,0 @@
-FROM ubuntu:24.04
-LABEL maintainer="brad@bradchoate.com"
-ENV PYTHONUNBUFFERED=1
-
-RUN apt-get -y update && \
- apt-get install -y \
- software-properties-common \
- build-essential \
- supervisor \
- cron \
- pkg-config \
- libmysqlclient-dev \
- mysql-client \
- python3-dev \
- python3-full \
- python3-pip \
- libjpeg-dev \
- libcurl4-openssl-dev \
- curl \
- run-one \
- ffmpeg \
- wget \
- vim \
- libpcre3 \
- libpcre3-dev \
- libssl-dev \
- libffi-dev && \
- rm -rf /var/lib/apt/lists/* && \
- groupadd mltshp --gid=1010 && \
- useradd mltshp --create-home --home-dir=/home/mltshp \
- --uid=1010 --gid=1010 && \
- mkdir -p /srv/mltshp.com/logs && \
- chown -R mltshp:mltshp /srv/mltshp.com
-
-COPY requirements.txt /tmp
-RUN pip install --break-system-packages -r /tmp/requirements.txt && rm /tmp/requirements.txt
-
-COPY setup/production/supervisord-worker.conf /etc/supervisor/conf.d/mltshp.conf
-
-# NOTE: /srv/mltshp.com/logs should be a mounted volume for this image
-ADD . /srv/mltshp.com/mltshp
-WORKDIR /srv/mltshp.com/mltshp
-RUN crontab -u mltshp setup/production/mltshp-worker--crontab
-
-CMD ["/usr/bin/supervisord"]
diff --git a/Makefile b/Makefile
index f63e59c9..1a702c9c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,34 +1,42 @@
-.PHONY: init start stop custom-build build shell test destroy migrate mysql
+.PHONY: init start stop custom-build build shell test destroy migrate mysql straw
init:
- cp settings.example.py settings.py
- cp celeryconfig.example.py celeryconfig.py
- mkdir -p mounts/mysql mounts/logs mounts/fakes3 mounts/uploaded
+ cp web/settings.example.py web/settings.py
+ cp web/celeryconfig.example.py web/celeryconfig.py
+ mkdir -p mounts/mysql mounts/logs mounts/fakes3 mounts/uploaded \
+ mounts/tmpuploads/0 mounts/tmpuploads/1 mounts/tmpuploads/2 \
+ mounts/tmpuploads/3 mounts/tmpuploads/4 mounts/tmpuploads/5 \
+ mounts/tmpuploads/6 mounts/tmpuploads/7 mounts/tmpuploads/8 \
+ mounts/tmpuploads/9
start:
- docker compose up -d
+ docker compose -f setup/web/docker-compose.dev.yml --project-directory=. up -d
stop:
- docker compose down
+ docker compose -f setup/web/docker-compose.dev.yml --project-directory=. down
custom-build:
@read -p "build tag (default is 'latest'): " build_tag; \
- docker build -t mltshp/mltshp-web:$${build_tag:-latest}
+ docker build -f setup/web/Dockerfile -t mltshp/mltshp-web:$${build_tag:-latest}
build:
- docker build -t mltshp/mltshp-web:latest .
+ docker build -f setup/web/Dockerfile -t mltshp/mltshp-web:latest .
shell:
- docker compose exec mltshp bash
+ docker compose -f setup/web/docker-compose.dev.yml --project-directory=. exec web ash
test:
- docker compose exec mltshp su mltshp -c "cd /srv/mltshp.com/mltshp; python3 -u test.py $(TEST)"
+ docker compose -f setup/test/docker-compose.yml --project-directory=. exec web su mltshp -c "python3 -u test/test.py $(TEST)"
destroy:
- docker compose down && rm -rf mounts
+ docker compose -f setup/web/docker-compose.dev.yml --project-directory=. down && rm -rf mounts
migrate:
- docker compose exec mltshp su mltshp -c "cd /srv/mltshp.com/mltshp; python3 migrate.py"
+ docker compose -f setup/web/docker-compose.dev.yml --project-directory=. exec web su mltshp -c "cd /srv/mltshp.com/mltshp; python3 migrate.py"
mysql:
- docker compose exec mltshp su mltshp -c "cd /srv/mltshp.com/mltshp; mysql -u root --host mysql mltshp"
+ docker compose -f setup/web/docker-compose.dev.yml --project-directory=. exec web su mltshp -c "cd /srv/mltshp.com/mltshp; mysql -u root --host mysql mltshp"
+
+web/static/straw/compact.js: web/static/straw/source.js
+ closure-compiler --js web/static/straw/source.js --js_output_file web/static/straw/compact.js || echo "closure-compiler not found"
+
diff --git a/README.md b/README.md
index 6b8f83c5..c6e2b6df 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
## Project Description
This project is the codebase for running [mltshp.com](https://mltshp.com).
-It's a Python 3 application, utilizing a MySQL database, Amazon S3 for
+It's a Python 3 application, utilizing a MySQL database, Backblaze B2 for
asset storage, and RabbitMQ for background jobs.
## Development Environment
@@ -97,26 +97,13 @@ The directory structure looks like this:
* mysql/
* (mysql data files)
-## AWS S3 Storage
+## S3 Storage
-MLTSHP utilizes AWS S3 for storing uploaded images. The development
-environment provides a dummy S3 server for local operation. But it requires
-a license key in order to use it. Visit [this page](https://supso.org/projects/fake-s3)
-to obtain a license key. For individual developers and small organizations,
-there is no cost. Add the following to a local `.env` file in the root
-of the project:
-
-```
-FAKES3_LICENSE_KEY=your-license-key-here
-```
+MLTSHP utilizes S3 for storing uploaded images. The development
+environment provides a dummy S3 server for local operation.
You will find any uploaded files under the `mounts/fakes3' directory.
-**Note:** As of this time, the Docker image for the fake S3 server is
-incompatible with Apple Silicon CPU architectures. If you are using a
-computer with an Apple ARM CPU, you will need to use a real S3 bucket
-(see below).
-
If you would rather use a real S3 bucket, you can do that too. Create
one and then assign these in your local settings.py file:
@@ -203,6 +190,23 @@ $ make stop
Then, run another `make start`.
+## Straw
+
+There's one resource that requires manual compilation when it is updated.
+That's the `static/straw/source.js` script. This script is for the bookmkarklet
+for MLTSHP. It can be compiled using this command:
+
+```shell
+$ make straw
+```
+
+After this, you'll need to check in the modified source.js and compact.js
+files.
+
+The command requires that the `closure-compiler` binary is available. This
+can be installed via Homebrew (`brew install closure-compiler`) or your
+favorite package manager.
+
## Relationship with MLTSHP-Patterns
The CSS in this repo is just the compiled version of the styles from the MLTSHP
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 4ea6c322..00000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,56 +0,0 @@
-services:
- mltshp:
- build: .
- env_file:
- - .env
- image: mltshp/mltshp-web:latest
- volumes:
- - .:/srv/mltshp.com/mltshp
- - ./mounts/logs:/srv/mltshp.com/logs
- - ./mounts/uploaded:/srv/mltshp.com/uploaded
- - ./setup/dev/nginx.conf:/etc/nginx/nginx.conf
- - ./setup/dev/supervisord.conf:/etc/supervisor/conf.d/mltshp.conf
- ports:
- - "8000:80"
- depends_on:
- - mysql
- - fakes3
- links:
- - mysql
- - fakes3
- networks:
- app_net:
- aliases:
- - mltshp.localhost
- fakes3:
- build: ./setup/dev/fakes3
- env_file:
- - .env
- entrypoint: fakes3 --root /srv --license ${FAKES3_LICENSE_KEY} --port 4567
- ports:
- - "4567:4567"
- volumes:
- - ./mounts/fakes3:/srv
- networks:
- app_net:
- aliases:
- - mltshp-testing.fakes3
- - mltshp-dev.fakes3
- mysql:
- image: mysql:8
- ports:
- - "3306:3306"
- volumes:
- - ./mounts/mysql:/var/lib/mysql
- - ./setup/dev/mysql-conf.d:/etc/mysql/conf.d
- - ./setup/db-install.sql:/docker-entrypoint-initdb.d/00_db-install.sql
- - ./setup/db-fixtures.sql:/docker-entrypoint-initdb.d/01_db-fixtures.sql
- environment:
- MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
- MYSQL_DATABASE: "mltshp"
- MYSQL_USER: "mltshp"
- networks:
- - app_net
-
-networks:
- app_net:
diff --git a/lib/feathers/__init__.py b/lib/feathers/__init__.py
deleted file mode 100644
index 9f9f9dc7..00000000
--- a/lib/feathers/__init__.py
+++ /dev/null
@@ -1,126 +0,0 @@
-import sys
-import urllib.request, urllib.parse, urllib.error
-import urllib.parse
-import hmac
-import hashlib
-import binascii
-import time
-import uuid
-
-import tornado.httpclient
-import tornado.ioloop
-
-class Feathers(object):
- """
- An experimental, minimalist twitter client to be used with Tornado.
-
- Translates an API method:
-
- (GET) statuses/public_timeline.format?include_entities=1
-
- Using chained property-access syntax:
-
- feather = Feathers(key='your app key', secret='your app secret')
- feather.statuses.public_timeline.get({'include_entitites' : 1})
- """
- __current_method = ''
-
- endpoint = "https://api.twitter.com/"
- api_version = '1'
- format = 'json'
- key = ''
- secret = ''
-
- def __init__(self, key='', secret=''):
- assert key
- assert secret
- self.key = key
- self.secret = secret
-
- def __getattr__(self, name):
- """
- Missing attributes get concatenated to final api path. Each
- new attribute is seperated by a / Returns self so its chainable.
- Gets reset when get() is called.
- """
- self.__current_method = self.__current_method + '/' + name
- return self
-
- def get(self, params={}, headers={}, callback=None, token_key=None, token_secret=None):
- """
- Send a GET request.
-
- If a user's token_key and token_secret are passed in, prepare an authenticated request
- on behalf of the user.
- """
- base_url = self._build_base_url(self.__current_method)
- if token_key and token_secret:
- params['oauth_consumer_key'] = self.key
- params['oauth_signature_method'] ="HMAC-SHA1"
- params['oauth_timestamp'] = str(int(time.time()))
- params['oauth_nonce'] = binascii.b2a_hex(uuid.uuid4().bytes)
- params['oauth_version'] = "1.0"
- params['oauth_token'] = token_key
- signature = self._oauth_signature(self.secret, "GET", base_url, params, token_secret)
- params['oauth_signature'] = signature
- url = self._build_url(base_url, params)
- self.__current_method = ''
- return self._fetch(url, headers, callback)
-
- def _build_base_url(self, method):
- return self.endpoint + self.api_version + method + '.' + self.format
-
- def _build_url(self, base_url, params):
- query = '' if not params else '?' + urllib.parse.urlencode(params)
- return base_url + query
-
- async def _fetch(self, url, headers={}, callback=None):
- """
- Make the request. If an IOloop is available make request asynchronous and use the
- passed in callback if it's provided.
- """
- request = tornado.httpclient.HTTPRequest(url=url, method="GET", headers=headers)
- if tornado.ioloop.IOLoop.initialized():
- http = tornado.httpclient.AsyncHTTPClient()
- fut = http.fetch(request)
- response = await fut
- callback(response)
- else:
- http = tornado.httpclient.HTTPClient()
- return http.fetch(request)
-
- @classmethod
- def _oauth_signature(self, consumer_secret, method, url, parameters={}, token_secret=None):
- """
- From tornado.auth
-
- Calculates the HMAC-SHA1 OAuth 1.0 signature for the given request.
-
- See http://oauth.net/core/1.0/#signing_process
- """
- parts = urllib.parse.urlparse(url)
- scheme, netloc, path = parts[:3]
- normalized_url = scheme.lower() + "://" + netloc.lower() + path
-
- base_elems = []
- base_elems.append(method.upper())
- base_elems.append(normalized_url)
- base_elems.append("&".join("%s=%s" % (k, self._oauth_escape(str(v)))
- for k, v in sorted(parameters.items())))
- base_string = "&".join(self._oauth_escape(e) for e in base_elems)
-
- key_elems = [consumer_secret]
- key_elems.append(token_secret if token_secret else "")
- key = "&".join(key_elems)
-
- hash = hmac.new(key.encode("ascii"), base_string.encode("ascii"), hashlib.sha1)
- return binascii.b2a_base64(hash.digest())[:-1]
-
- @classmethod
- def _oauth_escape(self, val):
- """
- From tornado.auth
- """
- if isinstance(val, str):
- val = val.encode("utf-8")
- return urllib.parse.quote(val, safe="~")
diff --git a/release/docker-compose.yml b/release/docker-compose.yml
deleted file mode 100644
index ca42909e..00000000
--- a/release/docker-compose.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-services:
- mltshp:
- image: "mltshp/mltshp-web:build-${build_number}"
- build: ..
- volumes:
- - ./settings.py:/srv/mltshp.com/mltshp/settings.py
- - ../.git:/srv/mltshp.com/mltshp/.git
- - ./supervisord.conf:/etc/supervisor/conf.d/mltshp.conf
- ports:
- - "8001:80"
- depends_on:
- - mysql
- links:
- - mysql
- networks:
- app_net:
- aliases:
- - mltshp.dev
- mysql:
- image: mysql:8
- ports:
- - "3306:3306"
- environment:
- MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
- MYSQL_DATABASE: "mltshp_testing"
- MYSQL_USER: "mltshp"
- networks:
- - app_net
-
-networks:
- app_net:
diff --git a/release/supervisord.conf b/release/supervisord.conf
deleted file mode 100644
index 54296b8b..00000000
--- a/release/supervisord.conf
+++ /dev/null
@@ -1,2 +0,0 @@
-[supervisord]
-nodaemon=true
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index bee4fe7c..00000000
--- a/requirements.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-amqplib==1.0.2
-beautifulsoup4==4.12.3
-boto3==1.35.86
-celery==5.4.0
-ffmpy==0.5.0
-kombu==5.3.7
-mock==5.1.0
-mysqlclient==2.2.6
-Pillow==11.0.0
-pycurl==7.45.4
-pyOpenSSL==24.3.0
-python-dateutil==2.9.0.post0
-python-postmark==0.6.0
-requests==2.32.3
-setuptools==75.8.0
-stripe==11.4.1
-tornado==6.4.2
-yoyo-migrations==8.2.0
diff --git a/run-tests.sh b/run-tests.sh
deleted file mode 100755
index aeaec8f6..00000000
--- a/run-tests.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-
-# exit if any command fails (e); strict variable substitution (u);
-# set exit code to non-zero for any failed piped commands (o pipefail)
-# See also: http://redsymbol.net/articles/unofficial-bash-strict-mode/
-set -euo pipefail
-
-export PYTHONUNBUFFERED=1
-
-# These commands expect to be run in a Docker container
-pip3 install --break-system-packages -r requirements-test.txt;
-coverage run --source=handlers,models,tasks,lib test.py
-coverage xml
diff --git a/setup/db-drop.sql b/setup/database/db-drop.sql
similarity index 100%
rename from setup/db-drop.sql
rename to setup/database/db-drop.sql
diff --git a/setup/db-fixtures.sql b/setup/database/db-fixtures.sql
similarity index 100%
rename from setup/db-fixtures.sql
rename to setup/database/db-fixtures.sql
diff --git a/setup/db-install.sql b/setup/database/db-install.sql
similarity index 100%
rename from setup/db-install.sql
rename to setup/database/db-install.sql
diff --git a/setup/db-truncate.sql b/setup/database/db-truncate.sql
similarity index 100%
rename from setup/db-truncate.sql
rename to setup/database/db-truncate.sql
diff --git a/setup/dev/fakes3/Dockerfile b/setup/dev/fakes3/Dockerfile
deleted file mode 100644
index de8f6717..00000000
--- a/setup/dev/fakes3/Dockerfile
+++ /dev/null
@@ -1,23 +0,0 @@
-FROM ruby:2.7 as builder
-
-ENV FAKES3_VERSION 2.0.0
-
-RUN gem install fakes3 -v ${FAKES3_VERSION} \
- && rm -rf /usr/local/bundle/cache/*
-
-FROM ruby:2.7-slim
-
-COPY --from=builder /usr/local/bundle /usr/local/bundle/
-
-VOLUME /srv
-RUN mkdir -p /srv \
- && chown nobody:nogroup /srv \
- && chmod 750 /srv \
- && ln -s /usr/local/bundle/bin/fakes3 /usr/bin/fakes3
-WORKDIR /srv
-
-EXPOSE 4567
-
-USER nobody
-ENTRYPOINT ["fakes3", "--port", "4567"]
-CMD ["--root", "/srv"]
diff --git a/setup/dev/start-nginx.sh b/setup/dev/start-nginx.sh
deleted file mode 100644
index d4e2a157..00000000
--- a/setup/dev/start-nginx.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-echo resolver $(awk 'BEGIN{ORS=" "} $1=="nameserver" {print $2}' /etc/resolv.conf) ";" > /etc/nginx/resolvers.conf
-exec /usr/sbin/nginx -g 'daemon off;'
\ No newline at end of file
diff --git a/setup/dev/mysql-conf.d/mltshp.cnf b/setup/mysql/dev.conf
similarity index 100%
rename from setup/dev/mysql-conf.d/mltshp.cnf
rename to setup/mysql/dev.conf
diff --git a/setup/production/mysql.conf b/setup/mysql/prod.conf
similarity index 88%
rename from setup/production/mysql.conf
rename to setup/mysql/prod.conf
index da44f0d8..2610191e 100644
--- a/setup/production/mysql.conf
+++ b/setup/mysql/prod.conf
@@ -8,11 +8,11 @@ thread_cache_size = 8
query_cache_limit = 1M
query_cache_size = 32M
query_cache_type = 1
-innodb_buffer_pool_size = 8G
+innodb_buffer_pool_size = 12G
innodb_ft_min_token_size = 2
#
# Here you can see queries with especially long duration
#log_slow_queries = /var/log/mysql/mysql-slow.log
#long_query_time = 2
-#log-queries-not-using-indexes
\ No newline at end of file
+#log-queries-not-using-indexes
diff --git a/setup/dev/nginx.conf b/setup/nginx/dev.conf
similarity index 98%
rename from setup/dev/nginx.conf
rename to setup/nginx/dev.conf
index 6397b275..124da6df 100644
--- a/setup/dev/nginx.conf
+++ b/setup/nginx/dev.conf
@@ -6,6 +6,8 @@ worker_rlimit_nofile 20480;
error_log /srv/mltshp.com/logs/error.log;
+include /etc/nginx/modules/*.conf;
+
events {
worker_connections 20480;
use epoll;
@@ -16,7 +18,7 @@ http {
default_type application/octet-stream;
upstream frontends {
- server 127.0.0.1:8000;
+ server mltshp-web:8001;
}
sendfile on;
diff --git a/setup/production/nginx.conf b/setup/nginx/prod.conf
similarity index 99%
rename from setup/production/nginx.conf
rename to setup/nginx/prod.conf
index 1c640ab9..e1a66150 100644
--- a/setup/production/nginx.conf
+++ b/setup/nginx/prod.conf
@@ -4,6 +4,8 @@ worker_processes auto;
worker_rlimit_nofile 20480;
+include /etc/nginx/modules/*.conf;
+
events {
worker_connections 20480;
use epoll;
diff --git a/setup/production/update_linode_web_firewall.py b/setup/production/update_linode_web_firewall.py
new file mode 100755
index 00000000..c8e7d22b
--- /dev/null
+++ b/setup/production/update_linode_web_firewall.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+
+# This script makes dynamic updates to our Linode web firewall, allowing access
+# from the published Fastly IP address list. It should be run as a cron job, updating
+# on a regular basis. It only applies updates when changes are detected, comparing
+# against the existing rules. Linode issues email notifications when the firewall rules
+# change, so we shouldn't blindly make updates, but only when changes are required.
+
+import os, requests
+
+# Retrieve settings from the environment
+linode_api_token = os.environ.get("LINODE_API_TOKEN")
+if not linode_api_token:
+ print("LINODE_API_TOKEN not set")
+ exit(1)
+
+linode_firewall_label = "mltshp-web-us-ord"
+linode_inbound_rule_label = "Fastly"
+
+all_firewalls = requests.get(
+ "https://api.linode.com/v4/networking/firewalls",
+ headers={
+ "accept": "application/json",
+ "authorization": f"Bearer { linode_api_token }",
+ }).json()
+
+web_firewall = next(iter([fw
+ for fw in all_firewalls["data"]
+ if fw["label"] == linode_firewall_label]), None)
+if not web_firewall:
+ print(f"No Linode firewall found with label '{ linode_firewall_label }'")
+ exit(1)
+
+# Look for an existing Fastly rule and gather the IP addresses for comparison
+# with what is currently provided by Fastly.
+fastly_inbound = next(iter([data
+ for data in web_firewall["rules"]["inbound"]
+ if data["label"] == linode_inbound_rule_label]), None)
+other_inbound = [data
+ for data in web_firewall["rules"]["inbound"]
+ if data["label"] != linode_inbound_rule_label]
+if fastly_inbound:
+ existing_ipv4s = set(fastly_inbound["addresses"]["ipv4"])
+ existing_ipv6s = set(fastly_inbound["addresses"]["ipv6"])
+else:
+ existing_ipv4s = set()
+ existing_ipv6s = set()
+
+fastly_ips = requests.get(
+ "https://api.fastly.com/public-ip-list",
+ headers={
+ "accept": "application/json",
+ }).json()
+fastly_ipv4s = set(fastly_ips.get("addresses"))
+fastly_ipv6s = set(fastly_ips.get("ipv6_addresses"))
+
+# If no IP addresses have changed, we're done.
+if existing_ipv4s == fastly_ipv4s and existing_ipv6s == fastly_ipv6s:
+ print("Fastly IPs have not changed")
+ exit(0)
+
+print("Updating firewall with new Fastly IPs")
+
+response = requests.put(
+ f"https://api.linode.com/v4/networking/firewalls/{ web_firewall["id"] }/rules",
+ json={
+ "inbound": [
+ {
+ "label": "Fastly",
+ "description": "DO NOT EDIT - Updated programmatically",
+ "addresses": {
+ "ipv4": fastly_ips.get("addresses"),
+ "ipv6": fastly_ips.get("ipv6_addresses"),
+ },
+ "action": "ACCEPT",
+ "ports": "80",
+ "protocol": "TCP",
+ },
+ ] + other_inbound,
+ "inbound_policy": "DROP",
+ "outbound_policy": "ACCEPT",
+ },
+ headers={
+ "accept": "application/json",
+ "authorization": f"Bearer { linode_api_token }",
+ "content-type": "application/json",
+ }
+)
+
+if response.status_code == 200:
+ print("Firewall updated successfully")
+else:
+ print(f"Failed to update firewall (status { response.status_code }): { response.text }")
+ exit(1)
diff --git a/setup/test/docker-compose.yml b/setup/test/docker-compose.yml
new file mode 100644
index 00000000..2568af82
--- /dev/null
+++ b/setup/test/docker-compose.yml
@@ -0,0 +1,35 @@
+services:
+ mltshp:
+ image: mltshp/mltshp-web:${build_number:-latest}
+ build:
+ context: .
+ dockerfile: setup/web/Dockerfile
+ command: "sleep infinity"
+ volumes:
+ - ./setup/test/settings.py:/srv/mltshp.com/mltshp/settings.py
+ - .git:/srv/mltshp.com/mltshp/.git
+ - ./test:/srv/mltshp.com/mltshp/test
+ - ./worker/scripts:/srv/mltshp.com/mltshp/scripts
+ - ./setup/database:/srv/mltshp.com/mltshp/setup/database
+ - ./mounts/logs:/srv/mltshp.com/logs
+ depends_on:
+ - mysql
+ links:
+ - mysql
+ networks:
+ app_net:
+ aliases:
+ - mltshp.dev
+ mysql:
+ image: mysql:8
+ ports:
+ - "3306:3306"
+ environment:
+ MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
+ MYSQL_DATABASE: "mltshp_testing"
+ MYSQL_USER: "mltshp"
+ networks:
+ - app_net
+
+networks:
+ app_net:
diff --git a/release/settings.py b/setup/test/settings.py
similarity index 71%
rename from release/settings.py
rename to setup/test/settings.py
index 52d0eb1f..b8ecb8d2 100644
--- a/release/settings.py
+++ b/setup/test/settings.py
@@ -14,7 +14,6 @@
"xsrf_cookies": True,
"uploaded_files" : "/srv/mltshp.com/uploaded",
"debug": True,
- # docker container's host ip address
"database_user": "root",
"database_name": "mltshp_testing",
"database_password" : "",
@@ -28,9 +27,4 @@
"debug_workers": True,
"superuser_list": "admin",
"tornado_logging": False,
- # these must be set for testing test/unit/externalservice_tests.py
- # "twitter_consumer_key" : "twitter_consumer_key_here",
- # "twitter_consumer_secret" : "twitter_consumer_secret_key_here",
- # "twitter_access_key" : "twitter_access_key_here",
- # "twitter_access_secret" : "twitter_access_secret_here",
}
diff --git a/setup/web/Dockerfile b/setup/web/Dockerfile
new file mode 100644
index 00000000..396e6707
--- /dev/null
+++ b/setup/web/Dockerfile
@@ -0,0 +1,39 @@
+FROM python:3.12-alpine
+LABEL maintainer="brad@bradchoate.com"
+ENV PYTHONUNBUFFERED=1
+ENV PYTHONPATH=/srv/mltshp.com/mltshp:/srv/mltshp.com/mltshp/test:/usr/lib/python3.12/site-packages
+
+# Installs the base system dependencies for running the site.
+# None of this will change with the codebase itself, so this
+# whole layer and steps to build it should be cached.
+
+RUN apk add --no-cache \
+ py3-mysqlclient \
+ py3-beautifulsoup4 \
+ py3-boto3 \
+ py3-curl \
+ py3-dateutil \
+ py3-kombu \
+ py3-mock \
+ py3-mysqlclient \
+ py3-pip \
+ py3-pillow \
+ py3-requests \
+ py3-tornado \
+ py3-yoyo-migrations \
+ supervisor
+
+# Install python dependencies which will be cached on the
+# contents of requirements.txt:
+RUN --mount=type=bind,source=web/requirements.txt,target=/tmp/requirements.txt \
+ pip install --break-system-packages -r /tmp/requirements.txt
+
+RUN mkdir -p /srv/mltshp.com/uploaded /srv/mltshp.com/logs /srv/mltshp.com/bin && \
+ adduser -D mltshp && \
+ chown -R mltshp /srv/mltshp.com
+
+COPY web /srv/mltshp.com/mltshp/
+
+WORKDIR /srv/mltshp.com/mltshp
+
+CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/mltshp/supervisor.conf", "--logfile", "/srv/mltshp.com/logs/supervisord.log", "--pidfile", "/srv/mltshp.com/logs/supervisord.pid"]
diff --git a/setup/web/docker-compose.dev.yml b/setup/web/docker-compose.dev.yml
new file mode 100644
index 00000000..bcbeb46f
--- /dev/null
+++ b/setup/web/docker-compose.dev.yml
@@ -0,0 +1,90 @@
+services:
+ web:
+ build:
+ context: .
+ dockerfile: setup/web/Dockerfile
+ env_file:
+ - .env
+ image: mltshp/mltshp-web:latest
+ volumes:
+ - ./web:/srv/mltshp.com/mltshp
+ - ./test:/srv/mltshp.com/mltshp/test
+ - ./worker/scripts:/srv/mltshp.com/mltshp/scripts
+ - ./setup/database:/srv/mltshp.com/mltshp/setup/database
+ - ./mounts/logs:/srv/mltshp.com/logs
+ - ./mounts/uploaded:/srv/mltshp.com/uploaded
+ - ./mounts/tmpuploads/0:/mnt/tmpuploads/0
+ - ./mounts/tmpuploads/1:/mnt/tmpuploads/1
+ - ./mounts/tmpuploads/2:/mnt/tmpuploads/2
+ - ./mounts/tmpuploads/3:/mnt/tmpuploads/3
+ - ./mounts/tmpuploads/4:/mnt/tmpuploads/4
+ - ./mounts/tmpuploads/5:/mnt/tmpuploads/5
+ - ./mounts/tmpuploads/6:/mnt/tmpuploads/6
+ - ./mounts/tmpuploads/7:/mnt/tmpuploads/7
+ - ./mounts/tmpuploads/8:/mnt/tmpuploads/8
+ - ./mounts/tmpuploads/9:/mnt/tmpuploads/9
+ - ./setup/web/supervisor/dev.conf:/etc/mltshp/supervisor.conf
+ depends_on:
+ - mysql
+ - fakes3
+ links:
+ - mysql
+ - fakes3
+ networks:
+ app_net:
+ aliases:
+ - mltshp-web
+ nginx:
+ image: mltshp/nginx:latest
+ ports:
+ - "8000:80"
+ volumes:
+ - ./setup/nginx/dev.conf:/etc/nginx/nginx.conf
+ - ./mounts/logs:/srv/mltshp.com/logs
+ - ./mounts/uploaded:/srv/mltshp.com/uploaded
+ - ./mounts/tmpuploads/0:/mnt/tmpuploads/0
+ - ./mounts/tmpuploads/1:/mnt/tmpuploads/1
+ - ./mounts/tmpuploads/2:/mnt/tmpuploads/2
+ - ./mounts/tmpuploads/3:/mnt/tmpuploads/3
+ - ./mounts/tmpuploads/4:/mnt/tmpuploads/4
+ - ./mounts/tmpuploads/5:/mnt/tmpuploads/5
+ - ./mounts/tmpuploads/6:/mnt/tmpuploads/6
+ - ./mounts/tmpuploads/7:/mnt/tmpuploads/7
+ - ./mounts/tmpuploads/8:/mnt/tmpuploads/8
+ - ./mounts/tmpuploads/9:/mnt/tmpuploads/9
+ depends_on:
+ - web
+ links:
+ - web
+ networks:
+ app_net:
+ aliases:
+ - mltshp.localhost
+ fakes3:
+ image: mltshp/fakes3:latest
+ entrypoint: fakes3 --root /srv --license dummy --port 4567
+ volumes:
+ - ./mounts/fakes3:/srv
+ networks:
+ app_net:
+ aliases:
+ - mltshp-testing.fakes3
+ - mltshp-dev.fakes3
+ mysql:
+ image: mysql:8
+ ports:
+ - "3306:3306"
+ volumes:
+ - ./mounts/mysql:/var/lib/mysql
+ - ./setup/mysql/dev.conf:/etc/mysql/conf.d/mltshp.cnf
+ - ./setup/database/db-install.sql:/docker-entrypoint-initdb.d/00_db-install.sql
+ - ./setup/database/db-fixtures.sql:/docker-entrypoint-initdb.d/01_db-fixtures.sql
+ environment:
+ MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
+ MYSQL_DATABASE: "mltshp"
+ MYSQL_USER: "mltshp"
+ networks:
+ - app_net
+
+networks:
+ app_net:
diff --git a/setup/web/docker-compose.prod.yml b/setup/web/docker-compose.prod.yml
new file mode 100644
index 00000000..d8ae2c2e
--- /dev/null
+++ b/setup/web/docker-compose.prod.yml
@@ -0,0 +1,62 @@
+services:
+ mltshp-web:
+ build:
+ context: ../..
+ dockerfile: setup/web/Dockerfile
+ env_file:
+ - ../../.env
+ image: mltshp/mltshp-web:latest
+ volumes:
+ - .:/srv/mltshp.com/mltshp
+ - ./mounts/logs:/srv/mltshp.com/logs
+ - ./mounts/uploaded:/srv/mltshp.com/uploaded
+ - ./mounts/tmpuploads/0:/mnt/tmpuploads/0
+ - ./mounts/tmpuploads/1:/mnt/tmpuploads/1
+ - ./mounts/tmpuploads/2:/mnt/tmpuploads/2
+ - ./mounts/tmpuploads/3:/mnt/tmpuploads/3
+ - ./mounts/tmpuploads/4:/mnt/tmpuploads/4
+ - ./mounts/tmpuploads/5:/mnt/tmpuploads/5
+ - ./mounts/tmpuploads/6:/mnt/tmpuploads/6
+ - ./mounts/tmpuploads/7:/mnt/tmpuploads/7
+ - ./mounts/tmpuploads/8:/mnt/tmpuploads/8
+ - ./mounts/tmpuploads/9:/mnt/tmpuploads/9
+ - ./setup/web/supervisor/prod.conf:/etc/mltshp/supervisor.conf
+ depends_on:
+ - mysql
+ - fakes3
+ links:
+ - mysql
+ - fakes3
+ networks:
+ app_net:
+ aliases:
+ - mltshp-web
+ nginx:
+ image: mltshp/nginx:latest
+ ports:
+ - "8000:80"
+ volumes:
+ - ./setup/nginx/prod.conf:/etc/nginx/nginx.conf
+ - ./mounts/logs:/srv/mltshp.com/logs
+ - ./mounts/uploaded:/srv/mltshp.com/uploaded
+ - ./mounts/tmpuploads/0:/mnt/tmpuploads/0
+ - ./mounts/tmpuploads/1:/mnt/tmpuploads/1
+ - ./mounts/tmpuploads/2:/mnt/tmpuploads/2
+ - ./mounts/tmpuploads/3:/mnt/tmpuploads/3
+ - ./mounts/tmpuploads/4:/mnt/tmpuploads/4
+ - ./mounts/tmpuploads/5:/mnt/tmpuploads/5
+ - ./mounts/tmpuploads/6:/mnt/tmpuploads/6
+ - ./mounts/tmpuploads/7:/mnt/tmpuploads/7
+ - ./mounts/tmpuploads/8:/mnt/tmpuploads/8
+ - ./mounts/tmpuploads/9:/mnt/tmpuploads/9
+ depends_on:
+ - mltshp-web
+ links:
+ - mltshp-web
+ networks:
+ app_net:
+ aliases:
+ - mltshp.localhost
+
+networks:
+ app_net:
diff --git a/setup/dev/supervisord.conf b/setup/web/supervisor/dev.conf
similarity index 57%
rename from setup/dev/supervisord.conf
rename to setup/web/supervisor/dev.conf
index 9fa59427..a3b6308b 100644
--- a/setup/dev/supervisord.conf
+++ b/setup/web/supervisor/dev.conf
@@ -1,19 +1,15 @@
[supervisord]
nodaemon=true
-[program:nginx]
-command=sh /srv/mltshp.com/mltshp/setup/dev/start-nginx.sh
-autorestart=true
-
[group:mltshp]
-programs=main-8000
+programs=main-8001
-[program:main-8000]
-command=python3 main.py --on_port=8000
+[program:main-8001]
+command=python3 main.py --on_port=8001
directory=/srv/mltshp.com/mltshp
autorestart=true
redirect_stderr=true
-stdout_logfile=/srv/mltshp.com/logs/main-8000.log
+stdout_logfile=/srv/mltshp.com/logs/main-8001.log
stdout_logfile_maxbytes=500MB
stdout_logfile_backups=50
stdout_capture_maxbytes=1MB
diff --git a/setup/production/supervisord-web.conf b/setup/web/supervisor/prod.conf
similarity index 86%
rename from setup/production/supervisord-web.conf
rename to setup/web/supervisor/prod.conf
index 9697d33e..199aa22f 100644
--- a/setup/production/supervisord-web.conf
+++ b/setup/web/supervisor/prod.conf
@@ -1,10 +1,6 @@
[supervisord]
nodaemon=true
-[program:nginx]
-command=/usr/sbin/nginx -g 'daemon off;'
-autorestart=true
-
[program:main]
process_name=mltshp-%(process_num)d
command=python3 main.py --on_port=80%(process_num)02d
diff --git a/setup/worker/Dockerfile b/setup/worker/Dockerfile
new file mode 100644
index 00000000..ee0c4340
--- /dev/null
+++ b/setup/worker/Dockerfile
@@ -0,0 +1,44 @@
+FROM python:3.12-alpine
+LABEL maintainer="brad@bradchoate.com"
+ENV PYTHONUNBUFFERED=1
+ENV PYTHONPATH=/usr/lib/python3.12/site-packages
+
+# Installs the base system dependencies for running the site.
+# None of this will change with the codebase itself, so this
+# whole layer and steps to build it should be cached.
+
+RUN apk add --no-cache \
+ apk-cron \
+ py3-mysqlclient \
+ py3-beautifulsoup4 \
+ py3-boto3 \
+ py3-curl \
+ py3-dateutil \
+ py3-kombu \
+ py3-mysqlclient \
+ py3-pip \
+ py3-pillow \
+ py3-requests \
+ py3-tornado \
+ py3-yoyo-migrations \
+ ffmpeg \
+ supervisor
+
+RUN mkdir -p /srv/mltshp.com/logs /srv/mltshp.com/bin && \
+ adduser -D mltshp && \
+ chown -R mltshp /srv/mltshp.com
+
+# Install python dependencies which will be cached on the
+# contents of requirements.txt:
+RUN --mount=type=bind,source=worker/requirements.txt,target=/tmp/requirements.txt \
+ pip install --break-system-packages -r /tmp/requirements.txt
+
+# Copy configuration settings into place
+COPY setup/worker/supervisor/prod.conf /etc/mltshp/supervisor.conf
+COPY web/lib web/models web/tasks web/mltshpoptions.py web/torndb.py worker /srv/mltshp.com/mltshp/
+
+WORKDIR /srv/mltshp.com/mltshp
+
+RUN crontab -u mltshp setup/worker/crontab/prod.cron
+
+CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/mltshp/supervisor.conf", "--logfile", "/srv/mltshp.com/logs/supervisord.log", "--pidfile", "/srv/mltshp.com/logs/supervisord.pid"]
diff --git a/setup/production/mltshp-worker--crontab b/setup/worker/crontab/prod.cron
similarity index 100%
rename from setup/production/mltshp-worker--crontab
rename to setup/worker/crontab/prod.cron
diff --git a/setup/production/supervisord-worker.conf b/setup/worker/supervisor/prod.conf
similarity index 100%
rename from setup/production/supervisord-worker.conf
rename to setup/worker/supervisor/prod.conf
diff --git a/static/straw/compact.js b/static/straw/compact.js
deleted file mode 100644
index fec3f3e2..00000000
--- a/static/straw/compact.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
-
- Straw - a MLTSHP Bookmarklet.
- Source: http://mltshp.com/static/straw/source.js
- Bookmarklet URL: javascript:void((function(){var%20e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','https://mltshp.com/static/straw/compact.js?r='+Math.random()*99999999);document.body.appendChild(e)})());
-*/
-(function(e){if(e.MLTSHPStraw!==void 0)e.MLTSHPStraw.toggle();else{var i=function(){var a=!!e.document.addEventListener;return{bind:function(b,d,c){var e=function(a){return c.apply(b,arguments)};a?b.addEventListener(d,e,!1):b.attachEvent("on"+d,e)}}}(),j=function(a,b){return function(){a.apply(b,arguments)}},k={style:function(a,b){var d=null;a.currentStyle?d=a.currentStyle[b]:e.getComputedStyle&&(d=e.document.defaultView.getComputedStyle(a,null).getPropertyValue(b));return d}},f=function(a,b,d){this.category=
-a;this.asset=b;this.el=d};f.prototype.append_root=e.document.body;f.prototype.position=function(){var a=this.el,b=0,d=0;do{if(k.style(a,"position")=="relative"){this.append_root=a;break}b+=a.offsetLeft;d+=a.offsetTop}while(a=a.offsetParent);return[b,d]};f.prototype.hide=function(){if(this.view!==void 0)this.view.style.display="none"};f.prototype.show=function(){if(this.view!==void 0)this.view.style.display="block"};f.prototype.draw_overlay=function(){var a=this.position(),b=this.width(),d=this.height(),
-c=document.createElement("div"),e=this.append_root;c.className="mltshp-sf-overlay";c.style.left=a[0]+"px";c.style.top=a[1]+"px";c.style.width=b-4+"px";c.style.height=d-4+"px";if(b>300&&d>100)html='Save on MLTSHP',c.innerHTML=html;e.appendChild(c);this.view=c;i.bind(c,"click",j(this.click_overlay,this))};f.prototype.click_overlay=function(a){a.stopPropagation?
-a.stopPropagation():a.cancelBubble=!0;left_location=screen.width/2-450;top_location=screen.height/2-300;e.open("https://mltshp.com/tools/p?url="+encodeURI(this.asset)+"&title="+encodeURI(e.document.title)+"&source_url="+encodeURI(e.location.href),"save_image","width=850,height=650,menubar=yes,toolbar=yes,scrollbars=yes,resizable=yes,left="+left_location+",top="+top_location+"screenX="+left_location+",screenY="+top_location)};f.prototype.height=function(){if(this._height===void 0)this._height=this.el.offsetHeight;
-return this._height};f.prototype.width=function(){if(this._width===void 0)this._width=this.el.offsetWidth;return this._width};var h={_parse_query:function(a){for(var a=a.split("&"),b=a.length,d={},c=0;c0)b=a.getAttribute("id").replace("player","").split("_")[0],e.push({el:a,url:"http://vimeo.com/"+b});return e},images:function(){for(var a=document.getElementsByTagName("img"),b=a.length,d=[],c=0;cSave on MLTSHP',c.innerHTML=html);f.appendChild(c);this.view=c;n.bind(c,"click",p(this.click_overlay,this))};g.prototype.click_overlay=function(a){a.stopPropagation?
+a.stopPropagation():a.cancelBubble=!0;left_location=screen.width/2-450;top_location=screen.height/2-300;a="width=850,height=650,menubar=yes,toolbar=yes,scrollbars=yes,resizable=yes,left="+left_location+",top="+top_location+"screenX="+left_location+",screenY="+top_location;e.open("https://mltshp.com/tools/p?url="+encodeURI(this.asset)+"&title="+encodeURI(e.document.title)+"&source_url="+encodeURI(e.location.href),"save_image",a)};g.prototype.height=function(){void 0===this._height&&(this._height=this.el.offsetHeight);
+return this._height};g.prototype.width=function(){void 0===this._width&&(this._width=this.el.offsetWidth);return this._width};var m={_parse_query:function(a){a=a.split("&");for(var b=a.length,d={},c=0;ca[c].offsetWidth||100>a[c].offsetHeight||d.push(a[c]);return d}};e.MLTSHPStraw={found:[],init:function(){this.initiated=!0;this.init_styles();this.find_assets();this.draw_overlays()},find_assets:function(){var a=
+m.images(),b=a.length;m.videos();for(var d=[],c=0;c