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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
12 |
--------------------------------------------------------------------------------
/static/images/home-comment.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/comment-tiny.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/static/images/icon-reply-arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/like-this.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
3 | {% if previous_link %}
4 | « Prev
5 | {% else %}
6 | « Prev
7 | {% end %}
8 | {% if next_link %}
9 | Next »
10 | {% else %}
11 | Next »
12 | {% end %}
13 | {% for page in pages %}
14 | {% if page == current_page %}
15 | {{page}}
16 | {% elif page == '...' %}
17 | ...
18 | {% else %}
19 | {{page}}
20 | {% end %}
21 | {% end %}
22 |
23 |
--------------------------------------------------------------------------------
/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 |
15 | {% end %}
16 |
--------------------------------------------------------------------------------
/static/images/bookmarklet.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 |
12 | {% for shake_category in shake_categories %}
13 |
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 |
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 |
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 |
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 |
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 |
7 | {% include "search/_sidebar.html" %}
8 |
9 |
10 | {% if len(sharedfiles) > 0 %}
11 |
12 | {% for sharedfile in sharedfiles %}
13 | {% if sharedfile.__class__.__name__ == 'Sharedfile' %}
14 | {% if sharedfile == sharedfiles[-1] %}
15 |
16 | {% end %}
17 | {{modules.Image(sharedfile, current_user=current_user_obj, list_view=True, show_attribution_in_title=True)}}
18 | {% end %}
19 | {% end %}
20 |
21 |
22 |
23 | {% if older_link %}
24 | « Older
25 | {% end %}
26 |
27 |
28 | {% if newer_link %}
29 | Newer »
30 | {% end %}
31 |
32 |
33 | {% else %}
34 | {% if q %}
35 |
36 |
Couldn't find anything like that.
37 |
38 | {% end %}
39 | {% end %}
40 |
41 |
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 |
12 |
13 |
Some Good Folks
14 |
15 | {% if len(users_sidebar) > 0 %}
16 |
People who posted files you’ve liked.
17 | {% for user in users_sidebar %}
18 | {% if user.id != current_user_obj.id %}
19 | {{modules.ShakeFollow(follow_user=user, current_user=current_user_obj, avatar_size=50)}}
20 | {% end %}
21 | {% end %}
22 | {% else %}
23 | Like some files to generate more recommendations.
24 | {% end %}
25 |
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 |
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 |
44 |
45 |
46 |
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 |
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 |