2017-08-18 22:31:30 +02:00
|
|
|
from datetime import timedelta, datetime
|
2017-07-25 09:52:24 +02:00
|
|
|
|
|
|
|
from app import db
|
2017-07-27 01:17:28 +02:00
|
|
|
import secrets
|
2017-08-01 20:57:15 +02:00
|
|
|
from lib import decompose_interval
|
2017-07-27 00:35:53 +02:00
|
|
|
|
2017-07-25 10:10:44 +02:00
|
|
|
class TimestampMixin(object):
|
2017-08-03 11:51:37 +02:00
|
|
|
created_at = db.Column(db.DateTime, server_default=db.func.now(), nullable=False)
|
|
|
|
updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now(), nullable=False)
|
2017-07-25 09:52:24 +02:00
|
|
|
|
2017-07-27 00:35:53 +02:00
|
|
|
def touch(self):
|
|
|
|
self.updated_at=db.func.now()
|
2017-07-25 09:52:24 +02:00
|
|
|
|
2017-07-29 17:43:09 +02:00
|
|
|
class RemoteIDMixin(object):
|
|
|
|
@property
|
|
|
|
def service(self):
|
|
|
|
if not self.id:
|
|
|
|
return None
|
|
|
|
return self.id.split(":")[0]
|
2017-07-25 09:52:24 +02:00
|
|
|
|
2017-07-29 17:43:09 +02:00
|
|
|
@property
|
|
|
|
def twitter_id(self):
|
2017-07-30 01:35:29 +02:00
|
|
|
if not self.id:
|
|
|
|
return None
|
2017-07-29 17:43:09 +02:00
|
|
|
if self.service != "twitter":
|
2017-08-03 11:51:37 +02:00
|
|
|
raise Exception("tried to get twitter id for a {} {}".format(self.service, type(self)))
|
2017-07-29 17:43:09 +02:00
|
|
|
return self.id.split(":")[1]
|
|
|
|
|
|
|
|
@twitter_id.setter
|
|
|
|
def twitter_id(self, id):
|
|
|
|
self.id = "twitter:{}".format(id)
|
|
|
|
|
2017-08-18 22:31:30 +02:00
|
|
|
@property
|
|
|
|
def mastodon_instance(self):
|
|
|
|
if not self.id:
|
|
|
|
return None
|
|
|
|
if self.service != "mastodon":
|
|
|
|
raise Exception("tried to get mastodon instance for a {} {}".format(self.service, type(self)))
|
|
|
|
return self.id.split(":", 1)[1].split('@')[1]
|
|
|
|
|
|
|
|
@mastodon_instance.setter
|
|
|
|
def mastodon_instance(self, instance):
|
|
|
|
self.id = "mastodon:{}@{}".format(self.mastodon_id, instance)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def mastodon_id(self):
|
|
|
|
if not self.id:
|
|
|
|
return None
|
|
|
|
if self.service != "mastodon":
|
|
|
|
raise Exception("tried to get mastodon id for a {} {}".format(self.service, type(self)))
|
|
|
|
return self.id.split(":", 1)[1].split('@')[0]
|
|
|
|
|
|
|
|
@mastodon_id.setter
|
|
|
|
def mastodon_id(self, id):
|
|
|
|
self.id = "mastodon:{}@{}".format(id, self.mastodon_instance)
|
|
|
|
|
2017-07-29 17:43:09 +02:00
|
|
|
|
2017-08-01 20:57:15 +02:00
|
|
|
@decompose_interval('policy_delete_every')
|
|
|
|
@decompose_interval('policy_keep_younger')
|
|
|
|
class Account(TimestampMixin, RemoteIDMixin):
|
2017-07-27 00:35:53 +02:00
|
|
|
__tablename__ = 'accounts'
|
2017-07-29 17:43:09 +02:00
|
|
|
id = db.Column(db.String, primary_key=True)
|
2017-07-25 09:52:24 +02:00
|
|
|
|
2017-07-31 18:29:09 +02:00
|
|
|
policy_enabled = db.Column(db.Boolean, server_default='FALSE', nullable=False)
|
2017-08-03 20:01:06 +02:00
|
|
|
policy_keep_latest = db.Column(db.Integer, server_default='100', nullable=False)
|
2017-08-03 11:51:37 +02:00
|
|
|
policy_keep_favourites = db.Column(db.Boolean, server_default='TRUE', nullable=False)
|
2017-08-08 15:38:54 +02:00
|
|
|
policy_keep_media = db.Column(db.Boolean, server_default='FALSE', nullable=False)
|
2017-08-03 20:01:06 +02:00
|
|
|
policy_delete_every = db.Column(db.Interval, server_default='30 minutes', nullable=False)
|
|
|
|
policy_keep_younger = db.Column(db.Interval, server_default='365 days', nullable=False)
|
2017-08-20 18:17:33 +02:00
|
|
|
policy_keep_direct = db.Column(db.Boolean, server_default='TRUE', nullable=False)
|
2017-07-25 09:52:24 +02:00
|
|
|
|
2017-08-03 11:51:37 +02:00
|
|
|
display_name = db.Column(db.String)
|
|
|
|
screen_name = db.Column(db.String)
|
|
|
|
avatar_url = db.Column(db.String)
|
2017-08-03 20:01:06 +02:00
|
|
|
reported_post_count = db.Column(db.Integer)
|
2017-07-27 00:35:53 +02:00
|
|
|
|
2017-08-14 20:29:49 +02:00
|
|
|
last_fetch = db.Column(db.DateTime, server_default='epoch', index=True)
|
|
|
|
last_refresh = db.Column(db.DateTime, server_default='epoch', index=True)
|
2017-08-14 20:58:22 +02:00
|
|
|
next_delete = db.Column(db.DateTime, server_default='epoch', index=True)
|
2017-07-27 20:20:59 +02:00
|
|
|
|
2017-07-31 00:39:40 +02:00
|
|
|
def touch_fetch(self):
|
|
|
|
self.last_fetch = db.func.now()
|
|
|
|
|
2017-08-03 11:51:37 +02:00
|
|
|
def touch_delete(self):
|
2017-08-14 21:01:34 +02:00
|
|
|
# if it's been more than 1 delete cycle ago that we've deleted a post,
|
|
|
|
# reset next_delete to be 1 cycle away
|
2017-08-14 21:12:12 +02:00
|
|
|
if(datetime.now() - self.next_delete > self.policy_delete_every):
|
|
|
|
self.next_delete = db.func.now() + self.policy_delete_every
|
2017-08-14 20:58:22 +02:00
|
|
|
else:
|
2017-08-14 21:12:12 +02:00
|
|
|
self.next_delete += self.policy_delete_every
|
2017-08-03 11:51:37 +02:00
|
|
|
|
2017-08-12 23:07:16 +02:00
|
|
|
def touch_refresh(self):
|
|
|
|
self.last_refresh = db.func.now()
|
|
|
|
|
2017-08-01 22:48:33 +02:00
|
|
|
@db.validates('policy_keep_younger', 'policy_delete_every')
|
|
|
|
def validate_intervals(self, key, value):
|
|
|
|
if not (value == timedelta(0) or value >= timedelta(minutes=1)):
|
|
|
|
value = timedelta(minutes=1)
|
2017-08-19 13:11:16 +02:00
|
|
|
if key == 'policy_delete_every' and datetime.now() + value < self.next_delete:
|
|
|
|
# make sure that next delete is not in the far future
|
|
|
|
self.next_delete = datetime.now() + value
|
2017-08-01 22:48:33 +02:00
|
|
|
return value
|
|
|
|
|
2017-08-07 16:26:25 +02:00
|
|
|
@db.validates('policy_keep_latest')
|
|
|
|
def validate_empty_string_is_zero(self, key, value):
|
|
|
|
if type(value) == str and value.strip() == '':
|
|
|
|
return 0
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2017-07-28 00:08:20 +02:00
|
|
|
# backref: tokens
|
2017-07-31 20:49:03 +02:00
|
|
|
# backref: twitter_archives
|
2017-08-01 22:48:33 +02:00
|
|
|
# backref: posts
|
2017-08-08 00:29:38 +02:00
|
|
|
# backref: sessions
|
2017-07-27 20:20:59 +02:00
|
|
|
|
2017-07-29 12:01:32 +02:00
|
|
|
def __repr__(self):
|
2017-08-03 11:51:37 +02:00
|
|
|
return f"<Account({self.id}, {self.screen_name}, {self.display_name})>"
|
2017-07-29 12:01:32 +02:00
|
|
|
|
2017-07-31 00:07:34 +02:00
|
|
|
def post_count(self):
|
2017-08-01 22:48:33 +02:00
|
|
|
return Post.query.with_parent(self).count()
|
2017-07-31 00:07:34 +02:00
|
|
|
|
2017-08-03 21:37:00 +02:00
|
|
|
def estimate_eligible_for_delete(self):
|
|
|
|
"""
|
|
|
|
this is an estimation because we do not know if favourite status has changed since last time a post was refreshed
|
|
|
|
and it is unfeasible to refresh every single post every time we need to know how many posts are eligible to delete
|
|
|
|
"""
|
2017-08-09 10:25:41 +02:00
|
|
|
latest_n_posts = Post.query.with_parent(self).order_by(db.desc(Post.created_at)).limit(self.policy_keep_latest)
|
2017-08-03 21:37:00 +02:00
|
|
|
query = Post.query.with_parent(self).\
|
2017-08-15 23:58:33 +02:00
|
|
|
filter(Post.created_at <= db.func.now() - self.policy_keep_younger).\
|
2017-08-09 10:25:41 +02:00
|
|
|
except_(latest_n_posts)
|
2017-08-03 21:37:00 +02:00
|
|
|
if(self.policy_keep_favourites):
|
|
|
|
query = query.filter_by(favourite = False)
|
2017-08-08 15:38:54 +02:00
|
|
|
if(self.policy_keep_media):
|
|
|
|
query = query.filter_by(has_media = False)
|
2017-08-03 21:37:00 +02:00
|
|
|
return query.count()
|
|
|
|
|
|
|
|
|
2017-08-08 00:29:38 +02:00
|
|
|
def force_log_out(self):
|
|
|
|
Session.query.with_parent(self).delete()
|
|
|
|
db.session.commit()
|
2017-08-01 20:57:15 +02:00
|
|
|
|
2017-08-03 20:01:06 +02:00
|
|
|
|
2017-08-01 20:57:15 +02:00
|
|
|
class Account(Account, db.Model):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2017-07-27 00:35:53 +02:00
|
|
|
class OAuthToken(db.Model, TimestampMixin):
|
|
|
|
__tablename__ = 'oauth_tokens'
|
|
|
|
|
|
|
|
token = db.Column(db.String, primary_key=True)
|
2017-08-18 22:31:30 +02:00
|
|
|
token_secret = db.Column(db.String, nullable=True)
|
2017-07-25 23:05:46 +02:00
|
|
|
|
2017-08-14 20:29:49 +02:00
|
|
|
account_id = db.Column(db.String, db.ForeignKey('accounts.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=True, index=True)
|
2017-07-28 00:08:20 +02:00
|
|
|
account = db.relationship(Account, backref=db.backref('tokens', order_by=lambda: db.desc(OAuthToken.created_at)))
|
2017-07-25 09:52:24 +02:00
|
|
|
|
2017-08-03 11:51:37 +02:00
|
|
|
# note: account_id is nullable here because we don't know what account a token is for
|
|
|
|
# until we call /account/verify_credentials with it
|
2017-08-08 15:38:54 +02:00
|
|
|
|
|
|
|
|
2017-07-27 01:17:28 +02:00
|
|
|
class Session(db.Model, TimestampMixin):
|
|
|
|
__tablename__ = 'sessions'
|
|
|
|
|
|
|
|
id = db.Column(db.String, primary_key=True, default=lambda: secrets.token_urlsafe())
|
|
|
|
|
2017-08-14 20:29:49 +02:00
|
|
|
account_id = db.Column(db.String, db.ForeignKey('accounts.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False, index=True)
|
2017-08-08 00:29:38 +02:00
|
|
|
account = db.relationship(Account, lazy='joined', backref='sessions')
|
2017-07-27 20:20:59 +02:00
|
|
|
|
2017-08-08 15:38:54 +02:00
|
|
|
|
2017-07-29 17:43:09 +02:00
|
|
|
class Post(db.Model, TimestampMixin, RemoteIDMixin):
|
2017-07-27 20:20:59 +02:00
|
|
|
__tablename__ = 'posts'
|
|
|
|
|
2017-07-29 17:43:09 +02:00
|
|
|
id = db.Column(db.String, primary_key=True)
|
2017-07-27 20:20:59 +02:00
|
|
|
|
2017-08-15 23:58:33 +02:00
|
|
|
author_id = db.Column(db.String, db.ForeignKey('accounts.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
|
2017-08-01 22:48:33 +02:00
|
|
|
author = db.relationship(Account,
|
|
|
|
backref=db.backref('posts', order_by=lambda: db.desc(Post.created_at)))
|
2017-07-31 00:07:34 +02:00
|
|
|
|
2017-08-02 01:35:09 +02:00
|
|
|
favourite = db.Column(db.Boolean, server_default='FALSE', nullable=False)
|
2017-08-08 15:38:54 +02:00
|
|
|
has_media = db.Column(db.Boolean, server_default='FALSE', nullable=False)
|
2017-08-20 18:17:33 +02:00
|
|
|
direct = db.Column(db.Boolean, server_default='FALSE', nullable=False)
|
2017-08-08 15:38:54 +02:00
|
|
|
|
2017-08-02 01:35:09 +02:00
|
|
|
def __repr__(self):
|
2017-08-20 18:05:01 +02:00
|
|
|
return '<Post ({}, Author: {})>'.format(self.id, self.author_id)
|
2017-08-02 01:35:09 +02:00
|
|
|
|
2017-08-15 23:58:33 +02:00
|
|
|
db.Index('ix_posts_author_id_created_at', Post.author_id, Post.created_at)
|
|
|
|
|
2017-07-31 00:07:34 +02:00
|
|
|
class TwitterArchive(db.Model, TimestampMixin):
|
|
|
|
__tablename__ = 'twitter_archives'
|
|
|
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
2017-08-03 11:51:37 +02:00
|
|
|
account_id = db.Column(db.String, db.ForeignKey('accounts.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False)
|
2017-07-31 20:49:03 +02:00
|
|
|
account = db.relationship(Account, backref=db.backref('twitter_archives', order_by=lambda: db.desc(TwitterArchive.id)))
|
|
|
|
body = db.deferred(db.Column(db.LargeBinary, nullable=False))
|
2017-07-31 01:57:03 +02:00
|
|
|
chunks = db.Column(db.Integer)
|
2017-08-03 11:51:37 +02:00
|
|
|
chunks_successful = db.Column(db.Integer, server_default='0', nullable=False)
|
|
|
|
chunks_failed = db.Column(db.Integer, server_default='0', nullable=False)
|
2017-08-07 15:53:34 +02:00
|
|
|
|
2017-08-07 15:54:42 +02:00
|
|
|
def status(self):
|
2017-08-07 15:56:22 +02:00
|
|
|
if self.chunks is None or self.chunks_failed > 0:
|
2017-08-07 15:53:34 +02:00
|
|
|
return 'failed'
|
|
|
|
if self.chunks_successful == self.chunks:
|
|
|
|
return 'successful'
|
|
|
|
return 'pending'
|
2017-08-18 22:31:30 +02:00
|
|
|
|
|
|
|
ProtoEnum = db.Enum('http', 'https', name='enum_protocol')
|
|
|
|
|
|
|
|
class MastodonApp(db.Model, TimestampMixin):
|
|
|
|
__tablename__ = 'mastodon_apps'
|
|
|
|
|
|
|
|
instance = db.Column(db.String, primary_key=True)
|
|
|
|
client_id = db.Column(db.String, nullable=False)
|
|
|
|
client_secret = db.Column(db.String, nullable=False)
|
|
|
|
protocol = db.Column(ProtoEnum, nullable=False)
|