Unpacking modules
This commit is contained in:
parent
1e7787e24e
commit
a9006bf1bc
36 changed files with 971 additions and 822 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,3 +5,4 @@ uploads/
|
||||||
*.pyc
|
*.pyc
|
||||||
**~
|
**~
|
||||||
**/.*.swp
|
**/.*.swp
|
||||||
|
**/__pycache__/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.7-dev
|
||||||
|
|
||||||
|
* Biggest change: unpacking modules. The single `app.py` file has become an `app` package, with submodules `models.py`, `utils.py`, `filters.py`, `website.py` and `ajax.py`.
|
||||||
|
* Now `/about/` shows Python and Flask versions.
|
||||||
|
* Now the error 404 handler returns HTTP 404.
|
||||||
|
* Added user followers and following lists, accessible via `/+<username>/followers` and `/+<username>/following` and from the profile info box, linked to the followers/following number.
|
||||||
|
* Schema changes: added column `telegram` to `UserProfile` table. To update schema, execute the script `migrate_0_6_to_0_7.py`
|
||||||
|
|
||||||
## 0.6.0
|
## 0.6.0
|
||||||
|
|
||||||
* Added user adminship. Admins are users with very high privileges. Adminship can be assigned only at script level (not from the web).
|
* Added user adminship. Admins are users with very high privileges. Adminship can be assigned only at script level (not from the web).
|
||||||
|
|
|
||||||
796
app.py
796
app.py
|
|
@ -1,796 +0,0 @@
|
||||||
from flask import (
|
|
||||||
Flask, Markup, abort, flash, g, jsonify, redirect, render_template, request,
|
|
||||||
send_from_directory, session, url_for)
|
|
||||||
import hashlib
|
|
||||||
from peewee import *
|
|
||||||
import datetime, time, re, os, sys, string, json, html
|
|
||||||
from functools import wraps
|
|
||||||
import argparse
|
|
||||||
from flask_login import LoginManager, login_user, logout_user, login_required
|
|
||||||
|
|
||||||
__version__ = '0.6.0'
|
|
||||||
|
|
||||||
# we want to support Python 3 only.
|
|
||||||
# Python 2 has too many caveats.
|
|
||||||
if sys.version_info[0] < 3:
|
|
||||||
raise RuntimeError('Python 3 required')
|
|
||||||
|
|
||||||
arg_parser = argparse.ArgumentParser()
|
|
||||||
arg_parser.add_argument('--norun', action='store_true',
|
|
||||||
help='Don\'t run the app. Useful for debugging.')
|
|
||||||
arg_parser.add_argument('--debug', action='store_true',
|
|
||||||
help='Run the app in debug mode.')
|
|
||||||
arg_parser.add_argument('-p', '--port', type=int, default=5000,
|
|
||||||
help='The port where to run the app. Defaults to 5000')
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.config.from_pyfile('config.py')
|
|
||||||
|
|
||||||
login_manager = LoginManager(app)
|
|
||||||
|
|
||||||
### DATABASE ###
|
|
||||||
|
|
||||||
database = SqliteDatabase(app.config['DATABASE'])
|
|
||||||
|
|
||||||
class BaseModel(Model):
|
|
||||||
class Meta:
|
|
||||||
database = database
|
|
||||||
|
|
||||||
# A user. The user is separated from its page.
|
|
||||||
class User(BaseModel):
|
|
||||||
# The unique username.
|
|
||||||
username = CharField(unique=True)
|
|
||||||
# The password hash.
|
|
||||||
password = CharField()
|
|
||||||
# An email address.
|
|
||||||
email = CharField()
|
|
||||||
# The date of birth (required because of Terms of Service)
|
|
||||||
birthday = DateField()
|
|
||||||
# The date joined
|
|
||||||
join_date = DateTimeField()
|
|
||||||
# A disabled flag. 0 = active, 1 = disabled by user, 2 = banned
|
|
||||||
is_disabled = IntegerField(default=0)
|
|
||||||
|
|
||||||
# Helpers for flask_login
|
|
||||||
def get_id(self):
|
|
||||||
return str(self.id)
|
|
||||||
@property
|
|
||||||
def is_active(self):
|
|
||||||
return not self.is_disabled
|
|
||||||
@property
|
|
||||||
def is_anonymous(self):
|
|
||||||
return False
|
|
||||||
@property
|
|
||||||
def is_authenticated(self):
|
|
||||||
return self == get_current_user()
|
|
||||||
|
|
||||||
# it often makes sense to put convenience methods on model instances, for
|
|
||||||
# example, "give me all the users this user is following":
|
|
||||||
def following(self):
|
|
||||||
# query other users through the "relationship" table
|
|
||||||
return (User
|
|
||||||
.select()
|
|
||||||
.join(Relationship, on=Relationship.to_user)
|
|
||||||
.where(Relationship.from_user == self)
|
|
||||||
.order_by(User.username))
|
|
||||||
|
|
||||||
def followers(self):
|
|
||||||
return (User
|
|
||||||
.select()
|
|
||||||
.join(Relationship, on=Relationship.from_user)
|
|
||||||
.where(Relationship.to_user == self)
|
|
||||||
.order_by(User.username))
|
|
||||||
|
|
||||||
def is_following(self, user):
|
|
||||||
return (Relationship
|
|
||||||
.select()
|
|
||||||
.where(
|
|
||||||
(Relationship.from_user == self) &
|
|
||||||
(Relationship.to_user == user))
|
|
||||||
.exists())
|
|
||||||
|
|
||||||
def unseen_notification_count(self):
|
|
||||||
return len(Notification
|
|
||||||
.select()
|
|
||||||
.where(
|
|
||||||
(Notification.target == self) & (Notification.seen == 0)
|
|
||||||
))
|
|
||||||
# user adminship is stored into a separate table; new in 0.6
|
|
||||||
@property
|
|
||||||
def is_admin(self):
|
|
||||||
return UserAdminship.select().where(UserAdminship.user == self).exists()
|
|
||||||
# user profile info; new in 0.6
|
|
||||||
@property
|
|
||||||
def profile(self):
|
|
||||||
# lazy initialization; I don't want (and don't know how)
|
|
||||||
# to do schema changes.
|
|
||||||
try:
|
|
||||||
return UserProfile.get(UserProfile.user == self)
|
|
||||||
except UserProfile.DoesNotExist:
|
|
||||||
return UserProfile.create(user=self, full_name=self.username)
|
|
||||||
|
|
||||||
# User adminship.
|
|
||||||
# A very high privilege where users can review posts.
|
|
||||||
# For very few users only; new in 0.6
|
|
||||||
class UserAdminship(BaseModel):
|
|
||||||
user = ForeignKeyField(User, primary_key=True)
|
|
||||||
|
|
||||||
# User profile.
|
|
||||||
# Additional info for identifying users.
|
|
||||||
# New in 0.6
|
|
||||||
class UserProfile(BaseModel):
|
|
||||||
user = ForeignKeyField(User, primary_key=True)
|
|
||||||
full_name = TextField()
|
|
||||||
biography = TextField(default='')
|
|
||||||
location = IntegerField(null=True)
|
|
||||||
year = IntegerField(null=True)
|
|
||||||
website = TextField(null=True)
|
|
||||||
instagram = TextField(null=True)
|
|
||||||
facebook = TextField(null=True)
|
|
||||||
|
|
||||||
# The message privacy values.
|
|
||||||
MSGPRV_PUBLIC = 0 # everyone
|
|
||||||
MSGPRV_UNLISTED = 1 # everyone, doesn't show up in public timeline
|
|
||||||
MSGPRV_FRIENDS = 2 # only accounts which follow each other
|
|
||||||
MSGPRV_ONLYME = 3 # only the poster
|
|
||||||
|
|
||||||
# A single public message.
|
|
||||||
# New in v0.5: removed type and info fields; added privacy field.
|
|
||||||
class Message(BaseModel):
|
|
||||||
# The user who posted the message.
|
|
||||||
user = ForeignKeyField(User, backref='messages')
|
|
||||||
# The text of the message.
|
|
||||||
text = TextField()
|
|
||||||
# The posted date.
|
|
||||||
pub_date = DateTimeField()
|
|
||||||
# Info about privacy of the message.
|
|
||||||
privacy = IntegerField(default=MSGPRV_PUBLIC)
|
|
||||||
|
|
||||||
def is_visible(self, is_public_timeline=False):
|
|
||||||
user = self.user
|
|
||||||
cur_user = get_current_user()
|
|
||||||
privacy = self.privacy
|
|
||||||
if user == cur_user:
|
|
||||||
# short path
|
|
||||||
# also: don't show user's messages in public timeline
|
|
||||||
return not is_public_timeline
|
|
||||||
elif privacy == MSGPRV_PUBLIC:
|
|
||||||
return True
|
|
||||||
elif privacy == MSGPRV_UNLISTED:
|
|
||||||
# even if unlisted
|
|
||||||
return not is_public_timeline
|
|
||||||
elif privacy == MSGPRV_FRIENDS:
|
|
||||||
if cur_user is None:
|
|
||||||
return False
|
|
||||||
return user.is_following(cur_user) and cur_user.is_following(user)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# this model contains two foreign keys to user -- it essentially allows us to
|
|
||||||
# model a "many-to-many" relationship between users. by querying and joining
|
|
||||||
# on different columns we can expose who a user is "related to" and who is
|
|
||||||
# "related to" a given user
|
|
||||||
class Relationship(BaseModel):
|
|
||||||
from_user = ForeignKeyField(User, backref='relationships')
|
|
||||||
to_user = ForeignKeyField(User, backref='related_to')
|
|
||||||
created_date = DateTimeField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
indexes = (
|
|
||||||
# Specify a unique multi-column index on from/to-user.
|
|
||||||
(('from_user', 'to_user'), True),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
UPLOAD_DIRECTORY = 'uploads/'
|
|
||||||
|
|
||||||
# fixing directory name because of imports from other directory
|
|
||||||
if __name__ != '__main__':
|
|
||||||
UPLOAD_DIRECTORY = os.path.join(os.path.dirname(__file__), UPLOAD_DIRECTORY)
|
|
||||||
class Upload(BaseModel):
|
|
||||||
# the extension of the media
|
|
||||||
type = TextField()
|
|
||||||
# the message bound to this media
|
|
||||||
message = ForeignKeyField(Message, backref='uploads')
|
|
||||||
# helper to retrieve contents
|
|
||||||
def filename(self):
|
|
||||||
return str(self.id) + '.' + self.type
|
|
||||||
|
|
||||||
class Notification(BaseModel):
|
|
||||||
type = TextField()
|
|
||||||
target = ForeignKeyField(User, backref='notifications')
|
|
||||||
detail = TextField()
|
|
||||||
pub_date = DateTimeField()
|
|
||||||
seen = IntegerField(default=0)
|
|
||||||
|
|
||||||
def create_tables():
|
|
||||||
with database:
|
|
||||||
database.create_tables([
|
|
||||||
User, UserAdminship, UserProfile, Message, Relationship,
|
|
||||||
Upload, Notification])
|
|
||||||
if not os.path.isdir(UPLOAD_DIRECTORY):
|
|
||||||
os.makedirs(UPLOAD_DIRECTORY)
|
|
||||||
|
|
||||||
### UTILS ###
|
|
||||||
|
|
||||||
_forbidden_extensions = 'com net org txt'.split()
|
|
||||||
_username_characters = frozenset(string.ascii_letters + string.digits + '_')
|
|
||||||
|
|
||||||
def is_username(username):
|
|
||||||
username_splitted = username.split('.')
|
|
||||||
if username_splitted and username_splitted[-1] in _forbidden_extensions:
|
|
||||||
return False
|
|
||||||
return all(x and set(x) < _username_characters for x in username_splitted)
|
|
||||||
|
|
||||||
_mention_re = r'\+([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)'
|
|
||||||
|
|
||||||
def validate_birthday(date):
|
|
||||||
today = datetime.date.today()
|
|
||||||
if today.year - date.year > 13:
|
|
||||||
return True
|
|
||||||
if today.year - date.year < 13:
|
|
||||||
return False
|
|
||||||
if today.month > date.month:
|
|
||||||
return True
|
|
||||||
if today.month < date.month:
|
|
||||||
return False
|
|
||||||
if today.day >= date.day:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def validate_website(website):
|
|
||||||
return re.match(r'(?:https?://)?(?:[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*'
|
|
||||||
r'|\[[A-Fa-f0-9:]+\])(?::\d+)?(?:/[^\s]*)?(?:\?[^\s]*)?(?:#[^\s]*)?$',
|
|
||||||
website)
|
|
||||||
|
|
||||||
def human_short_date(timestamp):
|
|
||||||
return ''
|
|
||||||
|
|
||||||
@app.template_filter()
|
|
||||||
def human_date(date):
|
|
||||||
timestamp = date.timestamp()
|
|
||||||
today = int(time.time())
|
|
||||||
offset = today - timestamp
|
|
||||||
if offset <= 1:
|
|
||||||
return '1 second ago'
|
|
||||||
elif offset < 60:
|
|
||||||
return '%d seconds ago' % offset
|
|
||||||
elif offset < 120:
|
|
||||||
return '1 minute ago'
|
|
||||||
elif offset < 3600:
|
|
||||||
return '%d minutes ago' % (offset // 60)
|
|
||||||
elif offset < 7200:
|
|
||||||
return '1 hour ago'
|
|
||||||
elif offset < 86400:
|
|
||||||
return '%d hours ago' % (offset // 3600)
|
|
||||||
elif offset < 172800:
|
|
||||||
return '1 day ago'
|
|
||||||
elif offset < 604800:
|
|
||||||
return '%d days ago' % (offset // 86400)
|
|
||||||
else:
|
|
||||||
d = datetime.datetime.fromtimestamp(timestamp)
|
|
||||||
return d.strftime('%B %e, %Y')
|
|
||||||
|
|
||||||
def int_to_b64(n):
|
|
||||||
b = int(n).to_bytes(48, 'big')
|
|
||||||
return base64.b64encode(b).lstrip(b'A').decode()
|
|
||||||
|
|
||||||
def pwdhash(s):
|
|
||||||
return hashlib.md5((request.form['password']).encode('utf-8')).hexdigest()
|
|
||||||
|
|
||||||
def get_object_or_404(model, *expressions):
|
|
||||||
try:
|
|
||||||
return model.get(*expressions)
|
|
||||||
except model.DoesNotExist:
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
class Visibility(object):
|
|
||||||
'''
|
|
||||||
Workaround for the visibility problem for posts.
|
|
||||||
Cannot be directly resolved with filter().
|
|
||||||
|
|
||||||
TODO find a better solution, this seems to be too slow.
|
|
||||||
'''
|
|
||||||
def __init__(self, query, is_public_timeline=False):
|
|
||||||
self.query = query
|
|
||||||
self.is_public_timeline = is_public_timeline
|
|
||||||
def __iter__(self):
|
|
||||||
for i in self.query:
|
|
||||||
if i.is_visible(self.is_public_timeline):
|
|
||||||
yield i
|
|
||||||
def count(self):
|
|
||||||
counter = 0
|
|
||||||
for i in self.query:
|
|
||||||
if i.is_visible(self.is_public_timeline):
|
|
||||||
counter += 1
|
|
||||||
return counter
|
|
||||||
def paginate(self, page):
|
|
||||||
counter = 0
|
|
||||||
pages_no = range((page - 1) * 20, page * 20)
|
|
||||||
for i in self.query:
|
|
||||||
if i.is_visible(self.is_public_timeline):
|
|
||||||
if counter in pages_no:
|
|
||||||
yield i
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
def get_locations():
|
|
||||||
data = {}
|
|
||||||
with open('locations.txt') as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.rstrip()
|
|
||||||
if line.startswith('#'):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
key, value = line.split(None, 1)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
data[key] = value
|
|
||||||
return data
|
|
||||||
|
|
||||||
try:
|
|
||||||
locations = get_locations()
|
|
||||||
except OSError:
|
|
||||||
locations = {}
|
|
||||||
|
|
||||||
# get the user from the session
|
|
||||||
# changed in 0.5 to comply with flask_login
|
|
||||||
def get_current_user():
|
|
||||||
user_id = session.get('user_id')
|
|
||||||
if user_id:
|
|
||||||
return User[user_id]
|
|
||||||
|
|
||||||
login_manager.login_view = 'login'
|
|
||||||
|
|
||||||
def push_notification(type, target, **kwargs):
|
|
||||||
try:
|
|
||||||
if isinstance(target, str):
|
|
||||||
target = User.get(User.username == target)
|
|
||||||
Notification.create(
|
|
||||||
type=type,
|
|
||||||
target=target,
|
|
||||||
detail=json.dumps(kwargs),
|
|
||||||
pub_date=datetime.datetime.now()
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
sys.excepthook(*sys.exc_info())
|
|
||||||
|
|
||||||
def unpush_notification(type, target, **kwargs):
|
|
||||||
try:
|
|
||||||
if isinstance(target, str):
|
|
||||||
target = User.get(User.username == target)
|
|
||||||
(Notification
|
|
||||||
.delete()
|
|
||||||
.where(
|
|
||||||
(Notification.type == type) &
|
|
||||||
(Notification.target == target) &
|
|
||||||
(Notification.detail == json.dumps(kwargs))
|
|
||||||
)
|
|
||||||
.execute())
|
|
||||||
except Exception:
|
|
||||||
sys.excepthook(*sys.exc_info())
|
|
||||||
|
|
||||||
# given a template and a SelectQuery instance, render a paginated list of
|
|
||||||
# objects from the query inside the template
|
|
||||||
def object_list(template_name, qr, var_name='object_list', **kwargs):
|
|
||||||
kwargs.update(
|
|
||||||
page=int(request.args.get('page', 1)),
|
|
||||||
pages=qr.count() // 20 + 1)
|
|
||||||
kwargs[var_name] = qr.paginate(kwargs['page'])
|
|
||||||
return render_template(template_name, **kwargs)
|
|
||||||
|
|
||||||
### WEB ###
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def before_request():
|
|
||||||
g.db = database
|
|
||||||
try:
|
|
||||||
g.db.connect()
|
|
||||||
except OperationalError:
|
|
||||||
sys.stderr.write('database connected twice.\n')
|
|
||||||
|
|
||||||
@app.after_request
|
|
||||||
def after_request(response):
|
|
||||||
g.db.close()
|
|
||||||
return response
|
|
||||||
|
|
||||||
@app.context_processor
|
|
||||||
def _inject_variables():
|
|
||||||
return {'site_name': app.config['SITE_NAME'], 'locations': locations}
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
|
||||||
def _inject_user(userid):
|
|
||||||
return User[userid]
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
|
||||||
def error_404(body):
|
|
||||||
return render_template('404.html')
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def homepage():
|
|
||||||
if get_current_user():
|
|
||||||
return private_timeline()
|
|
||||||
else:
|
|
||||||
return render_template('homepage.html')
|
|
||||||
|
|
||||||
def private_timeline():
|
|
||||||
# the private timeline (aka feed) exemplifies the use of a subquery -- we are asking for
|
|
||||||
# messages where the person who created the message is someone the current
|
|
||||||
# user is following. these messages are then ordered newest-first.
|
|
||||||
user = get_current_user()
|
|
||||||
messages = Visibility(Message
|
|
||||||
.select()
|
|
||||||
.where((Message.user << user.following())
|
|
||||||
| (Message.user == user))
|
|
||||||
.order_by(Message.pub_date.desc()))
|
|
||||||
# TODO change to "feed.html"
|
|
||||||
return object_list('private_messages.html', messages, 'message_list')
|
|
||||||
|
|
||||||
@app.route('/explore/')
|
|
||||||
def public_timeline():
|
|
||||||
messages = Visibility(Message
|
|
||||||
.select()
|
|
||||||
.order_by(Message.pub_date.desc()), True)
|
|
||||||
return object_list('explore.html', messages, 'message_list')
|
|
||||||
|
|
||||||
@app.route('/signup/', methods=['GET', 'POST'])
|
|
||||||
def register():
|
|
||||||
if request.method == 'POST' and request.form['username']:
|
|
||||||
try:
|
|
||||||
birthday = datetime.datetime.fromisoformat(request.form['birthday'])
|
|
||||||
except ValueError:
|
|
||||||
flash('Invalid date format')
|
|
||||||
return render_template('join.html')
|
|
||||||
username = request.form['username'].lower()
|
|
||||||
if not is_username(username):
|
|
||||||
flash('This username is invalid')
|
|
||||||
return render_template('join.html')
|
|
||||||
if username == getattr(get_current_user(), 'username', None) and not request.form.get('confirm_another'):
|
|
||||||
flash('You are already logged in. Please confirm you want to '
|
|
||||||
'create another account by checking the option.')
|
|
||||||
return render_template('join.html')
|
|
||||||
try:
|
|
||||||
with database.atomic():
|
|
||||||
# Attempt to create the user. If the username is taken, due to the
|
|
||||||
# unique constraint, the database will raise an IntegrityError.
|
|
||||||
user = User.create(
|
|
||||||
username=username,
|
|
||||||
password=pwdhash(request.form['password']),
|
|
||||||
email=request.form['email'],
|
|
||||||
birthday=birthday,
|
|
||||||
join_date=datetime.datetime.now())
|
|
||||||
UserProfile.create(
|
|
||||||
user=user,
|
|
||||||
full_name=request.form.get('full_name') or username
|
|
||||||
)
|
|
||||||
|
|
||||||
# mark the user as being 'authenticated' by setting the session vars
|
|
||||||
login_user(user)
|
|
||||||
return redirect(request.args.get('next','/'))
|
|
||||||
|
|
||||||
except IntegrityError:
|
|
||||||
flash('That username is already taken')
|
|
||||||
|
|
||||||
return render_template('join.html')
|
|
||||||
|
|
||||||
@app.route('/login/', methods=['GET', 'POST'])
|
|
||||||
def login():
|
|
||||||
if request.method == 'POST' and request.form['username']:
|
|
||||||
try:
|
|
||||||
username = request.form['username']
|
|
||||||
pw_hash = pwdhash(request.form['password'])
|
|
||||||
if '@' in username:
|
|
||||||
user = User.get(User.email == username)
|
|
||||||
else:
|
|
||||||
user = User.get(User.username == username)
|
|
||||||
if user.password != pw_hash:
|
|
||||||
flash('The password entered is incorrect.')
|
|
||||||
return render_template('login.html')
|
|
||||||
except User.DoesNotExist:
|
|
||||||
flash('A user with this username or email does not exist.')
|
|
||||||
else:
|
|
||||||
remember_for = int(request.form['remember'])
|
|
||||||
if remember_for > 0:
|
|
||||||
login_user(user, remember=True,
|
|
||||||
duration=datetime.timedelta(days=remember_for))
|
|
||||||
else:
|
|
||||||
login_user(user)
|
|
||||||
return redirect(request.args.get('next', '/'))
|
|
||||||
return render_template('login.html')
|
|
||||||
|
|
||||||
@app.route('/logout/')
|
|
||||||
def logout():
|
|
||||||
logout_user()
|
|
||||||
flash('You were logged out')
|
|
||||||
return redirect(request.args.get('next','/'))
|
|
||||||
|
|
||||||
@app.route('/+<username>/')
|
|
||||||
def user_detail(username):
|
|
||||||
user = get_object_or_404(User, User.username == username)
|
|
||||||
|
|
||||||
# get all the users messages ordered newest-first -- note how we're accessing
|
|
||||||
# the messages -- user.message_set. could also have written it as:
|
|
||||||
# Message.select().where(Message.user == user)
|
|
||||||
messages = Visibility(user.messages.order_by(Message.pub_date.desc()))
|
|
||||||
# TODO change to "profile.html"
|
|
||||||
return object_list('user_detail.html', messages, 'message_list', user=user)
|
|
||||||
|
|
||||||
@app.route('/+<username>/follow/', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
def user_follow(username):
|
|
||||||
cur_user = get_current_user()
|
|
||||||
user = get_object_or_404(User, User.username == username)
|
|
||||||
try:
|
|
||||||
with database.atomic():
|
|
||||||
Relationship.create(
|
|
||||||
from_user=cur_user,
|
|
||||||
to_user=user,
|
|
||||||
created_date=datetime.datetime.now())
|
|
||||||
except IntegrityError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
flash('You are following %s' % user.username)
|
|
||||||
push_notification('follow', user, user=cur_user.id)
|
|
||||||
return redirect(url_for('user_detail', username=user.username))
|
|
||||||
|
|
||||||
@app.route('/+<username>/unfollow/', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
def user_unfollow(username):
|
|
||||||
cur_user = get_current_user()
|
|
||||||
user = get_object_or_404(User, User.username == username)
|
|
||||||
(Relationship
|
|
||||||
.delete()
|
|
||||||
.where(
|
|
||||||
(Relationship.from_user == cur_user) &
|
|
||||||
(Relationship.to_user == user))
|
|
||||||
.execute())
|
|
||||||
flash('You are no longer following %s' % user.username)
|
|
||||||
unpush_notification('follow', user, user=cur_user.id)
|
|
||||||
return redirect(url_for('user_detail', username=user.username))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/create/', methods=['GET', 'POST'])
|
|
||||||
@login_required
|
|
||||||
def create():
|
|
||||||
user = get_current_user()
|
|
||||||
if request.method == 'POST' and request.form['text']:
|
|
||||||
text = request.form['text']
|
|
||||||
privacy = int(request.form.get('privacy', '0'))
|
|
||||||
message = Message.create(
|
|
||||||
user=user,
|
|
||||||
text=text,
|
|
||||||
pub_date=datetime.datetime.now(),
|
|
||||||
privacy=privacy)
|
|
||||||
file = request.files.get('file')
|
|
||||||
if file:
|
|
||||||
print('Uploading', file.filename)
|
|
||||||
ext = file.filename.split('.')[-1]
|
|
||||||
upload = Upload.create(
|
|
||||||
type=ext,
|
|
||||||
message=message
|
|
||||||
)
|
|
||||||
file.save(UPLOAD_DIRECTORY + str(upload.id) + '.' + ext)
|
|
||||||
# create mentions
|
|
||||||
mention_usernames = set()
|
|
||||||
for mo in re.finditer(_mention_re, text):
|
|
||||||
mention_usernames.add(mo.group(1))
|
|
||||||
# to avoid self mention
|
|
||||||
mention_usernames.difference_update({user.username})
|
|
||||||
for u in mention_usernames:
|
|
||||||
try:
|
|
||||||
mention_user = User.get(User.username == u)
|
|
||||||
if privacy in (MSGPRV_PUBLIC, MSGPRV_UNLISTED) or \
|
|
||||||
(privacy == MSGPRV_FRIENDS and
|
|
||||||
mention_user.is_following(user) and
|
|
||||||
user.is_following(mention_user)):
|
|
||||||
push_notification('mention', mention_user, user=user.id)
|
|
||||||
except User.DoesNotExist:
|
|
||||||
pass
|
|
||||||
flash('Your message has been posted successfully')
|
|
||||||
return redirect(url_for('user_detail', username=user.username))
|
|
||||||
return render_template('create.html')
|
|
||||||
|
|
||||||
@app.route('/edit/<int:id>', methods=['GET', 'POST'])
|
|
||||||
@login_required
|
|
||||||
def edit(id):
|
|
||||||
user = get_current_user()
|
|
||||||
message = get_object_or_404(Message, Message.id == id)
|
|
||||||
if message.user != user:
|
|
||||||
abort(404)
|
|
||||||
if request.method == 'POST' and (request.form['text'] != message.text or
|
|
||||||
request.form['privacy'] != message.privacy):
|
|
||||||
text = request.form['text']
|
|
||||||
privacy = int(request.form.get('privacy', '0'))
|
|
||||||
Message.update(
|
|
||||||
text=text,
|
|
||||||
privacy=privacy,
|
|
||||||
pub_date=datetime.datetime.now()
|
|
||||||
).where(Message.id == id).execute()
|
|
||||||
# edit uploads (skipped for now)
|
|
||||||
# create mentions
|
|
||||||
mention_usernames = set()
|
|
||||||
for mo in re.finditer(_mention_re, text):
|
|
||||||
mention_usernames.add(mo.group(1))
|
|
||||||
# to avoid self mention
|
|
||||||
mention_usernames.difference_update({user.username})
|
|
||||||
for u in mention_usernames:
|
|
||||||
try:
|
|
||||||
mention_user = User.get(User.username == u)
|
|
||||||
if privacy in (MSGPRV_PUBLIC, MSGPRV_UNLISTED) or \
|
|
||||||
(privacy == MSGPRV_FRIENDS and
|
|
||||||
mention_user.is_following(user) and
|
|
||||||
user.is_following(mention_user)):
|
|
||||||
push_notification('mention', mention_user, user=user.id)
|
|
||||||
except User.DoesNotExist:
|
|
||||||
pass
|
|
||||||
flash('Your message has been edited successfully')
|
|
||||||
return redirect(url_for('user_detail', username=user.username))
|
|
||||||
return render_template('edit.html', message=message)
|
|
||||||
|
|
||||||
#@app.route('/delete/<int:id>', methods=['GET', 'POST'])
|
|
||||||
#def confirm_delete(id):
|
|
||||||
# return render_template('confirm_delete.html')
|
|
||||||
|
|
||||||
# Workaround for problems related to invalid data.
|
|
||||||
# Without that, changes will be lost across requests.
|
|
||||||
def profile_checkpoint():
|
|
||||||
return UserProfile(
|
|
||||||
user=get_current_user(),
|
|
||||||
full_name=request.form['full_name'],
|
|
||||||
biography=request.form['biography'],
|
|
||||||
location=int(request.form['location']),
|
|
||||||
year=int(request.form['year'] if request.form.get('has_year') else '0'),
|
|
||||||
website=request.form['website'] or None,
|
|
||||||
instagram=request.form['instagram'] or None,
|
|
||||||
facebook=request.form['facebook'] or None
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.route('/edit_profile/', methods=['GET', 'POST'])
|
|
||||||
def edit_profile():
|
|
||||||
if request.method == 'POST':
|
|
||||||
user = get_current_user()
|
|
||||||
username = request.form['username']
|
|
||||||
if not username:
|
|
||||||
# prevent username to be set to empty
|
|
||||||
username = user.username
|
|
||||||
if username != user.username:
|
|
||||||
try:
|
|
||||||
User.update(username=username).where(User.id == user.id).execute()
|
|
||||||
except IntegrityError:
|
|
||||||
flash('That username is already taken')
|
|
||||||
return render_template('edit_profile.html', profile=profile_checkpoint())
|
|
||||||
website = request.form['website'].strip().replace(' ', '%20')
|
|
||||||
if website and not validate_website(website):
|
|
||||||
flash('You should enter a valid URL.')
|
|
||||||
return render_template('edit_profile.html', profile=profile_checkpoint())
|
|
||||||
location = int(request.form.get('location'))
|
|
||||||
if location == 0:
|
|
||||||
location = None
|
|
||||||
UserProfile.update(
|
|
||||||
full_name=request.form['full_name'] or username,
|
|
||||||
biography=request.form['biography'],
|
|
||||||
year=request.form['year'] if request.form.get('has_year') else None,
|
|
||||||
location=location,
|
|
||||||
website=website,
|
|
||||||
instagram=request.form['instagram'],
|
|
||||||
facebook=request.form['facebook']
|
|
||||||
).where(UserProfile.user == user).execute()
|
|
||||||
return redirect(url_for('user_detail', username=username))
|
|
||||||
return render_template('edit_profile.html')
|
|
||||||
|
|
||||||
@app.route('/notifications/')
|
|
||||||
@login_required
|
|
||||||
def notifications():
|
|
||||||
user = get_current_user()
|
|
||||||
notifications = (Notification
|
|
||||||
.select()
|
|
||||||
.where(Notification.target == user)
|
|
||||||
.order_by(Notification.pub_date.desc()))
|
|
||||||
|
|
||||||
with database.atomic():
|
|
||||||
(Notification
|
|
||||||
.update(seen=1)
|
|
||||||
.where((Notification.target == user) & (Notification.seen == 0))
|
|
||||||
.execute())
|
|
||||||
return object_list('notifications.html', notifications, 'notification_list', json=json, User=User)
|
|
||||||
|
|
||||||
@app.route('/about/')
|
|
||||||
def about():
|
|
||||||
return render_template('about.html', version=__version__)
|
|
||||||
|
|
||||||
# The two following routes are mandatory by law.
|
|
||||||
@app.route('/terms/')
|
|
||||||
def terms():
|
|
||||||
return render_template('terms.html')
|
|
||||||
|
|
||||||
@app.route('/privacy/')
|
|
||||||
def privacy():
|
|
||||||
return render_template('privacy.html')
|
|
||||||
|
|
||||||
@app.route('/robots.txt')
|
|
||||||
def robots_txt():
|
|
||||||
return send_from_directory(os.getcwd(), 'robots.txt')
|
|
||||||
|
|
||||||
@app.route('/uploads/<id>.<type>')
|
|
||||||
def uploads(id, type='jpg'):
|
|
||||||
return send_from_directory(UPLOAD_DIRECTORY, id + '.' + type)
|
|
||||||
|
|
||||||
@app.route('/ajax/username_availability/<username>')
|
|
||||||
def username_availability(username):
|
|
||||||
current = get_current_user()
|
|
||||||
if current:
|
|
||||||
current = current.username
|
|
||||||
else:
|
|
||||||
current = None
|
|
||||||
is_valid = is_username(username)
|
|
||||||
if is_valid:
|
|
||||||
try:
|
|
||||||
user = User.get(User.username == username)
|
|
||||||
is_available = current == user.username
|
|
||||||
except User.DoesNotExist:
|
|
||||||
is_available = True
|
|
||||||
else:
|
|
||||||
is_available = False
|
|
||||||
return jsonify({'is_valid':is_valid, 'is_available':is_available, 'status':'ok'})
|
|
||||||
|
|
||||||
@app.route('/ajax/location_search/<name>')
|
|
||||||
def location_search(name):
|
|
||||||
results = []
|
|
||||||
for key, value in locations.items():
|
|
||||||
if value.lower().startswith(name.lower()):
|
|
||||||
results.append({'value': key, 'display': value})
|
|
||||||
return jsonify({'results': results})
|
|
||||||
|
|
||||||
_enrich_symbols = [
|
|
||||||
(r'\n', 'NEWLINE'),
|
|
||||||
(r'https?://(?:[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*|\[[A-Fa-f0-9:]+\])'
|
|
||||||
r'(?::\d+)?(?:/[^\s]*)?(?:\?[^\s]*)?(?:#[^\s]*)?', 'URL'),
|
|
||||||
(_mention_re, 'MENTION'),
|
|
||||||
(r'[^h\n+]+', 'TEXT'),
|
|
||||||
(r'.', 'TEXT')
|
|
||||||
]
|
|
||||||
|
|
||||||
def _tokenize(characters, table):
|
|
||||||
pos = 0
|
|
||||||
tokens = []
|
|
||||||
while pos < len(characters):
|
|
||||||
mo = None
|
|
||||||
for pattern, tag in table:
|
|
||||||
mo = re.compile(pattern).match(characters, pos)
|
|
||||||
if mo:
|
|
||||||
if tag:
|
|
||||||
text = mo.group(0)
|
|
||||||
tokens.append((text, tag))
|
|
||||||
break
|
|
||||||
pos = mo.end(0)
|
|
||||||
return tokens
|
|
||||||
|
|
||||||
@app.template_filter()
|
|
||||||
def enrich(s):
|
|
||||||
tokens = _tokenize(s, _enrich_symbols)
|
|
||||||
r = []
|
|
||||||
for text, tag in tokens:
|
|
||||||
if tag == 'TEXT':
|
|
||||||
r.append(html.escape(text))
|
|
||||||
elif tag == 'URL':
|
|
||||||
r.append('<a href="{0}">{0}</a>'.format(html.escape(text)))
|
|
||||||
elif tag == 'MENTION':
|
|
||||||
r.append('<span class="weak">+</span><a href="/{0}">{1}</a>'.format(text, text.lstrip('+')))
|
|
||||||
elif tag == 'NEWLINE':
|
|
||||||
r.append('<br>')
|
|
||||||
return Markup(''.join(r))
|
|
||||||
|
|
||||||
@app.template_filter('is_following')
|
|
||||||
def is_following(from_user, to_user):
|
|
||||||
return from_user.is_following(to_user)
|
|
||||||
|
|
||||||
@app.template_filter('locationdata')
|
|
||||||
def locationdata(key):
|
|
||||||
if key > 0:
|
|
||||||
return locations[str(key)]
|
|
||||||
|
|
||||||
# allow running from the command line
|
|
||||||
if __name__ == '__main__':
|
|
||||||
args = arg_parser.parse_args()
|
|
||||||
create_tables()
|
|
||||||
if not args.norun:
|
|
||||||
app.run(port=args.port, debug=args.debug)
|
|
||||||
87
app/__init__.py
Normal file
87
app/__init__.py
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
'''
|
||||||
|
Cori+
|
||||||
|
=====
|
||||||
|
|
||||||
|
The root module of the package.
|
||||||
|
This module also contains very basic web hooks, such as robots.txt.
|
||||||
|
|
||||||
|
For the website hooks, see `app.website`.
|
||||||
|
For the AJAX hook, see `app.ajax`.
|
||||||
|
For template filters, see `app.filters`.
|
||||||
|
For the database models, see `app.models`.
|
||||||
|
For other, see `app.utils`.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Flask, abort, flash, g, jsonify, redirect, render_template, request,
|
||||||
|
send_from_directory, session, url_for, __version__ as flask_version)
|
||||||
|
import hashlib
|
||||||
|
from peewee import *
|
||||||
|
import datetime, time, re, os, sys, string, json, html
|
||||||
|
from functools import wraps
|
||||||
|
from flask_login import LoginManager
|
||||||
|
|
||||||
|
__version__ = '0.7-dev'
|
||||||
|
|
||||||
|
# we want to support Python 3 only.
|
||||||
|
# Python 2 has too many caveats.
|
||||||
|
if sys.version_info[0] < 3:
|
||||||
|
raise RuntimeError('Python 3 required')
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_pyfile('../config.py')
|
||||||
|
|
||||||
|
login_manager = LoginManager(app)
|
||||||
|
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
from .utils import *
|
||||||
|
|
||||||
|
from .filters import *
|
||||||
|
|
||||||
|
### WEB ###
|
||||||
|
|
||||||
|
login_manager.login_view = 'website.login'
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def before_request():
|
||||||
|
g.db = database
|
||||||
|
try:
|
||||||
|
g.db.connect()
|
||||||
|
except OperationalError:
|
||||||
|
sys.stderr.write('database connected twice.\n')
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def after_request(response):
|
||||||
|
g.db.close()
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def _inject_variables():
|
||||||
|
return {'site_name': app.config['SITE_NAME'], 'locations': locations}
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def _inject_user(userid):
|
||||||
|
return User[userid]
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def error_404(body):
|
||||||
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
|
@app.route('/robots.txt')
|
||||||
|
def robots_txt():
|
||||||
|
return send_from_directory(os.getcwd(), 'robots.txt')
|
||||||
|
|
||||||
|
@app.route('/uploads/<id>.<type>')
|
||||||
|
def uploads(id, type='jpg'):
|
||||||
|
return send_from_directory(UPLOAD_DIRECTORY, id + '.' + type)
|
||||||
|
|
||||||
|
from .website import bp
|
||||||
|
app.register_blueprint(bp)
|
||||||
|
|
||||||
|
from .ajax import bp
|
||||||
|
app.register_blueprint(bp)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
29
app/__main__.py
Normal file
29
app/__main__.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
'''
|
||||||
|
Run the app as module.
|
||||||
|
|
||||||
|
You can also use `flask run` on the parent directory of the package.
|
||||||
|
|
||||||
|
XXX Using "--debug" argument currently causes an ImportError.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from . import app
|
||||||
|
from .models import create_tables
|
||||||
|
|
||||||
|
arg_parser = argparse.ArgumentParser()
|
||||||
|
arg_parser.add_argument('--norun', action='store_true',
|
||||||
|
help='Don\'t run the app. Useful for debugging.')
|
||||||
|
arg_parser.add_argument('--no-create-tables', action='store_true',
|
||||||
|
help='Don\'t create tables.')
|
||||||
|
arg_parser.add_argument('--debug', action='store_true',
|
||||||
|
help='Run the app in debug mode.')
|
||||||
|
arg_parser.add_argument('-p', '--port', type=int, default=5000,
|
||||||
|
help='The port where to run the app. Defaults to 5000')
|
||||||
|
|
||||||
|
args = arg_parser.parse_args()
|
||||||
|
|
||||||
|
if not args.no_create_tables:
|
||||||
|
create_tables()
|
||||||
|
|
||||||
|
if not args.norun:
|
||||||
|
app.run(port=args.port, debug=args.debug)
|
||||||
37
app/ajax.py
Normal file
37
app/ajax.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
'''
|
||||||
|
AJAX hooks for the website.
|
||||||
|
|
||||||
|
Warning: this is not the public API.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify
|
||||||
|
from .models import User
|
||||||
|
from .utils import locations, get_current_user, is_username
|
||||||
|
|
||||||
|
bp = Blueprint('ajax', __name__, url_prefix='/ajax')
|
||||||
|
|
||||||
|
@bp.route('/username_availability/<username>')
|
||||||
|
def username_availability(username):
|
||||||
|
current = get_current_user()
|
||||||
|
if current:
|
||||||
|
current = current.username
|
||||||
|
else:
|
||||||
|
current = None
|
||||||
|
is_valid = is_username(username)
|
||||||
|
if is_valid:
|
||||||
|
try:
|
||||||
|
user = User.get(User.username == username)
|
||||||
|
is_available = current == user.username
|
||||||
|
except User.DoesNotExist:
|
||||||
|
is_available = True
|
||||||
|
else:
|
||||||
|
is_available = False
|
||||||
|
return jsonify({'is_valid':is_valid, 'is_available':is_available, 'status':'ok'})
|
||||||
|
|
||||||
|
@bp.route('/location_search/<name>')
|
||||||
|
def location_search(name):
|
||||||
|
results = []
|
||||||
|
for key, value in locations.items():
|
||||||
|
if value.lower().startswith(name.lower()):
|
||||||
|
results.append({'value': key, 'display': value})
|
||||||
|
return jsonify({'results': results})
|
||||||
66
app/filters.py
Normal file
66
app/filters.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
'''
|
||||||
|
Filter functions used in the website templates.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from flask import Markup
|
||||||
|
import html, datetime, re, time
|
||||||
|
from .utils import tokenize
|
||||||
|
from . import app
|
||||||
|
|
||||||
|
@app.template_filter()
|
||||||
|
def human_date(date):
|
||||||
|
timestamp = date.timestamp()
|
||||||
|
today = int(time.time())
|
||||||
|
offset = today - timestamp
|
||||||
|
if offset <= 1:
|
||||||
|
return '1 second ago'
|
||||||
|
elif offset < 60:
|
||||||
|
return '%d seconds ago' % offset
|
||||||
|
elif offset < 120:
|
||||||
|
return '1 minute ago'
|
||||||
|
elif offset < 3600:
|
||||||
|
return '%d minutes ago' % (offset // 60)
|
||||||
|
elif offset < 7200:
|
||||||
|
return '1 hour ago'
|
||||||
|
elif offset < 86400:
|
||||||
|
return '%d hours ago' % (offset // 3600)
|
||||||
|
elif offset < 172800:
|
||||||
|
return '1 day ago'
|
||||||
|
elif offset < 604800:
|
||||||
|
return '%d days ago' % (offset // 86400)
|
||||||
|
else:
|
||||||
|
d = datetime.datetime.fromtimestamp(timestamp)
|
||||||
|
return d.strftime('%B %e, %Y')
|
||||||
|
|
||||||
|
_enrich_symbols = [
|
||||||
|
(r'\n', 'NEWLINE'),
|
||||||
|
(r'https?://(?:[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*|\[[A-Fa-f0-9:]+\])'
|
||||||
|
r'(?::\d+)?(?:/[^\s]*)?(?:\?[^\s]*)?(?:#[^\s]*)?', 'URL'),
|
||||||
|
(r'\+([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)', 'MENTION'),
|
||||||
|
(r'[^h\n+]+', 'TEXT'),
|
||||||
|
(r'.', 'TEXT')
|
||||||
|
]
|
||||||
|
|
||||||
|
@app.template_filter()
|
||||||
|
def enrich(s):
|
||||||
|
tokens = tokenize(s, _enrich_symbols)
|
||||||
|
r = []
|
||||||
|
for text, tag in tokens:
|
||||||
|
if tag == 'TEXT':
|
||||||
|
r.append(html.escape(text))
|
||||||
|
elif tag == 'URL':
|
||||||
|
r.append('<a href="{0}">{0}</a>'.format(html.escape(text)))
|
||||||
|
elif tag == 'MENTION':
|
||||||
|
r.append('<span class="weak">+</span><a href="/{0}">{1}</a>'.format(text, text.lstrip('+')))
|
||||||
|
elif tag == 'NEWLINE':
|
||||||
|
r.append('<br>')
|
||||||
|
return Markup(''.join(r))
|
||||||
|
|
||||||
|
@app.template_filter('is_following')
|
||||||
|
def is_following(from_user, to_user):
|
||||||
|
return from_user.is_following(to_user)
|
||||||
|
|
||||||
|
@app.template_filter('locationdata')
|
||||||
|
def locationdata(key):
|
||||||
|
if key > 0:
|
||||||
|
return locations[str(key)]
|
||||||
198
app/models.py
Normal file
198
app/models.py
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
'''
|
||||||
|
Database models for the application.
|
||||||
|
|
||||||
|
The tables are:
|
||||||
|
* user - the basic account info, such as username and password
|
||||||
|
* useradminship - relationship which existence determines whether a user is admin or not; new in 0.6
|
||||||
|
* userprofile - additional account info for self describing; new in 0.6
|
||||||
|
* message - a status update, appearing in profile and feeds
|
||||||
|
* relationship - a follow relationship between users
|
||||||
|
* upload - a file upload attached to a message; new in 0.2
|
||||||
|
* notification - a in-site notification to a user; new in 0.3
|
||||||
|
'''
|
||||||
|
|
||||||
|
from peewee import *
|
||||||
|
import os
|
||||||
|
# here should go `from .utils import get_current_user`, but it will cause
|
||||||
|
# import errors. It's instead imported at function level.
|
||||||
|
|
||||||
|
database = SqliteDatabase(os.path.join(os.getcwd(), 'coriplus.sqlite'))
|
||||||
|
|
||||||
|
class BaseModel(Model):
|
||||||
|
class Meta:
|
||||||
|
database = database
|
||||||
|
|
||||||
|
# A user. The user is separated from its page.
|
||||||
|
class User(BaseModel):
|
||||||
|
# The unique username.
|
||||||
|
username = CharField(unique=True)
|
||||||
|
# The password hash.
|
||||||
|
password = CharField()
|
||||||
|
# An email address.
|
||||||
|
email = CharField()
|
||||||
|
# The date of birth (required because of Terms of Service)
|
||||||
|
birthday = DateField()
|
||||||
|
# The date joined
|
||||||
|
join_date = DateTimeField()
|
||||||
|
# A disabled flag. 0 = active, 1 = disabled by user, 2 = banned
|
||||||
|
is_disabled = IntegerField(default=0)
|
||||||
|
|
||||||
|
# Helpers for flask_login
|
||||||
|
def get_id(self):
|
||||||
|
return str(self.id)
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
return not self.is_disabled
|
||||||
|
@property
|
||||||
|
def is_anonymous(self):
|
||||||
|
return False
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
from .utils import get_current_user
|
||||||
|
return self == get_current_user()
|
||||||
|
|
||||||
|
# it often makes sense to put convenience methods on model instances, for
|
||||||
|
# example, "give me all the users this user is following":
|
||||||
|
def following(self):
|
||||||
|
# query other users through the "relationship" table
|
||||||
|
return (User
|
||||||
|
.select()
|
||||||
|
.join(Relationship, on=Relationship.to_user)
|
||||||
|
.where(Relationship.from_user == self)
|
||||||
|
.order_by(User.username))
|
||||||
|
|
||||||
|
def followers(self):
|
||||||
|
return (User
|
||||||
|
.select()
|
||||||
|
.join(Relationship, on=Relationship.from_user)
|
||||||
|
.where(Relationship.to_user == self)
|
||||||
|
.order_by(User.username))
|
||||||
|
|
||||||
|
def is_following(self, user):
|
||||||
|
return (Relationship
|
||||||
|
.select()
|
||||||
|
.where(
|
||||||
|
(Relationship.from_user == self) &
|
||||||
|
(Relationship.to_user == user))
|
||||||
|
.exists())
|
||||||
|
|
||||||
|
def unseen_notification_count(self):
|
||||||
|
return len(Notification
|
||||||
|
.select()
|
||||||
|
.where(
|
||||||
|
(Notification.target == self) & (Notification.seen == 0)
|
||||||
|
))
|
||||||
|
# user adminship is stored into a separate table; new in 0.6
|
||||||
|
@property
|
||||||
|
def is_admin(self):
|
||||||
|
return UserAdminship.select().where(UserAdminship.user == self).exists()
|
||||||
|
# user profile info; new in 0.6
|
||||||
|
@property
|
||||||
|
def profile(self):
|
||||||
|
# lazy initialization; I don't want (and don't know how)
|
||||||
|
# to do schema changes.
|
||||||
|
try:
|
||||||
|
return UserProfile.get(UserProfile.user == self)
|
||||||
|
except UserProfile.DoesNotExist:
|
||||||
|
return UserProfile.create(user=self, full_name=self.username)
|
||||||
|
|
||||||
|
# User adminship.
|
||||||
|
# A very high privilege where users can review posts.
|
||||||
|
# For very few users only; new in 0.6
|
||||||
|
class UserAdminship(BaseModel):
|
||||||
|
user = ForeignKeyField(User, primary_key=True)
|
||||||
|
|
||||||
|
# User profile.
|
||||||
|
# Additional info for identifying users.
|
||||||
|
# New in 0.6
|
||||||
|
class UserProfile(BaseModel):
|
||||||
|
user = ForeignKeyField(User, primary_key=True)
|
||||||
|
full_name = TextField()
|
||||||
|
biography = TextField(default='')
|
||||||
|
location = IntegerField(null=True)
|
||||||
|
year = IntegerField(null=True)
|
||||||
|
website = TextField(null=True)
|
||||||
|
instagram = TextField(null=True)
|
||||||
|
facebook = TextField(null=True)
|
||||||
|
telegram = TextField(null=True)
|
||||||
|
|
||||||
|
# The message privacy values.
|
||||||
|
MSGPRV_PUBLIC = 0 # everyone
|
||||||
|
MSGPRV_UNLISTED = 1 # everyone, doesn't show up in public timeline
|
||||||
|
MSGPRV_FRIENDS = 2 # only accounts which follow each other
|
||||||
|
MSGPRV_ONLYME = 3 # only the poster
|
||||||
|
|
||||||
|
# A single public message.
|
||||||
|
# New in v0.5: removed type and info fields; added privacy field.
|
||||||
|
class Message(BaseModel):
|
||||||
|
# The user who posted the message.
|
||||||
|
user = ForeignKeyField(User, backref='messages')
|
||||||
|
# The text of the message.
|
||||||
|
text = TextField()
|
||||||
|
# The posted date.
|
||||||
|
pub_date = DateTimeField()
|
||||||
|
# Info about privacy of the message.
|
||||||
|
privacy = IntegerField(default=MSGPRV_PUBLIC)
|
||||||
|
|
||||||
|
def is_visible(self, is_public_timeline=False):
|
||||||
|
from .utils import get_current_user
|
||||||
|
user = self.user
|
||||||
|
cur_user = get_current_user()
|
||||||
|
privacy = self.privacy
|
||||||
|
if user == cur_user:
|
||||||
|
# short path
|
||||||
|
# also: don't show user's messages in public timeline
|
||||||
|
return not is_public_timeline
|
||||||
|
elif privacy == MSGPRV_PUBLIC:
|
||||||
|
return True
|
||||||
|
elif privacy == MSGPRV_UNLISTED:
|
||||||
|
# even if unlisted
|
||||||
|
return not is_public_timeline
|
||||||
|
elif privacy == MSGPRV_FRIENDS:
|
||||||
|
if cur_user is None:
|
||||||
|
return False
|
||||||
|
return user.is_following(cur_user) and cur_user.is_following(user)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# this model contains two foreign keys to user -- it essentially allows us to
|
||||||
|
# model a "many-to-many" relationship between users. by querying and joining
|
||||||
|
# on different columns we can expose who a user is "related to" and who is
|
||||||
|
# "related to" a given user
|
||||||
|
class Relationship(BaseModel):
|
||||||
|
from_user = ForeignKeyField(User, backref='relationships')
|
||||||
|
to_user = ForeignKeyField(User, backref='related_to')
|
||||||
|
created_date = DateTimeField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
indexes = (
|
||||||
|
# Specify a unique multi-column index on from/to-user.
|
||||||
|
(('from_user', 'to_user'), True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
UPLOAD_DIRECTORY = os.path.join(os.path.split(os.path.dirname(__file__))[0], 'uploads')
|
||||||
|
|
||||||
|
class Upload(BaseModel):
|
||||||
|
# the extension of the media
|
||||||
|
type = TextField()
|
||||||
|
# the message bound to this media
|
||||||
|
message = ForeignKeyField(Message, backref='uploads')
|
||||||
|
# helper to retrieve contents
|
||||||
|
def filename(self):
|
||||||
|
return str(self.id) + '.' + self.type
|
||||||
|
|
||||||
|
class Notification(BaseModel):
|
||||||
|
type = TextField()
|
||||||
|
target = ForeignKeyField(User, backref='notifications')
|
||||||
|
detail = TextField()
|
||||||
|
pub_date = DateTimeField()
|
||||||
|
seen = IntegerField(default=0)
|
||||||
|
|
||||||
|
def create_tables():
|
||||||
|
with database:
|
||||||
|
database.create_tables([
|
||||||
|
User, UserAdminship, UserProfile, Message, Relationship,
|
||||||
|
Upload, Notification])
|
||||||
|
if not os.path.isdir(UPLOAD_DIRECTORY):
|
||||||
|
os.makedirs(UPLOAD_DIRECTORY)
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h1>About {{ site_name }}</h1>
|
<h1>About {{ site_name }}</h1>
|
||||||
|
|
||||||
<p>Version: {{ version }}</p>
|
<p>{{ site_name }} {{ version }} – Python {{ python_version }} –
|
||||||
|
Flask {{ flask_version }}</p>
|
||||||
<p>Copyright © 2019 Sakuragasaki46.</p>
|
<p>Copyright © 2019 Sakuragasaki46.</p>
|
||||||
|
|
||||||
<h2>License</h2>
|
<h2>License</h2>
|
||||||
|
|
@ -9,21 +9,21 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1><a href="{{ url_for('homepage') }}">{{ site_name }}</a></h1>
|
<h1><a href="{{ url_for('website.homepage') }}">{{ site_name }}</a></h1>
|
||||||
<div class="metanav">
|
<div class="metanav">
|
||||||
{% if current_user.is_anonymous %}
|
{% if current_user.is_anonymous %}
|
||||||
<a href="{{ url_for('login') }}">log in</a>
|
<a href="{{ url_for('website.login', next=request.full_path) }}">log in</a>
|
||||||
<a href="{{ url_for('register') }}">register</a>
|
<a href="{{ url_for('website.register', next=request.full_path) }}">register</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('user_detail', username=current_user.username) }}">{{ current_user.username }}</a>
|
<a href="{{ url_for('website.user_detail', username=current_user.username) }}">{{ current_user.username }}</a>
|
||||||
{% set notification_count = current_user.unseen_notification_count() %}
|
{% set notification_count = current_user.unseen_notification_count() %}
|
||||||
{% if notification_count > 0 %}
|
{% if notification_count > 0 %}
|
||||||
<a href="{{ url_for('notifications') }}">(<strong>{{ notification_count }}</strong>)</a>
|
<a href="{{ url_for('website.notifications') }}">(<strong>{{ notification_count }}</strong>)</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
-
|
-
|
||||||
<a href="{{ url_for('public_timeline') }}">explore</a>
|
<a href="{{ url_for('website.public_timeline') }}">explore</a>
|
||||||
<a href="{{ url_for('create') }}">create</a>
|
<a href="{{ url_for('website.create') }}">create</a>
|
||||||
<a href="{{ url_for('logout') }}">log out</a>
|
<a href="{{ url_for('website.logout') }}">log out</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2>Create</h2>
|
<h2>Create</h2>
|
||||||
<form action="{{ url_for('create') }}" method="POST" enctype="multipart/form-data">
|
<form action="{{ url_for('website.create') }}" method="POST" enctype="multipart/form-data">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Message:</dt>
|
<dt>Message:</dt>
|
||||||
<dd><textarea name="text" placeholder="What's happening?" class="create_text">{{ request.args['preload'] }}</textarea></dd>
|
<dd><textarea name="text" placeholder="What's happening?" class="create_text">{{ request.args['preload'] }}</textarea></dd>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2>Edit</h2>
|
<h2>Edit</h2>
|
||||||
<form action="{{ url_for('edit', id=message.id) }}" method="POST" enctype="multipart/form-data">
|
<form action="{{ url_for('website.edit', id=message.id) }}" method="POST" enctype="multipart/form-data">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Message:</dt>
|
<dt>Message:</dt>
|
||||||
<dd><textarea name="text" required="" class="create_text">{{ message.text }}</textarea></dd>
|
<dd><textarea name="text" required="" class="create_text">{{ message.text }}</textarea></dd>
|
||||||
|
|
@ -27,6 +27,8 @@
|
||||||
<dd><input type="text" name="instagram" value="{{ profile.instagram or '' }}"></dd>
|
<dd><input type="text" name="instagram" value="{{ profile.instagram or '' }}"></dd>
|
||||||
<dt>Facebook:</dt>
|
<dt>Facebook:</dt>
|
||||||
<dd><input type="text" name="facebook" value="{{ profile.facebook or '' }}"></dd>
|
<dd><input type="text" name="facebook" value="{{ profile.facebook or '' }}"></dd>
|
||||||
|
<dt>Telegram:</dt>
|
||||||
|
<dd><input type="text" name="telegram" value="{{ profile.telegram or '' }}"></dd>
|
||||||
<dd><input type="submit" value="Save"></dd>
|
<dd><input type="submit" value="Save"></dd>
|
||||||
</dl>
|
</dl>
|
||||||
</form>
|
</form>
|
||||||
7
app/templates/homepage.html
Normal file
7
app/templates/homepage.html
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block body %}
|
||||||
|
<h2>Hello</h2>
|
||||||
|
|
||||||
|
<p>{{ site_name }} is made by people like you. <br/>
|
||||||
|
<a href="{{ url_for('website.login') }}">Log in</a> or <a href="{{ url_for('website.register') }}">register</a> to see more.</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -19,12 +19,15 @@
|
||||||
{% if profile.facebook %}
|
{% if profile.facebook %}
|
||||||
<p><span class="weak">Facebook:</span> <a href="https://facebook.com/{{ profile.facebook }}">{{ profile.facebook }}</a></p>
|
<p><span class="weak">Facebook:</span> <a href="https://facebook.com/{{ profile.facebook }}">{{ profile.facebook }}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if profile.telegram %}
|
||||||
|
<p><span class="weak">Telegram:</span> <a href="https://t.me/{{ profile.facebook }}">{{ profile.telegram }}</a></p>
|
||||||
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ user.messages|count }}</strong> messages
|
<strong>{{ user.messages|count }}</strong> messages
|
||||||
-
|
-
|
||||||
<strong>{{ user.followers()|count }}</strong> followers
|
<a href="{{ url_for('website.user_followers', username=user.username) }}"><strong>{{ user.followers()|count }}</strong></a> followers
|
||||||
-
|
-
|
||||||
<strong>{{ user.following()|count }}</strong> following
|
<a href="{{ url_for('website.user_following', username=user.username) }}"><strong>{{ user.following()|count }}</strong></a> following
|
||||||
</p>
|
</p>
|
||||||
{% if user == current_user %}
|
{% if user == current_user %}
|
||||||
<p><a href="/edit_profile/">Edit profile</a></p>
|
<p><a href="/edit_profile/">Edit profile</a></p>
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<p class="message-content">{{ message.text|enrich }}</p>
|
<p class="message-content">{{ message.text|enrich }}</p>
|
||||||
{% if message.uploads %}
|
{% if message.uploads %}
|
||||||
<div class="message-visual">
|
<div class="message-visual">
|
||||||
<img src="/uploads/{{message.uploads[0].filename()}}">
|
<img src="/uploads/{{ message.uploads[0].filename() }}">
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="message-footer">
|
<p class="message-footer">
|
||||||
<a href="{{ url_for('user_detail', username=message.user.username) }}">{{ message.user.username }}</a>
|
<a href="{{ url_for('website.user_detail', username=message.user.username) }}">{{ message.user.username }}</a>
|
||||||
-
|
-
|
||||||
{% set message_privacy = message.privacy %}
|
{% set message_privacy = message.privacy %}
|
||||||
{% if message.privacy in (0, 1) %} Public
|
{% if message.privacy in (0, 1) %} Public
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2>Join {{ site_name }}</h2>
|
<h2>Join {{ site_name }}</h2>
|
||||||
<form action="{{ url_for('register') }}" method="POST">
|
<form action="{{ url_for('website.register') }}" method="POST">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Username:</dt>
|
<dt>Username:</dt>
|
||||||
<dd><input type="text" class="username-input" name="username" autocomplete="off"></dd>
|
<dd><input type="text" class="username-input" name="username" autocomplete="off"></dd>
|
||||||
|
|
@ -5,11 +5,11 @@
|
||||||
{% if not current_user.is_anonymous %}
|
{% if not current_user.is_anonymous %}
|
||||||
{% if user.username != current_user.username %}
|
{% if user.username != current_user.username %}
|
||||||
{% if current_user|is_following(user) %}
|
{% if current_user|is_following(user) %}
|
||||||
<form action="{{ url_for('user_unfollow', username=user.username) }}" method="post">
|
<form action="{{ url_for('website.user_unfollow', username=user.username) }}" method="post">
|
||||||
<input type="submit" class="follow_button following" value="- Un-follow" />
|
<input type="submit" class="follow_button following" value="- Un-follow" />
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form action="{{ url_for('user_follow', username=user.username) }}" method="post">
|
<form action="{{ url_for('website.user_follow', username=user.username) }}" method="post">
|
||||||
<input type="submit" class="follow_button" value="+ Follow" />
|
<input type="submit" class="follow_button" value="+ Follow" />
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
10
app/templates/user_list.html
Executable file
10
app/templates/user_list.html
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<ul>
|
||||||
|
{% for user in user_list %}
|
||||||
|
<li><a href="/+{{ user.username }}">{{ user.username }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
162
app/utils.py
Normal file
162
app/utils.py
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
'''
|
||||||
|
A list of utilities used across modules.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import datetime, re, base64, hashlib, string
|
||||||
|
from .models import User, Notification
|
||||||
|
from flask import abort, render_template, request, session
|
||||||
|
import sys, json
|
||||||
|
|
||||||
|
_forbidden_extensions = 'com net org txt'.split()
|
||||||
|
_username_characters = frozenset(string.ascii_letters + string.digits + '_')
|
||||||
|
|
||||||
|
def is_username(username):
|
||||||
|
username_splitted = username.split('.')
|
||||||
|
if username_splitted and username_splitted[-1] in _forbidden_extensions:
|
||||||
|
return False
|
||||||
|
return all(x and set(x) < _username_characters for x in username_splitted)
|
||||||
|
|
||||||
|
def validate_birthday(date):
|
||||||
|
today = datetime.date.today()
|
||||||
|
if today.year - date.year > 13:
|
||||||
|
return True
|
||||||
|
if today.year - date.year < 13:
|
||||||
|
return False
|
||||||
|
if today.month > date.month:
|
||||||
|
return True
|
||||||
|
if today.month < date.month:
|
||||||
|
return False
|
||||||
|
if today.day >= date.day:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def validate_website(website):
|
||||||
|
return re.match(r'(?:https?://)?(?:[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*'
|
||||||
|
r'|\[[A-Fa-f0-9:]+\])(?::\d+)?(?:/[^\s]*)?(?:\?[^\s]*)?(?:#[^\s]*)?$',
|
||||||
|
website)
|
||||||
|
|
||||||
|
def human_short_date(timestamp):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def int_to_b64(n):
|
||||||
|
b = int(n).to_bytes(48, 'big')
|
||||||
|
return base64.b64encode(b).lstrip(b'A').decode()
|
||||||
|
|
||||||
|
def pwdhash(s):
|
||||||
|
return hashlib.md5((request.form['password']).encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
def get_object_or_404(model, *expressions):
|
||||||
|
try:
|
||||||
|
return model.get(*expressions)
|
||||||
|
except model.DoesNotExist:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
class Visibility(object):
|
||||||
|
'''
|
||||||
|
Workaround for the visibility problem for posts.
|
||||||
|
Cannot be directly resolved with filter().
|
||||||
|
|
||||||
|
TODO find a better solution, this seems to be too slow.
|
||||||
|
'''
|
||||||
|
def __init__(self, query, is_public_timeline=False):
|
||||||
|
self.query = query
|
||||||
|
self.is_public_timeline = is_public_timeline
|
||||||
|
def __iter__(self):
|
||||||
|
for i in self.query:
|
||||||
|
if i.is_visible(self.is_public_timeline):
|
||||||
|
yield i
|
||||||
|
def count(self):
|
||||||
|
counter = 0
|
||||||
|
for i in self.query:
|
||||||
|
if i.is_visible(self.is_public_timeline):
|
||||||
|
counter += 1
|
||||||
|
return counter
|
||||||
|
def paginate(self, page):
|
||||||
|
counter = 0
|
||||||
|
pages_no = range((page - 1) * 20, page * 20)
|
||||||
|
for i in self.query:
|
||||||
|
if i.is_visible(self.is_public_timeline):
|
||||||
|
if counter in pages_no:
|
||||||
|
yield i
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
def get_locations():
|
||||||
|
data = {}
|
||||||
|
with open('locations.txt') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.rstrip()
|
||||||
|
if line.startswith('#'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
key, value = line.split(None, 1)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
data[key] = value
|
||||||
|
return data
|
||||||
|
|
||||||
|
try:
|
||||||
|
locations = get_locations()
|
||||||
|
except OSError:
|
||||||
|
locations = {}
|
||||||
|
|
||||||
|
# get the user from the session
|
||||||
|
# changed in 0.5 to comply with flask_login
|
||||||
|
def get_current_user():
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if user_id:
|
||||||
|
return User[user_id]
|
||||||
|
|
||||||
|
def push_notification(type, target, **kwargs):
|
||||||
|
try:
|
||||||
|
if isinstance(target, str):
|
||||||
|
target = User.get(User.username == target)
|
||||||
|
Notification.create(
|
||||||
|
type=type,
|
||||||
|
target=target,
|
||||||
|
detail=json.dumps(kwargs),
|
||||||
|
pub_date=datetime.datetime.now()
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
|
||||||
|
def unpush_notification(type, target, **kwargs):
|
||||||
|
try:
|
||||||
|
if isinstance(target, str):
|
||||||
|
target = User.get(User.username == target)
|
||||||
|
(Notification
|
||||||
|
.delete()
|
||||||
|
.where(
|
||||||
|
(Notification.type == type) &
|
||||||
|
(Notification.target == target) &
|
||||||
|
(Notification.detail == json.dumps(kwargs))
|
||||||
|
)
|
||||||
|
.execute())
|
||||||
|
except Exception:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
|
||||||
|
# given a template and a SelectQuery instance, render a paginated list of
|
||||||
|
# objects from the query inside the template
|
||||||
|
def object_list(template_name, qr, var_name='object_list', **kwargs):
|
||||||
|
kwargs.update(
|
||||||
|
page=int(request.args.get('page', 1)),
|
||||||
|
pages=qr.count() // 20 + 1)
|
||||||
|
kwargs[var_name] = qr.paginate(kwargs['page'])
|
||||||
|
return render_template(template_name, **kwargs)
|
||||||
|
|
||||||
|
def tokenize(characters, table):
|
||||||
|
'''
|
||||||
|
A useful tokenizer.
|
||||||
|
'''
|
||||||
|
pos = 0
|
||||||
|
tokens = []
|
||||||
|
while pos < len(characters):
|
||||||
|
mo = None
|
||||||
|
for pattern, tag in table:
|
||||||
|
mo = re.compile(pattern).match(characters, pos)
|
||||||
|
if mo:
|
||||||
|
if tag:
|
||||||
|
text = mo.group(0)
|
||||||
|
tokens.append((text, tag))
|
||||||
|
break
|
||||||
|
pos = mo.end(0)
|
||||||
|
return tokens
|
||||||
332
app/website.py
Normal file
332
app/website.py
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
'''
|
||||||
|
All website hooks, excluding AJAX.
|
||||||
|
'''
|
||||||
|
|
||||||
|
from .utils import *
|
||||||
|
from .models import *
|
||||||
|
from . import __version__ as app_version
|
||||||
|
from sys import version as python_version
|
||||||
|
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for, __version__ as flask_version
|
||||||
|
from flask_login import login_required, login_user, logout_user
|
||||||
|
import json
|
||||||
|
|
||||||
|
bp = Blueprint('website', __name__)
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
def homepage():
|
||||||
|
if get_current_user():
|
||||||
|
return private_timeline()
|
||||||
|
else:
|
||||||
|
return render_template('homepage.html')
|
||||||
|
|
||||||
|
def private_timeline():
|
||||||
|
# the private timeline (aka feed) exemplifies the use of a subquery -- we are asking for
|
||||||
|
# messages where the person who created the message is someone the current
|
||||||
|
# user is following. these messages are then ordered newest-first.
|
||||||
|
user = get_current_user()
|
||||||
|
messages = Visibility(Message
|
||||||
|
.select()
|
||||||
|
.where((Message.user << user.following())
|
||||||
|
| (Message.user == user))
|
||||||
|
.order_by(Message.pub_date.desc()))
|
||||||
|
# TODO change to "feed.html"
|
||||||
|
return object_list('private_messages.html', messages, 'message_list')
|
||||||
|
|
||||||
|
@bp.route('/explore/')
|
||||||
|
def public_timeline():
|
||||||
|
messages = Visibility(Message
|
||||||
|
.select()
|
||||||
|
.order_by(Message.pub_date.desc()), True)
|
||||||
|
return object_list('explore.html', messages, 'message_list')
|
||||||
|
|
||||||
|
@bp.route('/signup/', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
if request.method == 'POST' and request.form['username']:
|
||||||
|
try:
|
||||||
|
birthday = datetime.datetime.fromisoformat(request.form['birthday'])
|
||||||
|
except ValueError:
|
||||||
|
flash('Invalid date format')
|
||||||
|
return render_template('join.html')
|
||||||
|
username = request.form['username'].lower()
|
||||||
|
if not is_username(username):
|
||||||
|
flash('This username is invalid')
|
||||||
|
return render_template('join.html')
|
||||||
|
if username == getattr(get_current_user(), 'username', None) and not request.form.get('confirm_another'):
|
||||||
|
flash('You are already logged in. Please confirm you want to '
|
||||||
|
'create another account by checking the option.')
|
||||||
|
return render_template('join.html')
|
||||||
|
try:
|
||||||
|
with database.atomic():
|
||||||
|
# Attempt to create the user. If the username is taken, due to the
|
||||||
|
# unique constraint, the database will raise an IntegrityError.
|
||||||
|
user = User.create(
|
||||||
|
username=username,
|
||||||
|
password=pwdhash(request.form['password']),
|
||||||
|
email=request.form['email'],
|
||||||
|
birthday=birthday,
|
||||||
|
join_date=datetime.datetime.now())
|
||||||
|
UserProfile.create(
|
||||||
|
user=user,
|
||||||
|
full_name=request.form.get('full_name') or username
|
||||||
|
)
|
||||||
|
|
||||||
|
# mark the user as being 'authenticated' by setting the session vars
|
||||||
|
login_user(user)
|
||||||
|
return redirect(request.args.get('next','/'))
|
||||||
|
|
||||||
|
except IntegrityError:
|
||||||
|
flash('That username is already taken')
|
||||||
|
|
||||||
|
return render_template('join.html')
|
||||||
|
|
||||||
|
@bp.route('/login/', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if request.method == 'POST' and request.form['username']:
|
||||||
|
try:
|
||||||
|
username = request.form['username']
|
||||||
|
pw_hash = pwdhash(request.form['password'])
|
||||||
|
if '@' in username:
|
||||||
|
user = User.get(User.email == username)
|
||||||
|
else:
|
||||||
|
user = User.get(User.username == username)
|
||||||
|
if user.password != pw_hash:
|
||||||
|
flash('The password entered is incorrect.')
|
||||||
|
return render_template('login.html')
|
||||||
|
except User.DoesNotExist:
|
||||||
|
flash('A user with this username or email does not exist.')
|
||||||
|
else:
|
||||||
|
remember_for = int(request.form['remember'])
|
||||||
|
if remember_for > 0:
|
||||||
|
login_user(user, remember=True,
|
||||||
|
duration=datetime.timedelta(days=remember_for))
|
||||||
|
else:
|
||||||
|
login_user(user)
|
||||||
|
return redirect(request.args.get('next', '/'))
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
@bp.route('/logout/')
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
flash('You were logged out')
|
||||||
|
return redirect(request.args.get('next','/'))
|
||||||
|
|
||||||
|
@bp.route('/+<username>/')
|
||||||
|
def user_detail(username):
|
||||||
|
user = get_object_or_404(User, User.username == username)
|
||||||
|
|
||||||
|
# get all the users messages ordered newest-first -- note how we're accessing
|
||||||
|
# the messages -- user.message_set. could also have written it as:
|
||||||
|
# Message.select().where(Message.user == user)
|
||||||
|
messages = Visibility(user.messages.order_by(Message.pub_date.desc()))
|
||||||
|
# TODO change to "profile.html"
|
||||||
|
return object_list('user_detail.html', messages, 'message_list', user=user)
|
||||||
|
|
||||||
|
@bp.route('/+<username>/follow/', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def user_follow(username):
|
||||||
|
cur_user = get_current_user()
|
||||||
|
user = get_object_or_404(User, User.username == username)
|
||||||
|
try:
|
||||||
|
with database.atomic():
|
||||||
|
Relationship.create(
|
||||||
|
from_user=cur_user,
|
||||||
|
to_user=user,
|
||||||
|
created_date=datetime.datetime.now())
|
||||||
|
except IntegrityError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
flash('You are following %s' % user.username)
|
||||||
|
push_notification('follow', user, user=cur_user.id)
|
||||||
|
return redirect(url_for('website.user_detail', username=user.username))
|
||||||
|
|
||||||
|
@bp.route('/+<username>/unfollow/', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def user_unfollow(username):
|
||||||
|
cur_user = get_current_user()
|
||||||
|
user = get_object_or_404(User, User.username == username)
|
||||||
|
(Relationship
|
||||||
|
.delete()
|
||||||
|
.where(
|
||||||
|
(Relationship.from_user == cur_user) &
|
||||||
|
(Relationship.to_user == user))
|
||||||
|
.execute())
|
||||||
|
flash('You are no longer following %s' % user.username)
|
||||||
|
unpush_notification('follow', user, user=cur_user.id)
|
||||||
|
return redirect(url_for('website.user_detail', username=user.username))
|
||||||
|
|
||||||
|
@bp.route('/+<username>/followers/')
|
||||||
|
@login_required
|
||||||
|
def user_followers(username):
|
||||||
|
user = get_object_or_404(User, User.username == username)
|
||||||
|
return object_list('user_list.html', user.followers(), 'user_list',
|
||||||
|
title='%s\'s followers' % username)
|
||||||
|
|
||||||
|
@bp.route('/+<username>/following/')
|
||||||
|
@login_required
|
||||||
|
def user_following(username):
|
||||||
|
user = get_object_or_404(User, User.username == username)
|
||||||
|
return object_list('user_list.html', user.following(), 'user_list',
|
||||||
|
title='Accounts followed by %s' % username)
|
||||||
|
|
||||||
|
@bp.route('/create/', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def create():
|
||||||
|
user = get_current_user()
|
||||||
|
if request.method == 'POST' and request.form['text']:
|
||||||
|
text = request.form['text']
|
||||||
|
privacy = int(request.form.get('privacy', '0'))
|
||||||
|
message = Message.create(
|
||||||
|
user=user,
|
||||||
|
text=text,
|
||||||
|
pub_date=datetime.datetime.now(),
|
||||||
|
privacy=privacy)
|
||||||
|
file = request.files.get('file')
|
||||||
|
if file:
|
||||||
|
print('Uploading', file.filename)
|
||||||
|
ext = file.filename.split('.')[-1]
|
||||||
|
upload = Upload.create(
|
||||||
|
type=ext,
|
||||||
|
message=message
|
||||||
|
)
|
||||||
|
file.save(UPLOAD_DIRECTORY + str(upload.id) + '.' + ext)
|
||||||
|
# create mentions
|
||||||
|
mention_usernames = set()
|
||||||
|
for mo in re.finditer(r'\+([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)', text):
|
||||||
|
mention_usernames.add(mo.group(1))
|
||||||
|
# to avoid self mention
|
||||||
|
mention_usernames.difference_update({user.username})
|
||||||
|
for u in mention_usernames:
|
||||||
|
try:
|
||||||
|
mention_user = User.get(User.username == u)
|
||||||
|
if privacy in (MSGPRV_PUBLIC, MSGPRV_UNLISTED) or \
|
||||||
|
(privacy == MSGPRV_FRIENDS and
|
||||||
|
mention_user.is_following(user) and
|
||||||
|
user.is_following(mention_user)):
|
||||||
|
push_notification('mention', mention_user, user=user.id)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
|
flash('Your message has been posted successfully')
|
||||||
|
return redirect(url_for('website.user_detail', username=user.username))
|
||||||
|
return render_template('create.html')
|
||||||
|
|
||||||
|
@bp.route('/edit/<int:id>', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def edit(id):
|
||||||
|
user = get_current_user()
|
||||||
|
message = get_object_or_404(Message, Message.id == id)
|
||||||
|
if message.user != user:
|
||||||
|
abort(404)
|
||||||
|
if request.method == 'POST' and (request.form['text'] != message.text or
|
||||||
|
request.form['privacy'] != message.privacy):
|
||||||
|
text = request.form['text']
|
||||||
|
privacy = int(request.form.get('privacy', '0'))
|
||||||
|
Message.update(
|
||||||
|
text=text,
|
||||||
|
privacy=privacy,
|
||||||
|
pub_date=datetime.datetime.now()
|
||||||
|
).where(Message.id == id).execute()
|
||||||
|
# edit uploads (skipped for now)
|
||||||
|
# create mentions
|
||||||
|
mention_usernames = set()
|
||||||
|
for mo in re.finditer(r'\+([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)', text):
|
||||||
|
mention_usernames.add(mo.group(1))
|
||||||
|
# to avoid self mention
|
||||||
|
mention_usernames.difference_update({user.username})
|
||||||
|
for u in mention_usernames:
|
||||||
|
try:
|
||||||
|
mention_user = User.get(User.username == u)
|
||||||
|
if privacy in (MSGPRV_PUBLIC, MSGPRV_UNLISTED) or \
|
||||||
|
(privacy == MSGPRV_FRIENDS and
|
||||||
|
mention_user.is_following(user) and
|
||||||
|
user.is_following(mention_user)):
|
||||||
|
push_notification('mention', mention_user, user=user.id)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
|
flash('Your message has been edited successfully')
|
||||||
|
return redirect(url_for('website.user_detail', username=user.username))
|
||||||
|
return render_template('edit.html', message=message)
|
||||||
|
|
||||||
|
#@bp.route('/delete/<int:id>', methods=['GET', 'POST'])
|
||||||
|
#def confirm_delete(id):
|
||||||
|
# return render_template('confirm_delete.html')
|
||||||
|
|
||||||
|
# Workaround for problems related to invalid data.
|
||||||
|
# Without that, changes will be lost across requests.
|
||||||
|
def profile_checkpoint():
|
||||||
|
return UserProfile(
|
||||||
|
user=get_current_user(),
|
||||||
|
full_name=request.form['full_name'],
|
||||||
|
biography=request.form['biography'],
|
||||||
|
location=int(request.form['location']),
|
||||||
|
year=int(request.form['year'] if request.form.get('has_year') else '0'),
|
||||||
|
website=request.form['website'] or None,
|
||||||
|
instagram=request.form['instagram'] or None,
|
||||||
|
facebook=request.form['facebook'] or None
|
||||||
|
)
|
||||||
|
|
||||||
|
@bp.route('/edit_profile/', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def edit_profile():
|
||||||
|
if request.method == 'POST':
|
||||||
|
user = get_current_user()
|
||||||
|
username = request.form['username']
|
||||||
|
if not username:
|
||||||
|
# prevent username to be set to empty
|
||||||
|
username = user.username
|
||||||
|
if username != user.username:
|
||||||
|
try:
|
||||||
|
User.update(username=username).where(User.id == user.id).execute()
|
||||||
|
except IntegrityError:
|
||||||
|
flash('That username is already taken')
|
||||||
|
return render_template('edit_profile.html', profile=profile_checkpoint())
|
||||||
|
website = request.form['website'].strip().replace(' ', '%20')
|
||||||
|
if website and not validate_website(website):
|
||||||
|
flash('You should enter a valid URL.')
|
||||||
|
return render_template('edit_profile.html', profile=profile_checkpoint())
|
||||||
|
location = int(request.form.get('location'))
|
||||||
|
if location == 0:
|
||||||
|
location = None
|
||||||
|
UserProfile.update(
|
||||||
|
full_name=request.form['full_name'] or username,
|
||||||
|
biography=request.form['biography'],
|
||||||
|
year=request.form['year'] if request.form.get('has_year') else None,
|
||||||
|
location=location,
|
||||||
|
website=website,
|
||||||
|
instagram=request.form['instagram'],
|
||||||
|
facebook=request.form['facebook'],
|
||||||
|
telegram=request.form['telegram']
|
||||||
|
).where(UserProfile.user == user).execute()
|
||||||
|
return redirect(url_for('website.user_detail', username=username))
|
||||||
|
return render_template('edit_profile.html')
|
||||||
|
|
||||||
|
@bp.route('/notifications/')
|
||||||
|
@login_required
|
||||||
|
def notifications():
|
||||||
|
user = get_current_user()
|
||||||
|
notifications = (Notification
|
||||||
|
.select()
|
||||||
|
.where(Notification.target == user)
|
||||||
|
.order_by(Notification.pub_date.desc()))
|
||||||
|
|
||||||
|
with database.atomic():
|
||||||
|
(Notification
|
||||||
|
.update(seen=1)
|
||||||
|
.where((Notification.target == user) & (Notification.seen == 0))
|
||||||
|
.execute())
|
||||||
|
return object_list('notifications.html', notifications, 'notification_list', json=json, User=User)
|
||||||
|
|
||||||
|
@bp.route('/about/')
|
||||||
|
def about():
|
||||||
|
return render_template('about.html', version=app_version,
|
||||||
|
python_version=python_version, flask_version=flask_version)
|
||||||
|
|
||||||
|
# The two following routes are mandatory by law.
|
||||||
|
@bp.route('/terms/')
|
||||||
|
def terms():
|
||||||
|
return render_template('terms.html')
|
||||||
|
|
||||||
|
@bp.route('/privacy/')
|
||||||
|
def privacy():
|
||||||
|
return render_template('privacy.html')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
DATABASE = 'coriplus.sqlite'
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
SECRET_KEY = 'hin6bab8ge25*r=x&+5$0kn=-#log$pt^#@vrqjld!^2ci@g*b'
|
SECRET_KEY = 'hin6bab8ge25*r=x&+5$0kn=-#log$pt^#@vrqjld!^2ci@g*b'
|
||||||
SITE_NAME = 'Cori+'
|
SITE_NAME = 'Cori+'
|
||||||
|
|
|
||||||
10
migrate_0_6_to_0_7.py
Normal file
10
migrate_0_6_to_0_7.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
conn = sqlite3.connect('coriplus.sqlite')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
conn.executescript('''
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
ALTER TABLE userprofile ADD COLUMN telegram TEXT;
|
||||||
|
COMMIT;
|
||||||
|
''')
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block body %}
|
|
||||||
<h2>Hello</h2>
|
|
||||||
|
|
||||||
<p>{{ site_name }} is made by people like you. <br/>
|
|
||||||
<a href="{{url_for('login')}}">Log in</a> or <a href="{{url_for('register')}}">register</a> to see more.</p>
|
|
||||||
{% endblock %}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue