├── lib ├── __init__.py ├── db │ └── __init__.py └── flyingcow │ ├── __init__.py │ ├── README.md │ ├── error.py │ ├── db.py │ └── properties.py ├── test ├── __init__.py ├── unit │ ├── __init__.py │ ├── app_tests.py │ ├── script_log_tests.py │ ├── apihit_tests.py │ ├── external_relationship_tests.py │ ├── base.py │ └── fileview_tests.py ├── functional │ ├── __init__.py │ ├── misc_tests.py │ ├── error_tests.py │ ├── tools_find_people_tests.py │ ├── verify_email_tests.py │ └── image_nsfw_tests.py ├── scripts │ ├── __init__.py │ └── calculate_views_tests.py ├── files │ ├── 1.png │ ├── blue.gif │ ├── love.gif │ ├── red.gif │ └── green.gif ├── SimpleTests.py └── factories.py ├── scripts ├── __init__.py ├── populate-deletes.py ├── migrate-user.py ├── populate-shake-subscriptions.py ├── check-payment.py ├── populate-timelines.py ├── transcode-sharedfile.py ├── populate-save-like.py ├── delete-user.py └── calculate-views.py ├── .gitattributes ├── templates ├── tools │ ├── p.html │ ├── save-video-error.html │ ├── picker-error.html │ ├── find-shakes-quick-fetch-category.html │ ├── save-video.html │ ├── base-sign-in.html │ ├── picker-success.html │ ├── base.html │ ├── plugins.html │ ├── sign-in.html │ ├── find-shakes-people.html │ └── bookmarklet.html ├── misc │ ├── googlead81c5d028a3e443.html │ ├── coming-soon.html │ └── promo.html ├── shakes │ ├── no-create.html │ ├── followers.html │ ├── shake.html │ ├── members.html │ ├── rss.html │ └── create.html ├── tags │ ├── _sidebar.html │ └── tag.html ├── admin │ ├── index.html │ ├── interesting-stats.html │ ├── _sidebar.html │ ├── create-users.html │ └── shake-categories.html ├── account │ ├── signupsclosed.html │ ├── resend-verification-email.html │ ├── welcome-to-mltshp.html │ ├── return-subscription-completed.html │ ├── return-subscription-error.html │ ├── forgot-password-sent.html │ ├── followers.html │ ├── confirm.html │ ├── _password-managers.html │ ├── following.html │ ├── announcement-tou.html │ ├── forgot-password.html │ ├── settings-connections.html │ ├── reset-password.html │ ├── sign-in.html │ ├── redeem.html │ └── migrate.html ├── 404.html ├── waitlist │ ├── error.html │ ├── verified.html │ ├── success.html │ └── index.html ├── 500.html ├── uimodules │ ├── user-counts.html │ ├── fun-form-field.html │ ├── pagination.html │ ├── notification-invitation-approved.html │ ├── shake-dropdown.html │ ├── image-medium.html │ ├── notification-invitation-request.html │ └── notification-invitations.html ├── 403.html ├── developers │ ├── apps.html │ ├── view-app.html │ ├── guide.html │ ├── docindex.html │ └── new-api-application.html ├── incoming │ └── closed-for-repairs.html ├── upload │ └── error.html ├── search │ ├── _sidebar.html │ └── search.html ├── api │ └── authorize.html └── services │ └── oembed.json ├── developerdocs ├── requirements.txt ├── guide.rst ├── themes │ └── developerdocs │ │ ├── theme.conf │ │ └── layout.html ├── docindex.rst ├── Makefile ├── exts │ └── developerdocs.py └── README.md ├── release ├── supervisord.conf ├── docker-compose.yml └── settings.py ├── requirements-test.txt ├── .prettierignore ├── setup ├── dev │ ├── mysql-conf.d │ │ └── mltshp.cnf │ ├── start-nginx.sh │ ├── fakes3 │ │ └── Dockerfile │ └── supervisord.conf ├── production │ ├── mltshp-web-key.pub │ ├── mltshp-worker--crontab │ ├── mysql.conf │ ├── supervisord-web.conf │ └── supervisord-worker.conf ├── db-fixtures.sql ├── db-truncate.sql └── db-drop.sql ├── static ├── favicon.ico ├── images │ ├── 1password.png │ ├── fb_icon.png │ ├── keepass.png │ ├── lastpass.png │ ├── twitter1.png │ ├── twitter2.png │ ├── twitter3.png │ ├── twitter4.png │ ├── twitter5.png │ ├── twitter6.png │ ├── chrome-icon.png │ ├── fastly-logo.png │ ├── safari-icon.png │ ├── stumbleupon.png │ ├── test-avatar.png │ ├── tweet-this.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── firefox-icon.png │ ├── loading-mltshp.gif │ ├── mstile-150x150.png │ ├── apple-touch-icon.png │ ├── home-screenshot.jpg │ ├── cool-tools-chrome.png │ ├── cool-tools-firefox.png │ ├── cool-tools-safari.png │ ├── cool-tools-twitter.png │ ├── android-chrome-192x192.png │ ├── android-chrome-256x256.png │ ├── find-shakes-block-back.png │ ├── home-screenshot-dropdown.jpg │ ├── home-screenshot-dashboard.jpg │ ├── home-screenshot-createshake.jpg │ ├── home-screenshot-friendshake.jpg │ ├── flag-this-here.svg │ ├── icon_plus.svg │ ├── tumblr-flat-t-button-white.svg │ ├── error-tail.svg │ ├── icon-delete-comment.svg │ ├── comment-quip-left.svg │ ├── home-comment.svg │ ├── comment-tiny.svg │ ├── default-icon-tall.svg │ ├── icon-reply-arrow.svg │ ├── like-this.svg │ ├── like-tiny.svg │ ├── home-heart.svg │ ├── liked-this.svg │ ├── bookmarklet.svg │ ├── save-tiny.svg │ ├── safari-pinned-tab.svg │ ├── save-this.svg │ ├── saved-this.svg │ ├── home-piggy.svg │ ├── mltshp-flag.svg │ └── default-icon-venti.svg ├── promos │ ├── merch │ │ ├── 1.jpg │ │ ├── 10.jpg │ │ ├── 11.jpg │ │ ├── 12.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ └── 9.jpg │ └── pause-sub │ │ └── 1.png ├── swf │ └── ZeroClipboard.swf ├── plugins │ ├── mltshp.chrome.crx │ ├── mltshp.safari.safariextz │ ├── updates.xml │ └── update.plist ├── robots.txt ├── manifest.json ├── opensearch.xml └── js │ └── tools.js ├── tools └── compiler.jar ├── coveralls-report.sh ├── yoyo.ini ├── .gitignore ├── .dockerignore ├── .coveragerc ├── handlers ├── error.py ├── __init__.py ├── friends.py └── popular.py ├── .editorconfig ├── migrations ├── 20190901_01_6HEw7-adds-a-fulltext-index-to-the-sharedfile-table.py ├── 20190902_01_73ZB1-create-post-index-on-user-seen-deleted-columns.py ├── 20170513_02_hJ1nf-add-stripe-plan-rate-column.py ├── 20230627_01_J71Z3-add-sharefile-alt-text.py ├── 20170423_01_TwSED-adds-a-migration-state-table.py ├── 20170423_02_j66Zl-add-stripe-plan-id-to-user-table.py ├── 20170509_01_bP9rC-add-disable-autoplay-column-to-user.py ├── README.md ├── 20170429_01_0ad8s-add-deleted-column-to-shake-table.py ├── 20170513_01_JutYy-use-null-video-flags.py ├── 20170507_01_mRphn-add-video-key-columns-to-sourcefile.py ├── 20250131_01_nFypG-some-new-indexes-for-performance.py └── 20250127_01_nF5uM-dmca-takedown-table.py ├── requirements.txt ├── tasks ├── admin.py └── __init__.py ├── run-tests.sh ├── celeryconfig.example.py ├── migrate.py ├── models ├── nsfw_log.py ├── migration_state.py ├── shake_category.py ├── tagged_file.py ├── apilog.py ├── waitlist.py ├── script_log.py ├── post.py ├── shakesharedfile.py ├── fileview.py ├── invitation_request.py ├── comment_like.py ├── promotion.py ├── favorite.py ├── __init__.py ├── shakemanager.py ├── apihit.py ├── external_relationship.py ├── authorizationcode.py ├── tag.py └── subscription.py ├── .github └── workflows │ └── test.yml ├── Makefile ├── Dockerfile.worker ├── docker-compose.yml └── settings.example.py /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/unit/app_tests.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/functional/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /templates/tools/p.html: -------------------------------------------------------------------------------- 1 | Some thing here. -------------------------------------------------------------------------------- /developerdocs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinxcontrib-httpdomain 2 | -------------------------------------------------------------------------------- /release/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | coverage==7.6.9 2 | coveralls==4.0.1 3 | -------------------------------------------------------------------------------- /developerdocs/guide.rst: -------------------------------------------------------------------------------- 1 | Guide to the API 2 | ================ 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | static/straw/compact.js 2 | static/js/jquery-3.7.1.min.js 3 | -------------------------------------------------------------------------------- /setup/dev/mysql-conf.d/mltshp.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | mysql-native-password=ON 3 | -------------------------------------------------------------------------------- /test/files/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/test/files/1.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /test/files/blue.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/test/files/blue.gif -------------------------------------------------------------------------------- /test/files/love.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/test/files/love.gif -------------------------------------------------------------------------------- /test/files/red.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/test/files/red.gif -------------------------------------------------------------------------------- /tools/compiler.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/tools/compiler.jar -------------------------------------------------------------------------------- /coveralls-report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | apt-get update && apt-get install -y git 3 | coveralls 4 | -------------------------------------------------------------------------------- /test/files/green.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/test/files/green.gif -------------------------------------------------------------------------------- /templates/misc/googlead81c5d028a3e443.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googlead81c5d028a3e443.html -------------------------------------------------------------------------------- /static/images/1password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/1password.png -------------------------------------------------------------------------------- /static/images/fb_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/fb_icon.png -------------------------------------------------------------------------------- /static/images/keepass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/keepass.png -------------------------------------------------------------------------------- /static/images/lastpass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/lastpass.png -------------------------------------------------------------------------------- /static/images/twitter1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/twitter1.png -------------------------------------------------------------------------------- /static/images/twitter2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/twitter2.png -------------------------------------------------------------------------------- /static/images/twitter3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/twitter3.png -------------------------------------------------------------------------------- /static/images/twitter4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/twitter4.png -------------------------------------------------------------------------------- /static/images/twitter5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/twitter5.png -------------------------------------------------------------------------------- /static/images/twitter6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/twitter6.png -------------------------------------------------------------------------------- /static/promos/merch/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/merch/1.jpg -------------------------------------------------------------------------------- /static/promos/merch/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/merch/10.jpg -------------------------------------------------------------------------------- /static/promos/merch/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/merch/11.jpg -------------------------------------------------------------------------------- /static/promos/merch/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/merch/12.jpg -------------------------------------------------------------------------------- /static/promos/merch/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/merch/2.jpg -------------------------------------------------------------------------------- /static/promos/merch/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/merch/3.jpg -------------------------------------------------------------------------------- /static/promos/merch/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/merch/4.jpg -------------------------------------------------------------------------------- /static/promos/merch/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/merch/5.jpg -------------------------------------------------------------------------------- /static/promos/merch/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/merch/6.jpg -------------------------------------------------------------------------------- /static/promos/merch/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/merch/7.jpg -------------------------------------------------------------------------------- /static/promos/merch/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/merch/8.jpg -------------------------------------------------------------------------------- /static/promos/merch/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/merch/9.jpg -------------------------------------------------------------------------------- /static/images/chrome-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/chrome-icon.png -------------------------------------------------------------------------------- /static/images/fastly-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/fastly-logo.png -------------------------------------------------------------------------------- /static/images/safari-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/safari-icon.png -------------------------------------------------------------------------------- /static/images/stumbleupon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/stumbleupon.png -------------------------------------------------------------------------------- /static/images/test-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/test-avatar.png -------------------------------------------------------------------------------- /static/images/tweet-this.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/tweet-this.png -------------------------------------------------------------------------------- /static/promos/pause-sub/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/promos/pause-sub/1.png -------------------------------------------------------------------------------- /static/swf/ZeroClipboard.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/swf/ZeroClipboard.swf -------------------------------------------------------------------------------- /static/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/favicon-16x16.png -------------------------------------------------------------------------------- /static/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/favicon-32x32.png -------------------------------------------------------------------------------- /static/images/firefox-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/firefox-icon.png -------------------------------------------------------------------------------- /static/images/loading-mltshp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/loading-mltshp.gif -------------------------------------------------------------------------------- /static/images/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/mstile-150x150.png -------------------------------------------------------------------------------- /static/plugins/mltshp.chrome.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/plugins/mltshp.chrome.crx -------------------------------------------------------------------------------- /templates/shakes/no-create.html: -------------------------------------------------------------------------------- 1 | Your account only allows you to make 100 shakes in addition to your user shake. -------------------------------------------------------------------------------- /static/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/apple-touch-icon.png -------------------------------------------------------------------------------- /static/images/home-screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/home-screenshot.jpg -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | # Hello, little robots. Say hi to Mr. Asimov. 2 | 3 | User-agent: Twitterbot 4 | Disallow: / 5 | -------------------------------------------------------------------------------- /static/images/cool-tools-chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/cool-tools-chrome.png -------------------------------------------------------------------------------- /static/images/cool-tools-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/cool-tools-firefox.png -------------------------------------------------------------------------------- /static/images/cool-tools-safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/cool-tools-safari.png -------------------------------------------------------------------------------- /static/images/cool-tools-twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/cool-tools-twitter.png -------------------------------------------------------------------------------- /static/plugins/mltshp.safari.safariextz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/plugins/mltshp.safari.safariextz -------------------------------------------------------------------------------- /static/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/images/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/android-chrome-256x256.png -------------------------------------------------------------------------------- /static/images/find-shakes-block-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/find-shakes-block-back.png -------------------------------------------------------------------------------- /static/images/home-screenshot-dropdown.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/home-screenshot-dropdown.jpg -------------------------------------------------------------------------------- /templates/tags/_sidebar.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /templates/tools/save-video-error.html: -------------------------------------------------------------------------------- 1 |

Couldn’t save it.

2 | {% if message %} 3 |

4 | {{message}} 5 |

6 | {% end %} -------------------------------------------------------------------------------- /lib/flyingcow/__init__.py: -------------------------------------------------------------------------------- 1 | from .db import register_connection, connection 2 | from .model import Model 3 | from .properties import Property -------------------------------------------------------------------------------- /static/images/home-screenshot-dashboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/home-screenshot-dashboard.jpg -------------------------------------------------------------------------------- /static/images/home-screenshot-createshake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/home-screenshot-createshake.jpg -------------------------------------------------------------------------------- /static/images/home-screenshot-friendshake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MLTSHP/mltshp/HEAD/static/images/home-screenshot-friendshake.jpg -------------------------------------------------------------------------------- /yoyo.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | sources = migrations 3 | migration_table = _yoyo_migration 4 | batch_mode = off 5 | verbosity = 3 6 | database = mysql+mysqldb://root:@127.0.0.1/mltshp 7 | 8 | -------------------------------------------------------------------------------- /developerdocs/themes/developerdocs/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = none 4 | pygments_style = none 5 | 6 | [options] 7 | nosidebar = true 8 | embedded = true 9 | -------------------------------------------------------------------------------- /setup/dev/start-nginx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo resolver $(awk 'BEGIN{ORS=" "} $1=="nameserver" {print $2}' /etc/resolv.conf) ";" > /etc/nginx/resolvers.conf 3 | exec /usr/sbin/nginx -g 'daemon off;' -------------------------------------------------------------------------------- /templates/tools/picker-error.html: -------------------------------------------------------------------------------- 1 | {% extends 'tools/base.html' %} 2 | 3 | {% block main %} 4 |
5 |

We had a problem ({{ error }}).

6 |
7 | {% end %} 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /settings.py 3 | celeryconfig.py 4 | .eggs/ 5 | build/ 6 | html/ 7 | .figleaf 8 | pip-log.txt 9 | .DS_Store 10 | *.swp 11 | env 12 | mounts 13 | .env 14 | .deploy.env 15 | .coverage 16 | coverage.xml 17 | .vscode/ 18 | -------------------------------------------------------------------------------- /templates/tools/find-shakes-quick-fetch-category.html: -------------------------------------------------------------------------------- 1 | {% for shake in shakes %} 2 |
3 | {{modules.ShakeFollow(follow_shake=shake, current_user=current_user_obj, avatar_size=50, extended=True)}} 4 |
5 | {% end %} 6 | -------------------------------------------------------------------------------- /templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html" %} 2 | 3 | {% block title %}Admin{% end %} 4 | 5 | {% block main %} 6 |
7 | {% include "admin/_sidebar.html" %} 8 |
9 | {% end %} 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .buildkite 3 | .vscode 4 | env 5 | mounts 6 | *.pyc 7 | __pycache__/ 8 | settings.py 9 | celeryconfig.py 10 | .eggs/ 11 | build/ 12 | html/ 13 | web/ 14 | worker/ 15 | pip-log.txt 16 | .DS_Store 17 | *.swp 18 | .deploy.env 19 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | pragma: no cover 4 | def __repr__ 5 | raise AssertionError 6 | raise NotImplementedError 7 | if __name__ == .__main__.: 8 | 9 | omit = 10 | */lib/flyingcow/* 11 | 12 | [xml] 13 | output = coverage.xml 14 | -------------------------------------------------------------------------------- /templates/misc/coming-soon.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Sign In{% end %} 4 | 5 | {% block main %} 6 |
7 |

Coming Soon!

8 |

9 | This feature or thing is coming soon. 10 |

11 |
12 | {% end %} -------------------------------------------------------------------------------- /handlers/error.py: -------------------------------------------------------------------------------- 1 | import tornado 2 | from . import base 3 | 4 | class NotFoundHandler(base.BaseHandler): 5 | def check_xsrf_cookie(self): 6 | pass 7 | 8 | def get(self): 9 | raise tornado.web.HTTPError(404) 10 | 11 | def post(self): 12 | raise tornado.web.HTTPError(404) 13 | -------------------------------------------------------------------------------- /static/images/flag-this-here.svg: -------------------------------------------------------------------------------- 1 | flag-this-here -------------------------------------------------------------------------------- /developerdocs/docindex.rst: -------------------------------------------------------------------------------- 1 | .. mltshp-api documentation master file, created by 2 | sphinx-quickstart on Fri Jun 17 15:38:27 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | reference 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{md,pug}] 13 | trim_trailing_whitespace = false 14 | 15 | [*.{yaml,yml}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /templates/account/signupsclosed.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Create Account{% end %} 4 | 5 | {% block main %} 6 | 7 |
8 |

Account Creation Closed

9 |

10 | MLTSHP is no longer accepting new users. 11 |

12 |
13 | {% end %} 14 | -------------------------------------------------------------------------------- /static/images/icon_plus.svg: -------------------------------------------------------------------------------- 1 | icon_plus -------------------------------------------------------------------------------- /static/images/tumblr-flat-t-button-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/flyingcow/README.md: -------------------------------------------------------------------------------- 1 | # What is it? 2 | 3 | flyingcow is a no-frills way interact with database persisted objects when working with the Tornado framework. It's totally beta. 4 | 5 | It uses Tornado's MySQL connection wrapper with no intention of supporting other datastores. 6 | 7 | # Requirements 8 | 9 | * Python 3 (that's all I'm testing on) 10 | * Tornado 11 | -------------------------------------------------------------------------------- /migrations/20190901_01_6HEw7-adds-a-fulltext-index-to-the-sharedfile-table.py: -------------------------------------------------------------------------------- 1 | """ 2 | Adds a FULLTEXT index to the sharedfile table. 3 | """ 4 | 5 | from yoyo import step 6 | 7 | __depends__ = {'20170513_02_hJ1nf-add-stripe-plan-rate-column'} 8 | 9 | steps = [ 10 | step("alter table sharedfile add fulltext titledesc_fulltext_idx (title, description)") 11 | ] 12 | -------------------------------------------------------------------------------- /static/images/error-tail.svg: -------------------------------------------------------------------------------- 1 | error-tail-only -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}404 - Uh oh! {% if page_type == 'image' %}Image{% else %}Page{% end %} not found.{% end %} 3 | {% block main %} 4 |
5 |

{% if page_type == 'image' %}Image{% elif page_type=='shake'%}That shake{% else %}Page{% end %} couldn't be found.

6 |
7 | {% end %} -------------------------------------------------------------------------------- /lib/flyingcow/error.py: -------------------------------------------------------------------------------- 1 | 2 | class Errors(dict): 3 | """ 4 | A light wrapper for a dict, so we can test for existance of field errors in templates 5 | without getting AttributeErrors. We access keys as attributes. 6 | """ 7 | 8 | def __getattr__(self, key): 9 | if key in self: 10 | return self[key] 11 | else: 12 | return None 13 | -------------------------------------------------------------------------------- /scripts/populate-deletes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from torndb import Connection 4 | from tornado.options import options 5 | 6 | 7 | def main(): 8 | db1 = Connection(options.database_host, options.database_user, options.database_password) 9 | db1.execute("UPDATE shakesharedfile SET deleted = 1 WHERE deleted = 0 AND sharedfile_id IN (SELECT id FROM sharedfile WHERE deleted = 1)") 10 | -------------------------------------------------------------------------------- /templates/admin/interesting-stats.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html" %} 2 | 3 | {% block title %}Interesting Stats{% end %} 4 | 5 | {% block main %} 6 |
7 | {% include "admin/_sidebar.html" %} 8 | 9 |
10 |

11 | Total number of files uploaded: {{total_files}} 12 |

13 |
14 |
15 | {% end %} 16 | -------------------------------------------------------------------------------- /templates/waitlist/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Error Putting You On The List 5 | {% end %} 6 | {% block main %} 7 |

Error

8 |

9 | Not sure what happened but there was an error when trying to access that URL. Could you mail 10 | us at hello@mltshp.com and tell us what happened? Thanks. 11 |

12 | {% end %} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | amqplib==1.0.2 2 | beautifulsoup4==4.12.3 3 | boto3==1.35.86 4 | celery==5.4.0 5 | ffmpy==0.5.0 6 | kombu==5.3.7 7 | mock==5.1.0 8 | mysqlclient==2.2.6 9 | Pillow==11.0.0 10 | pycurl==7.45.4 11 | pyOpenSSL==24.3.0 12 | python-dateutil==2.9.0.post0 13 | python-postmark==0.6.0 14 | requests==2.32.3 15 | setuptools==75.8.0 16 | stripe==11.4.1 17 | tornado==6.4.2 18 | yoyo-migrations==8.2.0 19 | -------------------------------------------------------------------------------- /migrations/20190902_01_73ZB1-create-post-index-on-user-seen-deleted-columns.py: -------------------------------------------------------------------------------- 1 | """ 2 | Create post index on user, seen, deleted columns 3 | """ 4 | 5 | from yoyo import step 6 | 7 | __depends__ = {'20190901_01_6HEw7-adds-a-fulltext-index-to-the-sharedfile-table'} 8 | 9 | steps = [ 10 | step("ALTER TABLE post ADD INDEX userseendeletedid_idx (user_id, seen, deleted, sharedfile_id) USING BTREE;") 11 | ] 12 | -------------------------------------------------------------------------------- /static/images/icon-delete-comment.svg: -------------------------------------------------------------------------------- 1 | icon-delete-comment -------------------------------------------------------------------------------- /migrations/20170513_02_hJ1nf-add-stripe-plan-rate-column.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add stripe_plan_rate column 3 | """ 4 | 5 | from yoyo import step 6 | 7 | __depends__ = {'20170513_01_JutYy-use-null-video-flags'} 8 | 9 | steps = [ 10 | step( 11 | "ALTER TABLE user ADD COLUMN `stripe_plan_rate` INT(11) NULL AFTER `stripe_plan_id`", 12 | "ALTER TABLE user DROP COLUMN `stripe_plan_rate`", 13 | ) 14 | ] 15 | -------------------------------------------------------------------------------- /setup/production/mltshp-web-key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAnN8TD1y1BfqgsmRIJG+W0CTy5bAvKqEcSaZMgUGcTl57aE+GzYOS0jQrCvW+Jm9luLtUO3iSv6YQksG3ghpPQ+98L1tIiY3QCu7lLw/ii6wELoXsGjXb+OMWum8MkOslPAlO1XGD0/Zi1hQ1xcjbKJEO/aymRqRRH2N49X5/g2KY2WpI1sJoxJdjievG1Uobyw+1KeiV/lFCUDkgHe80gC3HKOqxNOX7D7QsPe9S4Qqb7UFTo+dKn8pBKC41pT4ArvFn9ikNSPy0zylYQ+79QrWIKWCRD5E4qP2ykjDumAXQOGB7y4czmpu6rmGeNKhS/kpjoc+nB/ML+phgkN+z root@mltshp-web 2 | -------------------------------------------------------------------------------- /migrations/20230627_01_J71Z3-add-sharefile-alt-text.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add an alt_text column to sharedfile 3 | """ 4 | 5 | from yoyo import step 6 | 7 | steps = [ 8 | step("ALTER TABLE sharedfile ADD COLUMN alt_text TEXT AFTER description;"), 9 | step("ALTER TABLE sharedfile DROP KEY titledesc_fulltext_idx;"), 10 | step("ALTER TABLE sharedfile ADD FULLTEXT KEY `titledesc_fulltext_idx` (`title`,`description`,`alt_text`);") 11 | ] 12 | -------------------------------------------------------------------------------- /static/images/comment-quip-left.svg: -------------------------------------------------------------------------------- 1 | comment-quip-left -------------------------------------------------------------------------------- /templates/waitlist/verified.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | You're Verified 5 | {% end %} 6 | {% block main %} 7 |
8 |

Verified

9 |

10 | Thanks for verifying your email address. When there's room we'll surely contact you. If you have 11 | questions send them to hello@mltshp.com. 12 |

13 |
14 | {% end %} -------------------------------------------------------------------------------- /migrations/20170423_01_TwSED-adds-a-migration-state-table.py: -------------------------------------------------------------------------------- 1 | """ 2 | Adds a migration_state table. 3 | """ 4 | 5 | from yoyo import step 6 | 7 | __depends__ = {} 8 | 9 | steps = [ 10 | step(""" 11 | CREATE TABLE migration_state ( 12 | user_id int(11) NOT NULL PRIMARY KEY, 13 | is_migrated tinyint(1) NOT NULL DEFAULT '0' 14 | ) 15 | """, 16 | """DROP TABLE migration_state""") 17 | ] 18 | -------------------------------------------------------------------------------- /migrations/20170423_02_j66Zl-add-stripe-plan-id-to-user-table.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add stripe_plan_id column to user table 3 | """ 4 | 5 | from yoyo import step 6 | 7 | __depends__ = {'20170423_01_TwSED-adds-a-migration-state-table'} 8 | 9 | steps = [ 10 | step( 11 | "ALTER TABLE `user` ADD COLUMN `stripe_plan_id` VARCHAR(40) NULL AFTER `stripe_customer_id`", 12 | "ALTER TABLE `user` DROP COLUMN `stripe_plan_id`" 13 | ) 14 | ] 15 | -------------------------------------------------------------------------------- /migrations/20170509_01_bP9rC-add-disable-autoplay-column-to-user.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add disable_autoplay column to user 3 | """ 4 | 5 | from yoyo import step 6 | 7 | __depends__ = {'20170507_01_mRphn-add-video-key-columns-to-sourcefile'} 8 | 9 | steps = [ 10 | step( 11 | "ALTER TABLE user ADD COLUMN `disable_autoplay` tinyint(1) DEFAULT 0 AFTER `show_stats`", 12 | "ALTER TABLE user DROP COLUMN `disable_autoplay`", 13 | ) 14 | ] 15 | -------------------------------------------------------------------------------- /templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}500 - Uh oh! We had a problem.{% end %} 3 | {% block main %} 4 |
5 |

We had a problem.

6 |

7 | We made a note of the error. 8 |

9 |

10 | You can also send us an email. 11 |

12 |
13 | {% end %} 14 | -------------------------------------------------------------------------------- /setup/production/mltshp-worker--crontab: -------------------------------------------------------------------------------- 1 | # Crontab jobs for MLTSHP worker server 2 | 3 | # Run calculate web views every 5 minutes... 4 | 0,5,10,15,20,25,30,35,40,45,50,55 * * * * cd /srv/mltshp.com/mltshp; run-one timeout 290s python3 runner.py scripts/calculate-views.py 5 | 6 | # Post the popular stuff to the "best_of_mltshp" configured user's shake every hour 7 | 3 * * * * cd /srv/mltshp.com/mltshp; run-one timeout 30s python3 runner.py scripts/repost-best-of.py 8 | -------------------------------------------------------------------------------- /templates/uimodules/user-counts.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 | -------------------------------------------------------------------------------- /templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}403 - Uh oh! We had a problem.{% end %} 3 | {% block main %} 4 |
5 |

That didn't work.

6 |

7 | If you're not signed in, that could be the cause. 8 |

9 |

10 | You can also send us an email. 11 |

12 |
13 | {% end %} 14 | -------------------------------------------------------------------------------- /templates/admin/_sidebar.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /static/images/home-comment.svg: -------------------------------------------------------------------------------- 1 | comment-icon-8px -------------------------------------------------------------------------------- /static/images/comment-tiny.svg: -------------------------------------------------------------------------------- 1 | comment-tiny -------------------------------------------------------------------------------- /tasks/admin.py: -------------------------------------------------------------------------------- 1 | from tasks import mltshp_task 2 | from models import User 3 | 4 | 5 | @mltshp_task() 6 | def delete_account(user_id=0, **kwargs): 7 | """ 8 | This task deletes a user account. This is meant to do the full deletion work of 9 | related records for a User object that has a deleted flag already set. 10 | 11 | """ 12 | user = User.get('id = %s', user_id) 13 | if not user or user.is_admin() or user.deleted == 0: 14 | return 15 | user.delete() 16 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "icons": [ 4 | { 5 | "src": "/static/images/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/static/images/android-chrome-256x256.png", 11 | "sizes": "256x256", 12 | "type": "image/png" 13 | } 14 | ], 15 | "theme_color": "#dbfaff", 16 | "background_color": "#dbfaff" 17 | } 18 | -------------------------------------------------------------------------------- /migrations/README.md: -------------------------------------------------------------------------------- 1 | ## About this folder 2 | 3 | This folder will contain all the database migration scripts required 4 | to bring a schema forward in case it is behind. We're using "yoyo" 5 | for handling the migration work. See https://bitbucket.org/ollyc/yoyo 6 | for more information. Note: the `setup/db-install.sql` file should always 7 | have the latest schema. So please keep that file up to date, since it 8 | is used by tests to create a fresh database with the schema the 9 | application expects. 10 | -------------------------------------------------------------------------------- /templates/developers/apps.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block main %} 5 |
6 |
7 | 8 |

List of Your API Applications

9 | 14 |

15 | Main Developer Page 16 |

17 | 18 | 19 |
20 |
21 | {% end %} 22 | -------------------------------------------------------------------------------- /handlers/__init__.py: -------------------------------------------------------------------------------- 1 | #handlers 2 | from . import base 3 | from . import account 4 | from . import admin 5 | from . import image 6 | from . import home 7 | from . import misc 8 | from . import tools 9 | from . import upload 10 | from . import incoming 11 | from . import friends 12 | from . import api 13 | from . import developers 14 | from . import conversations 15 | from . import error 16 | from . import popular 17 | from . import shake 18 | from . import tag 19 | from . import stripe_hooks 20 | from . import search 21 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # exit if any command fails (e); strict variable substitution (u); 4 | # set exit code to non-zero for any failed piped commands (o pipefail) 5 | # See also: http://redsymbol.net/articles/unofficial-bash-strict-mode/ 6 | set -euo pipefail 7 | 8 | export PYTHONUNBUFFERED=1 9 | 10 | # These commands expect to be run in a Docker container 11 | pip3 install --break-system-packages -r requirements-test.txt; 12 | coverage run --source=handlers,models,tasks,lib test.py 13 | coverage xml 14 | -------------------------------------------------------------------------------- /static/images/default-icon-tall.svg: -------------------------------------------------------------------------------- 1 | default-icon-tall -------------------------------------------------------------------------------- /static/images/icon-reply-arrow.svg: -------------------------------------------------------------------------------- 1 | icon-reply-arrow -------------------------------------------------------------------------------- /static/images/like-this.svg: -------------------------------------------------------------------------------- 1 | like-this -------------------------------------------------------------------------------- /templates/waitlist/success.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | You're On The List 5 | {% end %} 6 | {% block main %} 7 |
8 |

Success

9 |

10 | As soon as we have space for more people we'll be sure to contact you. 11 | We've sent you an email so you can verify that your email address is correct. 12 | If you don't get the email please contact us at 13 | hello@mltshp.com and we'll see what's what. 14 |

15 |
16 | {% end %} -------------------------------------------------------------------------------- /static/images/like-tiny.svg: -------------------------------------------------------------------------------- 1 | like-tiny -------------------------------------------------------------------------------- /static/images/home-heart.svg: -------------------------------------------------------------------------------- 1 | like-icon-8px -------------------------------------------------------------------------------- /templates/account/resend-verification-email.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Verification Email Sent. Check Your Inbox!{% end %} 4 | 5 | {% block main %} 6 | 7 |
8 |

Verification Email Has Been Sent

9 |

10 | We just sent you an email that explains how to verify your email address. If for some reason this is not working for you 11 | please contact us at hello@mltshp.com 12 |

13 |
14 | {% end %} 15 | -------------------------------------------------------------------------------- /setup/dev/fakes3/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.7 as builder 2 | 3 | ENV FAKES3_VERSION 2.0.0 4 | 5 | RUN gem install fakes3 -v ${FAKES3_VERSION} \ 6 | && rm -rf /usr/local/bundle/cache/* 7 | 8 | FROM ruby:2.7-slim 9 | 10 | COPY --from=builder /usr/local/bundle /usr/local/bundle/ 11 | 12 | VOLUME /srv 13 | RUN mkdir -p /srv \ 14 | && chown nobody:nogroup /srv \ 15 | && chmod 750 /srv \ 16 | && ln -s /usr/local/bundle/bin/fakes3 /usr/bin/fakes3 17 | WORKDIR /srv 18 | 19 | EXPOSE 4567 20 | 21 | USER nobody 22 | ENTRYPOINT ["fakes3", "--port", "4567"] 23 | CMD ["--root", "/srv"] 24 | -------------------------------------------------------------------------------- /migrations/20170429_01_0ad8s-add-deleted-column-to-shake-table.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add deleted column to shake table 3 | """ 4 | 5 | from yoyo import step 6 | 7 | __depends__ = {'20170423_02_j66Zl-add-stripe-plan-id-to-user-table'} 8 | 9 | steps = [ 10 | step( 11 | "ALTER TABLE `shake` ADD COLUMN `deleted` TINYINT(1) DEFAULT '0' AFTER `shake_category_id`", 12 | "ALTER TABLE `shake` DROP COLUMN `deleted`" 13 | ), 14 | step("UPDATE `shake` SET `deleted`=0 WHERE `deleted` IS NULL"), 15 | step("ALTER TABLE `shake` MODIFY COLUMN `deleted` TINYINT(1) NOT NULL DEFAULT '0'") 16 | ] 17 | -------------------------------------------------------------------------------- /setup/production/mysql.conf: -------------------------------------------------------------------------------- 1 | # /etc/mysql/mysql.conf.d/mltshp.cnf 2 | [mysqld] 3 | skip-name-resolve = 1 4 | key_buffer_size = 2048M 5 | sort_buffer_size = 16M 6 | table_open_cache = 4000 7 | thread_cache_size = 8 8 | query_cache_limit = 1M 9 | query_cache_size = 32M 10 | query_cache_type = 1 11 | innodb_buffer_pool_size = 8G 12 | innodb_ft_min_token_size = 2 13 | 14 | # 15 | # Here you can see queries with especially long duration 16 | #log_slow_queries = /var/log/mysql/mysql-slow.log 17 | #long_query_time = 2 18 | #log-queries-not-using-indexes -------------------------------------------------------------------------------- /migrations/20170513_01_JutYy-use-null-video-flags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Use NULL video flags 3 | """ 4 | 5 | from yoyo import step 6 | 7 | __depends__ = {'20170509_01_bP9rC-add-disable-autoplay-column-to-user'} 8 | 9 | steps = [ 10 | step( 11 | "ALTER TABLE sourcefile MODIFY COLUMN webm_flag TINYINT(1) DEFAULT NULL", 12 | "ALTER TABLE sourcefile MODIFY COLUMN webm_flag TINYINT(1) DEFAULT 0", 13 | ), 14 | step( 15 | "ALTER TABLE sourcefile MODIFY COLUMN mp4_flag TINYINT(1) DEFAULT NULL", 16 | "ALTER TABLE sourcefile MODIFY COLUMN mp4_flag TINYINT(1) DEFAULT 0", 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /migrations/20170507_01_mRphn-add-video-key-columns-to-sourcefile.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add video key columns to sourcefile 3 | """ 4 | 5 | from yoyo import step 6 | 7 | __depends__ = {'20170429_01_0ad8s-add-deleted-column-to-shake-table'} 8 | 9 | steps = [ 10 | step( 11 | "ALTER TABLE sourcefile ADD COLUMN `webm_flag` tinyint(1) DEFAULT 0 AFTER `thumb_key`", 12 | "ALTER TABLE sourcefile DROP COLUMN `webm_flag`", 13 | ), 14 | step( 15 | "ALTER TABLE sourcefile ADD COLUMN `mp4_flag` tinyint(1) DEFAULT 0 AFTER `thumb_key`", 16 | "ALTER TABLE sourcefile DROP COLUMN `mp4_flag`", 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /setup/dev/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:nginx] 5 | command=sh /srv/mltshp.com/mltshp/setup/dev/start-nginx.sh 6 | autorestart=true 7 | 8 | [group:mltshp] 9 | programs=main-8000 10 | 11 | [program:main-8000] 12 | command=python3 main.py --on_port=8000 13 | directory=/srv/mltshp.com/mltshp 14 | autorestart=true 15 | redirect_stderr=true 16 | stdout_logfile=/srv/mltshp.com/logs/main-8000.log 17 | stdout_logfile_maxbytes=500MB 18 | stdout_logfile_backups=50 19 | stdout_capture_maxbytes=1MB 20 | stdout_events_enabled=false 21 | loglevel=info 22 | user=mltshp 23 | environment=HOME="/home/mltshp",USER="mltshp" 24 | -------------------------------------------------------------------------------- /celeryconfig.example.py: -------------------------------------------------------------------------------- 1 | ## mltshp settings initialization 2 | 3 | import mltshpoptions 4 | import settings as app_settings 5 | mltshpoptions.parse_dictionary(app_settings.settings) 6 | 7 | ## Celery configuration 8 | 9 | # List of modules to import when celery starts. 10 | imports = ("tasks.timeline", "tasks.counts", "tasks.migration", "tasks.transcode", "tasks.admin") 11 | 12 | task_routes = { 13 | "tasks.transcode.*": { "queue": "transcode" }, 14 | } 15 | 16 | ## Result store settings. 17 | result_backend = "rpc://" 18 | result_persistent = False 19 | 20 | ## Broker settings. 21 | broker_url = "amqp://user:password@broker_host:5672/vhost" 22 | -------------------------------------------------------------------------------- /static/plugins/updates.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /scripts/migrate-user.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from torndb import Connection 3 | from tornado.options import options 4 | 5 | from models import User 6 | from tasks.migration import migrate_for_user 7 | 8 | import logging 9 | logging.basicConfig(level=logging.INFO) 10 | 11 | 12 | def main(): 13 | names = sys.argv[2:] 14 | 15 | options.use_workers = False 16 | for name in names: 17 | user = User.get("name=%s and deleted=2", name) 18 | if user is not None: 19 | print("Migrating %s..." % name) 20 | migrate_for_user.delay_or_run(user.id) 21 | else: 22 | print("Could not find user named: %s" % name) 23 | -------------------------------------------------------------------------------- /static/images/liked-this.svg: -------------------------------------------------------------------------------- 1 | liked-this -------------------------------------------------------------------------------- /migrate.py: -------------------------------------------------------------------------------- 1 | from tornado.options import options 2 | import mltshpoptions 3 | from settings import settings 4 | mltshpoptions.parse_dictionary(settings) 5 | 6 | from yoyo import read_migrations, get_backend 7 | import logging 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | 11 | backend = get_backend( 12 | "mysql+mysqldb://%s:%s@%s/%s" % 13 | (options.database_user, options.database_password, 14 | options.database_host, options.database_name)) 15 | migrations = read_migrations("migrations") 16 | with backend.lock(): 17 | print("Applying migrations...") 18 | backend.apply_migrations(backend.to_apply(migrations)) 19 | print("...complete!") 20 | -------------------------------------------------------------------------------- /templates/uimodules/fun-form-field.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | {% if field_type == 'text' %} 6 | 7 | {% elif field_type == 'textarea' %} 8 | 9 | {% end %} 10 | {% if getattr(errors, name) %} 11 |
12 | 13 | {{errors[name] }} 14 | 15 |
16 | {% end %} 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /setup/production/supervisord-web.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:nginx] 5 | command=/usr/sbin/nginx -g 'daemon off;' 6 | autorestart=true 7 | 8 | [program:main] 9 | process_name=mltshp-%(process_num)d 10 | command=python3 main.py --on_port=80%(process_num)02d 11 | numprocs=6 12 | numprocs_start=1 13 | directory=/srv/mltshp.com/mltshp 14 | autorestart=true 15 | redirect_stderr=true 16 | stdout_logfile=/srv/mltshp.com/logs/main-80%(process_num)02d.log 17 | stdout_logfile_maxbytes=500MB 18 | stdout_logfile_backups=2 19 | stdout_capture_maxbytes=1MB 20 | stdout_events_enabled=false 21 | loglevel=warn 22 | user=mltshp 23 | environment=HOME="/home/mltshp",USER="mltshp" 24 | -------------------------------------------------------------------------------- /migrations/20250131_01_nFypG-some-new-indexes-for-performance.py: -------------------------------------------------------------------------------- 1 | """ 2 | Some new indexes for performance 3 | """ 4 | 5 | from yoyo import step 6 | 7 | __depends__ = {'20190902_01_73ZB1-create-post-index-on-user-seen-deleted-columns', '20250127_01_nF5uM-dmca-takedown-table'} 8 | 9 | steps = [ 10 | step("create index tag_id_idx on tagged_file (tag_id)"), 11 | step("create index sharedfile_id_idx on tagged_file (sharedfile_id)"), 12 | step("create index user_type_idx on externalservice (user_id, type)"), 13 | step("create index name_idx on shake (name)"), 14 | step("create index shake_idx on subscription (shake_id)"), 15 | step("create index user_id_idx on favorite (user_id, id)"), 16 | ] 17 | -------------------------------------------------------------------------------- /models/nsfw_log.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.utilities import utcnow 3 | from tornado.options import options 4 | 5 | 6 | class NSFWLog(Model): 7 | user_id = Property() 8 | sharedfile_id = Property() 9 | sourcefile_id = Property() 10 | created_at = Property() 11 | 12 | def save(self, *args, **kwargs): 13 | if options.readonly: 14 | self.add_error('_', 'Site is read-only.') 15 | return False 16 | 17 | self._set_dates() 18 | return super(NSFWLog, self).save(*args, **kwargs) 19 | 20 | def _set_dates(self): 21 | if self.id is None or self.created_at is None: 22 | self.created_at = utcnow() 23 | -------------------------------------------------------------------------------- /handlers/friends.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | from .base import BaseHandler, require_membership 3 | 4 | 5 | class FriendHandler(BaseHandler): 6 | """ 7 | DEPRECATED 8 | 9 | Only here to prevent 404's from user's on the site when 10 | the URL change goes live. 11 | 12 | path: /friends 13 | """ 14 | @tornado.web.authenticated 15 | @require_membership 16 | def get(self, before_or_after=None, base36_id=None): 17 | if not before_or_after: 18 | return self.redirect('/') 19 | 20 | if before_or_after == 'before': 21 | return self.redirect('/before/%s' % base36_id) 22 | else: 23 | return self.redirect('/after/%s' % base36_id) 24 | -------------------------------------------------------------------------------- /templates/account/welcome-to-mltshp.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Welcome to MLTSHP!{% end %} 3 | 4 | {% block main %} 5 |
6 | 7 |

Welcome to MLTSHP!

8 | 9 |

Hi, {{ name }}! We are thrilled to have you join us here.

10 | 11 |

Here's what to do next:

12 | 13 |
    14 |
  1. Choose your membership plan
  2. 15 |
  3. Optionally restore your MLKSHK.com content
  4. 16 |
17 | 18 |
19 | 21 |
22 | 23 |
24 | {% end %} 25 | -------------------------------------------------------------------------------- /models/migration_state.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | 3 | 4 | class MigrationState(Model): 5 | user_id = Property() 6 | is_migrated = Property() 7 | 8 | @staticmethod 9 | def has_migration_data(user_id): 10 | state = MigrationState.get("user_id=%s", user_id) 11 | if state is None: 12 | # no state row means this user has nothing to migrate 13 | return False 14 | return True 15 | 16 | @staticmethod 17 | def has_migrated(user_id): 18 | state = MigrationState.get("user_id=%s", user_id) 19 | if state is None: 20 | # no state row means this user has nothing to migrate 21 | return True 22 | 23 | return state.is_migrated == 1 -------------------------------------------------------------------------------- /static/plugins/update.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Extension Updates 6 | 7 | 8 | CFBundleIdentifier 9 | com.mltshp.picker 10 | Developer Identifier 11 | 8RJL56QY4M 12 | CFBundleVersion 13 | 2.0.7 14 | CFBundleShortVersionString 15 | 2.0 16 | URL 17 | http://mltshp.com/static/plugins/mltshp.safari.safariextz 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /models/shake_category.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.utilities import utcnow 3 | from tornado.options import options 4 | 5 | 6 | class ShakeCategory(Model): 7 | name = Property(default='') 8 | short_name = Property(default='') 9 | created_at = Property() 10 | updated_at = Property() 11 | 12 | def save(self, *args, **kwargs): 13 | if options.readonly: 14 | self.add_error('_', 'Site is read-only.') 15 | return False 16 | 17 | self._set_dates() 18 | return super(ShakeCategory, self).save(*args, **kwargs) 19 | 20 | def _set_dates(self): 21 | if self.id is None or self.created_at is None: 22 | self.created_at = utcnow() 23 | self.updated_at = utcnow() 24 | -------------------------------------------------------------------------------- /test/functional/misc_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import test.base 3 | 4 | class MiscTests(test.base.BaseAsyncTestCase): 5 | 6 | def test_terms_of_use_exists(self): 7 | """ 8 | /terms-of-use should be accessible. 9 | """ 10 | response = self.fetch('/terms-of-use') 11 | self.assertEqual(200, response.code) 12 | 13 | def test_faq_exists(self): 14 | """ 15 | /faq should be accessible. 16 | """ 17 | response = self.fetch('/faq') 18 | self.assertEqual(200, response.code) 19 | 20 | def test_coming_soon_exists(self): 21 | """ 22 | /coming-soon should be accessible. 23 | """ 24 | response = self.fetch('/coming-soon') 25 | self.assertEqual(200, response.code) 26 | -------------------------------------------------------------------------------- /models/tagged_file.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.flyingcow.cache import ModelQueryCache 3 | from lib.utilities import utcnow 4 | from tornado.options import options 5 | 6 | 7 | class TaggedFile(ModelQueryCache, Model): 8 | sharedfile_id = Property() 9 | tag_id = Property() 10 | deleted = Property() 11 | created_at = Property() 12 | 13 | def save(self, *args, **kwargs): 14 | if options.readonly: 15 | self.add_error('_', 'Site is read-only.') 16 | return False 17 | 18 | self._set_dates() 19 | return super(TaggedFile, self).save(*args, **kwargs) 20 | 21 | def _set_dates(self): 22 | if self.id is None or self.created_at is None: 23 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 24 | -------------------------------------------------------------------------------- /release/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mltshp: 3 | image: "mltshp/mltshp-web:build-${build_number}" 4 | build: .. 5 | volumes: 6 | - ./settings.py:/srv/mltshp.com/mltshp/settings.py 7 | - ../.git:/srv/mltshp.com/mltshp/.git 8 | - ./supervisord.conf:/etc/supervisor/conf.d/mltshp.conf 9 | ports: 10 | - "8001:80" 11 | depends_on: 12 | - mysql 13 | links: 14 | - mysql 15 | networks: 16 | app_net: 17 | aliases: 18 | - mltshp.dev 19 | mysql: 20 | image: mysql:8 21 | ports: 22 | - "3306:3306" 23 | environment: 24 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 25 | MYSQL_DATABASE: "mltshp_testing" 26 | MYSQL_USER: "mltshp" 27 | networks: 28 | - app_net 29 | 30 | networks: 31 | app_net: 32 | -------------------------------------------------------------------------------- /templates/developers/view-app.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block main %} 5 |
6 |
7 | 8 |

{% if app.title %}{{app.title}}{% else %}(No App Title){% end %}

9 |

10 | {% if app.description != None%}{{app.description}}{% end %} 11 |

12 |

13 | {% if app.redirect_url != None%}{{app.redirect_url}}{% end %} 14 |

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
Key: {{app.key()}}
Secret:
{{app.secret}}
25 |

26 | Edit This Application
27 | My MLTSHP API Applications 28 |

29 |
30 |
31 | {% end %} 32 | -------------------------------------------------------------------------------- /scripts/populate-shake-subscriptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from torndb import Connection 4 | from tornado.options import options 5 | 6 | 7 | def main(): 8 | db1 = Connection(options.database_host, options.database_user, options.database_password) 9 | 10 | #grab all group shakes 11 | shakes = db1.query("""SELECT id, user_id FROM shake WHERE type=%s""", "group") 12 | 13 | for shake in shakes: 14 | db1.execute("""INSERT IGNORE INTO subscription (user_id, shake_id, deleted, created_at, updated_at) 15 | VALUES (%s, %s, 0, NOW(), NOW())""", shake['user_id'], shake['id']) 16 | #print """INSERT INTO subscription (user_id, shake_id, deleted, created_at, updated_at) 17 | # VALUES (%s, %s, 0, NOW(), NOW())""" % (shake['user_id'], shake['id']) 18 | -------------------------------------------------------------------------------- /test/functional/error_tests.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import test.base 4 | import test.factories 5 | from models import User, Sourcefile, Sharedfile, Shakesharedfile, Bookmark 6 | 7 | class ErrorTests(test.base.BaseAsyncTestCase): 8 | def setUp(self): 9 | super(ErrorTests, self).setUp() 10 | self.admin = User(name='admin', email='admin@mltshp.com', email_confirmed=1, is_paid=1) 11 | self.admin.set_password('asdfasdf') 12 | self.admin.save() 13 | self.sign_in('admin', 'asdfasdf') 14 | 15 | def test_custom_404(self): 16 | """ 17 | Request an invalid route. 18 | - Should error! 19 | """ 20 | response = self.fetch_url('/lakjsdlfka') 21 | self.assertEqual(404, response.code) 22 | self.assertIn("404 - Uh oh!", response.body) 23 | -------------------------------------------------------------------------------- /templates/uimodules/pagination.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/account/return-subscription-completed.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Thank You!{% end %} 3 | {% block main %} 4 |
5 |

Thank You!

6 |

7 | We really greatly appreciate your support. Having a MLTSHP membership enables us to 8 | create new features and continually improve the site. We hope you continue to enjoy 9 | MLTSHP as much as we do. 10 |

11 | {% if has_data_to_migrate and not site_is_readonly %} 12 |

13 | You can now restore your MLKSHK content to MLTSHP! 14 |

15 | {% else %} 16 |

17 | Now get out there and share some stuff! 18 |

19 | {% end %} 20 |
21 | {% end %} 22 | -------------------------------------------------------------------------------- /setup/db-fixtures.sql: -------------------------------------------------------------------------------- 1 | -- create some basic users and user shakes for our "Best of" posts to collect 2 | -- hashed passwords are generated with sha1('password' + settings.auth_secret) 3 | -- auth_secret for local testing: dummy-secret 4 | INSERT INTO `user` (`name`, `disable_notifications`, `created_at`) VALUES ('mltshp', 1, now()); 5 | -- password for "admin" is "password" if you're using the local testing auth_secret key 6 | INSERT INTO `user` (`name`, `hashed_password`, `email`, `full_name`, `email_confirmed`, `is_paid`, `stripe_plan_id`, `created_at`) 7 | VALUES ('admin', '9bbdccf408a2420e20fcd157c6315d5f77427c64', 'admin@example.com', 'Site Admin', 1, 1, 'mltshp-double', now()); 8 | 9 | INSERT INTO `shake` (`user_id`, `type`, `name`) VALUES (1, 'user', 'Best of MLTSHP'); 10 | INSERT INTO `shake` (`user_id`, `type`) VALUES (2, 'user'); 11 | -------------------------------------------------------------------------------- /test/functional/tools_find_people_tests.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from tornado.options import options 4 | 5 | import test.base 6 | import models 7 | 8 | 9 | class ToolsFindPeopleTests(test.base.BaseAsyncTestCase): 10 | 11 | def setUp(self): 12 | super(ToolsFindPeopleTests, self).setUp() 13 | self.user = models.User(name='admin', email='admin@mltshp.com', email_confirmed=1, is_paid=1) 14 | self.user.set_password('pass') 15 | self.user.save() 16 | 17 | def test_tools_find_shakes(self): 18 | """ 19 | /tools/find-people should be accessible. 20 | """ 21 | response = self.fetch('/tools/find-shakes') 22 | self.assertEqual(200, response.code) 23 | response = self.fetch('/tools/find-shakes/people') 24 | self.assertEqual(200, response.code) 25 | -------------------------------------------------------------------------------- /templates/account/return-subscription-error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Uh oh! We had a problem.{% end %} 3 | {% block main %} 4 |
5 |

We had a problem.

6 |

7 | We weren't able to process your payment. 8 | {% if error_message %} 9 | Our credit card processor returned this message: 10 | {% end %} 11 |

12 | {% if error_message %} 13 |
14 |

15 | {{ error_message }} 16 |

17 |
18 | {% end %} 19 |

20 | You may want to try again with a different credit card. 21 | You can also contact us if you need any help. 22 |

23 |
24 | {% end %} 25 | -------------------------------------------------------------------------------- /templates/uimodules/notification-invitation-approved.html: -------------------------------------------------------------------------------- 1 | {% if len(notifications) > 0 %} 2 |
3 |
{{len(notifications)}} new shake membership{% if len(notifications) != 1%}s{% end %}
4 |
5 | {% for notification in notifications %} 6 |
7 | You are now a member of {{notification['related_object'].display_name()}}. 8 |
9 | {% end %} 10 |
11 | clear all 12 |
13 |
14 |
15 | {% end %} 16 | -------------------------------------------------------------------------------- /static/images/bookmarklet.svg: -------------------------------------------------------------------------------- 1 | bookmarklet -------------------------------------------------------------------------------- /templates/account/forgot-password-sent.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Check Your Email{% end %} 4 | 5 | {% block main %} 6 |
7 |

We Sent You Instructions!

8 |

We just sent your email address instructions on how to reset your password. If you don't get it within the next 5 minutes 9 | contact us at hello@mltshp.com and we'll figure out what's going on. 10 |

11 |
12 |

13 | Incidentally, if you seem to be forgetting your passwords a lot, you might be interested in knowing there are password managers 14 | that store your passwords for you. Here are a few we like: 15 |

16 | {% include "account/_password-managers.html"%} 17 |
18 |
19 | {% end %} 20 | -------------------------------------------------------------------------------- /developerdocs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = ../templates/developers 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | all: justhtml 18 | 19 | clean: 20 | @echo "Oops, dunno which templates to delete so not cleaning anything :( " 21 | 22 | justhtml: 23 | $(SPHINXBUILD) -b justhtml $(ALLSPHINXOPTS) $(BUILDDIR) 24 | @echo 25 | @echo "Build finished. The HTML pages are in $(BUILDDIR)." 26 | -------------------------------------------------------------------------------- /scripts/check-payment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import json 5 | import models 6 | from settings import settings 7 | """ 8 | Check if a user's subscription is current. 9 | """ 10 | 11 | NAME = "check-payment" 12 | 13 | def main(): 14 | users = models.User.all() 15 | for user in users: 16 | last_payments = models.PaymentLog.last_payments(count=1, user_id=user.id) 17 | if last_payments: 18 | lp = last_payments[0] 19 | ### TODO: this has not been converted to Stripe yet 20 | #details = b.get_subscription_details(SubscriptionId=lp.subscription_id) 21 | #if details.GetSubscriptionDetailsResult.SubscriptionStatus == 'Cancelled': 22 | # user.is_paid = 0 23 | # user.save() 24 | 25 | return json.dumps({'status':'finished', 'message':'Processed subscriptions.'}) 26 | -------------------------------------------------------------------------------- /developerdocs/themes/developerdocs/layout.html: -------------------------------------------------------------------------------- 1 | 7 | {%- if url_root == '#' %}{% set url_root = '' %}{% endif %} 8 | 9 | {{ '{%' }} extends "base.html" %} 10 | 11 | {{ '{%' }} block title %}{{ title|striptags|e }}{{ '{%' }} end %} 12 | 13 | {{ '{%' }} block included_headers %} 14 | 15 | {{ '{%' }} end %} 16 | 17 | {% block content -%} 18 | {{ '{%' }} block main %} 19 |
20 |
21 | 22 |
23 | {{ toc }} 24 |
25 | 26 | {% block body %}{% endblock %} 27 | 28 |
29 |
30 | {{ '{%' }} end %} 31 | {%- endblock %} 32 | -------------------------------------------------------------------------------- /test/unit/script_log_tests.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from models import ScriptLog 4 | from .base import BaseTestCase 5 | import test.factories 6 | 7 | class ScriptLogTests(BaseTestCase): 8 | 9 | def test_get_last_successful(self): 10 | """ 11 | ScriptLog.get_last_successfull() should return the latest entry that 12 | matches passed in name with a success state of 1. 13 | """ 14 | script_log = ScriptLog(name='test-script', success=0) 15 | script_log.save() 16 | self.assertEqual(None, ScriptLog.last_successful('test-script')) 17 | 18 | script_log2 = ScriptLog(name='test-script', success=1) 19 | script_log2.save() 20 | script_log3 = ScriptLog(name='test-script', success=1) 21 | script_log3.save() 22 | self.assertEqual(script_log3.id, ScriptLog.last_successful('test-script').id) 23 | -------------------------------------------------------------------------------- /scripts/populate-timelines.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from torndb import Connection 4 | from tornado.options import options 5 | from tasks.timeline import add_posts 6 | 7 | 8 | def main(): 9 | db1 = Connection(options.database_host, options.database_user, options.database_password) 10 | 11 | db1.execute("DELETE FROM post WHERE 1") 12 | ssfs = db1.query("""SELECT shake_id, sharedfile_id from shakesharedfile order by created_at""") 13 | for shakesharedfile in ssfs: 14 | sf = db1.get("""SELECT id, source_id, name, deleted, created_at FROM sharedfile WHERE id = %s""", shakesharedfile['sharedfile_id']) 15 | print("%s. Adding posts for sharedfile: %s created at %s." % (sf.id, sf.name, sf.created_at)) 16 | add_posts(shake_id=shakesharedfile['shake_id'], sharedfile_id=sf['id'], sourcefile_id=sf['source_id'], deleted=sf['deleted'], created_at=sf['created_at']) 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Pipeline 2 | 3 | env: 4 | build_number: ${{ github.run_id }} 5 | 6 | on: 7 | push: 8 | branches-ignore: 9 | - master 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Build and start services with Docker Compose 18 | run: docker compose -f release/docker-compose.yml up -d --build 19 | 20 | - name: Wait for services to be ready 21 | run: sleep 10 22 | 23 | - name: Run tests 24 | run: docker exec -t release-mltshp-1 ./run-tests.sh 25 | 26 | - name: Stop services and clean up 27 | run: | 28 | docker compose -f release/docker-compose.yml down 29 | docker container prune -f 30 | -------------------------------------------------------------------------------- /models/apilog.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.utilities import utcnow 3 | from tornado.options import options 4 | 5 | 6 | class Apilog(Model): 7 | accesstoken_id = Property() 8 | nonce = Property() 9 | created_at = Property() 10 | 11 | def save(self, *args, **kwargs): 12 | if options.readonly: 13 | self.add_error('_', 'Site is read-only.') 14 | return False 15 | 16 | self._set_dates() 17 | return super(Apilog, self).save(*args, **kwargs) 18 | 19 | def _set_dates(self): 20 | """ 21 | Sets the created_at and updated_at fields. This should be something 22 | a subclass of Property that takes care of this during the save cycle. 23 | """ 24 | if self.id is None or self.created_at is None: 25 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 26 | -------------------------------------------------------------------------------- /templates/account/followers.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{user_info.name}}’s followers 5 | {% end %} 6 | 7 | 8 | {% block main %} 9 |
10 | 15 |
16 | {% for follower in followers %} 17 |
18 | {{modules.ShakeFollow(follow_user=follower, current_user=current_user_object, avatar_size=50, extended=True)}} 19 |
20 | {% end %} 21 |
22 |
23 | {{modules.Pagination(object_count=follower_count, current_page=page, url_format=url_format,per_page=20)}} 24 |
25 |
26 |
27 | {% end %} -------------------------------------------------------------------------------- /templates/shakes/followers.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{shake_info.name}}’s followers 5 | {% end %} 6 | 7 | 8 | {% block main %} 9 |
10 | 15 |
16 | {% for follower in followers %} 17 |
18 | {{modules.ShakeFollow(follow_user=follower, current_user=current_user_object, avatar_size=50, extended=True)}} 19 |
20 | {% end %} 21 |
22 |
23 | {{modules.Pagination(object_count=follower_count, current_page=page, url_format=url_format,per_page=20)}} 24 |
25 |
26 |
27 | {% end %} -------------------------------------------------------------------------------- /templates/developers/guide.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | {% extends "base.html" %} 9 | 10 | {% block title %}Guide to the API{% end %} 11 | 12 | {% block included_headers %} 13 | 14 | {% end %} 15 | 16 | {% block main %} 17 |
18 |
19 | 20 |
21 | 24 | 25 |
26 | 27 | 28 |
29 |

Guide to the API

30 |
31 | 32 | 33 | 34 |
35 |
36 | {% end %} 37 | -------------------------------------------------------------------------------- /static/images/save-tiny.svg: -------------------------------------------------------------------------------- 1 | reallysmall-save -------------------------------------------------------------------------------- /templates/account/confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Welcome to MLTSHP and thanks!{% end %} 4 | 5 | {% block main %} 6 | 7 |
8 |

Hello, {{current_user['name']}}!

9 | 10 | {% if promotion %} 11 |

Thanks for joining MLTSHP through {{ escape(promotion.name) }}!

12 | {% end %} 13 | 14 |

15 | Please check your email account, we need to verify that you gave us the right one! 16 |

17 | 18 | {% if promotion and promotion_shake %} 19 |

20 | Get started by following the {{ escape(promotion_shake.title) }} shake.

21 |
22 | 23 |
24 | {{modules.ShakeFollow(follow_shake=promotion_shake, current_user=current_user_obj, avatar_size=100)}} 25 |
26 | 27 |
28 | {% end %} 29 |
30 | {% end %} 31 | -------------------------------------------------------------------------------- /models/waitlist.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.utilities import utcnow 3 | from tornado.options import options 4 | 5 | 6 | class Waitlist(Model): 7 | email = Property() 8 | verification_key = Property() 9 | verified = Property() 10 | invited = Property() 11 | created_at = Property() 12 | 13 | def save(self, *args, **kwargs): 14 | if options.readonly: 15 | self.add_error('_', 'Site is read-only.') 16 | return False 17 | 18 | self._set_dates() 19 | return super(Waitlist, self).save(*args, **kwargs) 20 | 21 | def _set_dates(self): 22 | """ 23 | Sets the created_at and updated_at fields. This should be something 24 | a subclass of Property that takes care of this during the save cycle. 25 | """ 26 | if self.id is None or self.created_at is None: 27 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 28 | -------------------------------------------------------------------------------- /migrations/20250127_01_nF5uM-dmca-takedown-table.py: -------------------------------------------------------------------------------- 1 | """ 2 | Creates the dmca_takedown table that is used to log DMCA takedown 3 | actions. This table already exists in production, so is created conditionally, 4 | but this adds a new column as well to support identifying the user who did 5 | the takedown. 6 | """ 7 | 8 | from yoyo import step 9 | 10 | __depends__ = {"20230627_01_J71Z3-add-sharefile-alt-text"} 11 | 12 | steps = [ 13 | step("""CREATE TABLE IF NOT EXISTS dmca_takedown 14 | (id int unsigned not null auto_increment primary key, 15 | share_key varchar(40) not null, 16 | source_id int unsigned not null, 17 | comment text, 18 | created_at datetime default null, 19 | updated_at datetime default null, 20 | processed tinyint default 0);"""), 21 | step("ALTER TABLE dmca_takedown ADD COLUMN admin_user_id int unsigned default 0;"), 22 | ] 23 | -------------------------------------------------------------------------------- /setup/db-truncate.sql: -------------------------------------------------------------------------------- 1 | TRUNCATE `sharedfile`; 2 | TRUNCATE `sourcefile`; 3 | TRUNCATE `user`; 4 | TRUNCATE `invitation`; 5 | TRUNCATE `fileview`; 6 | TRUNCATE `externalservice`; 7 | TRUNCATE `favorite`; 8 | TRUNCATE `shake`; 9 | TRUNCATE `subscription`; 10 | TRUNCATE `shakesharedfile`; 11 | TRUNCATE `waitlist`; 12 | TRUNCATE `comment`; 13 | TRUNCATE `post`; 14 | TRUNCATE `notification`; 15 | TRUNCATE `app`; 16 | TRUNCATE `authorizationcode`; 17 | TRUNCATE `accesstoken`; 18 | TRUNCATE `apilog`; 19 | TRUNCATE `conversation`; 20 | TRUNCATE `external_relationship`; 21 | TRUNCATE `shake_manager`; 22 | TRUNCATE `payment_log`; 23 | TRUNCATE `bookmark`; 24 | TRUNCATE `invitation_request`; 25 | TRUNCATE `apihit`; 26 | TRUNCATE `nsfw_log`; 27 | TRUNCATE `magicfile`; 28 | TRUNCATE `script_log`; 29 | TRUNCATE `shake_category`; 30 | TRUNCATE `comment_like`; 31 | TRUNCATE `tag`; 32 | TRUNCATE `tagged_file`; 33 | TRUNCATE `voucher`; 34 | TRUNCATE `promotion`; 35 | TRUNCATE `dmca_takedown`; 36 | -------------------------------------------------------------------------------- /templates/account/_password-managers.html: -------------------------------------------------------------------------------- 1 |

2 | 1Password is a native Macintosh application that is also available for the iPad and iPhone. We use this one. 3 |

4 |

5 | Lastpass is not only very popular, but it works on Mac or Windows as well as many mobile phones and devices. 6 |

7 |

8 | Keepass is an open source password manager that also runs on a variety of platforms and devices. 9 |

10 | -------------------------------------------------------------------------------- /templates/incoming/closed-for-repairs.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html" %} 2 | 3 | {% block main %} 4 |
5 |
6 |

Our most recent files

7 |
8 | 9 | 12 |
13 |

Incoming Is Closed For Repairs

14 |

15 | I’m hunting down an error with the server, and the tens to hundreds of requests coming 16 | into incoming through automated robots is negatively affecting performance. 17 |

18 |

19 | I am shutting down incoming temporarily until I can figure out a way to keep the automated 20 | bots hitting the server so much. 21 |

22 |

23 | If you are a bot writer, please consider using the API to make your requests. 24 |

25 |
26 |
27 | {% end %} 28 | -------------------------------------------------------------------------------- /templates/tools/save-video.html: -------------------------------------------------------------------------------- 1 |
2 | {% if url %} 3 |

Is this the video?

4 | {{html}} 5 |
6 | {{ xsrf_form_html() }} 7 | 8 | 9 | Yes! Post it please! 10 |
11 | {% else %} 12 |

Post a Video

13 |

To post a video enter the URL to the video below. We currently support videos from YouTube, Flickr, & Vimeo.

14 |
15 | 16 |
17 | Go get it! 18 |
19 | {% end %} 20 |
21 | -------------------------------------------------------------------------------- /templates/misc/promo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 22 | 23 | 24 | 30 | 31 |

{{banner['text']}}

32 | 33 | 34 | -------------------------------------------------------------------------------- /models/script_log.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.utilities import utcnow 3 | from tornado.options import options 4 | 5 | class ScriptLog(Model): 6 | name = Property(default='') 7 | result = Property(default='') 8 | success = Property(default=0) 9 | started_at = Property() 10 | finished_at = Property() 11 | 12 | def start_running(self): 13 | if options.readonly: 14 | self.add_error('_', 'Site is read-only.') 15 | return False 16 | 17 | self.started_at = utcnow() 18 | 19 | def save(self, *args, **kwargs): 20 | self._set_dates() 21 | return super(ScriptLog, self).save(*args, **kwargs) 22 | 23 | def _set_dates(self): 24 | if self.id is None or self.created_at is None: 25 | self.finished_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 26 | 27 | @classmethod 28 | def last_successful(cls, name): 29 | return cls.get("name = %s and success = 1 order by id desc limit 1", name) 30 | -------------------------------------------------------------------------------- /templates/tags/tag.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Files Tagged With “{{tag.name}}” 5 | {% end %} 6 | 7 | {% block main %} 8 | 9 |
10 | 11 | {% include "tags/_sidebar.html" %} 12 | 13 |
14 |
15 | {% for sharedfile in shared_files %} 16 | {{modules.Image(sharedfile, current_user=current_user_obj, list_view=True, show_attribution_in_title=True)}} 17 | {% end %} 18 |
19 |
20 |
21 | {% if older_link %} 22 | « Older 23 | {% end %} 24 |
25 |
26 | {% if newer_link %} 27 | Newer » 28 | {% end %} 29 |
30 |
31 | 32 |
33 |
34 | {% end %} 35 | -------------------------------------------------------------------------------- /models/post.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.utilities import utcnow 3 | 4 | from tornado.options import options 5 | 6 | 7 | class Post(Model): 8 | user_id = Property() 9 | sourcefile_id = Property() 10 | sharedfile_id = Property() 11 | shake_id = Property(default=0) 12 | seen = Property(default=0) 13 | deleted = Property(default=0) 14 | created_at = Property() 15 | 16 | def save(self, *args, **kwargs): 17 | if options.readonly: 18 | self.add_error('_', 'Site is read-only.') 19 | return False 20 | 21 | self._set_dates() 22 | return super(Post, self).save(*args, **kwargs) 23 | 24 | def _set_dates(self): 25 | """ 26 | Sets the created_at and updated_at fields. This should be something 27 | a subclass of Property that takes care of this during the save cycle. 28 | """ 29 | if self.id is None or self.created_at is None: 30 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 31 | -------------------------------------------------------------------------------- /models/shakesharedfile.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.utilities import utcnow 3 | from tornado.options import options 4 | 5 | 6 | class Shakesharedfile(Model): 7 | shake_id = Property() 8 | sharedfile_id = Property() 9 | deleted = Property(default=0) 10 | created_at = Property() 11 | 12 | def save(self, *args, **kwargs): 13 | if options.readonly: 14 | self.add_error('_', 'Site is read-only.') 15 | return False 16 | 17 | self._set_dates() 18 | return super(Shakesharedfile, self).save(*args, **kwargs) 19 | 20 | def _set_dates(self): 21 | """ 22 | Sets the created_at and updated_at fields. This should be something 23 | a subclass of Property that takes care of this during the save cycle. 24 | """ 25 | if self.id is None or self.created_at is None: 26 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 27 | 28 | def delete(self): 29 | self.deleted =1 30 | self.save() 31 | -------------------------------------------------------------------------------- /templates/admin/create-users.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html" %} 2 | 3 | {% block title %}Create Users{% end %} 4 | 5 | {% block main %} 6 |
7 | {% include "admin/_sidebar.html" %} 8 | 9 |
10 |

Create Users

11 |
12 | {{ xsrf_form_html() }} 13 |
14 | 15 |
16 | 17 |
18 |
19 | one per line 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 | {% end %} 29 | -------------------------------------------------------------------------------- /static/opensearch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | mltshp 4 | MLTSHP! Share and find interesting photos, images, animated gifs, icons, and memes from the web. 5 | UTF-8 6 | en-us 7 | mltshp photos images gifs memes 8 | 9 | https://mltshp-cdn.com/favicon.ico 10 | https://mltshp-cdn.com/static/images/favicon-32x32.png 11 | 12 | 13 | https://mltshp.com/search 14 | 15 | -------------------------------------------------------------------------------- /models/fileview.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from tornado.options import options 3 | 4 | 5 | class Fileview(Model): 6 | user_id = Property() 7 | sharedfile_id = Property() 8 | created_at = Property() 9 | 10 | def save(self, *args, **kwargs): 11 | if options.readonly: 12 | self.add_error('_', 'Site is read-only.') 13 | return False 14 | 15 | return super(Fileview, self).save(*args, **kwargs) 16 | 17 | @classmethod 18 | def last(cls): 19 | return cls.get("1 order by id desc limit 1") 20 | 21 | @classmethod 22 | def sharedfile_ids(cls, before_id=None): 23 | """ 24 | Return sharedfile_ids that have had views logged to them. 25 | 26 | Limit results to fileview's < passed in before_id. 27 | """ 28 | sql = "select distinct sharedfile_id from fileview " 29 | if before_id: 30 | sql = sql + " where id < %s" % int(before_id) 31 | return [result['sharedfile_id'] for result in cls.query(sql)] 32 | 33 | 34 | -------------------------------------------------------------------------------- /static/images/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/developers/docindex.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | {% extends "base.html" %} 9 | 10 | {% block title %}<no title>{% end %} 11 | 12 | {% block included_headers %} 13 | 14 | {% end %} 15 | 16 | {% block main %} 17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 | 33 |
34 | 35 | 36 | 37 |
38 |
39 | {% end %} 40 | -------------------------------------------------------------------------------- /lib/flyingcow/db.py: -------------------------------------------------------------------------------- 1 | import torndb 2 | 3 | class NoConnectionRegistered(Exception): 4 | """ 5 | Raised when a connection is attempted to be used without 6 | having one registered to begin with. 7 | """ 8 | 9 | 10 | class ConnectionManager(object): 11 | """ 12 | Helps manage a connection between modules. 13 | """ 14 | def __init__(self): 15 | self._connection = None 16 | 17 | def register(self, host='localhost', name=None, user=None, password=None, charset="utf8mb4"): 18 | self._connection = self._connection or torndb.Connection(host, name, user, password, charset=charset) 19 | return self._connection 20 | 21 | def connection(self): 22 | if not self._connection: 23 | raise NoConnectionRegistered 24 | return self._connection 25 | 26 | IntegrityError = torndb.IntegrityError 27 | OperationalError = torndb.OperationalError 28 | 29 | _connection_manager = ConnectionManager() 30 | register_connection = _connection_manager.register 31 | connection = _connection_manager.connection 32 | -------------------------------------------------------------------------------- /templates/uimodules/shake-dropdown.html: -------------------------------------------------------------------------------- 1 |
2 | 6 |
7 | 14 | {% if group_shakes %} 15 | 20 | {% end %} 21 | {% if can_create_shake and not site_is_readonly %} 22 | + Add a shake 23 | {% end %} 24 |
25 |
26 | -------------------------------------------------------------------------------- /models/invitation_request.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.utilities import utcnow 3 | from tornado.options import options 4 | 5 | 6 | class InvitationRequest(Model): 7 | user_id = Property() 8 | manager_id = Property() 9 | shake_id = Property() 10 | deleted = Property(default=0) 11 | created_at = Property() 12 | updated_at = Property() 13 | 14 | def save(self, *args, **kwargs): 15 | if options.readonly: 16 | self.add_error('_', 'Site is read-only.') 17 | return False 18 | 19 | self._set_dates() 20 | return super(InvitationRequest, self).save(*args, **kwargs) 21 | 22 | def _set_dates(self): 23 | """ 24 | Sets the created_at and updated_at fields. This should be something 25 | a subclass of Property that takes care of this during the save cycle. 26 | """ 27 | if self.id is None or self.created_at is None: 28 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 29 | self.updated_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 30 | -------------------------------------------------------------------------------- /static/images/save-this.svg: -------------------------------------------------------------------------------- 1 | save-this -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: init start stop custom-build build staging shell test destroy migrate mysql 2 | 3 | init: 4 | cp settings.example.py settings.py 5 | cp celeryconfig.example.py celeryconfig.py 6 | mkdir -p mounts/mysql mounts/logs mounts/fakes3 mounts/uploaded 7 | 8 | start: 9 | docker compose up -d 10 | 11 | stop: 12 | docker compose down 13 | 14 | custom-build: 15 | @read -p "build tag (default is 'latest'): " build_tag; \ 16 | docker build -t mltshp/mltshp-web:$${build_tag:-latest} 17 | 18 | build: 19 | docker build -t mltshp/mltshp-web:latest . 20 | 21 | staging: 22 | docker build --platform linux/amd64 -t mltshp/mltshp-web:staging . 23 | 24 | shell: 25 | docker compose exec mltshp bash 26 | 27 | test: 28 | docker compose exec mltshp su mltshp -c "cd /srv/mltshp.com/mltshp; python3 -u test.py $(TEST)" 29 | 30 | destroy: 31 | docker compose down && rm -rf mounts 32 | 33 | migrate: 34 | docker compose exec mltshp su mltshp -c "cd /srv/mltshp.com/mltshp; python3 migrate.py" 35 | 36 | mysql: 37 | docker compose exec mltshp su mltshp -c "cd /srv/mltshp.com/mltshp; mysql -u root --host mysql mltshp" 38 | -------------------------------------------------------------------------------- /templates/uimodules/image-medium.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% set sourcefile = sharedfile.sourcefile() %} 4 | 5 |
6 |
7 | {% if sharedfile_user %} 8 | 9 | {{escape(sharedfile_user.display_name())}} 10 | 11 | {% end %} 12 |
{{escape(sharedfile.get_title())}}
13 |
14 | 20 | 21 |
22 | -------------------------------------------------------------------------------- /lib/flyingcow/properties.py: -------------------------------------------------------------------------------- 1 | class Property(object): 2 | """ 3 | A way to denote a database property inside a model. Raw value of property is 4 | stored in the instance of the Model w/ the property name prefixed with _. 5 | """ 6 | def __init__(self, name=None, default=None): 7 | self.name = name 8 | self.default = default 9 | 10 | def __get__(self, model_instance, type): 11 | if model_instance is None: 12 | return self 13 | try: 14 | return getattr(model_instance, self._raw_value_name()) 15 | except AttributeError: 16 | return self.default 17 | 18 | def __set__(self, model_instance, value): 19 | setattr(model_instance, self._raw_value_name(), value) 20 | 21 | def _raw_value_name(self): 22 | return '_' + self.name 23 | 24 | def contribute_to_class(self, cls, name): 25 | """ 26 | We use this hook when we're building the Model class to 27 | pass in the name of the attribute this Property is attached to. 28 | """ 29 | if not self.name: 30 | self.name = name -------------------------------------------------------------------------------- /setup/production/supervisord-worker.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:cron] 5 | command=/usr/sbin/cron -f 6 | autorestart=true 7 | 8 | [group:mltshp] 9 | programs=celeryd-celery,celeryd-transcode 10 | 11 | [program:celeryd-celery] 12 | command=celery worker -n celery-worker%%h -Q celery --loglevel INFO 13 | directory=/srv/mltshp.com/mltshp 14 | autorestart=true 15 | redirect_stderr=true 16 | stdout_logfile=/srv/mltshp.com/logs/celeryd-celery.log 17 | stdout_logfile_maxbytes=500MB 18 | stdout_logfile_backups=10 19 | stdout_capture_maxbytes=1MB 20 | stdout_events_enabled=false 21 | loglevel=info 22 | user=mltshp 23 | environment=HOME="/home/mltshp",USER="mltshp" 24 | 25 | [program:celeryd-transcode] 26 | command=celery worker -n celery-transcode%%h -Q transcode --loglevel INFO 27 | directory=/srv/mltshp.com/mltshp 28 | autorestart=true 29 | redirect_stderr=true 30 | stdout_logfile=/srv/mltshp.com/logs/celeryd-transcode.log 31 | stdout_logfile_maxbytes=500MB 32 | stdout_logfile_backups=10 33 | stdout_capture_maxbytes=1MB 34 | stdout_events_enabled=false 35 | loglevel=info 36 | user=mltshp 37 | environment=HOME="/home/mltshp",USER="mltshp" 38 | -------------------------------------------------------------------------------- /tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from celery import shared_task, Task 2 | from tornado.options import define, options 3 | 4 | import mltshpoptions 5 | 6 | 7 | class MltshpTask(Task): 8 | 9 | def delay_or_run(self, *args, **kwargs): 10 | if not options.use_workers: 11 | self(*args, **kwargs) 12 | return 13 | try: 14 | asyncresult = self.delay(*args, **kwargs) 15 | except Exception: 16 | self(*args, **kwargs) 17 | 18 | #if options.postmark_api_key and not options.debug_workers: 19 | # pm = postmark.PMMail(api_key=options.postmark_api_key, 20 | # sender="hello@mltshp.com", to="alerts@mltshp.com", 21 | # subject="ALERT!!! RABBITMQ IS DOWN", 22 | # text_body="WTF.") 23 | # pm.send() 24 | else: 25 | if options.debug_workers: 26 | asyncresult.get() 27 | 28 | 29 | def mltshp_task(*args, **options): 30 | # This is how celery's periodic_task decorator customizes the class, so try it here too. 31 | return shared_task(**dict({"base": MltshpTask}, **options)) 32 | -------------------------------------------------------------------------------- /templates/upload/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Oops.{% end %} 3 | {% block main %} 4 |
5 | {% if error_type == 'content_type' %} 6 |

We don't support that file type.

7 |

8 | Right now we only support uploading GIF, JPEG & PNG images. 9 |

10 | {% elif error_type == 'upload_limit' %} 11 |

Single Scoop Account Limit.

12 |

13 | You've reached the limit on the number of megs you can upload in a month. That limit is 300 megabytes. If you'd like to upgrade to a Double Scoop account it's just $24/year and you get unlimited uploads. 14 |

15 | {% elif error_type == 'email_unconfirmed' %} 16 |

Your Email Address is Unconfirmed.

17 |

18 | You can't upload until you've confirmed your email address. Go to your account settings if you need the confirmation email re-sent. 19 |

20 | {% end %} 21 |
22 | {% end %} 23 | -------------------------------------------------------------------------------- /templates/tools/base-sign-in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}Sign In{% end %} 7 | 8 | 9 | 10 | 11 | {% block included_headers %}{% end %} 12 | 13 | 17 | 18 | 19 | 20 | 23 | 24 |
25 | {% block main %} 26 | {{content}} 27 | {% end %} 28 |
29 | 30 | 31 | {% block included_scripts %}{% end %} 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/SimpleTests.py: -------------------------------------------------------------------------------- 1 | from tornado.testing import AsyncHTTPTestCase 2 | import handlers 3 | import time 4 | from models import Sharedfile, Sourcefile, User 5 | from .base import BaseAsyncTestCase 6 | from handlers import base 7 | 8 | 9 | #This test uses the argument passing between self.stop and self.wait 10 | # for a simpler, more synchronous style 11 | class TwoHundredTests(BaseAsyncTestCase): 12 | 13 | def test_sign_in(self): 14 | response = self.fetch('/sign-in/') 15 | self.assertEqual(response.code, 200) 16 | 17 | def test_nonexistant(self): 18 | response = self.fetch('/asdf/asdf') 19 | self.assertEqual(response.code, 404) 20 | 21 | def test_non_signed_in_permalink_view(self): 22 | user = User(name='admin', email='admin@mltshp.com', email_confirmed=1) 23 | user.save() 24 | src = Sourcefile(width=100, height=100, file_key="asdf", thumb_key="asdfqwer") 25 | src.save() 26 | sf = Sharedfile(source_id=src.id, user_id=1, name="some.jpg", title="some", share_key="1", content_type="image/jpg") 27 | sf.save() 28 | 29 | response = self.fetch('/p/1') 30 | self.assertEqual(response.code, 200) 31 | -------------------------------------------------------------------------------- /models/comment_like.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.utilities import pretty_date, utcnow 3 | from . import user, notification 4 | from tornado.options import options 5 | 6 | 7 | class CommentLike(Model): 8 | user_id = Property() 9 | comment_id = Property() 10 | deleted = Property(default=0) 11 | created_at = Property() 12 | updated_at = Property() 13 | 14 | def save(self, *args, **kwargs): 15 | if options.readonly: 16 | self.add_error('_', 'Site is read-only.') 17 | return False 18 | 19 | self._set_dates() 20 | return super(CommentLike, self).save(*args, **kwargs) 21 | 22 | def _set_dates(self): 23 | """ 24 | Sets the created_at and updated_at fields. This should be something 25 | a subclass of Property that takes care of this during the save cycle. 26 | """ 27 | if self.id is None or self.created_at is None: 28 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 29 | 30 | def on_create(self): 31 | u = user.User.get('id = %s', self.user_id) 32 | n = notification.Notification.new_comment_like(self,u) 33 | n.save() 34 | 35 | -------------------------------------------------------------------------------- /test/unit/apihit_tests.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from functools import wraps 3 | 4 | from models import Apihit 5 | from .base import BaseTestCase 6 | 7 | 8 | class ApihitModelTests(BaseTestCase): 9 | 10 | def test_hit_counts(self): 11 | testtime = datetime.datetime(2011, 6, 2, 7, 33, 45) 12 | testtime_eight = datetime.datetime(2011, 6, 2, 8, 0, 1) 13 | 14 | ret = Apihit.hit(7, testtime) 15 | self.assertEqual(ret, 1) 16 | 17 | apih = Apihit.where("accesstoken_id=%s and hour_start=%s", 7, '2011-06-02 07:00:00') 18 | self.assertEqual(len(apih), 1) 19 | self.assertEqual(apih[0].hits, 1) 20 | 21 | ret = Apihit.hit(7, testtime) 22 | 23 | apih = Apihit.where("accesstoken_id=%s and hour_start=%s", 7, '2011-06-02 07:00:00') 24 | self.assertEqual(len(apih), 1) 25 | self.assertEqual(apih[0].hits, 2) 26 | 27 | self.assertEqual(ret, 2) 28 | 29 | ret = Apihit.hit(7, testtime_eight) 30 | self.assertEqual(ret, 1) 31 | 32 | apih = Apihit.where("accesstoken_id=%s and hour_start=%s", 7, '2011-06-02 08:00:00') 33 | self.assertEqual(len(apih), 1) 34 | self.assertEqual(apih[0].hits, 1) 35 | -------------------------------------------------------------------------------- /developerdocs/exts/developerdocs.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | 3 | import docutils.nodes 4 | from sphinx import addnodes 5 | from sphinx.builders.html import StandaloneHTMLBuilder 6 | from sphinx.util import ws_re 7 | from sphinx.util.console import bold 8 | from sphinx.util.docfields import GroupedField 9 | 10 | 11 | class JustHTMLBuilder(StandaloneHTMLBuilder): 12 | 13 | """ 14 | Builds only the pages of a set of HTML docs. 15 | """ 16 | 17 | name = 'justhtml' 18 | copysource = False 19 | 20 | def finish(self): 21 | pass 22 | 23 | 24 | def remove_doctreedir(app, exception): 25 | shutil.rmtree(app.doctreedir) 26 | 27 | 28 | def setup(app): 29 | app.add_builder(JustHTMLBuilder) 30 | app.connect('build-finished', remove_doctreedir) 31 | 32 | def entity_parse_node(env, sig, signode): 33 | signode += docutils.nodes.strong(sig, sig) 34 | return ws_re.sub('', sig) 35 | entity_doc_field_types = [ 36 | GroupedField('contents', label='Contents', names=('contents', 'key')), 37 | ] 38 | app.add_object_type('entity', 'entity', ref_nodeclass=docutils.nodes.emphasis, 39 | parse_node=entity_parse_node, doc_field_types=entity_doc_field_types) 40 | -------------------------------------------------------------------------------- /test/unit/external_relationship_tests.py: -------------------------------------------------------------------------------- 1 | from models import User, ExternalRelationship 2 | from .base import BaseTestCase 3 | 4 | 5 | class ExternalRelationshipTests(BaseTestCase): 6 | 7 | def test_add_relationship(self): 8 | """ 9 | ExternalRelationship.add_relationship should correctly save a new entry. 10 | If a duplicate entry is passed in, no errors are thrown and no new entries are saved. 11 | """ 12 | user = User(name='example',email='example@example.com', email_confirmed=1, is_paid=1) 13 | user.save() 14 | 15 | ExternalRelationship.add_relationship(user, '1000', ExternalRelationship.TWITTER) 16 | all_relationships = ExternalRelationship.all() 17 | self.assertEqual(1, len(all_relationships)) 18 | self.assertEqual(user.id, all_relationships[0].user_id) 19 | self.assertEqual(1000, all_relationships[0].service_id) 20 | self.assertNotEqual(None, all_relationships[0].created_at) 21 | self.assertNotEqual(None, all_relationships[0].updated_at) 22 | 23 | ExternalRelationship.add_relationship(user, '1000', ExternalRelationship.TWITTER) 24 | self.assertEqual(1, len(all_relationships)) 25 | -------------------------------------------------------------------------------- /templates/shakes/shake.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{shake.name}} shake 5 | {% end %} 6 | 7 | {% block included_headers %} 8 | 9 | {% end %} 10 | 11 | {% block main %} 12 | 13 |
14 | 15 | {% include "shakes/_sidebar.html" %} 16 | 17 |
18 |
19 | {% for sharedfile in shared_files %} 20 | {{modules.Image(sharedfile, current_user=current_user_obj, list_view=True, show_attribution_in_title=True, shake=shake)}} 21 | {% end %} 22 |
23 |
24 |
25 | {% if older_link %} 26 | « Older 27 | {% end %} 28 |
29 |
30 | {% if newer_link %} 31 | Newer » 32 | {% end %} 33 |
34 |
35 | 36 |
37 |
38 | {% end %} 39 | -------------------------------------------------------------------------------- /models/promotion.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.flyingcow.cache import ModelQueryCache 3 | from tornado.options import options 4 | 5 | from models import Shake 6 | 7 | 8 | class Promotion(ModelQueryCache, Model): 9 | # Name of promotion 10 | name = Property() 11 | 12 | # Shake this promotion relates to 13 | # (used for a profile pic and link to 14 | # the promotion shake) 15 | promotion_shake_id = Property() 16 | 17 | # Number of Pro membership months this 18 | # promotion is good for 19 | membership_months = Property() 20 | 21 | # a link to a web page about this promotion 22 | promotion_url = Property() 23 | 24 | # date that the promotion expires on 25 | expires_at = Property() 26 | 27 | def save(self, *args, **kwargs): 28 | if options.readonly: 29 | self.add_error('_', 'Site is read-only.') 30 | return False 31 | 32 | return super(Promotion, self).save(*args, **kwargs) 33 | 34 | def shake(self): 35 | return Shake.get('id=%s and deleted=0', self.promotion_shake_id) 36 | 37 | @classmethod 38 | def active(cls): 39 | return cls.where("expires_at > now() order by rand()") 40 | -------------------------------------------------------------------------------- /templates/account/following.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{user_info.name}} is following 5 | {% end %} 6 | 7 | 8 | {% block main %} 9 |
10 | 15 |
16 | {% for friend in following %} 17 | {% if friend['type'] == 'user' %} 18 |
19 | {{modules.ShakeFollow(follow_user=friend['related_object'], current_user=current_user_object, avatar_size=50, extended=True)}} 20 |
21 | {% else %} 22 |
23 | {{modules.ShakeFollow(follow_shake=friend['related_object'], current_user=current_user_object, avatar_size=50, extended=True)}} 24 |
25 | {% end %} 26 | {% end %} 27 | 28 | 29 |
30 |
31 | {{modules.Pagination(object_count=following_count, current_page=page, url_format=url_format,per_page=20)}} 32 |
33 |
34 |
35 | {% end %} -------------------------------------------------------------------------------- /static/images/saved-this.svg: -------------------------------------------------------------------------------- 1 | save-tiny -------------------------------------------------------------------------------- /test/factories.py: -------------------------------------------------------------------------------- 1 | """ 2 | Convenience functions for creating database-persisted mltshp objects to 3 | test with. 4 | """ 5 | import models 6 | import lib.utilities 7 | 8 | 9 | def sharedfile(user, **kwargs): 10 | """ 11 | Returns a sharedfile with a unique source file for the user. 12 | """ 13 | sourcefile = models.Sourcefile(width=20,height=20,file_key="asdf",thumb_key="asdf_t") 14 | sourcefile.save() 15 | defaults = { 16 | 'source_id' : sourcefile.id, 17 | 'user_id' : user.id, 18 | 'name' : "the name", 19 | 'content_type' : "image/png", 20 | 'description' : "description", 21 | 'source_url' : 'http://www.mltshp.com/?hi', 22 | } 23 | sharedfile = models.Sharedfile(**dict(defaults, **kwargs)) 24 | sharedfile.save() 25 | sharedfile.share_key = lib.utilities.base36encode(sharedfile.id) 26 | sharedfile.save() 27 | return sharedfile 28 | 29 | def user(**kwargs): 30 | defaults = { 31 | 'name' : 'admin', 32 | 'email' : 'admin@mltshp.com', 33 | 'email_confirmed' : 1, 34 | 'is_paid': 1, 35 | } 36 | user = models.User(**dict(defaults, **kwargs)) 37 | user.set_password('password') 38 | user.save() 39 | return user 40 | -------------------------------------------------------------------------------- /templates/search/_sidebar.html: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /static/images/home-piggy.svg: -------------------------------------------------------------------------------- 1 | home-piggy -------------------------------------------------------------------------------- /templates/api/authorize.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Approve App{% end %} 4 | 5 | {% block main %} 6 | 7 |
8 |

Allow Access?

9 |

10 | Do you want to let {{app.title}} read and write to your account? 11 |

12 |
13 | {{ xsrf_form_html() }} 14 | 15 | {% if redirect_url %} 16 | 17 | {% end %} 18 | 19 |
20 |
21 | {{ xsrf_form_html() }} 22 | 23 | {% if redirect_url %} 24 | 25 | {% end %} 26 | 27 |
28 |
29 |
30 | {% end %} 31 | -------------------------------------------------------------------------------- /test/unit/base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from lib.flyingcow import register_connection 3 | from tornado.options import options 4 | import string 5 | import random 6 | import re 7 | 8 | import logging 9 | 10 | logger = logging.getLogger('mltshp.test') 11 | logger.setLevel(logging.INFO) 12 | 13 | class BaseTestCase(unittest.TestCase): 14 | def __init__(self, *args, **kwargs): 15 | super(BaseTestCase, self).__init__(*args, **kwargs) 16 | self.db = register_connection( 17 | host=options.database_host, 18 | name="mysql", 19 | user=options.database_user, 20 | password=options.database_password, 21 | charset="utf8mb4") 22 | 23 | def setUp(self): 24 | super(BaseTestCase, self).setUp() 25 | if options.database_name != "mltshp_testing": 26 | raise Exception("Invalid database name for unit tests") 27 | self.reset_database() 28 | 29 | def reset_database(self): 30 | self.db.execute("USE %s" % (options.database_name)) 31 | with open("setup/db-truncate.sql") as f: 32 | query = f.read() 33 | self.db.execute(query) 34 | 35 | def generate_string_of_len(self, length): 36 | return ''.join(random.choice(string.ascii_letters) for i in range(length)) 37 | 38 | -------------------------------------------------------------------------------- /templates/tools/picker-success.html: -------------------------------------------------------------------------------- 1 | {% extends 'tools/base.html' %} 2 | 3 | {% block main %} 4 |
5 |

{{escape(sf.get_title())}} has been saved.

6 | 7 |
8 |
9 | 10 |
11 | 13 |
14 |
15 |
16 | 17 |
18 | 20 |
21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 | {% end %} 29 | -------------------------------------------------------------------------------- /models/favorite.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.utilities import pretty_date, utcnow 3 | from . import user 4 | from tornado.options import options 5 | 6 | 7 | class Favorite(Model): 8 | user_id = Property() 9 | sharedfile_id = Property() 10 | deleted = Property(default=0) 11 | created_at = Property() 12 | updated_at = Property() 13 | 14 | def user(self): 15 | return user.User.get('id = %s', self.user_id) 16 | 17 | def save(self, *args, **kwargs): 18 | if options.readonly: 19 | self.add_error('_', 'Site is read-only.') 20 | return False 21 | 22 | self._set_dates() 23 | return super(Favorite, self).save(*args, **kwargs) 24 | 25 | def _set_dates(self): 26 | """ 27 | Sets the created_at and updated_at fields. This should be something 28 | a subclass of Property that takes care of this during the save cycle. 29 | """ 30 | if self.id is None or self.created_at is None: 31 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 32 | self.updated_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 33 | 34 | def pretty_created_at(self): 35 | """ 36 | A friendly version of the created_at date. 37 | """ 38 | return pretty_date(self.created_at) 39 | -------------------------------------------------------------------------------- /templates/account/announcement-tou.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Terms of Use Notice{% end %} 4 | 5 | {% block main %} 6 |
7 |
8 |

9 | Hi everyone and welcome to MLTSHP! We have an updated Terms of Use / Privacy Policy document everyone 10 | today needs to agree to. Nothing that different from before, but important because Legal Reasons. 11 | We wanted to update it to be more up-front and open about the services we utilize to make the magic 12 | that is MLTSHP. 13 |

14 |

15 | By clicking this button, you agree to the terms listed on this page 16 | and our Code of Conduct. 17 |

18 |
19 | I Agree 20 |
21 |

22 | We just wanted to make a great site and we know we wouldn't be doing it without all of you showing up 23 | every day. So, thank you. Be sure to check 24 | out our release notes! 25 |

26 |
27 |
28 | {% end %} 29 | -------------------------------------------------------------------------------- /developerdocs/README.md: -------------------------------------------------------------------------------- 1 | # Developer Documentation 2 | 3 | ## About 4 | 5 | This directory contains some documentation regarding the MLTSHP API. The documentation 6 | is in REST format and is built using Sphinx. If any updates are made to the API, 7 | corresponding updates should be made to this documentation to keep things in sync. 8 | The documentation is readable through the site, but as static HTML, which has to be 9 | built from this directory. The resulting HTML files need to be checked in along with 10 | the API code changes. 11 | 12 | ## Setting Up 13 | 14 | To build the documentation HTML files, you'll need to do the usual Python virtualenv 15 | steps if you haven't already. Then, with the virtualenv active, install the special 16 | requirements like this: 17 | 18 | $ pip install -r developerdocs/requirements.txt 19 | 20 | (You'd issue this from the top of the MLTSHP repository, with the virtualenv for it 21 | already active.) 22 | 23 | ## Building 24 | 25 | With the setup steps done, building the documentation is just a matter of executing 26 | the `make` command from the directory. 27 | 28 | $ cd developerdocs 29 | $ make 30 | 31 | That should produce the updates to the HTML files (in templates/developers). You 32 | will need to add any updates to those files to your git branch and commit them 33 | along with your API changes. 34 | 35 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | from .favorite import Favorite 2 | from .subscription import Subscription 3 | from .user import User 4 | from .invitation import Invitation 5 | from .sharedfile import Sharedfile 6 | from .sourcefile import Sourcefile 7 | from .externalservice import Externalservice 8 | from .shake import Shake 9 | from .shakesharedfile import Shakesharedfile 10 | from .waitlist import Waitlist 11 | from .comment import Comment 12 | from .post import Post 13 | from .notification import Notification 14 | from .app import App 15 | from .authorizationcode import Authorizationcode 16 | from .accesstoken import Accesstoken 17 | from .apilog import Apilog 18 | from .conversation import Conversation 19 | from .external_relationship import ExternalRelationship 20 | from .shakemanager import ShakeManager 21 | from .payment_log import PaymentLog 22 | from .bookmark import Bookmark 23 | from .apihit import Apihit 24 | from .magicfile import Magicfile 25 | from .nsfw_log import NSFWLog 26 | from .script_log import ScriptLog 27 | from .fileview import Fileview 28 | from .shake_category import ShakeCategory 29 | from .comment_like import CommentLike 30 | from .tag import Tag 31 | from .tagged_file import TaggedFile 32 | from .voucher import Voucher 33 | from .promotion import Promotion 34 | from .migration_state import MigrationState 35 | from .dmca_takedown import DmcaTakedown 36 | -------------------------------------------------------------------------------- /setup/db-drop.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `sharedfile`; 2 | DROP TABLE IF EXISTS `sourcefile`; 3 | DROP TABLE IF EXISTS `user`; 4 | DROP TABLE IF EXISTS `invitation`; 5 | DROP TABLE IF EXISTS `fileview`; 6 | DROP TABLE IF EXISTS `externalservice`; 7 | DROP TABLE IF EXISTS `favorite`; 8 | DROP TABLE IF EXISTS `shake`; 9 | DROP TABLE IF EXISTS `subscription`; 10 | DROP TABLE IF EXISTS `shakesharedfile`; 11 | DROP TABLE IF EXISTS `waitlist`; 12 | DROP TABLE IF EXISTS `comment`; 13 | DROP TABLE IF EXISTS `post`; 14 | DROP TABLE IF EXISTS `notification`; 15 | DROP TABLE IF EXISTS `app`; 16 | DROP TABLE IF EXISTS `authorizationcode`; 17 | DROP TABLE IF EXISTS `accesstoken`; 18 | DROP TABLE IF EXISTS `apilog`; 19 | DROP TABLE IF EXISTS `conversation`; 20 | DROP TABLE IF EXISTS `external_relationship`; 21 | DROP TABLE IF EXISTS `shake_manager`; 22 | DROP TABLE IF EXISTS `payment_log`; 23 | DROP TABLE IF EXISTS `bookmark`; 24 | DROP TABLE IF EXISTS `invitation_request`; 25 | DROP TABLE IF EXISTS `apihit`; 26 | DROP TABLE IF EXISTS `nsfw_log`; 27 | DROP TABLE IF EXISTS `magicfile`; 28 | DROP TABLE IF EXISTS `script_log`; 29 | DROP TABLE IF EXISTS `shake_category`; 30 | DROP TABLE IF EXISTS `comment_like`; 31 | DROP TABLE IF EXISTS `tag`; 32 | DROP TABLE IF EXISTS `tagged_file`; 33 | DROP TABLE IF EXISTS `voucher`; 34 | DROP TABLE IF EXISTS `promotion`; 35 | DROP TABLE IF EXISTS `dmca_takedown`; 36 | -------------------------------------------------------------------------------- /templates/admin/shake-categories.html: -------------------------------------------------------------------------------- 1 | {%extends "base.html" %} 2 | 3 | {% block title %}Shake Categories{% end %} 4 | 5 | {% block main %} 6 |
7 | {% include "admin/_sidebar.html" %} 8 | 9 |
10 |

Shake Categories

11 | 16 |

17 | Save A New Category 18 |

19 |
20 | {{ xsrf_form_html() }} 21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 |
34 |

35 | < Back to the Admin 36 |

37 |
38 |
39 | {% end %} 40 | 41 | -------------------------------------------------------------------------------- /templates/tools/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title%}mltshp{% end %} 7 | 8 | 9 | 10 | 11 | {% block included_headers %}{% end %} 12 | 13 | 17 | 18 | 19 | 20 |
21 | MLTSHP 22 | you are signed in as {{current_user['name']}} | sign out 23 |
24 | 25 |
26 |
27 | {% block main %} 28 | {{content}} 29 | {% end %} 30 |
31 |
32 | 33 | 34 | 35 | {% block included_scripts %}{% end %} 36 | 37 | 38 | -------------------------------------------------------------------------------- /templates/tools/plugins.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block title %}Plugins{% end %} {% block main %} 2 |
3 |

Browser Plugins

4 |

5 | We now have THREE browser plugins for you to use. A Safari extension, a Firefox add-on, and a Chrome extension. We intend on providing a plugin for every browser out there, but if you would like to help us out by making one, please contact 6 | hello@mltshp.com. 8 |

9 |

10 | Safari Extension 11 |

12 |

13 | Firefox Add-On 14 |

15 |

16 | Chrome Extension 17 |

18 |
19 | {% end %} 20 | -------------------------------------------------------------------------------- /release/settings.py: -------------------------------------------------------------------------------- 1 | # This is a collection of settings specifically for use with our automated 2 | # build and test setup on buildkite.com 3 | 4 | # dummy dict so main module loads okay 5 | settings = {} 6 | 7 | # Default settings for running tests; app host/cdn host are wired for 8 | # expected values in tests. 9 | test_settings = { 10 | "app_host": "my-mltshp.com", 11 | "cdn_host": "some-cdn.com", 12 | "cookie_secret": "secretz", 13 | "auth_secret" : "dummy-secret", 14 | "xsrf_cookies": True, 15 | "uploaded_files" : "/srv/mltshp.com/uploaded", 16 | "debug": True, 17 | # docker container's host ip address 18 | "database_user": "root", 19 | "database_name": "mltshp_testing", 20 | "database_password" : "", 21 | "database_host": "mysql", 22 | "aws_bucket": "mltshp-testing", 23 | "aws_key": "dummy-key", 24 | "aws_secret": "dummy-secret", 25 | "max_mb_per_month" : 300, 26 | "api_hits_per_hour" : 150, 27 | "use_workers": False, 28 | "debug_workers": True, 29 | "superuser_list": "admin", 30 | "tornado_logging": False, 31 | # these must be set for testing test/unit/externalservice_tests.py 32 | # "twitter_consumer_key" : "twitter_consumer_key_here", 33 | # "twitter_consumer_secret" : "twitter_consumer_secret_key_here", 34 | # "twitter_access_key" : "twitter_access_key_here", 35 | # "twitter_access_secret" : "twitter_access_secret_here", 36 | } 37 | -------------------------------------------------------------------------------- /static/images/mltshp-flag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/tools/sign-in.html: -------------------------------------------------------------------------------- 1 | {% extends "tools/base-sign-in.html" %} 2 | 3 | {% block title %}Sign In{% end %} 4 | 5 | {% block main %} 6 |

Sign in to save your images to your shake

7 |
8 |
9 | {{ xsrf_form_html() }} 10 |
11 | 12 |
13 | 15 | {% if errors.name %} 16 |
17 | 18 | {{errors.name }} 19 | 20 |
21 | {% end %} 22 |
23 |
24 |
25 | 26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 | 34 |
35 |
36 |
37 | {% end %} 38 | -------------------------------------------------------------------------------- /scripts/transcode-sharedfile.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from tornado.options import options 3 | 4 | from models import Sharedfile 5 | from tasks.transcode import transcode_sharedfile 6 | 7 | import logging 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | 11 | def main(): 12 | keys = sys.argv[2:] 13 | 14 | options.use_workers = False 15 | if len(keys) == 0: 16 | print("Selecting untranscoded sharedfiles...") 17 | select = """SELECT share_key 18 | FROM sharedfile 19 | JOIN sourcefile ON sourcefile.id = sharedfile.source_id 20 | WHERE sharedfile.deleted = 0 21 | AND sharedfile.content_type = 'image/gif' 22 | AND sharedfile.parent_id = 0 23 | AND (sourcefile.webm_flag IS NULL OR sourcefile.mp4_flag IS NULL) 24 | ORDER BY sharedfile.created_at DESC""" 25 | results = Sharedfile.query(select) 26 | for result in results: 27 | keys.append(result["share_key"]) 28 | print("Found %d sharedfiles to transcode" % len(keys)) 29 | 30 | for key in keys: 31 | sf = Sharedfile.get("share_key=%s AND content_type='image/gif' AND deleted=0", key) 32 | if sf is not None: 33 | print("Transcoding %s..." % sf.share_key) 34 | transcode_sharedfile.delay_or_run(sf.id) 35 | else: 36 | print("Could not find sharedfile with key: %s" % key) 37 | -------------------------------------------------------------------------------- /templates/services/oembed.json: -------------------------------------------------------------------------------- 1 | {% if jsonp %}{{jsonp}}({%end %}{ {% if jsonp %} 2 | "callback": "{{jsonp}}",{% end %} 3 | "version": "1.0",{% if sourcefile.type=="link" %} 4 | "type": "link", 5 | "url": "{{sharedfile.source_url}}",{% else %}{% if sourcefile.mp4_flag or sourcefile.webm_flag %} 6 | "type": "video", 7 | "html": "",{% else %} 8 | "type": "photo", 9 | "url": "https://{{cdn_host}}/r/{{sharedfile.share_key}}",{% end %}{% end %} 10 | "width": {{sourcefile.width}}, 11 | "height": {{sourcefile.height}}, 12 | "title": {{json_encode(sharedfile.get_title())}}, 13 | "author_name": "{{escape(sharedfile_owner.name)}}", 14 | "author_url": "https://{{app_host}}/user/{{sharedfile_owner.name}}", 15 | "provider_name": "MLTSHP",{% if include_embed and sourcefile.type == "link" %} 16 | "embed_html": {{json_encode(sharedfile.render_data())}},{% end %} 17 | "provider_url": "https://{{app_host}}/" 18 | }{% if jsonp %}){%end %} 19 | -------------------------------------------------------------------------------- /Dockerfile.worker: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | LABEL maintainer="brad@bradchoate.com" 3 | ENV PYTHONUNBUFFERED=1 4 | 5 | RUN apt-get -y update && \ 6 | apt-get install -y \ 7 | software-properties-common \ 8 | build-essential \ 9 | supervisor \ 10 | cron \ 11 | pkg-config \ 12 | libmysqlclient-dev \ 13 | mysql-client \ 14 | python3-dev \ 15 | python3-full \ 16 | python3-pip \ 17 | libjpeg-dev \ 18 | libcurl4-openssl-dev \ 19 | curl \ 20 | run-one \ 21 | ffmpeg \ 22 | wget \ 23 | vim \ 24 | libpcre3 \ 25 | libpcre3-dev \ 26 | libssl-dev \ 27 | libffi-dev && \ 28 | rm -rf /var/lib/apt/lists/* && \ 29 | groupadd mltshp --gid=1010 && \ 30 | useradd mltshp --create-home --home-dir=/home/mltshp \ 31 | --uid=1010 --gid=1010 && \ 32 | mkdir -p /srv/mltshp.com/logs && \ 33 | chown -R mltshp:mltshp /srv/mltshp.com 34 | 35 | COPY requirements.txt /tmp 36 | RUN pip install --break-system-packages -r /tmp/requirements.txt && rm /tmp/requirements.txt 37 | 38 | COPY setup/production/supervisord-worker.conf /etc/supervisor/conf.d/mltshp.conf 39 | 40 | # NOTE: /srv/mltshp.com/logs should be a mounted volume for this image 41 | ADD . /srv/mltshp.com/mltshp 42 | WORKDIR /srv/mltshp.com/mltshp 43 | RUN crontab -u mltshp setup/production/mltshp-worker--crontab 44 | 45 | CMD ["/usr/bin/supervisord"] 46 | -------------------------------------------------------------------------------- /scripts/populate-save-like.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from torndb import Connection 4 | from tornado.options import options 5 | from tasks.timeline import add_posts 6 | 7 | 8 | def main(): 9 | db1 = Connection(options.database_host, options.database_user, options.database_password) 10 | 11 | #grab all shared files in order 12 | sfs = db1.query("""SELECT id FROM sharedfile ORDER BY created_at""") 13 | #for each, get counts 14 | 15 | for sf in sfs: 16 | likes = 0 17 | saves = 0 18 | 19 | like_counts = db1.query("SELECT count(id) as like_count from favorite where sharedfile_id = %s and deleted=0", (sf.id)) 20 | if like_counts: 21 | likes = like_counts[0]['like_count'] 22 | 23 | save_counts = db1.query("SELECT count(id) AS save_count FROM sharedfile WHERE original_id = %s and deleted = 0", sf.id) 24 | if save_counts[0]['save_count'] > 0: 25 | saves = save_counts[0]['save_count'] 26 | else: 27 | save_counts = db1.query("SELECT count(id) AS save_count FROM sharedfile WHERE parent_id = %s and deleted = 0", sf.id) 28 | saves = save_counts[0]['save_count'] 29 | 30 | if likes > 0 or saves > 0: 31 | print("UPDATE sharedfile SET like_count = %s, save_count = %s WHERE id = %s" % (likes, saves, sf.id)) 32 | print(db1.execute("UPDATE sharedfile SET like_count = %s, save_count = %s WHERE id = %s", likes, saves, sf.id)) 33 | -------------------------------------------------------------------------------- /templates/waitlist/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | Get On The Waitlist 5 | {% end %} 6 | {% block main %} 7 | 8 |
9 | {% if errors.email %} 10 |

Ooops, try again…

11 | 12 | {% else %} 13 |

The Wait List

14 |

15 | A lot of work has gone into this site, and while it is still in preview 16 | stage we think it's ready to be opened up to a wider audience. So fill out this form, 17 | verify your address in the email, and we'll contact you when we add more players. 18 |

19 | {% end %} 20 | 21 |
22 | {{ xsrf_form_html() }} 23 |
24 | 25 |
26 | {% if errors.email %} 27 |
28 | 29 | {{errors.email }} 30 | 31 |
32 | {% end %} 33 |
34 |
35 | 36 |
37 |
38 |
39 |

40 | If you don't get your verification email please send us one at 41 | hello@mltshp.com. 42 |

43 |
44 | {% end %} 45 | -------------------------------------------------------------------------------- /templates/account/forgot-password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Forgot Your Password{% end %} 4 | 5 | {% block main %} 6 | 7 |
8 |

Reset Password

9 | {% if not site_is_readonly %} 10 |

11 | Put the email address you signed up with in the box below and we'll send you instructions on how to reset your password. 12 |

13 |
14 |
15 | {{ xsrf_form_html() }} 16 |
17 | 18 |
19 | 20 | {% if errors.email %} 21 |
22 | 23 | {{errors.email }} 24 | 25 |
26 | {% end %} 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 | {% else %} 36 |

37 | Sorry! The web site is currently in a read-only state. Please check back later to reset your password! 38 |

39 | {% end %} 40 |
41 | {% end %} 42 | -------------------------------------------------------------------------------- /templates/search/search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Search{% end %} 4 | 5 | {% block main %} 6 | 42 | {% end %} 43 | -------------------------------------------------------------------------------- /scripts/delete-user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import json 5 | import models 6 | from tornado.options import options 7 | import tornado.escape 8 | import requests 9 | 10 | 11 | """ 12 | Delete a user from the system. 13 | - get the user id 14 | """ 15 | 16 | NAME = "delete-user" 17 | 18 | 19 | def main(): 20 | if len(sys.argv) > 2: 21 | 22 | #user = models.User.get("id=%s and name=%s", sys.argv[2], sys.argv[3]) 23 | user = models.User.get("name=%s and nsfw=1 and is_paid=0", sys.argv[2]) 24 | 25 | if not user: 26 | return json.dumps({'status': 'error', 'message': 'user not found or not eligible for deletion'}) 27 | 28 | user.delete() 29 | 30 | if options.slack_webhook_url: 31 | try: 32 | msg = "User {0} was just deleted.".format(user.name) 33 | 34 | body = "{0}".format( 35 | tornado.escape.json_encode( 36 | {"text": msg, 37 | "channel": "#moderation", 38 | "username": "modbot", 39 | "icon_emoji": ":ghost:"})) 40 | 41 | r = requests.post(options.slack_webhook_url, data=body) 42 | except Exception as e: 43 | pass 44 | 45 | return json.dumps({ 46 | 'user_id': user.id, 47 | 'user_name': user.name 48 | }) 49 | 50 | return json.dumps({'status': 'error', 51 | 'message': 'requires user name'}) 52 | -------------------------------------------------------------------------------- /models/shakemanager.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.utilities import utcnow 3 | from tornado.options import options 4 | 5 | 6 | class ShakeManager(Model): 7 | shake_id = Property() 8 | user_id = Property() 9 | deleted = Property(default=0) 10 | created_at = Property() 11 | updated_at = Property() 12 | 13 | def save(self, *args, **kwargs): 14 | if options.readonly: 15 | self.add_error('_', 'Site is read-only.') 16 | return False 17 | 18 | self._set_dates() 19 | if not self._validate_shake_and_user(): 20 | return False 21 | return super(ShakeManager, self).save(*args, **kwargs) 22 | 23 | def _validate_shake_and_user(self): 24 | if int(self.shake_id) <= 0: 25 | self.add_error('shake', "No shake specified") 26 | return False 27 | if int(self.user_id) <= 0: 28 | self.add_error('user', "No user specified") 29 | return False 30 | return True 31 | 32 | def _set_dates(self): 33 | """ 34 | Sets the created_at and updated_at fields. This should be something 35 | a subclass of Property that takes care of this during the save cycle. 36 | """ 37 | if self.id is None or self.created_at is None: 38 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 39 | self.updated_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 40 | 41 | def delete(self): 42 | self.deleted =1 43 | self.save() 44 | -------------------------------------------------------------------------------- /templates/tools/find-shakes-people.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | Find Other People to Follow. 4 | {% end %} 5 | {% block main %} 6 |
7 |
8 |

Find Shakes to Follow

9 |
10 | 11 | 28 | 29 |
30 | 34 | 35 | {% for friend in users %} 36 |
37 | {{modules.ShakeFollow(follow_user=friend, current_user=current_user_obj, avatar_size=50, extended=True)}} 38 |
39 | {% end %} 40 |
41 |
42 | {% end %} 43 | -------------------------------------------------------------------------------- /templates/account/settings-connections.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Your Connections Settings{% end %} 4 | 5 | {% block main %} 6 |
7 |
8 |

Your Settings

9 |
10 | 15 |
16 |
17 |
18 | {% if apps %} 19 |

You've allowed the following applications access to your account

20 | {% else %} 21 |

You haven't allowed any applications access to your account yet.

22 | {% end %} 23 | 37 |
38 |
39 | {% end %} 40 | -------------------------------------------------------------------------------- /models/apihit.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.flyingcow.db import connection 3 | from lib.utilities import utcnow 4 | from tornado.options import options 5 | 6 | 7 | class Apihit(Model): 8 | accesstoken_id = Property() 9 | hits = Property() 10 | hour_start = Property() 11 | 12 | def save(self, *args, **kwargs): 13 | if options.readonly: 14 | self.add_error('_', 'Site is read-only.') 15 | return False 16 | 17 | self._set_dates() 18 | return super(Apihit, self).save(*args, **kwargs) 19 | 20 | def _set_dates(self): 21 | if self.id is None or self.hour_start is None: 22 | self.hour_start = utcnow().strftime("%Y-%m-%d %H:00:00") 23 | 24 | @classmethod 25 | def hit(cls, accesstoken_id, ts=None): 26 | if ts is None: 27 | ts = utcnow() 28 | hour_start = ts.strftime('%Y-%m-%d %H:00:00') 29 | 30 | sql = """INSERT INTO apihit (accesstoken_id, hits, hour_start) VALUES (%s, 1, %s) 31 | ON DUPLICATE KEY UPDATE hits = hits + 1""" 32 | args = (accesstoken_id, hour_start) 33 | kwargs = () 34 | 35 | conn = connection() 36 | cursor = conn._cursor() 37 | try: 38 | conn._execute(cursor, sql, args, kwargs) 39 | conn._execute(cursor, 40 | "SELECT hits FROM apihit WHERE accesstoken_id=%s AND hour_start=%s", 41 | args, kwargs) 42 | (hits,) = cursor.fetchone() 43 | finally: 44 | cursor.close() 45 | 46 | return hits 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mltshp: 3 | build: . 4 | env_file: 5 | - .env 6 | image: mltshp/mltshp-web:latest 7 | volumes: 8 | - .:/srv/mltshp.com/mltshp 9 | - ./mounts/logs:/srv/mltshp.com/logs 10 | - ./mounts/uploaded:/srv/mltshp.com/uploaded 11 | - ./setup/dev/nginx.conf:/etc/nginx/nginx.conf 12 | - ./setup/dev/supervisord.conf:/etc/supervisor/conf.d/mltshp.conf 13 | ports: 14 | - "8000:80" 15 | depends_on: 16 | - mysql 17 | - fakes3 18 | links: 19 | - mysql 20 | - fakes3 21 | networks: 22 | app_net: 23 | aliases: 24 | - mltshp.localhost 25 | fakes3: 26 | build: ./setup/dev/fakes3 27 | env_file: 28 | - .env 29 | entrypoint: fakes3 --root /srv --license ${FAKES3_LICENSE_KEY} --port 4567 30 | ports: 31 | - "4567:4567" 32 | volumes: 33 | - ./mounts/fakes3:/srv 34 | networks: 35 | app_net: 36 | aliases: 37 | - mltshp-testing.fakes3 38 | - mltshp-dev.fakes3 39 | mysql: 40 | image: mysql:8 41 | ports: 42 | - "3306:3306" 43 | volumes: 44 | - ./mounts/mysql:/var/lib/mysql 45 | - ./setup/dev/mysql-conf.d:/etc/mysql/conf.d 46 | - ./setup/db-install.sql:/docker-entrypoint-initdb.d/00_db-install.sql 47 | - ./setup/db-fixtures.sql:/docker-entrypoint-initdb.d/01_db-fixtures.sql 48 | environment: 49 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 50 | MYSQL_DATABASE: "mltshp" 51 | MYSQL_USER: "mltshp" 52 | networks: 53 | - app_net 54 | 55 | networks: 56 | app_net: 57 | -------------------------------------------------------------------------------- /templates/tools/bookmarklet.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}[ mltshp ]{% end %} 4 | 5 | {% block main %} 6 |
7 |

Bookmarklet

8 |

9 | Drag this link up to your bookmarks bar. 10 |

11 |

12 | [ mltshp ] 13 |

14 |

15 | When you’re on a page of images you want to save, click the bookmarklet and all 16 | eligible images will be highlighted for you to click. 17 |

18 | 19 |
20 |
21 | 22 |
23 | 24 |
25 | 26 |
27 |
Copy & Paste This Into A Bookmark
28 |
29 |
30 |
31 |

 

32 |
33 |
34 | {% end %} 35 | 36 | 37 | -------------------------------------------------------------------------------- /templates/account/reset-password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Reset Your Password{% end %} 4 | 5 | {% block main %} 6 | 7 |
8 |

Reset Your Password

9 |

10 | Enter a new password for your account. 11 |

12 |
13 |
14 | {{ xsrf_form_html() }} 15 | 16 | 17 |
18 | 19 |
20 | 21 | {% if errors.password %} 22 |
23 | 24 | {{errors.password }} 25 | 26 |
27 | {% end %} 28 |
29 |
30 | 31 |
32 | 33 |
34 | 35 |
36 |
37 | Enter the same password as above for verification. 38 |
39 |
40 | 41 | 42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 | {% end %} 50 | -------------------------------------------------------------------------------- /templates/uimodules/notification-invitation-request.html: -------------------------------------------------------------------------------- 1 | {% if len(notifications) > 0 %} 2 |
3 |
{{len(notifications)}} request{% if len(notifications) != 1%}s{% end %} to join a shake
4 |
5 | {% for notification in notifications %} 6 |
7 | {{notification['sender'].display_name()}} has requested to join {{notification['related_object'].display_name()}}. 8 |
9 |
10 | {{ xsrf_form_html() }} 11 | 12 | 13 |
14 |
15 | {{ xsrf_form_html() }} 16 | 17 | 18 |
19 |
20 |
21 | {% end %} 22 |
23 |
24 | {% end %} 25 | -------------------------------------------------------------------------------- /templates/account/sign-in.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Sign In{% end %} 4 | 5 | {% block main %} 6 | 7 |
8 |

Sign in

9 | 10 |
11 |
12 | {{ xsrf_form_html() }} 13 |
14 | 15 |
16 | 18 | {% if errors.name %} 19 |
20 | 21 | {{errors.name }} 22 | 23 |
24 | {% end %} 25 |
26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 | Sign In! 35 |
36 | 37 |
38 |
39 | 42 |
43 |
44 | {% end %} 45 | -------------------------------------------------------------------------------- /templates/shakes/members.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {{escape(shake.display_name())}} members 5 | {% end %} 6 | 7 | {% block main %} 8 | 9 |
10 | {% include "shakes/_sidebar.html" %} 11 | 12 |
13 | 14 | 41 | 42 |
43 | 44 |
45 | {% end %} 46 | -------------------------------------------------------------------------------- /models/external_relationship.py: -------------------------------------------------------------------------------- 1 | import lib.flyingcow 2 | from tornado.options import options 3 | from lib.utilities import utcnow 4 | 5 | 6 | class ExternalRelationship(lib.flyingcow.Model): 7 | user_id = lib.flyingcow.Property() 8 | service_id = lib.flyingcow.Property() 9 | service_type = lib.flyingcow.Property(default=1) 10 | created_at = lib.flyingcow.Property() 11 | updated_at = lib.flyingcow.Property() 12 | 13 | # constants. 14 | TWITTER = 1 15 | 16 | def save(self, *args, **kwargs): 17 | if options.readonly: 18 | self.add_error('_', 'Site is read-only.') 19 | return False 20 | 21 | self._set_dates() 22 | return super(ExternalRelationship, self).save(*args, **kwargs) 23 | 24 | def _set_dates(self): 25 | """ 26 | Sets the created_at and updated_at fields. This should be something 27 | a subclass of Property that takes care of this during the save cycle. 28 | """ 29 | if self.id is None or self.created_at is None: 30 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 31 | self.updated_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 32 | 33 | @classmethod 34 | def add_relationship(self, user, service_id, service_type=TWITTER): 35 | """ 36 | Take a mltshp User an external service_id and service_type. And create a 37 | new ExternalRelationship record. 38 | """ 39 | try: 40 | relationship = ExternalRelationship(user_id=user.id, service_id=service_id, service_type=service_type) 41 | relationship.save() 42 | except lib.flyingcow.db.IntegrityError: 43 | pass 44 | 45 | -------------------------------------------------------------------------------- /templates/shakes/rss.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{xhtml_escape(shake.display_name())}} 4 | https://{{ app_host }}{{shake.path()}} 5 | 6 | 7 | {% if (shake.type == "group") and shake.description %}{{xhtml_escape(shake.description)}}{% end %} 8 | 9 | en-us 10 | {% if build_date %} 11 | {{build_date}} 12 | {% end %} 13 | 14 | {{shake.page_image(include_protocol=True)}} 15 | {{xhtml_escape(shake.display_name())}} 16 | https://{{ app_host }}{{shake.path()}} 17 | 18 | 19 | {% for sf in sharedfiles %} 20 | 21 | 22 | {{xhtml_escape(sf.get_title())}} 23 | 24 | https://{{ app_host }}/p/{{sf.share_key}} 25 | https://{{ app_host }}/p/{{sf.share_key}} 26 | 27 | 29 | {% if sf.type() == "link" %} 30 | {{sf.render_data(store_view=False)}} 31 | {% else %} 32 | {{escape(sf.get_alt_text(raw=True) or 'No alt provided')}} 33 | {% end %} 34 |

35 | {% if sf.description %} 36 | {{sf.get_description()}} 37 | {% end %} 38 | ]]> 39 |
40 | {{sf.feed_date()}} 41 | {% if sf.sourcefile().nsfw %} 42 | nsfw 43 | {% end %} 44 |
45 | {% end %} 46 |
47 |
48 | -------------------------------------------------------------------------------- /templates/shakes/create.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Create a Shake{% end %} 4 | 5 | {% block main %} 6 |
7 |

Create a Shake

8 | {% if not site_is_readonly %} 9 | {% if errors %} 10 |

Couldn't create the shake. You'll need to change a couple of things.

11 | {% end %} 12 |
13 |
14 | {{ xsrf_form_html() }} 15 | 16 | {{ modules.FunFormField(field_type='text', label='Title', name='title', value=escape(shake.title), css_class='large-input') }} 17 | 18 | {{ modules.FunFormField(field_type='textarea', label='Description', name='description', value=escape(shake.description), css_class='large-textarea-input') }} 19 | 20 |
21 | 22 | https://mltshp.com/ 23 |
24 | 25 | {% if errors.name %} 26 |
27 | 28 | {{errors.name }} 29 | 30 |
31 | {% end %} 32 |
33 |
34 | 35 |
36 | Make It! 37 |
38 |
39 |
40 |
41 | {% else %} 42 |

Sorry, the site is in read-only mode for now. Try again later.

43 | {% end %} 44 | {% end %} 45 | -------------------------------------------------------------------------------- /settings.example.py: -------------------------------------------------------------------------------- 1 | # Development settings; suitable for running against our Docker 2 | # image. 3 | settings = { 4 | # Host and any special port required 5 | "app_host": "mltshp.localhost:8000", 6 | # Optional secondary host for CDN service 7 | # "cdn_host": "mltshp-cdn.localhost:8000", 8 | "api_hits_per_hour" : 150, 9 | "auth_secret" : "dummy-secret", 10 | "aws_bucket": "mltshp-dev", 11 | "aws_host": "fakes3", 12 | "aws_port": 4567, 13 | "aws_key": "dummy-key", 14 | "aws_secret": "dummy-secret", 15 | "cookie_secret": "some secret string", 16 | "database_host": "mysql", 17 | "database_name": "mltshp", 18 | "database_user": "root", 19 | "database_password" : "", 20 | "debug": True, 21 | "debug_workers": False, 22 | "max_mb_per_month" : 300, 23 | "uploaded_files" : "/srv/mltshp.com/uploaded", 24 | "use_workers": False, 25 | "xsrf_cookies": True, 26 | "server_id": "mltshp-web-1", 27 | } 28 | 29 | # Default settings for running tests; app host/cdn host are wired for 30 | # expected values in tests. 31 | test_settings = { 32 | "app_host": "my-mltshp.com", 33 | "cdn_host": "some-cdn.com", 34 | "cookie_secret": "secretz", 35 | "auth_secret" : "dummy-secret", 36 | "xsrf_cookies": True, 37 | "uploaded_files" : "/srv/mltshp.com/uploaded", 38 | "debug": True, 39 | "database_user": "root", 40 | "database_name": "mltshp_testing", 41 | "database_password" : "", 42 | "database_host": "mysql", 43 | "aws_bucket": "mltshp-testing", 44 | "aws_host": "fakes3", 45 | "aws_port": 4567, 46 | "aws_key": "dummy-key", 47 | "aws_secret": "dummy-secret", 48 | "max_mb_per_month" : 300, 49 | "api_hits_per_hour" : 150, 50 | "use_workers": False, 51 | "debug_workers": True, 52 | "superuser_list": "admin", 53 | "tornado_logging": False, 54 | } 55 | -------------------------------------------------------------------------------- /static/images/default-icon-venti.svg: -------------------------------------------------------------------------------- 1 | default-icon-venti -------------------------------------------------------------------------------- /test/functional/verify_email_tests.py: -------------------------------------------------------------------------------- 1 | import time 2 | import hashlib 3 | 4 | import test.base 5 | from models import User 6 | 7 | class VerifyEmailTests(test.base.BaseAsyncTestCase): 8 | 9 | def test_verify_key_success(self): 10 | h = hashlib.sha1() 11 | h.update(("%s" % time.time()).encode('ascii')) 12 | verify_token = h.hexdigest() 13 | 14 | existant_user = User( 15 | name='admin', 16 | email='admin@mltshp.com', 17 | email_confirmed=0, 18 | verify_email_token=verify_token 19 | ) 20 | existant_user.set_password('asdfasdf') 21 | existant_user.save() 22 | response = self.fetch_url('/verify-email/%s' % verify_token) 23 | reloaded_user = User.get('id = %s', existant_user.id) 24 | self.assertEqual(reloaded_user.email_confirmed, 1) 25 | self.assertEqual(reloaded_user.verify_email_token, '') 26 | self.assertEqual(response.effective_url, self.get_url('/')) 27 | 28 | def test_verify_key_failures(self): 29 | existant_user = User( 30 | name='admin', 31 | email='admin@mltshp.com', 32 | email_confirmed=0, 33 | verify_email_token='some-token' 34 | ) 35 | existant_user.set_password('asdfasdf') 36 | existant_user.save() 37 | 38 | bad_keys = [ 39 | 'asdf', 40 | '', 41 | '%20', 42 | '?lksdfjlskdfj=sldkfjasdf', 43 | '282938898932893289328932' 44 | ] 45 | for key in bad_keys: 46 | response = self.fetch_url('/verify-email/%s' % key) 47 | reloaded_user = User.get('id = %s', existant_user.id) 48 | self.assertEqual(reloaded_user.email_confirmed, 0) 49 | self.assertEqual(reloaded_user.verify_email_token, 'some-token') 50 | self.assertTrue(response.code == 404) 51 | 52 | -------------------------------------------------------------------------------- /templates/account/redeem.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Redeem a Coupon{% end %} 4 | 5 | {% block main %} 6 | 7 |
8 |

Redeem Coupon

9 |
10 |
11 | {{ xsrf_form_html() }} 12 | 13 |
14 | 15 |
16 | 17 | {% if errors.key %} 18 |
19 | 20 | {{errors.key }} 21 | 22 |
23 | {% end %} 24 |
25 |
26 | If you have a discount code to apply 27 | towards a membership, enter it here. 28 | 29 | {% if promotions %} 30 |

If you don’t have a discount code, here’s 31 | our active promotions! 32 | 33 | 39 |
40 | {% end %} 41 |
42 |
43 | 44 |
45 | 46 |
47 |
48 |
49 |
50 |
51 | {% end %} 52 | -------------------------------------------------------------------------------- /test/scripts/calculate_views_tests.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | 4 | from models import Fileview, ScriptLog 5 | from test.unit.base import BaseTestCase 6 | import test.factories 7 | 8 | import runner 9 | 10 | class CalculateViewsTests(BaseTestCase): 11 | 12 | def test_main(self): 13 | """ 14 | When the script runs for the first time, it should calculate 15 | the view_count for all sharedfiles, store the results in the 16 | script_log results table. 17 | 18 | The second time it run, it should use the previous run to only 19 | calculate views for files that have had views since last time 20 | the script ran. 21 | """ 22 | user = test.factories.user() 23 | sharedfile = test.factories.sharedfile(user) 24 | sharedfile2 = test.factories.sharedfile(user) 25 | sharedfile3 = test.factories.sharedfile(user) 26 | 27 | sharedfile.add_view() 28 | sharedfile.add_view() 29 | sharedfile2.add_view() 30 | sharedfile2.add_view() 31 | sharedfile2.add_view() 32 | self.assertEqual(0, sharedfile.get("id = %s", sharedfile.id).view_count) 33 | self.assertEqual(0, sharedfile2.get("id = %s", sharedfile2.id).view_count) 34 | runner.run('scripts/calculate-views.py') 35 | self.assertEqual(2, sharedfile.get("id = %s", sharedfile.id).view_count) 36 | self.assertEqual(3, sharedfile2.get("id = %s", sharedfile2.id).view_count) 37 | 38 | script_log = ScriptLog.last_successful('calculate-views') 39 | results = json.loads(script_log.result) 40 | self.assertEqual(2, results['updated_sharedfiles']) 41 | 42 | sharedfile3.add_view() 43 | runner.run('scripts/calculate-views.py') 44 | script_log = ScriptLog.last_successful('calculate-views') 45 | results = json.loads(script_log.result) 46 | self.assertEqual(1, results['updated_sharedfiles']) 47 | -------------------------------------------------------------------------------- /models/authorizationcode.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from datetime import timedelta 3 | import random 4 | import time 5 | from lib.utilities import base36encode, base36decode, generate_digest_from_dictionary, utcnow 6 | from tornado.options import options 7 | 8 | 9 | class Authorizationcode(Model): 10 | user_id = Property() 11 | app_id = Property() 12 | code = Property() 13 | expires_at = Property() 14 | redeemed = Property(default=0) 15 | redirect_url = Property(default=0) 16 | created_at = Property() 17 | updated_at = Property() 18 | 19 | 20 | def save(self, *args, **kwargs): 21 | if options.readonly: 22 | self.add_error('_', 'Site is read-only.') 23 | return False 24 | 25 | self._set_dates() 26 | return super(Authorizationcode, self).save(*args, **kwargs) 27 | 28 | def _set_dates(self): 29 | """ 30 | Sets the created_at and updated_at fields. This should be something 31 | a subclass of Property that takes care of this during the save cycle. 32 | """ 33 | if self.id is None or self.created_at is None: 34 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 35 | self.updated_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 36 | 37 | @staticmethod 38 | def generate(app_id, redirect_url, user_id): 39 | """ 40 | Generate a code based on the app_id, time, and redirect_url 41 | Set expires_at to be 30 seconds from now. 42 | """ 43 | code = generate_digest_from_dictionary([app_id, random.random(), time.mktime(utcnow().timetuple())]) 44 | expires_at = utcnow() + timedelta(seconds=30) 45 | auth_code = Authorizationcode(user_id=user_id, app_id=app_id, code=code, redirect_url=redirect_url,expires_at=expires_at.strftime("%Y-%m-%d %H:%M:%S")) 46 | auth_code.save() 47 | return auth_code 48 | -------------------------------------------------------------------------------- /templates/account/migrate.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Restore from MLKSHK?{% end %} 3 | 4 | {% block main %} 5 |
6 | 7 | {% if has_migrated %} 8 |

MLKSHK Restore Complete!

9 | 10 |

Thanks for joining MLTSHP and for all your posts!

11 | 12 |
13 | 15 |
16 | {% else %} 17 |

Restore Your MLKSHK Content?

18 | 19 |

20 | Would you like to restore your content from MLKSHK.com 21 | or start over with a clean account? If you choose to restore, 22 | all your posts, shakes, and comments will be brought over (with 23 | the same IDs, exactly as on MLKSHK). 24 |

25 | 26 |

27 | Posts you have favorited may not all be restored, since those 28 | are owned by others; once those members restore their content, 29 | you'll see them again. 30 |

31 | 32 |

33 | You'll have until December 31st, 2017 to choose to 34 | restore your content. After that time, we will need to remove it 35 | permanently. 36 |

37 | 38 |
39 |
40 | {{ xsrf_form_html() }} 41 | 43 |
44 |
45 |
46 |
47 | 49 |
50 |
51 | 52 | {% end %} 53 | 54 |
55 | {% end %} 56 | -------------------------------------------------------------------------------- /scripts/calculate-views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Create celery tasks for all the sharefiles that have views since 4 | last time this script was run. 5 | 6 | This script is run at a regular interval via cron. 7 | """ 8 | import json 9 | 10 | import models 11 | from lib.flyingcow.db import connection 12 | 13 | 14 | NAME = "calculate-views" 15 | 16 | 17 | def main(): 18 | """ 19 | Find all sharedfiles and calculate their likes from fileview. 20 | """ 21 | updated_sharedfiles = 0 22 | last_fileview = models.Fileview.last() 23 | if last_fileview: 24 | sharedfile_ids = models.Fileview.sharedfile_ids(before_id=last_fileview.id+1) 25 | 26 | conn = connection() 27 | cursor = conn._cursor() 28 | try: 29 | for sharedfile_id in sharedfile_ids: 30 | sharedfile = models.Sharedfile.get("id = %s", sharedfile_id) 31 | if sharedfile: 32 | conn._execute( 33 | cursor, 34 | "DELETE FROM fileview WHERE sharedfile_id=%s AND user_id != %s AND id <= %s", 35 | [sharedfile_id, sharedfile.user_id, last_fileview.id], {}) 36 | count = cursor.rowcount 37 | if count > 0: 38 | sharedfile.increment_view_count(count) 39 | updated_sharedfiles += 1 40 | 41 | # delete the remaining rows; will only be cases where the image was 42 | # viewed by the owner of the sharedfile; we shouldn't actually have 43 | # these for new fileview records, just legacy ones... 44 | conn._execute( 45 | cursor, 46 | "DELETE FROM fileview WHERE id <= %s", 47 | [last_fileview.id], {}) 48 | finally: 49 | cursor.close() 50 | 51 | results = { 52 | 'updated_sharedfiles' : updated_sharedfiles 53 | } 54 | return json.dumps(results) 55 | -------------------------------------------------------------------------------- /models/tag.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.flyingcow.cache import ModelQueryCache 3 | from lib.utilities import utcnow 4 | 5 | from . import sharedfile 6 | from tornado.options import options 7 | 8 | 9 | class Tag(ModelQueryCache, Model): 10 | name = Property() 11 | created_at = Property() 12 | 13 | def save(self, *args, **kwargs): 14 | if options.readonly: 15 | self.add_error('_', 'Site is read-only.') 16 | return False 17 | 18 | self._set_dates() 19 | return super(Tag, self).save(*args, **kwargs) 20 | 21 | def _set_dates(self): 22 | if self.id is None or self.created_at is None: 23 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 24 | 25 | def path(self): 26 | return '/%s' % self.name.lower() 27 | 28 | def sharedfiles_paginated(self, per_page=10, since_id=None, max_id=None): 29 | """ 30 | Pulls a tags's timeline, can key off and go backwards (max_id) and forwards (since_id) 31 | in time to pull the per_page amount of posts. 32 | """ 33 | constraint_sql = "" 34 | order = "desc" 35 | if max_id: 36 | constraint_sql = "AND tagged_file.sharedfile_id < %s" % (int(max_id)) 37 | elif since_id: 38 | order = "asc" 39 | constraint_sql = "AND tagged_file.sharedfile_id > %s" % (int(since_id)) 40 | 41 | sql = """SELECT sharedfile.* FROM sharedfile, tagged_file 42 | WHERE tagged_file.tag_id = %s 43 | AND tagged_file.sharedfile_id = sharedfile.id 44 | AND tagged_file.deleted = 0 45 | %s 46 | ORDER BY tagged_file.sharedfile_id %s limit %s, %s""" % (int(self.id), constraint_sql, order, 0, int(per_page)) 47 | results = sharedfile.Sharedfile.object_query(sql) 48 | 49 | if order == "asc": 50 | results.reverse() 51 | 52 | return results 53 | -------------------------------------------------------------------------------- /test/unit/fileview_tests.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from models import Fileview 4 | from .base import BaseTestCase 5 | import test.factories 6 | 7 | class FileviewTests(BaseTestCase): 8 | 9 | def test_sharedfile_ids(self): 10 | """ 11 | Fileview.sharedfile_ids should return a list of distinct sharedfile_ids. 12 | """ 13 | user = test.factories.user() 14 | sharedfile = test.factories.sharedfile(user) 15 | sharedfile2 = test.factories.sharedfile(user) 16 | sharedfile3 = test.factories.sharedfile(user) 17 | 18 | sharedfile.add_view() 19 | sharedfile.add_view() 20 | sharedfile.add_view() 21 | self.assertEqual(1, len(Fileview.sharedfile_ids())) 22 | self.assertEqual(sharedfile.id, Fileview.sharedfile_ids()[0]) 23 | 24 | sharedfile2.add_view() 25 | sharedfile2.add_view() 26 | sharedfile2.add_view() 27 | self.assertEqual(2, len(Fileview.sharedfile_ids())) 28 | 29 | last_id = Fileview.last().id 30 | self.assertEqual(2, len(Fileview.sharedfile_ids(last_id+1))) 31 | 32 | sharedfile3.add_view() 33 | # still just "2", since we're selecting sharedfile ids 34 | # that are less than last_id+1 35 | self.assertEqual(2, len(Fileview.sharedfile_ids(last_id+1))) 36 | 37 | # last sharedfile id returned before last_id should be sharedfile2 38 | last_id = Fileview.last().id 39 | self.assertEqual(sharedfile2.id, Fileview.sharedfile_ids(last_id)[-1]) 40 | 41 | 42 | def test_last(self): 43 | """ 44 | Fileview.last() should return the last entry entered into fileview 45 | table. 46 | """ 47 | user = test.factories.user() 48 | sharedfile = test.factories.sharedfile(user) 49 | 50 | self.assertEqual(None, Fileview.last()) 51 | sharedfile.add_view() 52 | sharedfile.add_view() 53 | sharedfile.add_view() 54 | sharedfile.add_view() 55 | self.assertEqual(4, Fileview.last().id) 56 | -------------------------------------------------------------------------------- /models/subscription.py: -------------------------------------------------------------------------------- 1 | from lib.flyingcow import Model, Property 2 | from lib.utilities import utcnow 3 | from tornado.options import options 4 | 5 | from . import shake 6 | from . import user 7 | from . import post 8 | 9 | 10 | class Subscription(Model): 11 | user_id = Property() 12 | shake_id = Property() 13 | deleted = Property(default=0) 14 | created_at = Property() 15 | updated_at = Property() 16 | 17 | 18 | def save(self, *args, **kwargs): 19 | if options.readonly: 20 | self.add_error('_', 'Site is read-only.') 21 | return False 22 | 23 | self._set_dates() 24 | return super(Subscription, self).save(*args, **kwargs) 25 | 26 | def _set_dates(self): 27 | """ 28 | Sets the created_at and updated_at fields. This should be something 29 | a subclass of Property that takes care of this during the save cycle. 30 | """ 31 | if self.id is None or self.created_at is None: 32 | self.created_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 33 | self.updated_at = utcnow().strftime("%Y-%m-%d %H:%M:%S") 34 | 35 | def on_create(self): 36 | sub_shake = shake.Shake.get('id=%s and deleted=0', self.shake_id) 37 | sub_user = user.User.get('id = %s and deleted=0', self.user_id) 38 | shake_owner = user.User.get('id = %s and deleted=0', sub_shake.user_id) 39 | 40 | shared_files = sub_shake.sharedfiles() 41 | for sf in shared_files: 42 | existing_post = post.Post.where('user_id = %s and sourcefile_id = %s', sub_user.id, sf.source_id) 43 | seen = 0 44 | if existing_post: 45 | seen = 1 46 | new_post = post.Post(user_id=sub_user.id, sharedfile_id=sf.id, sourcefile_id=sf.source_id, seen=seen, shake_id=sub_shake.id) 47 | new_post.save() 48 | new_post.created_at = sf.created_at 49 | new_post.save() 50 | 51 | def shake(self): 52 | return shake.Shake.get("id = %s", self.shake_id) 53 | -------------------------------------------------------------------------------- /static/js/tools.js: -------------------------------------------------------------------------------- 1 | /* JS for the tools page */ 2 | 3 | (function ($) { 4 | $.fn.hint = function (text) { 5 | var default_text = text; 6 | var field = this; 7 | var original_color = $(this).css("color"); 8 | 9 | var setDefault = function () { 10 | if ($(field).val() == "") { 11 | $(field).css("color", "#999").val(default_text); 12 | } 13 | }; 14 | 15 | var resetStyles = function () { 16 | $(field).css("color", original_color); 17 | }; 18 | 19 | var clearField = function () { 20 | $(field).val(""); 21 | }; 22 | 23 | setDefault(); 24 | 25 | this.focus(function () { 26 | if ($(this).val() == default_text) { 27 | resetStyles(); 28 | clearField(); 29 | } 30 | }).blur(function () { 31 | setDefault(); 32 | }); 33 | 34 | // on submit, clear out the hint before submit. 35 | this.parents("form:first").submit(function () { 36 | if ($(field).val() == default_text) { 37 | clearField(); 38 | } 39 | }); 40 | 41 | return this; 42 | }; 43 | 44 | $(".picker-content .textarea-navigation .tab").click(function (e) { 45 | let tabId = e.target.getAttribute("data-tab"); 46 | let tab = document.getElementById(tabId); 47 | if (tab && !$(tab).hasClass("field-textarea--selected")) { 48 | $(".picker-content .textarea-navigation li").removeClass( 49 | "selected", 50 | ); 51 | $(e.target).closest("li").addClass("selected"); 52 | $("#description").removeClass("field-textarea--selected"); 53 | $("#alt-text").removeClass("field-textarea--selected"); 54 | $(tab).addClass("field-textarea--selected"); 55 | } 56 | }); 57 | })(jQuery); 58 | 59 | $(document).ready(function () { 60 | $("#description-field").hint("Write a description if you'd like!"); 61 | }); 62 | -------------------------------------------------------------------------------- /test/functional/image_nsfw_tests.py: -------------------------------------------------------------------------------- 1 | import test.base 2 | import test.factories 3 | import models 4 | 5 | class ImageNSFWTests(test.base.BaseAsyncTestCase): 6 | """ 7 | Tests: /p/{share_key}/nsfw 8 | """ 9 | def setUp(self): 10 | """ 11 | Create users to test different liking situations. 12 | """ 13 | super(ImageNSFWTests, self).setUp() 14 | # uploader 15 | self.admin = models.User(name='admin', email='admin@mltshp.com', email_confirmed=1, is_paid=1) 16 | self.admin.set_password('asdfasdf') 17 | self.admin.save() 18 | 19 | # another user 20 | self.bob = models.User(name='bob', email='bob@mltshp.com', email_confirmed=1, is_paid=1) 21 | self.bob.set_password('asdfasdf') 22 | self.bob.save() 23 | 24 | def test_non_logged_in_users_cant_set_nsfw(self): 25 | sharedfile = test.factories.sharedfile(self.admin) 26 | response = self.post_url(sharedfile.post_url(relative=True) + "/nsfw") 27 | self.assertEqual(403, response.code) 28 | 29 | def test_set_nsfw_on_anothers_file(self): 30 | """ 31 | Another user setting NSFW for a file should result in an OK response, 32 | the nsfw set on the file and a log entry for the logged in user. 33 | """ 34 | sharedfile = test.factories.sharedfile(self.admin) 35 | sourcefile = sharedfile.sourcefile() 36 | self.assertEqual(0, sourcefile.nsfw) 37 | self.sign_in('bob', 'asdfasdf') 38 | response = self.post_url(sharedfile.post_url(relative=True) + "/nsfw") 39 | self.assertEqual(200, response.code) 40 | sharedfile = models.Sharedfile.get("id = %s", sharedfile.id) 41 | sourcefile = sharedfile.sourcefile() 42 | self.assertEqual(1, sourcefile.nsfw) 43 | self.assertEqual(self.bob.id, models.NSFWLog.all()[0].user_id) 44 | self.assertEqual(sharedfile.id, models.NSFWLog.all()[0].sharedfile_id) 45 | self.assertEqual(sourcefile.id, models.NSFWLog.all()[0].sourcefile_id) 46 | 47 | -------------------------------------------------------------------------------- /handlers/popular.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from datetime import timedelta 3 | 4 | from tornado.options import options 5 | from .base import BaseHandler, require_membership 6 | 7 | from lib.utilities import utcnow 8 | from models import sharedfile, notification, user 9 | 10 | 11 | class IndexHandler(BaseHandler): 12 | def get(self): 13 | current_user_obj = self.get_current_user_object() 14 | now = utcnow() 15 | then = now - timedelta(hours=24) 16 | notifications_count = 0 17 | 18 | if current_user_obj: 19 | notifications_count = notification.Notification.for_user_count(current_user_obj) 20 | self.set_header("Cache-Control", "private") 21 | else: 22 | self.set_header("Cache-Control", "s-maxage=600, max-age=60") 23 | 24 | last_sf = sharedfile.Sharedfile.get('1 ORDER BY id desc LIMIT 1') 25 | if last_sf is not None: 26 | last_sf_id = last_sf.id - 1000 27 | else: 28 | last_sf_id = 0 29 | #sharedfiles = sharedfile.Sharedfile.where("original_id = 0 and created_at > %s ORDER BY like_count desc LIMIT 25", then) 30 | 31 | sharedfiles = sharedfile.Sharedfile.object_query("""SELECT *, (like_count)/(TIMESTAMPDIFF(minute, created_at, utc_timestamp())+3)^1.5 AS adjusted 32 | FROM sharedfile 33 | WHERE deleted=0 AND original_id = 0 AND like_count > 5 AND id > %s 34 | ORDER BY adjusted DESC LIMIT 30""", last_sf_id) 35 | 36 | best_of_user = user.User.get("name=%s", options.best_of_user_name) 37 | best_of_shake = best_of_user.shake() 38 | return self.render("popular/index.html", 39 | sharedfiles=sharedfiles, 40 | notifications_count=notifications_count, 41 | current_user_obj=current_user_obj, 42 | best_of_user=best_of_user, 43 | best_of_shake=best_of_shake) 44 | -------------------------------------------------------------------------------- /templates/developers/new-api-application.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block main %} 5 |
6 |
7 | 8 |

Create A MLTSHP Application

9 | 10 |
11 | 12 |
13 | {{ xsrf_form_html() }} 14 | 15 |
16 | 17 |
18 | 19 | {% if errors.title %} 20 |
21 | 22 | {{errors.title }} 23 | 24 |
25 | {% end %} 26 |
27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 | 35 | {% if errors.description %} 36 |
37 | 38 | {{errors.description }} 39 | 40 |
41 | {% end %} 42 |
43 |
44 |
255 characters max.
45 |
46 | 47 |
48 | 49 |
50 | 51 | {% if errors.redirect_url %} 52 |
53 | 54 | {{errors.redirect_url }} 55 | 56 |
57 | {% end %} 58 | 59 |
60 |
61 | 62 |
63 | Create It! 64 |
65 | 66 | 67 |
68 | 69 | 70 |
71 |
72 | {% end %} 73 | -------------------------------------------------------------------------------- /templates/uimodules/notification-invitations.html: -------------------------------------------------------------------------------- 1 | {% if len(notifications) > 0 %} 2 |
3 |
4 | {% if single %} 5 | You're invited! 6 | {% else %} 7 | {{len(notifications)}} new shake {% if len(notifications) == 1%}invitation{% else %}invitations{% end %} 8 | {% end %} 9 |
10 |
11 | {% for notification in notifications %} 12 | {% if notification['related_object'] %} 13 |
14 |
15 | 16 |
17 |

{{escape(notification['related_object'].display_name())}}

18 |
19 | {{notification['sender'].display_name()}} has invited you to join this shake. Wanna join? 20 |
21 |
22 |
23 | {{ xsrf_form_html() }} 24 | 25 | 26 |
27 |
28 | {{ xsrf_form_html() }} 29 | 30 |
31 |
32 |
33 | {% end %} 34 | {% end %} 35 |
36 |
37 | {% end %} 38 | --------------------------------------------------------------------------------