Compare commits

..

1 commit
master ... 0.7

Author SHA1 Message Date
b1d7dfb1bf Changing version number 2019-11-05 22:21:39 +01:00
77 changed files with 525 additions and 1730 deletions

17
.gitignore vendored
View file

@ -4,18 +4,5 @@ __pycache__/
uploads/ uploads/
*.pyc *.pyc
**~ **~
.*.swp **/.*.swp
__pycache__/ **/__pycache__/
venv
.env
.venv
env
data/
conf/
config/
\#*\#
.\#*
node_modules/
alembic.ini
**.egg-info
.vscode

View file

@ -1,43 +1,6 @@
# Changelog # Changelog
## 0.10.0 ## 0.7.1-dev
+ 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). * Adding `messages_count`, `followers_count` and `following_count` to `profile_info` API endpoint (forgot to release).

View file

@ -6,8 +6,6 @@ To run the app, do "flask run" in the package's parent directory.
Based on Tweepee example of [peewee](https://github.com/coleifer/peewee/). 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 ## Features
* Create text statuses, optionally with image * Create text statuses, optionally with image
@ -16,19 +14,10 @@ This is the server. For the client, see [coriplusapp](https://github.com/sakurag
* Add info to your profile * Add info to your profile
* In-site notifications * In-site notifications
* Public API * Public API
* SQLite (or PostgreSQL)-based app * SQLite-based app
## Requirements ## Requirements
* **Python 3.10+** with **pip**. * **Python 3** only. We don't want to support Python 2.
* **Flask** web framework. * **Flask** web framework (also required extension **Flask-Login**).
* **Peewee** ORM. * **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

View file

@ -7,46 +7,31 @@ This module also contains very basic web hooks, such as robots.txt.
For the website hooks, see `app.website`. For the website hooks, see `app.website`.
For the AJAX hook, see `app.ajax`. 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 template filters, see `app.filters`.
For the database models, see `app.models`. For the database models, see `app.models`.
For other, see `app.utils`. For other, see `app.utils`.
''' '''
from flask import ( from flask import (
Flask, g, jsonify, render_template, request, Flask, abort, flash, g, jsonify, redirect, render_template, request,
send_from_directory, __version__ as flask_version) send_from_directory, session, url_for, __version__ as flask_version)
import os, sys import hashlib
import datetime, time, re, os, sys, string, json, html
from functools import wraps
from flask_login import LoginManager from flask_login import LoginManager
from flask_wtf import CSRFProtect
import dotenv
import logging
__version__ = '0.10.0-dev50' __version__ = '0.7.1-dev'
# we want to support Python 3.10+ only. # we want to support Python 3 only.
# Python 2 has too many caveats. # Python 2 has too many caveats.
# Python <=3.9 has harder type support. if sys.version_info[0] < 3:
if sys.version_info[0:2] < (3, 10): raise RuntimeError('Python 3 required')
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 = Flask(__name__)
app.secret_key = os.environ['SECRET_KEY'] app.config.from_pyfile('../config.py')
login_manager = LoginManager(app) login_manager = LoginManager(app)
CSRFProtect(app)
from .models import * from .models import *
from .utils import * from .utils import *
@ -63,43 +48,28 @@ def before_request():
try: try:
g.db.connect() g.db.connect()
except OperationalError: except OperationalError:
logger.error('database connected twice') sys.stderr.write('database connected twice.\n')
@app.after_request @app.after_request
def after_request(response): def after_request(response):
try: g.db.close()
g.db.close()
except Exception:
logger.error('database closed twice')
return response return response
@app.context_processor @app.context_processor
def _inject_variables(): def _inject_variables():
return { return {'site_name': app.config['SITE_NAME'], 'locations': locations}
'site_name': os.environ.get('APP_NAME', 'Cori+'),
'locations': locations,
'inline_svg': inline_svg
}
@login_manager.user_loader @login_manager.user_loader
def _inject_user(userid): def _inject_user(userid):
return User[userid] return User[userid]
@app.errorhandler(403)
def error_403(body):
return render_template('403.html'), 403
@app.errorhandler(404) @app.errorhandler(404)
def error_404(body): def error_404(body):
return render_template('404.html'), 404 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') @app.route('/robots.txt')
def robots_txt(): def robots_txt():
return send_from_directory(BASEDIR, 'src/robots.txt') return send_from_directory(os.getcwd(), 'robots.txt')
@app.route('/uploads/<id>.<type>') @app.route('/uploads/<id>.<type>')
def uploads(id, type='jpg'): def uploads(id, type='jpg'):
@ -148,8 +118,4 @@ app.register_blueprint(bp)
from .api import bp from .api import bp
app.register_blueprint(bp) app.register_blueprint(bp)
from .reports import bp
app.register_blueprint(bp)
from .admin import bp
app.register_blueprint(bp)

View file

@ -5,10 +5,8 @@ Warning: this is not the public API.
''' '''
from flask import Blueprint, jsonify from flask import Blueprint, jsonify
from flask_login import current_user from .models import User
from .models import User, Message, MessageUpvote from .utils import locations, get_current_user, is_username
from .utils import locations, is_username
import datetime
bp = Blueprint('ajax', __name__, url_prefix='/ajax') bp = Blueprint('ajax', __name__, url_prefix='/ajax')
@ -37,30 +35,3 @@ def location_search(name):
if value.lower().startswith(name.lower()): if value.lower().startswith(name.lower()):
results.append({'value': key, 'display': value}) results.append({'value': key, 'display': value})
return jsonify({'results': results}) 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"
})

183
app/api.py Normal file
View file

@ -0,0 +1,183 @@
from flask import Blueprint, jsonify, request
import sys, datetime, re
from functools import wraps
from .models import User, Message, Relationship, \
MSGPRV_PUBLIC, MSGPRV_UNLISTED, MSGPRV_FRIENDS, MSGPRV_ONLYME
from .utils import check_access_token, Visibility
bp = Blueprint('api', __name__, url_prefix='/api/V1')
def get_message_info(message):
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()
}
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(date)
query = Visibility(Message
.select()
.where(((Message.user << self.following())
| (Message.user == self))
& (Message.pub_date < date))
.order_by(Message.pub_date.desc())
.limit(20))
for message in query:
timeline_media.append(get_message_info(message))
return {'timeline_media': 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)
# Currently, API does not support files.
# 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({self.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(self) and
self.is_following(mention_user)):
push_notification('mention', mention_user, user=user.id)
except User.DoesNotExist:
pass
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,
"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(date)
query = Visibility(Message
.select()
.where((Message.user == user)
& (Message.pub_date < date))
.order_by(Message.pub_date.desc())
.limit(20))
for message in query:
timeline_media.append(get_message_info(message))
return {'timeline_media': timeline_media}
@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'] + '%')).limit(20)
results = []
for result in query:
profile = result.profile
results.append({
"id": result.id,
"username": result.username,
"full_name": profile.full_name,
"followers_count": len(result.followers())
})
return {
"users": results
}

View file

@ -2,9 +2,9 @@
Filter functions used in the website templates. Filter functions used in the website templates.
''' '''
from markupsafe import Markup from flask import Markup
import html, datetime, re, time import html, datetime, re, time
from .utils import tokenize, inline_svg as _inline_svg from .utils import tokenize
from . import app from . import app
@app.template_filter() @app.template_filter()
@ -64,4 +64,3 @@ def is_following(from_user, to_user):
def locationdata(key): def locationdata(key):
if key > 0: if key > 0:
return locations[str(key)] return locations[str(key)]

View file

@ -11,45 +11,31 @@ The tables are:
* notification - a in-site notification to a user; new in 0.3 * notification - a in-site notification to a user; new in 0.3
''' '''
from flask import request
from peewee import * from peewee import *
from playhouse.db_url import connect
import os import os
from . import BASEDIR
# here should go `from .utils import get_current_user`, but it will cause # here should go `from .utils import get_current_user`, but it will cause
# import errors. It's instead imported at function level. # import errors. It's instead imported at function level.
database = connect(os.environ['DATABASE_URL']) database = SqliteDatabase(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'coriplus.sqlite'))
class BaseModel(Model): class BaseModel(Model):
id = AutoField(primary_key=True)
class Meta: class Meta:
database = database database = database
# A user. The user is separated from its page. # A user. The user is separated from its page.
class User(BaseModel): class User(BaseModel):
# The unique username. # The unique username.
username = CharField(30, unique=True) username = CharField(unique=True)
# The user's full name (here for better search since 0.8)
full_name = CharField(80)
# The password hash. # The password hash.
password = CharField(256) password = CharField()
# An email address. # An email address.
email = CharField(256) email = CharField()
# The date of birth (required because of Terms of Service) # The date of birth (required because of Terms of Service)
birthday = DateField() birthday = DateField()
# The date joined # The date joined
join_date = DateTimeField() join_date = DateTimeField()
# A disabled flag. 0 = active, 1 = disabled by user, 2 = banned # A disabled flag. 0 = active, 1 = disabled by user, 2 = banned
is_disabled = IntegerField(default=0) is_disabled = IntegerField(default=0)
# Short description of user.
biography = CharField(256, default='')
# Personal website.
website = TextField(null=True)
# Helpers for flask_login # Helpers for flask_login
def get_id(self): def get_id(self):
@ -62,7 +48,8 @@ class User(BaseModel):
return False return False
@property @property
def is_authenticated(self): def is_authenticated(self):
return True from .utils import get_current_user
return self == get_current_user()
# it often makes sense to put convenience methods on model instances, for # it often makes sense to put convenience methods on model instances, for
# example, "give me all the users this user is following": # example, "give me all the users this user is following":
@ -118,18 +105,16 @@ class UserAdminship(BaseModel):
# User profile. # User profile.
# Additional info for identifying users. # Additional info for identifying users.
# New in 0.6 # New in 0.6
# Deprecated in 0.10 and merged with User
class UserProfile(BaseModel): class UserProfile(BaseModel):
user = ForeignKeyField(User, primary_key=True) user = ForeignKeyField(User, primary_key=True)
full_name = TextField()
biography = TextField(default='') biography = TextField(default='')
location = IntegerField(null=True) location = IntegerField(null=True)
year = IntegerField(null=True)
website = TextField(null=True) website = TextField(null=True)
@property instagram = TextField(null=True)
def full_name(self): facebook = TextField(null=True)
''' telegram = TextField(null=True)
Moved to User in 0.8 for search improvement reasons.
'''
return self.user.full_name
# The message privacy values. # The message privacy values.
MSGPRV_PUBLIC = 0 # everyone MSGPRV_PUBLIC = 0 # everyone
@ -164,22 +149,11 @@ class Message(BaseModel):
# even if unlisted # even if unlisted
return not is_public_timeline return not is_public_timeline
elif privacy == MSGPRV_FRIENDS: elif privacy == MSGPRV_FRIENDS:
if not cur_user or cur_user.is_anonymous: if cur_user.is_anonymous:
return False return False
return user.is_following(cur_user) and cur_user.is_following(user) return user.is_following(cur_user) and cur_user.is_following(user)
else: else:
return False 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 # 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 # model a "many-to-many" relationship between users. by querying and joining
@ -197,7 +171,7 @@ class Relationship(BaseModel):
) )
UPLOAD_DIRECTORY = os.path.join(BASEDIR, 'uploads') UPLOAD_DIRECTORY = os.path.join(os.path.split(os.path.dirname(__file__))[0], 'uploads')
class Upload(BaseModel): class Upload(BaseModel):
# the extension of the media # the extension of the media
@ -207,8 +181,6 @@ class Upload(BaseModel):
# helper to retrieve contents # helper to retrieve contents
def filename(self): def filename(self):
return str(self.id) + '.' + self.type return str(self.id) + '.' + self.type
def url(self):
return request.host_url + 'uploads/' + self.filename()
class Notification(BaseModel): class Notification(BaseModel):
type = TextField() type = TextField()
@ -216,76 +188,11 @@ class Notification(BaseModel):
detail = TextField() detail = TextField()
pub_date = DateTimeField() pub_date = DateTimeField()
seen = IntegerField(default=0) 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(): def create_tables():
with database: with database:
database.create_tables([ database.create_tables([
User, UserAdminship, UserProfile, Message, Relationship, User, UserAdminship, UserProfile, Message, Relationship,
Upload, Notification, Report, MessageUpvote]) Upload, Notification])
if not os.path.isdir(UPLOAD_DIRECTORY): if not os.path.isdir(UPLOAD_DIRECTORY):
os.makedirs(UPLOAD_DIRECTORY) os.makedirs(UPLOAD_DIRECTORY)

View file

@ -98,29 +98,3 @@ function showHideMessageOptions(id){
options.style.display = 'block'; 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
app/static/style.css Normal file
View 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
app/templates/404.html Normal file
View file

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block body %}
<h2>Not Found</h2>
<p><a href="/">Back to homepage.</a></p>
{% endblock %}

34
app/templates/about.html Normal file
View file

@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block body %}
<h1>About {{ site_name }}</h1>
<p>{{ site_name }} {{ version }} &ndash; Python {{ python_version }} &ndash;
Flask {{ flask_version }}</p>
<p>Copyright &copy; 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
app/templates/base.html Normal file
View 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('website.homepage') }}">{{ site_name }}</a></h1>
<div class="metanav">
{% if current_user.is_anonymous %}
<a href="{{ url_for('website.login', next=request.full_path) }}">log in</a>
<a href="{{ url_for('website.register', next=request.full_path) }}">register</a>
{% else %}
<a href="{{ url_for('website.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('website.notifications') }}">(<strong>{{ notification_count }}</strong>)</a>
{% endif %}
-
<a href="{{ url_for('website.public_timeline') }}">explore</a>
<a href="{{ url_for('website.create') }}">create</a>
<a href="{{ url_for('website.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">&copy; 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>

View file

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block body %}
<h2>Change Password</h2>
<form method="POST">
<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>
{% endblock %}

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block body %}
<h2>Confirm Deletion</h2>
<p>Are you sure you want to permanently delete this post?
Neither you nor others will be able to see it;
you cannot recover a post after it's deleted.</p>
<p>If you only want to hide it from the public,
you can <a href="/edit/{{ message.id }}">set its privacy</a> to "Only me".</p>
<p>Here's the content of the message for reference:</p>
<ul>
<li>{% include "includes/message.html" %}</li>
</ul>
<form method="POST">
<input type="submit" value="Delete">
</form>
{% endblock %}

View file

@ -1,9 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
<div class="card">
<h2>Create</h2> <h2>Create</h2>
<form action="{{ url_for('website.create') }}" method="POST" enctype="multipart/form-data"> <form action="{{ url_for('website.create') }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<dl> <dl>
<dt>Message:</dt> <dt>Message:</dt>
<dd><textarea name="text" placeholder="What's happening?" class="create_text">{{ request.args['preload'] }}</textarea></dd> <dd><textarea name="text" placeholder="What's happening?" class="create_text">{{ request.args['preload'] }}</textarea></dd>
@ -17,5 +15,4 @@
<dd><input type="submit" value="Create" /></dd> <dd><input type="submit" value="Create" /></dd>
</dl> </dl>
</form> </form>
</div>
{% endblock %} {% endblock %}

View file

@ -1,9 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
<div class="card">
<h2>Edit</h2> <h2>Edit</h2>
<form action="{{ url_for('website.edit', id=message.id) }}" method="POST" enctype="multipart/form-data"> <form action="{{ url_for('website.edit', id=message.id) }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<dl> <dl>
<dt>Message:</dt> <dt>Message:</dt>
<dd><textarea name="text" required="" class="create_text">{{ message.text }}</textarea></dd> <dd><textarea name="text" required="" class="create_text">{{ message.text }}</textarea></dd>
@ -16,5 +14,4 @@
<dd><input type="submit" value="Save" /></dd> <dd><input type="submit" value="Save" /></dd>
</dl> </dl>
</form> </form>
</div>
{% endblock %} {% endblock %}

View file

@ -1,11 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
<div class="card">
<h2>Edit Profile</h2> <h2>Edit Profile</h2>
<form method="POST"> <form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<dl> <dl>
<dt>Username:</dt> <dt>Username:</dt>
<dd><input type="text" class="username-input" name="username" required value="{{ current_user.username }}" autocomplete="off"></dd> <dd><input type="text" class="username-input" name="username" required value="{{ current_user.username }}" autocomplete="off"></dd>
@ -34,5 +32,4 @@
<dd><input type="submit" value="Save"></dd> <dd><input type="submit" value="Save"></dd>
</dl> </dl>
</form> </form>
</div>
{% endblock %} {% endblock %}

View file

@ -1,10 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "macros/message.html" import feed_message with context %}
{% block body %} {% block body %}
<h2>Explore</h2> <h2>Explore</h2>
<ul class="timeline"> <ul>
{% for message in message_list %} {% for message in message_list %}
{{ feed_message(message) }} <li id="{{ message.id }}">{% include "includes/message.html" %}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% include "includes/pagination.html" %} {% include "includes/pagination.html" %}

View file

@ -1,10 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "macros/message.html" import feed_message with context %}
{% block body %} {% block body %}
<h2>Your Timeline</h2> <h2>Your Timeline</h2>
<ul class="timeline"> <ul>
{% for message in message_list %} {% for message in message_list %}
{{ feed_message(message) }} <li id="{{ message.id }}">{% include "includes/message.html" %}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% include "includes/pagination.html" %} {% include "includes/pagination.html" %}

View file

@ -1,9 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
<div class="card">
<h2>Hello</h2> <h2>Hello</h2>
<p>{{ site_name }} is made by people like you. <br/> <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> <a href="{{ url_for('website.login') }}">Log in</a> or <a href="{{ url_for('website.register') }}">register</a> to see more.</p>
</div>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,35 @@
{% 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 %}
{% if profile.telegram %}
<p><span class="weak">Telegram:</span> <a href="https://t.me/{{ profile.facebook }}">{{ profile.telegram }}</a></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/">Edit profile</a></p>
{% endif %}
</div>

View file

@ -5,16 +5,12 @@
</div> </div>
{% endif %} {% endif %}
<p class="message-footer"> <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('website.user_detail', username=message.user.username) }}">{{ message.user.username }}</a>
- -
{% set message_privacy = message.privacy %} {% set message_privacy = message.privacy %}
{% if message_privacy == 0 %} Public {% if message.privacy in (0, 1) %} Public
{% elif message_privacy == 1 %} Unlisted {% elif message.privacy == 2 %} Friends
{% elif message_privacy == 2 %} Friends {% elif message.privacy == 3 %} Only me
{% elif message_privacy == 3 %} Only me
{% endif %} {% endif %}
- -
<time datetime="{{ message.pub_date.isoformat() }}" title="{{ message.pub_date.ctime() }}">{{ message.pub_date | human_date }}</time> <time datetime="{{ message.pub_date.isoformat() }}" title="{{ message.pub_date.ctime() }}">{{ message.pub_date | human_date }}</time>
@ -26,6 +22,6 @@
<li><a href="/edit/{{ message.id }}">Edit or change privacy</a></li> <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="/delete/{{ message.id }}">Delete permanently</a></li>
{% else %} {% else %}
<li><a href="/report/message/{{ message.id }}" target="_blank">Report</a></li> <!--li><a href="/report/{{ message.id }}">Report</a></li-->
{% endif %} {% endif %}
</ul> </ul>

View file

@ -1,6 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
<div class="card">
<h2>Join {{ site_name }}</h2> <h2>Join {{ site_name }}</h2>
<form action="{{ url_for('website.register') }}" method="POST"> <form action="{{ url_for('website.register') }}" method="POST">
<dl> <dl>
@ -33,5 +32,4 @@
<dd><input type="submit" value="Join"> <dd><input type="submit" value="Join">
</dl> </dl>
</form> </form>
<div>
{% endblock %} {% endblock %}

View file

@ -1,10 +1,8 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
<h2>Login</h2> <h2>Login</h2>
{% if error %}<p class="error"><strong>Error:</strong> {{ error }}</p>{% endif %} {% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
<div class="card">
<form method="POST"> <form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<dl> <dl>
<dt>Username or email: <dt>Username or email:
<dd><input type="text" name="username"> <dd><input type="text" name="username">
@ -20,5 +18,4 @@
<dd><input type="submit" value="Login"> <dd><input type="submit" value="Login">
</dl> </dl>
</form> </form>
</div>
{% endblock %} {% endblock %}

View file

@ -3,7 +3,7 @@
<h2>Notifications</h2> <h2>Notifications</h2>
<ul> <ul>
{% for notification in notification_list %} {% for notification in notification_list %}
<li class="card">{% include "includes/notification.html" %}</li> <li>{% include "includes/notification.html" %}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% include "includes/pagination.html" %} {% include "includes/pagination.html" %}

View 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 %}

View file

@ -1,9 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
<div class="card">
<h1>Terms of Service</h1> <h1>Terms of Service</h1>
<p>[decline to state]</p>
</div>
{% endblock %} {% endblock %}

View file

@ -1,18 +1,15 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "macros/message.html" import feed_message with context %}
{% block body %} {% block body %}
{% include "includes/infobox_profile.html" %} {% include "includes/infobox_profile.html" %}
<h2>Messages from {{ user.username }}</h2> <h2>Messages from {{ user.username }}</h2>
{% if not current_user.is_anonymous %} {% if not current_user.is_anonymous %}
{% if user.username != current_user.username %} {% if user.username != current_user.username %}
{% if current_user|is_following(user) %} {% if current_user|is_following(user) %}
<form action="{{ url_for('website.user_unfollow', username=user.username) }}" method="POST"> <form action="{{ url_for('website.user_unfollow', username=user.username) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="submit" class="follow_button following" value="- Un-follow" /> <input type="submit" class="follow_button following" value="- Un-follow" />
</form> </form>
{% else %} {% else %}
<form action="{{ url_for('website.user_follow', username=user.username) }}" method="POST"> <form action="{{ url_for('website.user_follow', username=user.username) }}" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="submit" class="follow_button" value="+ Follow" /> <input type="submit" class="follow_button" value="+ Follow" />
</form> </form>
{% endif %} {% endif %}
@ -21,9 +18,9 @@
<a href="/create/">Create a message</a> <a href="/create/">Create a message</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<ul class="timeline"> <ul>
{% for message in message_list %} {% for message in message_list %}
{{ feed_message(message) }} <li id="{{ message.id }}">{% include "includes/message.html" %}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% include "includes/pagination.html" %} {% include "includes/pagination.html" %}

View file

@ -3,12 +3,8 @@ A list of utilities used across modules.
''' '''
import datetime, re, base64, hashlib, string, sys, json import datetime, re, base64, hashlib, string, sys, json
from .models import User, Notification
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 flask import abort, render_template, request, session
from markupsafe import Markup
_forbidden_extensions = 'com net org txt'.split() _forbidden_extensions = 'com net org txt'.split()
_username_characters = frozenset(string.ascii_letters + string.digits + '_') _username_characters = frozenset(string.ascii_letters + string.digits + '_')
@ -85,7 +81,7 @@ class Visibility(object):
def get_locations(): def get_locations():
data = {} data = {}
with open('locations.txt', encoding='utf-8') as f: with open('locations.txt') as f:
for line in f: for line in f:
line = line.rstrip() line = line.rstrip()
if line.startswith('#'): if line.startswith('#'):
@ -104,14 +100,15 @@ except OSError:
# get the user from the session # get the user from the session
# changed in 0.5 to comply with flask_login # changed in 0.5 to comply with flask_login
# DEPRECATED in 0.10; use current_user instead
def get_current_user(): def get_current_user():
# new in 0.7; need a different method to get current user id # new in 0.7; need a different method to get current user id
if request.path.startswith('/api/'): if request.path.startswith('/api/'):
# assume token validation is already done # assume token validation is already done
return User[request.args['access_token'].split(':')[0]] return User[request.args['access_token'].split(':')[0]]
elif current_user.is_authenticated: else:
return current_user user_id = session.get('user_id')
if user_id:
return User[user_id]
def push_notification(type, target, **kwargs): def push_notification(type, target, **kwargs):
try: try:
@ -199,26 +196,3 @@ def check_access_token(token):
h.update(str(user.password).encode('utf-8')) h.update(str(user.password).encode('utf-8'))
if h.hexdigest()[:32] == hh: if h.hexdigest()[:32] == hh:
return user 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)

View file

@ -7,17 +7,14 @@ from .models import *
from . import __version__ as app_version from . import __version__ as app_version
from sys import version as python_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 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 from flask_login import login_required, login_user, logout_user
import json import json
import logging
logger = logging.getLogger(__name__)
bp = Blueprint('website', __name__) bp = Blueprint('website', __name__)
@bp.route('/') @bp.route('/')
def homepage(): def homepage():
if current_user and current_user.is_authenticated: if get_current_user():
return private_timeline() return private_timeline()
else: else:
return render_template('homepage.html') return render_template('homepage.html')
@ -26,7 +23,7 @@ def private_timeline():
# the private timeline (aka feed) exemplifies the use of a subquery -- we are asking for # 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 # messages where the person who created the message is someone the current
# user is following. these messages are then ordered newest-first. # user is following. these messages are then ordered newest-first.
user = current_user user = get_current_user()
messages = Visibility(Message messages = Visibility(Message
.select() .select()
.where((Message.user << user.following()) .where((Message.user << user.following())
@ -63,13 +60,13 @@ def register():
# unique constraint, the database will raise an IntegrityError. # unique constraint, the database will raise an IntegrityError.
user = User.create( user = User.create(
username=username, username=username,
full_name=request.form.get('full_name') or username,
password=pwdhash(request.form['password']), password=pwdhash(request.form['password']),
email=request.form['email'], email=request.form['email'],
birthday=birthday, birthday=birthday,
join_date=datetime.datetime.now()) join_date=datetime.datetime.now())
UserProfile.create( UserProfile.create(
user=user user=user,
full_name=request.form.get('full_name') or username
) )
# mark the user as being 'authenticated' by setting the session vars # mark the user as being 'authenticated' by setting the session vars
@ -83,9 +80,6 @@ def register():
@bp.route('/login/', methods=['GET', 'POST']) @bp.route('/login/', methods=['GET', 'POST'])
def login(): 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']: if request.method == 'POST' and request.form['username']:
try: try:
username = request.form['username'] username = request.form['username']
@ -137,12 +131,11 @@ def user_follow(username):
from_user=cur_user, from_user=cur_user,
to_user=user, to_user=user,
created_date=datetime.datetime.now()) created_date=datetime.datetime.now())
push_notification('follow', user, user=cur_user.id)
flash('You are now following %s' % user.username)
except IntegrityError: except IntegrityError:
flash(f'Error following {user.username}') pass
flash('You are following %s' % user.username)
push_notification('follow', user, user=cur_user.id)
return redirect(url_for('website.user_detail', username=user.username)) return redirect(url_for('website.user_detail', username=user.username))
@bp.route('/+<username>/unfollow/', methods=['POST']) @bp.route('/+<username>/unfollow/', methods=['POST'])
@ -188,14 +181,29 @@ def create():
privacy=privacy) privacy=privacy)
file = request.files.get('file') file = request.files.get('file')
if file: if file:
logger.info('Uploading', file.filename) print('Uploading', file.filename)
ext = file.filename.split('.')[-1] ext = file.filename.split('.')[-1]
upload = Upload.create( upload = Upload.create(
type=ext, type=ext,
message=message message=message
) )
file.save(UPLOAD_DIRECTORY + str(upload.id) + '.' + ext) file.save(UPLOAD_DIRECTORY + str(upload.id) + '.' + ext)
create_mentions(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({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') flash('Your message has been posted successfully')
return redirect(url_for('website.user_detail', username=user.username)) return redirect(url_for('website.user_detail', username=user.username))
return render_template('create.html') return render_template('create.html')
@ -239,15 +247,12 @@ def edit(id):
@bp.route('/delete/<int:id>', methods=['GET', 'POST']) @bp.route('/delete/<int:id>', methods=['GET', 'POST'])
def confirm_delete(id): def confirm_delete(id):
user: User = current_user user = get_current_user()
message: Message = get_object_or_404(Message, Message.id == id) message = get_object_or_404(Message, Message.id == id)
if message.user != user: if message.user != user:
abort(404) abort(404)
if request.method == 'POST': if request.method == 'POST':
if message.user == user: abort(501, 'CSRF-Token missing.')
message.delete_instance()
flash('Your message has been deleted forever')
return redirect(request.args.get('next', '/'))
return render_template('confirm_delete.html', message=message) return render_template('confirm_delete.html', message=message)
# Workaround for problems related to invalid data. # Workaround for problems related to invalid data.
@ -255,6 +260,7 @@ def confirm_delete(id):
def profile_checkpoint(): def profile_checkpoint():
return UserProfile( return UserProfile(
user=get_current_user(), user=get_current_user(),
full_name=request.form['full_name'],
biography=request.form['biography'], biography=request.form['biography'],
location=int(request.form['location']), location=int(request.form['location']),
year=int(request.form['year'] if request.form.get('has_year') else '0'), year=int(request.form['year'] if request.form.get('has_year') else '0'),
@ -279,9 +285,6 @@ def edit_profile():
except IntegrityError: except IntegrityError:
flash('That username is already taken') flash('That username is already taken')
return render_template('edit_profile.html', profile=profile_checkpoint()) 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') website = request.form['website'].strip().replace(' ', '%20')
if website and not validate_website(website): if website and not validate_website(website):
flash('You should enter a valid URL.') flash('You should enter a valid URL.')
@ -290,6 +293,7 @@ def edit_profile():
if location == 0: if location == 0:
location = None location = None
UserProfile.update( UserProfile.update(
full_name=request.form['full_name'] or username,
biography=request.form['biography'], biography=request.form['biography'],
year=request.form['year'] if request.form.get('has_year') else None, year=request.form['year'] if request.form.get('has_year') else None,
location=location, location=location,

3
config.py Normal file
View file

@ -0,0 +1,3 @@
DEBUG = True
SECRET_KEY = 'hin6bab8ge25*r=x&amp;+5$0kn=-#log$pt^#@vrqjld!^2ci@g*b'
SITE_NAME = 'Cori+'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
View file

@ -0,0 +1 @@

16
run_example.py Normal file
View 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)

View file

@ -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))

View file

@ -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)
}

View file

@ -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')

View file

@ -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}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 &copy; 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 %}

View file

@ -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">&copy; 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>

View file

@ -1,9 +0,0 @@
{% extends "admin_base.html" %}
{% block body %}
<ul>
<li>
<a href="{{ url_for('admin.reports') }}">Reports</a>
</li>
</ul>
{% endblock %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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">&copy; 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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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>

View file

@ -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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Report &ndash; 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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -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')

View file

@ -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))

View file

@ -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;
''')

View file

@ -1,3 +0,0 @@
User-Agent: *
Disallow: /report/
Noindex: /admin/