Compare commits
No commits in common. "master" and "v0.6.0" have entirely different histories.
16
.gitignore
vendored
|
|
@ -4,18 +4,4 @@ __pycache__/
|
|||
uploads/
|
||||
*.pyc
|
||||
**~
|
||||
.*.swp
|
||||
__pycache__/
|
||||
venv
|
||||
.env
|
||||
.venv
|
||||
env
|
||||
data/
|
||||
conf/
|
||||
config/
|
||||
\#*\#
|
||||
.\#*
|
||||
node_modules/
|
||||
alembic.ini
|
||||
**.egg-info
|
||||
.vscode
|
||||
**/.*.swp
|
||||
|
|
|
|||
55
CHANGELOG.md
|
|
@ -1,60 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## 0.10.0
|
||||
+ Codebase refactor (with breaking changes!)
|
||||
+ Dropped support for Python<=3.9
|
||||
+ Switched database to PostgreSQL
|
||||
+ Move ALL config to .env (config.py is NO MORE supported)
|
||||
+ Config SITE_NAME replaced with APP_NAME
|
||||
+ Add CSRF token and flask_WTF
|
||||
+ Schema changes: biography and website moved to `User`; `UserProfile` table deprecated (and useless fields removed)
|
||||
+ Posts can now be permanently deleted
|
||||
+ Miscellaneous style changes
|
||||
|
||||
## 0.9.0
|
||||
|
||||
* Website redesign: added some material icons, implemented via a `inline_svg` function, injected by default in templates and defined in `utils.py`.
|
||||
* Added positive feedback mechanism: now you can +1 a message. So, `score_message_add` and `score_message_remove` API endpoints were added, and `MessageUpvote` table was created.
|
||||
* Added notifications support for API.
|
||||
* Added `create_account` endpoint to API. This endpoint does not require an access token.
|
||||
* Added `explore`, `notifications_count`, `notifications` and `notifications_seen` endpoints.
|
||||
* Added `has_more` field to feed endpoints (`feed`, `explore` and `profile_feed`).
|
||||
* Added `join_date` field into `user` object of `profile_info` endpoint, for more profile transparency.
|
||||
* Added `/favicon.ico`.
|
||||
* Fixed some bugs when creating mentions and using offsets in feeds.
|
||||
|
||||
## 0.8.0
|
||||
|
||||
* Added the admin dashboard, accessible from `/admin/` via basic auth. Only users with admin right can access it. Added endpoints `admin.reports` and `admin.reports_detail`.
|
||||
* Safety is our top priority: added the ability to report someone other's post for everything violating the site's Terms of Service. The current reasons for reporting are: spam, impersonation, pornography, violence, harassment or bullying, hate speech or symbols, self injury, sale or promotion of firearms or drugs, and underage use.
|
||||
* Schema changes: moved `full_name` field from table `userprofile` to table `user` for search improvement reasons. Added `Report` model.
|
||||
* Now `profile_search` API endpoint searches by full name too.
|
||||
* Adding `messages_count`, `followers_count` and `following_count` to `profile_info` API endpoint (what I've done to 0.7.1 too).
|
||||
* Adding `create2` API endpoint that accepts media, due to an issue with the `create` endpoint that would make it incompatible.
|
||||
* Adding media URLs to messages in API.
|
||||
* Added `relationships_follow`, `relationships_unfollow`, `username_availability`, `edit_profile`, `request_edit` and `confirm_edit` endpoints to API.
|
||||
* Added `url` utility to model `Upload`.
|
||||
* Changed default `robots.txt`, adding report and admin-related lines.
|
||||
* Released official [Android client](https://github.com/sakuragasaki46/coriplusapp/releases/tag/v0.8.0).
|
||||
|
||||
## 0.7.1
|
||||
|
||||
* Adding `messages_count`, `followers_count` and `following_count` to `profile_info` API endpoint (forgot to release).
|
||||
|
||||
## 0.7.0
|
||||
|
||||
* 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`. There is also a new module `api.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.
|
||||
* Added the page for permanent deletion of messages. Well, you cannot delete them yet. It's missing a function that checks the CSRF-Token.
|
||||
* Renamed template `private_messages.html` to `feed.html`.
|
||||
* Added the capability to change password.
|
||||
* Corrected a bug into `pwdhash`: it accepted an argument, but pulled data from the form instead of processing it. Now it uses the argument.
|
||||
* Schema changes: added column `telegram` to `UserProfile` table. To update schema, execute the script `migrate_0_6_to_0_7.py`
|
||||
* Adding public API. Each of the API endpoints take a mandatory query string argument: the access token, generated by a separate endpoint at `/get_access_token` and stored into the client. All API routes start with `/api/V1`. Added endpoints `feed`, `create`, `profile_info`, `profile_feed` and `profile_search`.
|
||||
* Planning to release mobile app for Android.
|
||||
|
||||
## 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).
|
||||
|
|
|
|||
20
README.md
|
|
@ -2,12 +2,10 @@
|
|||
|
||||
A simple social network, inspired by the now dead Google-Plus.
|
||||
|
||||
To run the app, do "flask run" in the package's parent directory.
|
||||
To run the app, run the file "run_example.py"
|
||||
|
||||
Based on Tweepee example of [peewee](https://github.com/coleifer/peewee/).
|
||||
|
||||
This is the server. For the client, see [coriplusapp](https://github.com/sakuragasaki46/coriplusapp/).
|
||||
|
||||
## Features
|
||||
|
||||
* Create text statuses, optionally with image
|
||||
|
|
@ -15,20 +13,10 @@ This is the server. For the client, see [coriplusapp](https://github.com/sakurag
|
|||
* Timeline feed
|
||||
* Add info to your profile
|
||||
* In-site notifications
|
||||
* Public API
|
||||
* SQLite (or PostgreSQL)-based app
|
||||
* SQLite-based app
|
||||
|
||||
## Requirements
|
||||
|
||||
* **Python 3.10+** with **pip**.
|
||||
* **Flask** web framework.
|
||||
* **Python 3** only. We don't want to support Python 2.
|
||||
* **Flask** web framework (also required extension **Flask-Login**).
|
||||
* **Peewee** ORM.
|
||||
* A \*nix-based OS.
|
||||
|
||||
## Installation
|
||||
|
||||
* Install dependencies: `pip install .`
|
||||
* Set the `DATABASE_URL` (must be SQLite or PostgreSQL)
|
||||
* Run the migrations: `sh ./genmig.sh @`
|
||||
* i forgor
|
||||
|
||||
|
|
|
|||
796
app.py
Normal file
|
|
@ -0,0 +1,796 @@
|
|||
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)
|
||||
4
config.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
DATABASE = 'coriplus.sqlite'
|
||||
DEBUG = True
|
||||
SECRET_KEY = 'hin6bab8ge25*r=x&+5$0kn=-#log$pt^#@vrqjld!^2ci@g*b'
|
||||
SITE_NAME = 'Cori+'
|
||||
10
genmig.sh
|
|
@ -1,10 +0,0 @@
|
|||
#!/usr/bin/bash
|
||||
# GENERATE MIGRATIONS
|
||||
|
||||
source venv/bin/activate && \
|
||||
source .env && \
|
||||
case "$1" in
|
||||
("+") pw_migrate create --auto --auto-source=coriplus.models --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;;
|
||||
("@") pw_migrate migrate --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;;
|
||||
(\\) pw_migrate rollback --directory=src/migrations --database="$DATABASE_URL" "${@:2}" ;;
|
||||
esac
|
||||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
||||
|
Before Width: | Height: | Size: 287 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 302 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M12 10.9c-.61 0-1.1.49-1.1 1.1s.49 1.1 1.1 1.1c.61 0 1.1-.49 1.1-1.1s-.49-1.1-1.1-1.1zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm2.19 12.19L6 18l3.81-8.19L18 6l-3.81 8.19z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
||||
|
Before Width: | Height: | Size: 333 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 261 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
||||
|
Before Width: | Height: | Size: 247 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 279 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"/></svg>
|
||||
|
Before Width: | Height: | Size: 311 B |
|
|
@ -1,24 +0,0 @@
|
|||
[project]
|
||||
name = "sakuragasaki46_coriplus"
|
||||
authors = [
|
||||
{ name = "Sakuragasaki46" }
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"Python-Dotenv>=1.0.0",
|
||||
"Flask",
|
||||
"Flask-Login",
|
||||
"Peewee",
|
||||
"Flask-WTF",
|
||||
"peewee-migrate",
|
||||
"PsycoPG2"
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
"Private :: X"
|
||||
]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = { attr = "coriplus.__version__" }
|
||||
|
||||
|
||||
1
robots.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
16
run_example.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, '../..')
|
||||
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-p', '--port', type=int, default=5000,
|
||||
help='An alternative port where to run the server.')
|
||||
|
||||
from app import app, create_tables
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parser.parse_args()
|
||||
create_tables()
|
||||
app.run(port=args.port)
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
'''
|
||||
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 public API, see `app.api`.
|
||||
For report pages, see `app.reports`.
|
||||
For site administration, see `app.admin`.
|
||||
For template filters, see `app.filters`.
|
||||
For the database models, see `app.models`.
|
||||
For other, see `app.utils`.
|
||||
'''
|
||||
|
||||
from flask import (
|
||||
Flask, g, jsonify, render_template, request,
|
||||
send_from_directory, __version__ as flask_version)
|
||||
import os, sys
|
||||
from flask_login import LoginManager
|
||||
from flask_wtf import CSRFProtect
|
||||
import dotenv
|
||||
import logging
|
||||
|
||||
__version__ = '0.10.0-dev50'
|
||||
|
||||
# we want to support Python 3.10+ only.
|
||||
# Python 2 has too many caveats.
|
||||
# Python <=3.9 has harder type support.
|
||||
if sys.version_info[0:2] < (3, 10):
|
||||
raise RuntimeError('Python 3.10+ required')
|
||||
|
||||
BASEDIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
os.chdir(BASEDIR)
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.environ['SECRET_KEY']
|
||||
|
||||
login_manager = LoginManager(app)
|
||||
|
||||
CSRFProtect(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:
|
||||
logger.error('database connected twice')
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
try:
|
||||
g.db.close()
|
||||
except Exception:
|
||||
logger.error('database closed twice')
|
||||
return response
|
||||
|
||||
@app.context_processor
|
||||
def _inject_variables():
|
||||
return {
|
||||
'site_name': os.environ.get('APP_NAME', 'Cori+'),
|
||||
'locations': locations,
|
||||
'inline_svg': inline_svg
|
||||
}
|
||||
|
||||
@login_manager.user_loader
|
||||
def _inject_user(userid):
|
||||
return User[userid]
|
||||
|
||||
@app.errorhandler(403)
|
||||
def error_403(body):
|
||||
return render_template('403.html'), 403
|
||||
|
||||
@app.errorhandler(404)
|
||||
def error_404(body):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
def favicon_ico():
|
||||
return send_from_directory(BASEDIR, 'src/favicon.ico')
|
||||
|
||||
@app.route('/robots.txt')
|
||||
def robots_txt():
|
||||
return send_from_directory(BASEDIR, 'src/robots.txt')
|
||||
|
||||
@app.route('/uploads/<id>.<type>')
|
||||
def uploads(id, type='jpg'):
|
||||
return send_from_directory(UPLOAD_DIRECTORY, id + '.' + type)
|
||||
|
||||
@app.route('/get_access_token', methods=['POST'])
|
||||
def send_access_token():
|
||||
try:
|
||||
data = request.get_json(True)
|
||||
try:
|
||||
user = User.get(
|
||||
(User.username == data['username']) &
|
||||
(User.password == pwdhash(data['password'])))
|
||||
except User.DoesNotExist:
|
||||
return jsonify({
|
||||
'message': 'Invalid username or password',
|
||||
'login_correct': False,
|
||||
'status': 'ok'
|
||||
})
|
||||
if user.is_disabled == 1:
|
||||
user.is_disabled = 0
|
||||
elif user.is_disabled == 2:
|
||||
return jsonify({
|
||||
'message': 'Your account has been disabled by violating our Terms.',
|
||||
'login_correct': False,
|
||||
'status': 'ok'
|
||||
})
|
||||
return jsonify({
|
||||
'token': generate_access_token(user),
|
||||
'login_correct': True,
|
||||
'status': 'ok'
|
||||
})
|
||||
except Exception:
|
||||
sys.excepthook(*sys.exc_info())
|
||||
return jsonify({
|
||||
'message': 'An unknown error has occurred.',
|
||||
'status': 'fail'
|
||||
})
|
||||
|
||||
from .website import bp
|
||||
app.register_blueprint(bp)
|
||||
|
||||
from .ajax import bp
|
||||
app.register_blueprint(bp)
|
||||
|
||||
from .api import bp
|
||||
app.register_blueprint(bp)
|
||||
|
||||
from .reports import bp
|
||||
app.register_blueprint(bp)
|
||||
|
||||
from .admin import bp
|
||||
app.register_blueprint(bp)
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
'''
|
||||
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)
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
'''
|
||||
Management of reports and the entire site.
|
||||
|
||||
New in 0.8.
|
||||
'''
|
||||
|
||||
from flask import Blueprint, abort, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
from .models import User, Message, Report, report_reasons, REPORT_STATUS_ACCEPTED, \
|
||||
REPORT_MEDIA_USER, REPORT_MEDIA_MESSAGE
|
||||
from .utils import pwdhash, object_list
|
||||
from functools import wraps
|
||||
|
||||
bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
def _check_auth(username) -> bool:
|
||||
try:
|
||||
return User.get((User.username == username)).is_admin
|
||||
except User.DoesNotExist:
|
||||
return False
|
||||
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def wrapped_view(**kwargs):
|
||||
if not _check_auth(current_user.username):
|
||||
abort(403)
|
||||
return f(**kwargs)
|
||||
return wrapped_view
|
||||
|
||||
def review_reports(status, media_type, media_id):
|
||||
(Report
|
||||
.update(status=status)
|
||||
.where((Report.media_type == media_type) & (Report.media_id == media_id))
|
||||
.execute())
|
||||
if status == REPORT_STATUS_ACCEPTED:
|
||||
if media_type == REPORT_MEDIA_USER:
|
||||
user = User[media_id]
|
||||
user.is_disabled = 2
|
||||
user.save()
|
||||
elif media_type == REPORT_MEDIA_MESSAGE:
|
||||
Message.delete().where(Message.id == media_id).execute()
|
||||
|
||||
@bp.route('/')
|
||||
@admin_required
|
||||
def homepage():
|
||||
return render_template('admin_home.html')
|
||||
|
||||
@bp.route('/reports')
|
||||
@admin_required
|
||||
def reports():
|
||||
return object_list('admin_reports.html', Report.select().order_by(Report.created_date.desc()), 'report_list', report_reasons=dict(report_reasons))
|
||||
|
||||
@bp.route('/reports/<int:id>', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def reports_detail(id):
|
||||
report = Report[id]
|
||||
if request.method == 'POST':
|
||||
if request.form.get('take_down'):
|
||||
review_reports(REPORT_STATUS_ACCEPTED, report.media_type, report.media_id)
|
||||
elif request.form.get('discard'):
|
||||
review_reports(REPORT_STATUS_DECLINED, report.media_type, report.media_id)
|
||||
return redirect(url_for('admin.reports'))
|
||||
return render_template('admin_report_detail.html', report=report, report_reasons=dict(report_reasons))
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
'''
|
||||
AJAX hooks for the website.
|
||||
|
||||
Warning: this is not the public API.
|
||||
'''
|
||||
|
||||
from flask import Blueprint, jsonify
|
||||
from flask_login import current_user
|
||||
from .models import User, Message, MessageUpvote
|
||||
from .utils import locations, is_username
|
||||
import datetime
|
||||
|
||||
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})
|
||||
|
||||
@bp.route('/score/<int:id>/toggle', methods=['POST'])
|
||||
def score_toggle(id):
|
||||
user = current_user
|
||||
message = Message[id]
|
||||
upvoted_by_self = (MessageUpvote
|
||||
.select()
|
||||
.where((MessageUpvote.message == message) & (MessageUpvote.user == user))
|
||||
.exists())
|
||||
if upvoted_by_self:
|
||||
(MessageUpvote
|
||||
.delete()
|
||||
.where(
|
||||
(MessageUpvote.message == message) &
|
||||
(MessageUpvote.user == user))
|
||||
.execute()
|
||||
)
|
||||
else:
|
||||
MessageUpvote.create(
|
||||
message=message,
|
||||
user=user,
|
||||
created_date=datetime.datetime.now()
|
||||
)
|
||||
return jsonify({
|
||||
"score": message.score,
|
||||
"status": "ok"
|
||||
})
|
||||
|
|
@ -1,436 +0,0 @@
|
|||
from flask import Blueprint, jsonify, request
|
||||
import sys, os, datetime, re, uuid
|
||||
from functools import wraps
|
||||
from peewee import IntegrityError
|
||||
from .models import User, UserProfile, Message, Upload, Relationship, Notification, \
|
||||
MessageUpvote, database, \
|
||||
MSGPRV_PUBLIC, MSGPRV_UNLISTED, MSGPRV_FRIENDS, MSGPRV_ONLYME, UPLOAD_DIRECTORY
|
||||
from .utils import check_access_token, Visibility, push_notification, unpush_notification, \
|
||||
create_mentions, is_username, generate_access_token, pwdhash
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
bp = Blueprint('api', __name__, url_prefix='/api/V1')
|
||||
|
||||
def get_message_info(message):
|
||||
try:
|
||||
media = message.uploads[0].url()
|
||||
except IndexError:
|
||||
media = None
|
||||
if media:
|
||||
logger.debug(media)
|
||||
return {
|
||||
'id': message.id,
|
||||
'user': {
|
||||
'id': message.user.id,
|
||||
'username': message.user.username,
|
||||
},
|
||||
'text': message.text,
|
||||
'privacy': message.privacy,
|
||||
'pub_date': message.pub_date.timestamp(),
|
||||
'media': media,
|
||||
'score': len(message.upvotes),
|
||||
'upvoted_by_self': message.upvoted_by_self(),
|
||||
}
|
||||
|
||||
def validate_access(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
access_token = request.args.get('access_token')
|
||||
if access_token is None:
|
||||
return jsonify({
|
||||
'message': 'missing access_token',
|
||||
'status': 'fail'
|
||||
})
|
||||
user = check_access_token(access_token)
|
||||
if user is None:
|
||||
return jsonify({
|
||||
'message': 'invalid access_token',
|
||||
'status': 'fail'
|
||||
})
|
||||
try:
|
||||
result = func(user, *args, **kwargs)
|
||||
assert isinstance(result, dict)
|
||||
except Exception:
|
||||
import traceback; traceback.print_exc()
|
||||
return jsonify({
|
||||
'message': str(sys.exc_info()[1]),
|
||||
'status': 'fail'
|
||||
})
|
||||
result['status'] = 'ok'
|
||||
return jsonify(result)
|
||||
return wrapper
|
||||
|
||||
@bp.route('/feed')
|
||||
@validate_access
|
||||
def feed(self):
|
||||
timeline_media = []
|
||||
date = request.args.get('offset')
|
||||
if date is None:
|
||||
date = datetime.datetime.now()
|
||||
else:
|
||||
date = datetime.datetime.fromtimestamp(float(date))
|
||||
query = Visibility(Message
|
||||
.select()
|
||||
.where(((Message.user << self.following())
|
||||
| (Message.user == self))
|
||||
& (Message.pub_date < date))
|
||||
.order_by(Message.pub_date.desc()))
|
||||
for message in query.paginate(1):
|
||||
timeline_media.append(get_message_info(message))
|
||||
return {'timeline_media': timeline_media, 'has_more': query.count() > len(timeline_media)}
|
||||
|
||||
@bp.route('/explore')
|
||||
@validate_access
|
||||
def explore(self):
|
||||
timeline_media = []
|
||||
date = request.args.get('offset')
|
||||
if date is None:
|
||||
date = datetime.datetime.now()
|
||||
else:
|
||||
date = datetime.datetime.fromtimestamp(float(date))
|
||||
query = Visibility(Message
|
||||
.select()
|
||||
.where(Message.pub_date < date)
|
||||
.order_by(Message.pub_date.desc()), True)
|
||||
for message in query.paginate(1):
|
||||
timeline_media.append(get_message_info(message))
|
||||
return {'timeline_media': timeline_media, 'has_more': query.count() > len(timeline_media)}
|
||||
|
||||
@bp.route('/create', methods=['POST'])
|
||||
@validate_access
|
||||
def create(self):
|
||||
data = request.get_json(True)
|
||||
text = data['text']
|
||||
privacy = int(data.get('privacy', 0))
|
||||
message = Message.create(
|
||||
user=self,
|
||||
text=text,
|
||||
pub_date=datetime.datetime.now(),
|
||||
privacy=privacy)
|
||||
# This API does not support files. Use create2 instead.
|
||||
create_mentions(self, text, privacy)
|
||||
return {}
|
||||
|
||||
@bp.route('/create2', methods=['POST'])
|
||||
@validate_access
|
||||
def create2(self):
|
||||
text = request.form['text']
|
||||
privacy = int(request.form.get('privacy', 0))
|
||||
message = Message.create(
|
||||
user=self,
|
||||
text=text,
|
||||
pub_date=datetime.datetime.now(),
|
||||
privacy=privacy)
|
||||
file = request.files.get('file')
|
||||
if file:
|
||||
logger.info('Uploading', file.filename)
|
||||
ext = file.filename.split('.')[-1]
|
||||
upload = Upload.create(
|
||||
type=ext,
|
||||
message=message
|
||||
)
|
||||
file.save(os.path.join(UPLOAD_DIRECTORY, str(upload.id) + '.' + ext))
|
||||
create_mentions(self, text, privacy)
|
||||
return {}
|
||||
|
||||
def get_relationship_info(self, other):
|
||||
if self == other:
|
||||
return
|
||||
return {
|
||||
"following": self.is_following(other),
|
||||
"followed_by": other.is_following(self)
|
||||
}
|
||||
|
||||
@bp.route('/profile_info/<userid>', methods=['GET'])
|
||||
@validate_access
|
||||
def profile_info(self, userid):
|
||||
if userid == 'self':
|
||||
user = self
|
||||
elif userid.startswith('+'):
|
||||
user = User.get(User.username == userid[1:])
|
||||
elif userid.isdigit():
|
||||
try:
|
||||
user = User[userid]
|
||||
except User.DoesNotExist:
|
||||
return {'user': None}
|
||||
else:
|
||||
raise ValueError('userid should be an integer or "self"')
|
||||
profile = user.profile
|
||||
return {
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"full_name": profile.full_name,
|
||||
"biography": profile.biography,
|
||||
"website": profile.website,
|
||||
"generation": profile.year,
|
||||
"instagram": profile.instagram,
|
||||
"facebook": profile.facebook,
|
||||
"join_date": user.join_date.timestamp(),
|
||||
"relationships": get_relationship_info(self, user),
|
||||
"messages_count": len(user.messages),
|
||||
"followers_count": len(user.followers()),
|
||||
"following_count": len(user.following())
|
||||
}
|
||||
}
|
||||
|
||||
@bp.route('/profile_info/feed/<userid>', methods=['GET'])
|
||||
@validate_access
|
||||
def profile_feed(self, userid):
|
||||
if userid == 'self':
|
||||
user = self
|
||||
elif userid.startswith('+'):
|
||||
user = User.get(User.username == userid[1:])
|
||||
elif userid.isdigit():
|
||||
user = User[userid]
|
||||
else:
|
||||
raise ValueError('userid should be an integer or "self"')
|
||||
timeline_media = []
|
||||
date = request.args.get('offset')
|
||||
if date is None:
|
||||
date = datetime.datetime.now()
|
||||
else:
|
||||
date = datetime.datetime.fromtimestamp(float(date))
|
||||
query = Visibility(Message
|
||||
.select()
|
||||
.where((Message.user == user)
|
||||
& (Message.pub_date < date))
|
||||
.order_by(Message.pub_date.desc()))
|
||||
for message in query.paginate(1):
|
||||
timeline_media.append(get_message_info(message))
|
||||
return {'timeline_media': timeline_media, 'has_more': query.count() > len(timeline_media)}
|
||||
|
||||
@bp.route('/relationships/<int:userid>/follow', methods=['POST'])
|
||||
@validate_access
|
||||
def relationships_follow(self, userid):
|
||||
user = User[userid]
|
||||
try:
|
||||
with database.atomic():
|
||||
Relationship.create(
|
||||
from_user=self,
|
||||
to_user=user,
|
||||
created_date=datetime.datetime.now())
|
||||
except IntegrityError:
|
||||
pass
|
||||
push_notification('follow', user, user=self.id)
|
||||
return get_relationship_info(self, user)
|
||||
|
||||
@bp.route('/relationships/<int:userid>/unfollow', methods=['POST'])
|
||||
@validate_access
|
||||
def relationships_unfollow(self, userid):
|
||||
user = User[userid]
|
||||
(Relationship
|
||||
.delete()
|
||||
.where(
|
||||
(Relationship.from_user == self) &
|
||||
(Relationship.to_user == user))
|
||||
.execute())
|
||||
unpush_notification('follow', user, user=self.id)
|
||||
return get_relationship_info(self, user)
|
||||
|
||||
@bp.route('/profile_search', methods=['POST'])
|
||||
@validate_access
|
||||
def profile_search(self):
|
||||
data = request.get_json(True)
|
||||
query = User.select().where((User.username ** ('%' + data['q'] + '%')) |
|
||||
(User.full_name ** ('%' + data['q'] + '%'))).limit(20)
|
||||
results = []
|
||||
for result in query:
|
||||
profile = result.profile
|
||||
results.append({
|
||||
"id": result.id,
|
||||
"username": result.username,
|
||||
"full_name": result.full_name,
|
||||
"followers_count": len(result.followers())
|
||||
})
|
||||
return {
|
||||
"users": results
|
||||
}
|
||||
|
||||
@bp.route('/username_availability/<username>')
|
||||
@validate_access
|
||||
def username_availability(self, username):
|
||||
current = self.username
|
||||
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 {
|
||||
'is_valid': is_valid,
|
||||
'is_available': is_available
|
||||
}
|
||||
|
||||
@bp.route('/edit_profile', methods=['POST'])
|
||||
@validate_access
|
||||
def edit_profile(user):
|
||||
data = request.get_json(True)
|
||||
username = data['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:
|
||||
raise ValueError('that username is already taken')
|
||||
full_name = data['full_name'] or username
|
||||
if full_name != user.full_name:
|
||||
User.update(full_name=full_name).where(User.id == user.id).execute()
|
||||
kwargs = {}
|
||||
if 'website' in data:
|
||||
website = data['website'].strip().replace(' ', '%20')
|
||||
if website and not validate_website(website):
|
||||
raise ValueError('You should enter a valid URL.')
|
||||
kwargs['website'] = website
|
||||
if 'location' in data:
|
||||
location = int(request.form.get('location'))
|
||||
if location == 0:
|
||||
location = None
|
||||
kwargs['location'] = location
|
||||
if 'year' in data:
|
||||
if data.get('has_year'):
|
||||
kwargs['year'] = data['year']
|
||||
else:
|
||||
kwargs['year'] = None
|
||||
if 'instagram' in data: kwargs['instagram'] = data['instagram']
|
||||
if 'facebook' in data: kwargs['facebook'] = data['facebook']
|
||||
if 'telegram' in data: kwargs['telegram'] = data['telegram']
|
||||
UserProfile.update(
|
||||
biography=data['biography'],
|
||||
**kwargs
|
||||
).where(UserProfile.user == user).execute()
|
||||
return {}
|
||||
|
||||
@bp.route('/request_edit/<int:id>')
|
||||
@validate_access
|
||||
def request_edit(self, id):
|
||||
message = Message[id]
|
||||
if message.user != self:
|
||||
raise ValueError('Attempt to edit a message from another')
|
||||
return {
|
||||
'message_info': get_message_info(message)
|
||||
}
|
||||
|
||||
@bp.route('/save_edit/<int:id>', methods=['POST'])
|
||||
@validate_access
|
||||
def save_edit(self, id):
|
||||
message = Message[id]
|
||||
if message.user != self:
|
||||
raise ValueError('Attempt to edit a message from another')
|
||||
data = request.get_json(True)
|
||||
Message.update(text=data['text'], privacy=data['privacy']).where(Message.id == id).execute()
|
||||
return {}
|
||||
|
||||
# no validate access for this endpoint!
|
||||
@bp.route('/create_account', methods=['POST'])
|
||||
def create_account():
|
||||
try:
|
||||
data = request.get_json(True)
|
||||
try:
|
||||
birthday = datetime.datetime.fromisoformat(data['birthday'])
|
||||
except ValueError:
|
||||
raise ValueError('invalid date format')
|
||||
username = data['username'].lower()
|
||||
if not is_username(username):
|
||||
raise ValueError('invalid username')
|
||||
with database.atomic():
|
||||
user = User.create(
|
||||
username=username,
|
||||
full_name=data.get('full_name') or username,
|
||||
password=pwdhash(data['password']),
|
||||
email=data['email'],
|
||||
birthday=birthday,
|
||||
join_date=datetime.datetime.now())
|
||||
UserProfile.create(
|
||||
user=user
|
||||
)
|
||||
|
||||
return jsonify({'access_token': generate_access_token(user), 'status': 'ok'})
|
||||
except Exception as e:
|
||||
return jsonify({'message': str(e), 'status': 'fail'})
|
||||
|
||||
def get_notification_info(notification):
|
||||
obj = {
|
||||
"id": notification.id,
|
||||
"type": notification.type,
|
||||
"timestamp": notification.pub_date.timestamp(),
|
||||
"seen": notification.seen
|
||||
}
|
||||
obj.update(json.loads(notification.detail))
|
||||
return obj
|
||||
|
||||
@bp.route('/notifications/count')
|
||||
@validate_access
|
||||
def notifications_count(self):
|
||||
count = len(Notification
|
||||
.select()
|
||||
.where((Notification.target == self) & (Notification.seen == 0)))
|
||||
return {
|
||||
'count': count
|
||||
}
|
||||
|
||||
@bp.route('/notifications')
|
||||
@validate_access
|
||||
def notifications(self):
|
||||
items = []
|
||||
query = (Notification
|
||||
.select()
|
||||
.where(Notification.target == self)
|
||||
.order_by(Notification.pub_date.desc())
|
||||
.limit(100))
|
||||
unseen_count = len(Notification
|
||||
.select()
|
||||
.where((Notification.target == self) & (Notification.seen == 0)))
|
||||
for notification in query:
|
||||
items.append(get_notification_info(query))
|
||||
return {
|
||||
"notifications": {
|
||||
"items": items,
|
||||
"unseen_count": unseen_count
|
||||
}
|
||||
}
|
||||
|
||||
@bp.route('/notifications/seen', methods=['POST'])
|
||||
@validate_access
|
||||
def notifications_seen(self):
|
||||
data = request.get_json(True)
|
||||
(Notification
|
||||
.update(seen=1)
|
||||
.where((Notification.target == self) & (Notification.pub_date < data['offset']))
|
||||
.execute())
|
||||
return {}
|
||||
|
||||
@bp.route('/score/message/<int:id>/add', methods=['POST'])
|
||||
@validate_access
|
||||
def score_message_add(self, id):
|
||||
message = Message[id]
|
||||
MessageUpvote.create(
|
||||
message=message,
|
||||
user=self,
|
||||
created_date=datetime.datetime.now()
|
||||
)
|
||||
return {
|
||||
'score': len(message.upvotes)
|
||||
}
|
||||
|
||||
@bp.route('/score/message/<int:id>/remove', methods=['POST'])
|
||||
@validate_access
|
||||
def score_message_remove(self, id):
|
||||
message = Message[id]
|
||||
(MessageUpvote
|
||||
.delete()
|
||||
.where(
|
||||
(MessageUpvote.message == message) &
|
||||
(MessageUpvote.user == self))
|
||||
.execute()
|
||||
)
|
||||
return {
|
||||
'score': len(message.upvotes)
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
'''
|
||||
Filter functions used in the website templates.
|
||||
'''
|
||||
|
||||
from markupsafe import Markup
|
||||
import html, datetime, re, time
|
||||
from .utils import tokenize, inline_svg as _inline_svg
|
||||
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)]
|
||||
|
||||
|
|
@ -1,291 +0,0 @@
|
|||
'''
|
||||
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 flask import request
|
||||
from peewee import *
|
||||
from playhouse.db_url import connect
|
||||
import os
|
||||
|
||||
from . import BASEDIR
|
||||
# here should go `from .utils import get_current_user`, but it will cause
|
||||
# import errors. It's instead imported at function level.
|
||||
|
||||
database = connect(os.environ['DATABASE_URL'])
|
||||
|
||||
class BaseModel(Model):
|
||||
id = AutoField(primary_key=True)
|
||||
|
||||
class Meta:
|
||||
database = database
|
||||
|
||||
# A user. The user is separated from its page.
|
||||
class User(BaseModel):
|
||||
# The unique username.
|
||||
username = CharField(30, unique=True)
|
||||
# The user's full name (here for better search since 0.8)
|
||||
full_name = CharField(80)
|
||||
# The password hash.
|
||||
password = CharField(256)
|
||||
# An email address.
|
||||
email = CharField(256)
|
||||
# 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)
|
||||
# Short description of user.
|
||||
biography = CharField(256, default='')
|
||||
# Personal website.
|
||||
website = TextField(null=True)
|
||||
|
||||
|
||||
|
||||
# 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 True
|
||||
|
||||
# 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
|
||||
# Deprecated in 0.10 and merged with User
|
||||
class UserProfile(BaseModel):
|
||||
user = ForeignKeyField(User, primary_key=True)
|
||||
biography = TextField(default='')
|
||||
location = IntegerField(null=True)
|
||||
website = TextField(null=True)
|
||||
@property
|
||||
def full_name(self):
|
||||
'''
|
||||
Moved to User in 0.8 for search improvement reasons.
|
||||
'''
|
||||
return self.user.full_name
|
||||
|
||||
# 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 not cur_user or cur_user.is_anonymous:
|
||||
return False
|
||||
return user.is_following(cur_user) and cur_user.is_following(user)
|
||||
else:
|
||||
return False
|
||||
@property
|
||||
def score(self):
|
||||
return self.upvotes.count()
|
||||
def upvoted_by_self(self):
|
||||
from .utils import get_current_user
|
||||
user = get_current_user()
|
||||
return (MessageUpvote
|
||||
.select()
|
||||
.where((MessageUpvote.message == self) & (MessageUpvote.user == user))
|
||||
.exists()
|
||||
)
|
||||
|
||||
# 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(BASEDIR, '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
|
||||
def url(self):
|
||||
return request.host_url + 'uploads/' + self.filename()
|
||||
|
||||
class Notification(BaseModel):
|
||||
type = TextField()
|
||||
target = ForeignKeyField(User, backref='notifications')
|
||||
detail = TextField()
|
||||
pub_date = DateTimeField()
|
||||
seen = IntegerField(default=0)
|
||||
|
||||
REPORT_MEDIA_USER = 1
|
||||
REPORT_MEDIA_MESSAGE = 2
|
||||
|
||||
REPORT_REASON_SPAM = 1
|
||||
REPORT_REASON_IMPERSONATION = 2
|
||||
REPORT_REASON_PORN = 3
|
||||
REPORT_REASON_VIOLENCE = 4
|
||||
REPORT_REASON_HATE = 5
|
||||
REPORT_REASON_BULLYING = 6
|
||||
REPORT_REASON_SELFINJURY = 7
|
||||
REPORT_REASON_FIREARMS = 8
|
||||
REPORT_REASON_DRUGS = 9
|
||||
REPORT_REASON_UNDERAGE = 10
|
||||
REPORT_REASON_LEAK = 11
|
||||
REPORT_REASON_DMCA = 12
|
||||
|
||||
report_reasons = [
|
||||
(REPORT_REASON_SPAM, "It's spam"),
|
||||
(REPORT_REASON_IMPERSONATION, "This profile is pretending to be someone else"),
|
||||
(REPORT_REASON_PORN, "Nudity or pornography"),
|
||||
(REPORT_REASON_VIOLENCE, "Violence or dangerous organization"),
|
||||
(REPORT_REASON_HATE, "Hate speech or symbols"),
|
||||
(REPORT_REASON_BULLYING, "Harassment or bullying"),
|
||||
(REPORT_REASON_SELFINJURY, "Self injury"),
|
||||
(REPORT_REASON_FIREARMS, "Sale or promotion of firearms"),
|
||||
(REPORT_REASON_DRUGS, "Sale or promotion of drugs"),
|
||||
(REPORT_REASON_UNDERAGE, "This user is less than 13 years old"),
|
||||
(REPORT_REASON_LEAK, "Leak of sensitive information"),
|
||||
(REPORT_REASON_DMCA, "Copyright violation")
|
||||
]
|
||||
|
||||
REPORT_STATUS_DELIVERED = 0
|
||||
REPORT_STATUS_ACCEPTED = 1
|
||||
REPORT_STATUS_DECLINED = 2
|
||||
|
||||
# New in 0.8.
|
||||
class Report(BaseModel):
|
||||
media_type = IntegerField()
|
||||
media_id = IntegerField()
|
||||
sender = ForeignKeyField(User, null=True)
|
||||
reason = IntegerField()
|
||||
status = IntegerField(default=REPORT_STATUS_DELIVERED)
|
||||
created_date = DateTimeField()
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
try:
|
||||
if self.media_type == REPORT_MEDIA_USER:
|
||||
return User[self.media_id]
|
||||
elif self.media_type == REPORT_MEDIA_MESSAGE:
|
||||
return Message[self.media_id]
|
||||
except DoesNotExist:
|
||||
return
|
||||
|
||||
# New in 0.9.
|
||||
class MessageUpvote(BaseModel):
|
||||
message = ForeignKeyField(Message, backref='upvotes')
|
||||
user = ForeignKeyField(User)
|
||||
created_date = DateTimeField()
|
||||
|
||||
class Meta:
|
||||
indexes = (
|
||||
(('message', 'user'), True),
|
||||
)
|
||||
|
||||
def create_tables():
|
||||
with database:
|
||||
database.create_tables([
|
||||
User, UserAdminship, UserProfile, Message, Relationship,
|
||||
Upload, Notification, Report, MessageUpvote])
|
||||
if not os.path.isdir(UPLOAD_DIRECTORY):
|
||||
os.makedirs(UPLOAD_DIRECTORY)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
'''
|
||||
Module for user and message reports.
|
||||
|
||||
New in 0.8.
|
||||
'''
|
||||
|
||||
from flask import Blueprint, redirect, request, render_template, url_for
|
||||
from .models import Report, REPORT_MEDIA_USER, REPORT_MEDIA_MESSAGE, report_reasons
|
||||
from .utils import get_current_user
|
||||
import datetime
|
||||
|
||||
bp = Blueprint('reports', __name__, url_prefix='/report')
|
||||
|
||||
@bp.route('/user/<int:userid>', methods=['GET', 'POST'])
|
||||
def report_user(userid):
|
||||
if request.method == "POST":
|
||||
Report.create(
|
||||
media_type=REPORT_MEDIA_USER,
|
||||
media_id=userid,
|
||||
sender=get_current_user(),
|
||||
reason=request.form['reason'],
|
||||
created_date=datetime.datetime.now()
|
||||
)
|
||||
return redirect(url_for('reports.report_done'))
|
||||
return render_template('report_user.html', report_reasons=report_reasons)
|
||||
|
||||
@bp.route('/message/<int:userid>', methods=['GET', 'POST'])
|
||||
def report_message(userid):
|
||||
if request.method == "POST":
|
||||
Report.create(
|
||||
media_type=REPORT_MEDIA_MESSAGE,
|
||||
media_id=userid,
|
||||
sender=get_current_user(),
|
||||
reason=request.form['reason'],
|
||||
created_date=datetime.datetime.now()
|
||||
)
|
||||
return redirect(url_for('reports.report_done'))
|
||||
return render_template('report_message.html', report_reasons=report_reasons)
|
||||
|
||||
@bp.route('/done', methods=['GET', 'POST'])
|
||||
def report_done():
|
||||
return render_template('report_done.html')
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
:root {
|
||||
--accent: #f0372e;
|
||||
--link: #3399ff;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body, button, input, select, textarea {
|
||||
font-family: Inter, Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
#site-name, h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Funnel Sans', Roboto, sans-serif;
|
||||
line-height: 1.2;
|
||||
}
|
||||
body{margin:0}
|
||||
main{margin:auto; max-width: 960px;}
|
||||
a{text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
.mobile-collapse {font-size: smaller;}
|
||||
@media (max-width:640px){
|
||||
.mobile-collapse{display:none}
|
||||
}
|
||||
.header{
|
||||
padding:12px;color:white; background-color:var(--accent); box-shadow:0 0 3px 3px #ccc; display: flex; position: relative;
|
||||
}
|
||||
.centered{
|
||||
text-align: center;
|
||||
}
|
||||
.content{padding:12px}
|
||||
.header a{color:white; }
|
||||
.header a svg{fill:white}
|
||||
.content a{color:var(--link)}
|
||||
.content a svg{fill:var(--link)}
|
||||
.content a.plus{color:var(--accent)}
|
||||
.leftnav, .rightnav{list-style: none; display: flex; padding: 0; margin: 0;position: absolute;}
|
||||
.leftnav {left: 0}
|
||||
.rightnav {right: 0}
|
||||
.card {background-color: #fafafa; border-radius: 12px; padding: 12px; margin-bottom: 12px; border-bottom: 2px solid #999999;}
|
||||
#site-name {text-align: center;flex: 1}
|
||||
.header h1{margin:0;display:inline-block}
|
||||
.flash{background-color:#ff9;border:yellow 1px solid}
|
||||
.infobox{width: 50%; float: right;}
|
||||
@media (max-width:639px) {
|
||||
.infobox{width: 100%;}
|
||||
}
|
||||
.weak{opacity:.5}
|
||||
.field_desc{display:block}
|
||||
ul.timeline{padding:0;margin:auto;max-width:960px;clear: both}
|
||||
ul.timeline > li{list-style:none;}
|
||||
.message-visual img{max-width:100%;margin:auto}
|
||||
.message-options-showhide::before{content:'\2026'}
|
||||
.message-options{display:none}
|
||||
.create_text{width:100%;height:8em}
|
||||
.biography_text{height:4em}
|
||||
.before-toggle:not(:checked) + input{display:none}
|
||||
.follow_button,input[type="submit"]{background-color:var(--accent);color:white;border-radius:3px;border:1px solid var(--accent)}
|
||||
.follow_button.following{background-color:transparent;color:var(--accent);border-color:var(--accent)}
|
||||
.copyright{font-size:smaller;text-align:center;color:#808080}
|
||||
.copyright a:link,.copyright a:visited{color:#31559e}
|
||||
.copyright ul{list-style:none;padding:0}
|
||||
.copyright ul > li{padding:0 3px}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="centered">
|
||||
<h2>Forbidden</h2>
|
||||
|
||||
<p><a href="/">Back to homepage.</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="centered">
|
||||
<h2>Not Found</h2>
|
||||
|
||||
<p><a href="/">Back to homepage.</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<h1>About {{ site_name }}</h1>
|
||||
|
||||
<ul>
|
||||
<li>{{ site_name }} {{ version }}</li>
|
||||
<li> Python {{ python_version }}</li>
|
||||
<li>Flask {{ flask_version }}</li>
|
||||
</ul>
|
||||
<p>Copyright © 2019, 2025 Sakuragasaki46.</p>
|
||||
|
||||
<h2>License</h2>
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files
|
||||
(the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
|
||||
|
||||
<p>Source code for this site: <a
|
||||
href="https://github.com/sakuragasaki46/coriplus/">
|
||||
https://github.com/sakuragasaki46/coriplus/</a>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ site_name }}</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css">
|
||||
<style>.done{opacity:.5}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><a href="{{ url_for('admin.homepage') }}">{{ site_name }}: Admin</a></h1>
|
||||
<div class="rightnav">
|
||||
<!-- what does it go here? -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="flash">{{ message }}</div>
|
||||
{% endfor %}
|
||||
<main>
|
||||
{% block body %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p class="copyright">© 2019 Sakuragasaki46.
|
||||
<a href="/about/">About</a> - <a href="/terms/">Terms</a> -
|
||||
<a href="/privacy/">Privacy</a></p>
|
||||
</div>
|
||||
<script src="/static/lib.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ url_for('admin.reports') }}">Reports</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<h2>Report detail #{{ report.id }}</h2>
|
||||
<div class="card">
|
||||
<p>Type: {{ [None, 'user', 'message'][report.media_type] }}</p>
|
||||
<p>Reason: <strong>{{ report_reasons[report.reason] }}</strong></p>
|
||||
<p>Status: <strong>{{ ['Unreviewed', 'Accepted', 'Declined'][report.status] }}</strong></p>
|
||||
|
||||
<h3>Detail</h3>
|
||||
{% if report.media is none %}
|
||||
<p><em>The media is unavailable.</em></p>
|
||||
{% elif report.media_type == 1 %}
|
||||
<p><em>Showing first 20 messages of the reported user.</em></p>
|
||||
<ul>
|
||||
{% for message in report.media.messages %}
|
||||
{% include "includes/reported_message.html" %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% elif report.media_type == 2 %}
|
||||
{% set message = report.media %}
|
||||
{% include "includes/reported_message.html" %}
|
||||
{% endif %}
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="submit" name="take_down" value="Take down">
|
||||
<input type="submit" name="discard" value="Discard">
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<ul>
|
||||
{% for report in report_list %}
|
||||
<li class="card {% if report.status > 0 %}done{% endif %}">
|
||||
<p><strong>#{{ report.id }}</strong>
|
||||
(<a href="{{ url_for('admin.reports_detail', id=report.id) }}">detail</a>)</p>
|
||||
<p>Type: {{ [None, 'user', 'message'][report.media_type] }}</p>
|
||||
<p>Reason: <strong>{{ report_reasons[report.reason] }}</strong></p>
|
||||
<p>Status: <strong>{{ ['Unreviewed', 'Accepted', 'Declined'][report.status] }}</strong></p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% include "includes/pagination.html" %}
|
||||
{% endblock %}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ site_name }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css">
|
||||
<meta name="og:title" content="Cori+">
|
||||
<meta name="og:description" content="A simple social network. Post text statuses, optionally with image.">
|
||||
<meta name="csrf_token" content="{{ csrf_token() }}">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Funnel+Sans:ital,wght@0,300..800;1,300..800&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<ul class="leftnav">
|
||||
|
||||
</ul>
|
||||
<h1 id="site-name"><a href="{{ url_for('website.homepage') }}">{{ site_name }}</a></h1>
|
||||
<ul class="rightnav">
|
||||
{% if current_user.is_anonymous %}
|
||||
<li><a href="{{ url_for('website.login', next=request.full_path) }}">{{ inline_svg('exit_to_app') }} <span class="mobile-collapse">log in</span></a></li>
|
||||
<li><a href="{{ url_for('website.register', next=request.full_path) }}">{{ inline_svg('person_add') }} <span class="mobile-collapse">register</span></a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ url_for('website.user_detail', username=current_user.username) }}">{{ inline_svg('person') }} {{ current_user.username }}</a></li>
|
||||
{% set notification_count = current_user.unseen_notification_count() %}
|
||||
<li><a href="{{ url_for('website.notifications') }}">{{ inline_svg('notifications') }} (<strong>{{ notification_count }}</strong>)</a></li>
|
||||
<li><a href="{{ url_for('website.public_timeline') }}">{{ inline_svg('explore') }} <span class="mobile-collapse">explore</span></a></li>
|
||||
<li><a href="{{ url_for('website.create') }}">{{ inline_svg('edit') }} <span class="mobile-collapse">create</span></a></li>
|
||||
<li><a href="{{ url_for('website.logout') }}">{{ inline_svg('exit_to_app') }} <span class="mobile-collapse">log out</span></a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content">
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="flash">{{ message }}</div>
|
||||
{% endfor %}
|
||||
<main>
|
||||
{% block body %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p class="copyright">© 2019, 2025 Sakuragasaki46.
|
||||
<a href="/about/">About</a> - <a href="/terms/">Terms</a> -
|
||||
<a href="/privacy/">Privacy</a> -
|
||||
<a href="https://github.com/sakuragasaki46/coriplus">GitHub</a></p>
|
||||
</div>
|
||||
<script src="/static/lib.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<h2>Change Password</h2>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<dl>
|
||||
<dt>Old password:</dt>
|
||||
<dd><input type="password" name="old_password"></dd>
|
||||
<dt>New password:</dt>
|
||||
<dd><input type="password" name="new_password"></dd>
|
||||
<dt>New password, again:</dt>
|
||||
<dd><input type="password" name="confirm_password"></dd>
|
||||
<dd><input type="submit" value="Save"></dd>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<h2>Confirm Deletion</h2>
|
||||
|
||||
<p>Are you sure you want to <u>permanently delete</u> this post?
|
||||
Neither you nor others will be able to see it;
|
||||
you cannot recover a post after it's deleted.</p>
|
||||
|
||||
<p><small>Tip: If you only want to hide it from the public,
|
||||
you can <a href="/edit/{{ message.id }}">set its privacy</a> to "Only me".</small></p>
|
||||
|
||||
<ul>
|
||||
<li>{% include "includes/message.html" %}</li>
|
||||
</ul>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="submit" value="Delete">
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
|
||||
<div class="card infobox">
|
||||
<h3>{{ user.full_name }}</h3>
|
||||
<p>{{ user.biography|enrich }}</p>
|
||||
{% if user.website %}
|
||||
{% set website = user.website %}
|
||||
{% set website = website if website.startswith(('http://', 'https://')) else 'http://' + website %}
|
||||
<p><span class="weak">Website:</span> {{ website|urlize }}</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<strong>{{ user.messages|count }}</strong> messages
|
||||
-
|
||||
<a href="{{ url_for('website.user_followers', username=user.username) }}"><strong>{{ user.followers()|count }}</strong></a> followers
|
||||
-
|
||||
<a href="{{ url_for('website.user_following', username=user.username) }}"><strong>{{ user.following()|count }}</strong></a> following
|
||||
</p>
|
||||
{% if user == current_user %}
|
||||
<p><a href="/edit_profile/">{{ inline_svg('edit') }} Edit profile</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<div>
|
||||
<p><strong>Message #{{ message.id }}</strong> (<a href="{# url_for('admin.message_info', id=message.id) #}">detail</a>)</p>
|
||||
<p>Author: <a href="{# url_for('admin.user_detail', id=message.user_id #}">{{ message.user.username }}</a></p>
|
||||
<p>Text:</p>
|
||||
<div style="border:1px solid gray;padding:12px">
|
||||
{{ message.text|enrich }}
|
||||
{% if message.uploads %}
|
||||
<div>
|
||||
<img src="/uploads/{{ message.uploads[0].filename() }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p>Privacy: {{ ['public', 'unlisted', 'friends', 'only me'][message.privacy] }}</p>
|
||||
<p>Date: {{ message.pub_date.strftime('%B %-d, %Y %H:%M:%S') }}</p>
|
||||
</div>
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
{% macro feed_message(message) %}
|
||||
<li class="card" id="{{ message.id }}">
|
||||
<p class="message-content">{{ message.text|enrich }}</p>
|
||||
{% if message.uploads %}
|
||||
<div class="message-visual">
|
||||
<img src="/uploads/{{ message.uploads[0].filename() }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="message-footer">
|
||||
<a href="javascript:void(0);" class="message-upvote" onclick="toggleUpvote({{ message.id }});">+</a>
|
||||
<span class="message-score">{{ message.score }}</span>
|
||||
-
|
||||
<a href="{{ url_for('website.user_detail', username=message.user.username) }}">{{ message.user.username }}</a>
|
||||
-
|
||||
{% set message_privacy = message.privacy %}
|
||||
{% if message_privacy == 0 %} Public
|
||||
{% elif message_privacy == 1 %} Unlisted
|
||||
{% elif message_privacy == 2 %} Friends
|
||||
{% elif message_privacy == 3 %} Only me
|
||||
{% endif %}
|
||||
-
|
||||
<time datetime="{{ message.pub_date.isoformat() }}" title="{{ message.pub_date.ctime() }}">{{ message.pub_date | human_date }}</time>
|
||||
-
|
||||
<a href="javascript:void(0);" onclick="showHideMessageOptions({{ message.id }});" class="message-options-showhide"></a>
|
||||
</p>
|
||||
<ul class="message-options">
|
||||
{% if message.user == current_user %}
|
||||
<li><a href="/edit/{{ message.id }}">Edit or change privacy</a></li>
|
||||
<li><a href="/delete/{{ message.id }}">Delete permanently</a></li>
|
||||
{% else %}
|
||||
<li><a href="/report/message/{{ message.id }}" target="_blank">Report</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<h1>Privacy Policy</h1>
|
||||
|
||||
<p>At {{ site_name }}, accessible from {{ request.host }}, one of our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information that is collected and recorded by {{ site_name }} and how we use it.</p>
|
||||
|
||||
<p>If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact us through email at sakuragasaki46@gmail.com</p>
|
||||
|
||||
<h2>Log Files</h2>
|
||||
|
||||
<p>{{ site_name }} follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users' movement on the website, and gathering demographic information.</p>
|
||||
|
||||
<h2>Cookies and Web Beacons</h2>
|
||||
|
||||
<p>Like any other website, {{ site_name }} uses 'cookies'. These cookies are used to store information including visitors' preferences, and the pages on the website that the visitor accessed or visited. The information is used to optimize the users' experience by customizing our web page content based on visitors' browser type and/or other information.</p>
|
||||
|
||||
<p>You can choose to disable cookies through your individual browser options. This, however, can and will hurt Your usage of {{ site_name }}</p>
|
||||
|
||||
<h2>Privacy Policies</h2>
|
||||
|
||||
<P>You may consult this list to find the Privacy Policy for each of the advertising partners of {{ site_name }}. Our Privacy Policy was created with the help of the <a href="https://www.privacypolicygenerator.info">Privacy Policy Generator</a> and the <a href="https://www.generateprivacypolicy.com">Generate Privacy Policy Generator</a>.</p>
|
||||
|
||||
<p>Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in their respective advertisements and links that appear on {{ site_name }}, which are sent directly to users' browser. They automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit.</p>
|
||||
|
||||
<p>Note that {{ site_name }} has no access to or control over these cookies that are used by third-party advertisers.</p>
|
||||
|
||||
<h2>Third Party Privacy Policies</h2>
|
||||
|
||||
<p>{{ site_name }}'s Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may include their practices and instructions about how to opt-out of certain options. You may find a complete list of these Privacy Policies and their links here: Privacy Policy Links.</p>
|
||||
|
||||
<h2>Legal Basis</h2>
|
||||
|
||||
<p>Legal Basis for treatment is Legitimate Interest, except:</p>
|
||||
<ul>
|
||||
<li>Transactional information, such as username, email and essential cookies, are treated according to Providing a Service.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Children's Information</h2>
|
||||
|
||||
<p>Another part of our priority is adding protection for children while using the internet. We encourage parents and guardians to observe, participate in, monitor, guide and/or exercise total control on their online activity.</p>
|
||||
|
||||
<p>{{ site_name }} does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.</p>
|
||||
|
||||
<h2>Online Privacy Policy Only</h2>
|
||||
|
||||
<p>This Privacy Policy applies only to our online activities and is valid for visitors to our website with regards to the information that they shared and/or collect in {{ site_name }}. This policy is not applicable to any information collected via channels other than this website.</p>
|
||||
|
||||
<h2>Consent</h2>
|
||||
|
||||
<p>By using our website, you hereby consent <u>irrevocably</u> to our Privacy Policy and agree to its Terms and Conditions.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Report – Cori+</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body{margin:0;font-family:sans-serif}
|
||||
.item{padding:12px;border-bottom:1px solid gray}
|
||||
.item h2{font-size:1em;margin:6px 0}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
<form id="reportForm" method="POST">
|
||||
<input id="reportFormValue" name="reason" type="hidden" value="">
|
||||
<input type="submit" style="display:none">
|
||||
</form>
|
||||
<script>
|
||||
function submitReport(value){
|
||||
reportFormValue.value = value;
|
||||
reportForm.submit();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{% extends "report_base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="item">
|
||||
<h1>Done</h1>
|
||||
|
||||
<p>Your report has been sent.<br />
|
||||
We'll review the user or message, and, if against our Community
|
||||
Guidelines, we'll remove it.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{% extends "report_base.html" %}
|
||||
|
||||
{% block body %}
|
||||
{% for reason in report_reasons %}
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<div class="item" onclick="submitReport({{ reason[0] }})">
|
||||
<h2>{{ reason[1] }}</h2>
|
||||
</div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{% extends "report_base.html" %}
|
||||
|
||||
{% block body %}
|
||||
{% for reason in report_reasons %}
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<div class="item" onclick="submitReport({{ reason[0] }})">
|
||||
<h2>{{ reason[1] }}</h2>
|
||||
</div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{% 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 %}
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
'''
|
||||
A list of utilities used across modules.
|
||||
'''
|
||||
|
||||
import datetime, re, base64, hashlib, string, sys, json
|
||||
|
||||
from flask_login import current_user
|
||||
from .models import User, Message, Notification, MSGPRV_PUBLIC, MSGPRV_UNLISTED, \
|
||||
MSGPRV_FRIENDS, MSGPRV_ONLYME
|
||||
from flask import abort, render_template, request, session
|
||||
from markupsafe import Markup
|
||||
|
||||
_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(s.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', encoding='utf-8') 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
|
||||
# DEPRECATED in 0.10; use current_user instead
|
||||
def get_current_user():
|
||||
# new in 0.7; need a different method to get current user id
|
||||
if request.path.startswith('/api/'):
|
||||
# assume token validation is already done
|
||||
return User[request.args['access_token'].split(':')[0]]
|
||||
elif current_user.is_authenticated:
|
||||
return current_user
|
||||
|
||||
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
|
||||
|
||||
def get_secret_key():
|
||||
from . import app
|
||||
secret_key = app.config['SECRET_KEY']
|
||||
if isinstance(secret_key, str):
|
||||
secret_key = secret_key.encode('utf-8')
|
||||
return secret_key
|
||||
|
||||
def generate_access_token(user):
|
||||
'''
|
||||
Generate access token for public API.
|
||||
'''
|
||||
h = hashlib.sha256(get_secret_key())
|
||||
h.update(b':')
|
||||
h.update(str(user.id).encode('utf-8'))
|
||||
h.update(b':')
|
||||
h.update(str(user.password).encode('utf-8'))
|
||||
return str(user.id) + ':' + h.hexdigest()[:32]
|
||||
|
||||
def check_access_token(token):
|
||||
uid, hh = token.split(':')
|
||||
try:
|
||||
user = User[uid]
|
||||
except User.DoesNotExist:
|
||||
return
|
||||
h = hashlib.sha256(get_secret_key())
|
||||
h.update(b':')
|
||||
h.update(str(user.id).encode('utf-8'))
|
||||
h.update(b':')
|
||||
h.update(str(user.password).encode('utf-8'))
|
||||
if h.hexdigest()[:32] == hh:
|
||||
return user
|
||||
|
||||
def create_mentions(cur_user, text, privacy):
|
||||
# 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({cur_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(cur_user) and
|
||||
cur_user.is_following(mention_user)):
|
||||
push_notification('mention', mention_user, user=user.id)
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
|
||||
# New in 0.9
|
||||
# changed in 0.10
|
||||
def inline_svg(name):
|
||||
return Markup('<span class="material-icons">{}</span>').format(name)
|
||||
|
|
@ -1,358 +0,0 @@
|
|||
'''
|
||||
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 current_user, login_required, login_user, logout_user
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
bp = Blueprint('website', __name__)
|
||||
|
||||
@bp.route('/')
|
||||
def homepage():
|
||||
if current_user and current_user.is_authenticated:
|
||||
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 = current_user
|
||||
messages = Visibility(Message
|
||||
.select()
|
||||
.where((Message.user << user.following())
|
||||
| (Message.user == user))
|
||||
.order_by(Message.pub_date.desc()))
|
||||
return object_list('feed.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,
|
||||
full_name=request.form.get('full_name') or username,
|
||||
password=pwdhash(request.form['password']),
|
||||
email=request.form['email'],
|
||||
birthday=birthday,
|
||||
join_date=datetime.datetime.now())
|
||||
UserProfile.create(
|
||||
user=user
|
||||
)
|
||||
|
||||
# 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 current_user and current_user.is_authenticated:
|
||||
flash('You are already logged in')
|
||||
return redirect(request.args.get('next', '/'))
|
||||
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())
|
||||
push_notification('follow', user, user=cur_user.id)
|
||||
flash('You are now following %s' % user.username)
|
||||
except IntegrityError:
|
||||
flash(f'Error following {user.username}')
|
||||
|
||||
|
||||
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:
|
||||
logger.info('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(user, text, privacy)
|
||||
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):
|
||||
user: User = current_user
|
||||
message: Message = get_object_or_404(Message, Message.id == id)
|
||||
if message.user != user:
|
||||
abort(404)
|
||||
if request.method == 'POST':
|
||||
if message.user == user:
|
||||
message.delete_instance()
|
||||
flash('Your message has been deleted forever')
|
||||
return redirect(request.args.get('next', '/'))
|
||||
return render_template('confirm_delete.html', message=message)
|
||||
|
||||
# Workaround for problems related to invalid data.
|
||||
# Without that, changes will be lost across requests.
|
||||
def profile_checkpoint():
|
||||
return UserProfile(
|
||||
user=get_current_user(),
|
||||
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,
|
||||
telegram=request.form['telegram'] 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())
|
||||
full_name = request.form['full_name'] or username
|
||||
if full_name != user.full_name:
|
||||
User.update(full_name=full_name).where(User.id == user.id).execute()
|
||||
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(
|
||||
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('/change_password/', methods=['GET', 'POST'])
|
||||
def change_password():
|
||||
user = get_current_user()
|
||||
if request.method == 'POST':
|
||||
old_password = request.form['old_password']
|
||||
new_password = request.form['new_password']
|
||||
confirm_password = request.form['confirm_password']
|
||||
errors = False
|
||||
if not new_password:
|
||||
flash('Password cannot be empty')
|
||||
errors = True
|
||||
if new_password != confirm_password:
|
||||
flash('Password mismatch')
|
||||
errors = True
|
||||
if pwdhash(old_password) != user.password:
|
||||
flash('The old password is incorrect')
|
||||
errors = True
|
||||
if not errors:
|
||||
user.update(
|
||||
password=pwdhash(new_password)
|
||||
)
|
||||
return redirect(url_for('website.edit_profile'))
|
||||
return render_template('change_password.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')
|
||||
|
||||
|
||||
BIN
src/favicon.ico
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,171 +0,0 @@
|
|||
"""Peewee migrations -- 001_0_9_to_0_10.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
@migrator.create_model
|
||||
class BaseModel(pw.Model):
|
||||
id = pw.AutoField()
|
||||
|
||||
class Meta:
|
||||
table_name = "basemodel"
|
||||
|
||||
@migrator.create_model
|
||||
class User(pw.Model):
|
||||
id = pw.AutoField()
|
||||
username = pw.CharField(max_length=255, unique=True)
|
||||
full_name = pw.TextField()
|
||||
password = pw.CharField(max_length=255)
|
||||
email = pw.CharField(max_length=255)
|
||||
birthday = pw.DateField()
|
||||
join_date = pw.DateTimeField()
|
||||
is_disabled = pw.IntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = "user"
|
||||
|
||||
@migrator.create_model
|
||||
class Message(pw.Model):
|
||||
id = pw.AutoField()
|
||||
user = pw.ForeignKeyField(column_name='user_id', field='id', model=migrator.orm['user'])
|
||||
text = pw.TextField()
|
||||
pub_date = pw.DateTimeField()
|
||||
privacy = pw.IntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = "message"
|
||||
|
||||
@migrator.create_model
|
||||
class MessageUpvote(pw.Model):
|
||||
id = pw.AutoField()
|
||||
message = pw.ForeignKeyField(column_name='message_id', field='id', model=migrator.orm['message'])
|
||||
user = pw.ForeignKeyField(column_name='user_id', field='id', model=migrator.orm['user'])
|
||||
created_date = pw.DateTimeField()
|
||||
|
||||
class Meta:
|
||||
table_name = "messageupvote"
|
||||
indexes = [(('message', 'user'), True)]
|
||||
|
||||
@migrator.create_model
|
||||
class Notification(pw.Model):
|
||||
id = pw.AutoField()
|
||||
type = pw.TextField()
|
||||
target = pw.ForeignKeyField(column_name='target_id', field='id', model=migrator.orm['user'])
|
||||
detail = pw.TextField()
|
||||
pub_date = pw.DateTimeField()
|
||||
seen = pw.IntegerField()
|
||||
|
||||
class Meta:
|
||||
table_name = "notification"
|
||||
|
||||
@migrator.create_model
|
||||
class Relationship(pw.Model):
|
||||
id = pw.AutoField()
|
||||
from_user = pw.ForeignKeyField(column_name='from_user_id', field='id', model=migrator.orm['user'])
|
||||
to_user = pw.ForeignKeyField(column_name='to_user_id', field='id', model=migrator.orm['user'])
|
||||
created_date = pw.DateTimeField()
|
||||
|
||||
class Meta:
|
||||
table_name = "relationship"
|
||||
indexes = [(('from_user', 'to_user'), True)]
|
||||
|
||||
@migrator.create_model
|
||||
class Report(pw.Model):
|
||||
id = pw.AutoField()
|
||||
media_type = pw.IntegerField()
|
||||
media_id = pw.IntegerField()
|
||||
sender = pw.ForeignKeyField(column_name='sender_id', field='id', model=migrator.orm['user'], null=True)
|
||||
reason = pw.IntegerField()
|
||||
status = pw.IntegerField()
|
||||
created_date = pw.DateTimeField()
|
||||
|
||||
class Meta:
|
||||
table_name = "report"
|
||||
|
||||
@migrator.create_model
|
||||
class Upload(pw.Model):
|
||||
id = pw.AutoField()
|
||||
type = pw.TextField()
|
||||
message = pw.ForeignKeyField(column_name='message_id', field='id', model=migrator.orm['message'])
|
||||
|
||||
class Meta:
|
||||
table_name = "upload"
|
||||
|
||||
@migrator.create_model
|
||||
class UserAdminship(pw.Model):
|
||||
user = pw.ForeignKeyField(column_name='user_id', field='id', model=migrator.orm['user'], primary_key=True)
|
||||
|
||||
class Meta:
|
||||
table_name = "useradminship"
|
||||
|
||||
@migrator.create_model
|
||||
class UserProfile(pw.Model):
|
||||
user = pw.ForeignKeyField(column_name='user_id', field='id', model=migrator.orm['user'], primary_key=True)
|
||||
biography = pw.TextField()
|
||||
location = pw.IntegerField(null=True)
|
||||
year = pw.IntegerField(null=True)
|
||||
website = pw.TextField(null=True)
|
||||
instagram = pw.TextField(null=True)
|
||||
facebook = pw.TextField(null=True)
|
||||
telegram = pw.TextField(null=True)
|
||||
|
||||
class Meta:
|
||||
table_name = "userprofile"
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_model('userprofile')
|
||||
|
||||
migrator.remove_model('useradminship')
|
||||
|
||||
migrator.remove_model('upload')
|
||||
|
||||
migrator.remove_model('report')
|
||||
|
||||
migrator.remove_model('relationship')
|
||||
|
||||
migrator.remove_model('notification')
|
||||
|
||||
migrator.remove_model('messageupvote')
|
||||
|
||||
migrator.remove_model('message')
|
||||
|
||||
migrator.remove_model('user')
|
||||
|
||||
migrator.remove_model('basemodel')
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
"""Peewee migrations -- 002_move_columns_from_userprofile.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
migrator.add_fields(
|
||||
'user',
|
||||
|
||||
biography=pw.CharField(max_length=256, default=""),
|
||||
website=pw.TextField(null=True))
|
||||
|
||||
migrator.change_fields('user', username=pw.CharField(max_length=30, unique=True))
|
||||
|
||||
migrator.change_fields('user', full_name=pw.CharField(max_length=80))
|
||||
|
||||
migrator.change_fields('user', password=pw.CharField(max_length=256))
|
||||
|
||||
migrator.change_fields('user', email=pw.CharField(max_length=256))
|
||||
|
||||
migrator.sql("""
|
||||
UPDATE "user" SET biography = (SELECT p.biography FROM userprofile p WHERE p.user_id = id LIMIT 1),
|
||||
website = (SELECT p.website FROM userprofile p WHERE p.user_id = id LIMIT 1);
|
||||
""")
|
||||
|
||||
migrator.remove_fields('userprofile', 'year', 'instagram', 'facebook', 'telegram')
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.add_fields(
|
||||
'userprofile',
|
||||
|
||||
year=pw.IntegerField(null=True),
|
||||
instagram=pw.TextField(null=True),
|
||||
facebook=pw.TextField(null=True),
|
||||
telegram=pw.TextField(null=True))
|
||||
|
||||
migrator.sql("""
|
||||
UPDATE "userprofile" SET biography = (SELECT p.biography FROM user p WHERE p.user_id = id LIMIT 1),
|
||||
website = (SELECT p.website FROM user p WHERE p.user_id = id LIMIT 1);
|
||||
""")
|
||||
|
||||
migrator.remove_fields('user', 'biography', 'website')
|
||||
|
||||
migrator.change_fields('user', username=pw.CharField(max_length=255, unique=True))
|
||||
|
||||
migrator.change_fields('user', full_name=pw.TextField())
|
||||
|
||||
migrator.change_fields('user', password=pw.CharField(max_length=255))
|
||||
|
||||
migrator.change_fields('user', email=pw.CharField(max_length=255))
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect('coriplus.sqlite')
|
||||
|
||||
if __name__ == '__main__':
|
||||
conn.executescript('''
|
||||
BEGIN TRANSACTION;
|
||||
ALTER TABLE userprofile ADD COLUMN telegram TEXT;
|
||||
COMMIT;
|
||||
''')
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import sqlite3
|
||||
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect('coriplus.sqlite')
|
||||
|
||||
if __name__ == '__main__':
|
||||
conn.executescript('''
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE "new_userprofile" ("user_id" INTEGER NOT NULL PRIMARY KEY, "biography" TEXT NOT NULL, "location" INTEGER, "year" INTEGER, "website" TEXT, "instagram" TEXT, "facebook" TEXT, telegram TEXT, FOREIGN KEY ("user_id") REFERENCES "user" ("id"));
|
||||
CREATE TABLE "new_user" ("id" INTEGER NOT NULL PRIMARY KEY, "username" VARCHAR(30) NOT NULL, "full_name" VARCHAR(30), "password" VARCHAR(255) NOT NULL, "email" VARCHAR(255) NOT NULL, "birthday" DATE NOT NULL, "join_date" DATETIME NOT NULL, "is_disabled" INTEGER NOT NULL);
|
||||
INSERT INTO new_user (id, username, full_name, password, email, birthday, join_date, is_disabled) SELECT t1.id, t1.username, t2.full_name, t1.password, t1.email, t1.birthday, t1.join_date, t1.is_disabled FROM user AS t1 LEFT JOIN userprofile AS t2 ON t1.id = t2.user_id;
|
||||
INSERT INTO new_userprofile (user_id, biography, location, year, website, instagram, facebook, telegram) SELECT user_id, biography, location, year, website, instagram, facebook, telegram FROM userprofile;
|
||||
UPDATE new_user SET full_name = username WHERE username IS NULL;
|
||||
DROP TABLE user;
|
||||
DROP TABLE userprofile;
|
||||
ALTER TABLE new_user RENAME TO user;
|
||||
ALTER TABLE new_userprofile RENAME TO userprofile;
|
||||
COMMIT;
|
||||
''')
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
User-Agent: *
|
||||
Disallow: /report/
|
||||
Noindex: /admin/
|
||||
|
|
@ -98,29 +98,3 @@ function showHideMessageOptions(id){
|
|||
options.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function getCsrfToken () {
|
||||
var csrf_token = document.querySelector('meta[name="csrf_token"]');
|
||||
return csrf_token?.getAttribute('content');
|
||||
}
|
||||
|
||||
function toggleUpvote(id){
|
||||
var msgElem = document.getElementById(id);
|
||||
//var upvoteLink = msgElem.getElementsByClassName('message-upvote')[0];
|
||||
var scoreCounter = msgElem.getElementsByClassName('message-score')[0];
|
||||
var body = "csrf_token=" + getCsrfToken();
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/ajax/score/" + id + "/toggle", true);
|
||||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
// TODO add csrf token somewhere
|
||||
xhr.onreadystatechange = function(){
|
||||
if(xhr.readyState == XMLHttpRequest.DONE){
|
||||
if(xhr.status == 200){
|
||||
console.log('liked #' + id);
|
||||
var data = JSON.parse(xhr.responseText);
|
||||
scoreCounter.innerHTML = data.score;
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(body);
|
||||
}
|
||||
28
static/style.css
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
body,button,input,select,textarea{font-family:'Segoe UI',Arial,Helvetica,sans-serif}
|
||||
body{margin:0}
|
||||
.header{padding:12px;color:white;background-color:#ff3018}
|
||||
.content{padding:12px}
|
||||
.header a{color:white}
|
||||
.content a{color:#3399ff}
|
||||
.content a.plus{color:#ff3018}
|
||||
.metanav{float:right}
|
||||
.header h1{margin:0;display:inline-block}
|
||||
.flash{background-color:#ff9;border:yellow 1px solid}
|
||||
.infobox{padding:12px;border:#ccc 1px solid}
|
||||
@media (min-width:640px) {
|
||||
.infobox{float:right;width:320px}
|
||||
}
|
||||
.weak{opacity:.5}
|
||||
.field_desc{display:block}
|
||||
.message-visual img{max-width:100%;max-height:8em}
|
||||
.message-options-showhide::before{content:'\2026'}
|
||||
.message-options{display:none}
|
||||
.create_text{width:100%;height:8em}
|
||||
.biography_text{height:4em}
|
||||
.before-toggle:not(:checked) + input{display:none}
|
||||
.follow_button,input[type="submit"]{background-color:#ff3018;color:white;border-radius:3px;border:1px solid #ff3018}
|
||||
.follow_button.following{background-color:transparent;color:#ff3018;border-color:#ff3018}
|
||||
.copyright{font-size:smaller;text-align:center;color:#808080}
|
||||
.copyright a:link,.copyright a:visited{color:#31559e}
|
||||
.copyright ul{list-style:none;padding:0}
|
||||
.copyright ul > li{padding:0 3px}
|
||||
7
templates/404.html
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<h2>Not Found</h2>
|
||||
|
||||
<p><a href="/">Back to homepage.</a></p>
|
||||
{% endblock %}
|
||||
33
templates/about.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<h1>About {{ site_name }}</h1>
|
||||
|
||||
<p>Version: {{ version }}</p>
|
||||
<p>Copyright © 2019 Sakuragasaki46.</p>
|
||||
|
||||
<h2>License</h2>
|
||||
<p>Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files
|
||||
(the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:</p>
|
||||
|
||||
<p>The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.</p>
|
||||
|
||||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
|
||||
|
||||
<p>Source code for this site: <a
|
||||
href="https://github.com/sakuragasaki46/coriplus/">
|
||||
https://github.com/sakuragasaki46/coriplus/</a>
|
||||
|
||||
{% endblock %}
|
||||
43
templates/base.html
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ site_name }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css">
|
||||
<meta name="og:title" content="Cori+">
|
||||
<meta name="og:description" content="A simple social network. Post text statuses, optionally with image.">
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><a href="{{ url_for('homepage') }}">{{ site_name }}</a></h1>
|
||||
<div class="metanav">
|
||||
{% if current_user.is_anonymous %}
|
||||
<a href="{{ url_for('login') }}">log in</a>
|
||||
<a href="{{ url_for('register') }}">register</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('user_detail', username=current_user.username) }}">{{ current_user.username }}</a>
|
||||
{% set notification_count = current_user.unseen_notification_count() %}
|
||||
{% if notification_count > 0 %}
|
||||
<a href="{{ url_for('notifications') }}">(<strong>{{ notification_count }}</strong>)</a>
|
||||
{% endif %}
|
||||
-
|
||||
<a href="{{ url_for('public_timeline') }}">explore</a>
|
||||
<a href="{{ url_for('create') }}">create</a>
|
||||
<a href="{{ url_for('logout') }}">log out</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="flash">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p class="copyright">© 2019 Sakuragasaki46.
|
||||
<a href="/about/">About</a> - <a href="/terms/">Terms</a> -
|
||||
<a href="/privacy/">Privacy</a></p>
|
||||
</div>
|
||||
<script src="/static/lib.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<h2>Create</h2>
|
||||
<form action="{{ url_for('website.create') }}" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<form action="{{ url_for('create') }}" method="POST" enctype="multipart/form-data">
|
||||
<dl>
|
||||
<dt>Message:</dt>
|
||||
<dd><textarea name="text" placeholder="What's happening?" class="create_text">{{ request.args['preload'] }}</textarea></dd>
|
||||
|
|
@ -17,5 +15,4 @@
|
|||
<dd><input type="submit" value="Create" /></dd>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<h2>Edit</h2>
|
||||
<form action="{{ url_for('website.edit', id=message.id) }}" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<form action="{{ url_for('edit', id=message.id) }}" method="POST" enctype="multipart/form-data">
|
||||
<dl>
|
||||
<dt>Message:</dt>
|
||||
<dd><textarea name="text" required="" class="create_text">{{ message.text }}</textarea></dd>
|
||||
|
|
@ -16,5 +14,4 @@
|
|||
<dd><input type="submit" value="Save" /></dd>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<h2>Edit Profile</h2>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<dl>
|
||||
<dt>Username:</dt>
|
||||
<dd><input type="text" class="username-input" name="username" required value="{{ current_user.username }}" autocomplete="off"></dd>
|
||||
|
|
@ -29,10 +27,7 @@
|
|||
<dd><input type="text" name="instagram" value="{{ profile.instagram or '' }}"></dd>
|
||||
<dt>Facebook:</dt>
|
||||
<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>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "macros/message.html" import feed_message with context %}
|
||||
{% block body %}
|
||||
<h2>Explore</h2>
|
||||
<ul class="timeline">
|
||||
<ul>
|
||||
{% for message in message_list %}
|
||||
{{ feed_message(message) }}
|
||||
<li id="{{ message.id }}">{% include "includes/message.html" %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% include "includes/pagination.html" %}
|
||||
7
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('login')}}">Log in</a> or <a href="{{url_for('register')}}">register</a> to see more.</p>
|
||||
{% endblock %}
|
||||
32
templates/includes/infobox_profile.html
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{% set profile = user.profile %}
|
||||
<div class="infobox">
|
||||
<h3>{{ profile.full_name }}</h3>
|
||||
<p>{{ profile.biography|enrich }}</p>
|
||||
{% if profile.location %}
|
||||
<p><span class="weak">Location:</span> {{ profile.location|locationdata }}</p>
|
||||
{% endif %}
|
||||
{% if profile.year %}
|
||||
<p><span class="weak">Year:</span> {{ profile.year }}</p>
|
||||
{% endif %}
|
||||
{% if profile.website %}
|
||||
{% set website = profile.website %}
|
||||
{% set website = website if website.startswith(('http://', 'https://')) else 'http://' + website %}
|
||||
<p><span class="weak">Website:</span> {{ profile.website|urlize }}</p>
|
||||
{% endif %}
|
||||
{% if profile.instagram %}
|
||||
<p><span class="weak">Instagram:</span> <a href="https://www.instagram.com/{{ profile.instagram }}">{{ profile.instagram }}</a></p>
|
||||
{% endif %}
|
||||
{% if profile.facebook %}
|
||||
<p><span class="weak">Facebook:</span> <a href="https://facebook.com/{{ profile.facebook }}">{{ profile.facebook }}</a></p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<strong>{{ user.messages|count }}</strong> messages
|
||||
-
|
||||
<strong>{{ user.followers()|count }}</strong> followers
|
||||
-
|
||||
<strong>{{ user.following()|count }}</strong> following
|
||||
</p>
|
||||
{% if user == current_user %}
|
||||
<p><a href="/edit_profile/">Edit profile</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
@ -1,20 +1,16 @@
|
|||
<p class="message-content">{{ message.text|enrich }}</p>
|
||||
{% if message.uploads %}
|
||||
<div class="message-visual">
|
||||
<img src="/uploads/{{ message.uploads[0].filename() }}">
|
||||
<img src="/uploads/{{message.uploads[0].filename()}}">
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="message-footer">
|
||||
<a href="javascript:void(0);" class="message-upvote" onclick="toggleUpvote({{ message.id }});">+</a>
|
||||
<span class="message-score">{{ message.score }}</span>
|
||||
-
|
||||
<a href="{{ url_for('website.user_detail', username=message.user.username) }}">{{ message.user.username }}</a>
|
||||
<a href="{{ url_for('user_detail', username=message.user.username) }}">{{ message.user.username }}</a>
|
||||
-
|
||||
{% set message_privacy = message.privacy %}
|
||||
{% if message_privacy == 0 %} Public
|
||||
{% elif message_privacy == 1 %} Unlisted
|
||||
{% elif message_privacy == 2 %} Friends
|
||||
{% elif message_privacy == 3 %} Only me
|
||||
{% if message.privacy in (0, 1) %} Public
|
||||
{% elif message.privacy == 2 %} Friends
|
||||
{% elif message.privacy == 3 %} Only me
|
||||
{% endif %}
|
||||
-
|
||||
<time datetime="{{ message.pub_date.isoformat() }}" title="{{ message.pub_date.ctime() }}">{{ message.pub_date | human_date }}</time>
|
||||
|
|
@ -24,8 +20,8 @@
|
|||
<ul class="message-options">
|
||||
{% if message.user == current_user %}
|
||||
<li><a href="/edit/{{ message.id }}">Edit or change privacy</a></li>
|
||||
<li><a href="/delete/{{ message.id }}">Delete permanently</a></li>
|
||||
<!--li><a href="/confirm_delete/{{ message.id }}">Delete</a></li-->
|
||||
{% else %}
|
||||
<li><a href="/report/message/{{ message.id }}" target="_blank">Report</a></li>
|
||||
<!--li><a href="/report/{{ message.id }}">Report</a></li-->
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<h2>Join {{ site_name }}</h2>
|
||||
<form action="{{ url_for('website.register') }}" method="POST">
|
||||
<form action="{{ url_for('register') }}" method="POST">
|
||||
<dl>
|
||||
<dt>Username:</dt>
|
||||
<dd><input type="text" class="username-input" name="username" autocomplete="off"></dd>
|
||||
|
|
@ -33,5 +32,4 @@
|
|||
<dd><input type="submit" value="Join">
|
||||
</dl>
|
||||
</form>
|
||||
<div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<h2>Login</h2>
|
||||
{% if error %}<p class="error"><strong>Error:</strong> {{ error }}</p>{% endif %}
|
||||
<div class="card">
|
||||
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<dl>
|
||||
<dt>Username or email:
|
||||
<dd><input type="text" name="username">
|
||||
|
|
@ -20,5 +18,4 @@
|
|||
<dd><input type="submit" value="Login">
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<h2>Notifications</h2>
|
||||
<ul>
|
||||
{% for notification in notification_list %}
|
||||
<li class="card">{% include "includes/notification.html" %}</li>
|
||||
<li>{% include "includes/notification.html" %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% include "includes/pagination.html" %}
|
||||
47
templates/privacy.html
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Privacy Policy</h1>
|
||||
|
||||
<p>At {{ site_name }}, accessible from {{ request.host }}, one of our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information that is collected and recorded by {{ site_name }} and how we use it.</p>
|
||||
|
||||
<p>If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact us through email at sakuragasaki46@gmail.com</p>
|
||||
|
||||
<h2>Log Files</h2>
|
||||
|
||||
<p>{{ site_name }} follows a standard procedure of using log files. These files log visitors when they visit websites. All hosting companies do this and a part of hosting services' analytics. The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users' movement on the website, and gathering demographic information.</p>
|
||||
|
||||
<h2>Cookies and Web Beacons</h2>
|
||||
|
||||
<p>Like any other website, {{ site_name }} uses 'cookies'. These cookies are used to store information including visitors' preferences, and the pages on the website that the visitor accessed or visited. The information is used to optimize the users' experience by customizing our web page content based on visitors' browser type and/or other information.</p>
|
||||
|
||||
|
||||
|
||||
<h2>Privacy Policies</h2>
|
||||
|
||||
<P>You may consult this list to find the Privacy Policy for each of the advertising partners of {{ site_name }}. Our Privacy Policy was created with the help of the <a href="https://www.privacypolicygenerator.info">Privacy Policy Generator</a> and the <a href="https://www.generateprivacypolicy.com">Generate Privacy Policy Generator</a>.</p>
|
||||
|
||||
<p>Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in their respective advertisements and links that appear on {{ site_name }}, which are sent directly to users' browser. They automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit.</p>
|
||||
|
||||
<p>Note that {{ site_name }} has no access to or control over these cookies that are used by third-party advertisers.</p>
|
||||
|
||||
<h2>Third Party Privacy Policies</h2>
|
||||
|
||||
<p>{{ site_name }}'s Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may include their practices and instructions about how to opt-out of certain options. You may find a complete list of these Privacy Policies and their links here: Privacy Policy Links.</p>
|
||||
|
||||
<p>You can choose to disable cookies through your individual browser options. To know more detailed information about cookie management with specific web browsers, it can be found at the browsers' respective websites. What Are Cookies?</p>
|
||||
|
||||
<h2>Children's Information</h2>
|
||||
|
||||
<p>Another part of our priority is adding protection for children while using the internet. We encourage parents and guardians to observe, participate in, and/or monitor and guide their online activity.</p>
|
||||
|
||||
<p>{{ site_name }} does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you think that your child provided this kind of information on our website, we strongly encourage you to contact us immediately and we will do our best efforts to promptly remove such information from our records.</p>
|
||||
|
||||
<h2>Online Privacy Policy Only</h2>
|
||||
|
||||
<p>This Privacy Policy applies only to our online activities and is valid for visitors to our website with regards to the information that they shared and/or collect in {{ site_name }}. This policy is not applicable to any information collected offline or via channels other than this website.</p>
|
||||
|
||||
<h2>Consent</h2>
|
||||
|
||||
<p>By using our website, you hereby consent to our Privacy Policy and agree to its Terms and Conditions.</p>
|
||||
{% endblock %}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "macros/message.html" import feed_message with context %}
|
||||
{% block body %}
|
||||
<h2>Your Timeline</h2>
|
||||
<ul class="timeline">
|
||||
<ul>
|
||||
{% for message in message_list %}
|
||||
{{ feed_message(message) }}
|
||||
<li id="{{ message.id }}">{% include "includes/message.html" %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% include "includes/pagination.html" %}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<h1>Terms of Service</h1>
|
||||
|
||||
<p>[decline to state]</p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -1,18 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "macros/message.html" import feed_message with context %}
|
||||
{% block body %}
|
||||
{% include "includes/infobox_profile.html" %}
|
||||
<h2>Messages from {{ user.username }}</h2>
|
||||
{% if not current_user.is_anonymous %}
|
||||
{% if user.username != current_user.username %}
|
||||
{% if current_user|is_following(user) %}
|
||||
<form action="{{ url_for('website.user_unfollow', username=user.username) }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<form action="{{ url_for('user_unfollow', username=user.username) }}" method="post">
|
||||
<input type="submit" class="follow_button following" value="- Un-follow" />
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="{{ url_for('website.user_follow', username=user.username) }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<form action="{{ url_for('user_follow', username=user.username) }}" method="post">
|
||||
<input type="submit" class="follow_button" value="+ Follow" />
|
||||
</form>
|
||||
{% endif %}
|
||||
|
|
@ -21,9 +18,9 @@
|
|||
<a href="/create/">Create a message</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<ul class="timeline">
|
||||
<ul>
|
||||
{% for message in message_list %}
|
||||
{{ feed_message(message) }}
|
||||
<li id="{{ message.id }}">{% include "includes/message.html" %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% include "includes/pagination.html" %}
|
||||