init commit
This commit is contained in:
commit
e5f74e88d7
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 VideoToblin
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
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.
|
|
@ -0,0 +1,12 @@
|
|||
# morsel
|
||||
WIP microblogging service.
|
||||
|
||||
### setup
|
||||
First, git clone the source code to a folder of your choosing. `cd` to that folder, then:
|
||||
|
||||
1) Open config.json in a text editor and switch `owner` to your username. (You'll make your account later.
|
||||
2) Run `python -m flask --app main.py run`. Then, navigate to port 5000 in your web browser and create an account using the username you specified earlier.
|
||||
|
||||
Note that changes in permissions in config.json don't affect the owner account.
|
||||
|
||||
Just like that, Morsel is set up and ready to go!
|
|
@ -0,0 +1 @@
|
|||
{"boards":[]}
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"owner": "your_username",
|
||||
"permissions": {
|
||||
"can_create_accounts": true,
|
||||
"can_create_boards": true,
|
||||
"can_reply": true,
|
||||
"can_post": true,
|
||||
"can_subscribe": true,
|
||||
"can_follow": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -0,0 +1,12 @@
|
|||
from flask import Flask
|
||||
from flask import render_template
|
||||
from flask import request
|
||||
from flask import url_for
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
import morsel_home
|
||||
import morsel_login
|
||||
import morsel_boards
|
||||
import morsel_users
|
||||
import morsel_avatar
|
|
@ -0,0 +1,22 @@
|
|||
from flask import request
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import send_file
|
||||
from main import app
|
||||
from morsel_util import *
|
||||
from PIL import Image
|
||||
from requests import get as fetch
|
||||
from io import BytesIO as bio
|
||||
|
||||
@app.route('/avatar/<user>', methods=['POST', 'GET'])
|
||||
def getAvatar(user):
|
||||
avatar_url = libravatar_geturl(user)
|
||||
if avatar_url == None:
|
||||
return redirect("/static/default_avatar.png", 303)
|
||||
img_io = bio()
|
||||
response = fetch(avatar_url)
|
||||
img = Image.open(bio(response.content))
|
||||
img = img.resize((64,64))
|
||||
img.save(img_io, format="png")
|
||||
img_io.seek(0)
|
||||
return send_file(img_io, mimetype="image/png")
|
|
@ -0,0 +1,148 @@
|
|||
from flask import request
|
||||
from flask import render_template
|
||||
from flask import redirect
|
||||
from markupsafe import escape
|
||||
from main import app
|
||||
from morsel_util import *
|
||||
|
||||
@app.route('/b')
|
||||
@app.route('/b/')
|
||||
def boards():
|
||||
uname = request.cookies.get('username')
|
||||
token = request.cookies.get('token')
|
||||
loggedin = tokenchk(uname, token)
|
||||
if loggedin == True:
|
||||
# get every board and a list of the user's subscribed boards
|
||||
boards = json_read("boards.json")
|
||||
subbed = getSubbedArray(uname, boards)
|
||||
# render boards template
|
||||
return render_template('boards.html', name=uname, boards=boards["boards"], subbed=subbed)
|
||||
elif loggedin == None:
|
||||
return render_template('landing.html')
|
||||
|
||||
@app.route('/new/b', methods=['POST','GET'])
|
||||
def createboard():
|
||||
uname = request.cookies.get('username')
|
||||
token = request.cookies.get('token')
|
||||
loggedin = tokenchk(uname, token)
|
||||
if loggedin == True:
|
||||
if not "bname" in request.form:
|
||||
return render_template('nboard.html', name=uname)
|
||||
else:
|
||||
# if required fields missing return an error page
|
||||
if (not "bname" in request.form) or (not "bmods" in request.form):
|
||||
return render_template('err.html', err="Required fields missing.", refer="/new/b")
|
||||
# create new board and set description
|
||||
bname = safechars(request.form['bname'])
|
||||
if bname == "": return render_template('err.html', err='Board name cannot be blank.', refer='/new/b')
|
||||
newboard(bname, uname)
|
||||
bsetdesc(bname, request.form['bdesc'])
|
||||
# knight every moderator in the list
|
||||
for mod in request.form['bmods'].split(","):
|
||||
mod = mod.strip()
|
||||
if mod.lower() != uname.lower():
|
||||
bknight(bname, mod)
|
||||
# redirect to new board
|
||||
return redirect(f"/b/{bname}")
|
||||
else:
|
||||
return render_template('landing.html')
|
||||
|
||||
@app.route('/postto/<board>', methods=['POST'])
|
||||
def createpost(board):
|
||||
uname = request.cookies.get('username')
|
||||
token = request.cookies.get('token')
|
||||
loggedin = tokenchk(uname, token)
|
||||
if loggedin == True:
|
||||
# create the post and redirect to the referrer
|
||||
add_post(uname, board, escape(request.form["postbody"]))
|
||||
return redirect(request.referrer, 303)
|
||||
|
||||
@app.route('/subs')
|
||||
@app.route('/subs/')
|
||||
def subs():
|
||||
uname = request.cookies.get('username')
|
||||
token = request.cookies.get('token')
|
||||
loggedin = tokenchk(uname, token)
|
||||
if loggedin == True:
|
||||
boards = json_read("boards.json")
|
||||
subbed = getSubbedArray(uname, boards)
|
||||
if len(subbed) != 0:
|
||||
subboards = []
|
||||
for board in boards["boards"]:
|
||||
if board["name"] in subbed:
|
||||
subboards.append(board)
|
||||
return render_template('boards.html', name=uname, boards=subboards, subbed=subbed)
|
||||
else:
|
||||
return render_template('err.html', err="You are not subscribed to any boards.", refer="/b/")
|
||||
elif loggedin == None:
|
||||
return render_template('landing.html')
|
||||
|
||||
@app.route('/b/<board>')
|
||||
def viewboard(board):
|
||||
uname = request.cookies.get('username')
|
||||
token = request.cookies.get('token')
|
||||
loggedin = tokenchk(uname, token)
|
||||
if loggedin == True:
|
||||
boardget = bexist(board)
|
||||
if boardget == False:
|
||||
return render_template('noboard.html', name=uname, board=board)
|
||||
else:
|
||||
args = request.args.to_dict()
|
||||
if "subscribe" in args:
|
||||
subscribe(uname, boardget["name"])
|
||||
return redirect(request.referrer, 303)
|
||||
elif "unsubscribe" in args:
|
||||
unsubscribe(uname, boardget["name"])
|
||||
return redirect(request.referrer, 303)
|
||||
return render_template(
|
||||
'board.html', name=uname,
|
||||
bname=boardget["name"],
|
||||
bdesc=boardget["description"],
|
||||
bmods=", ".join(boardget["moderators"]),
|
||||
subbed=is_subbed(uname, board),
|
||||
posts=getRecentPosts(board, 20),
|
||||
uavatar=libravatar_geturl(uname),
|
||||
ismod=(uname in boardget["moderators"])
|
||||
)
|
||||
elif loggedin == None:
|
||||
return render_template('landing.html')
|
||||
|
||||
@app.route("/del/<board>/<post>")
|
||||
def delete(board, post):
|
||||
uname = request.cookies.get('username')
|
||||
token = request.cookies.get('token')
|
||||
loggedin = tokenchk(uname, token)
|
||||
if loggedin == True:
|
||||
if "confirm" in request.args:
|
||||
delPostById(board, post)
|
||||
return redirect(f"/b/{board}")
|
||||
return render_template('del.html', name=uname, post=getPostById(board, post), bname=board, id=post)
|
||||
|
||||
@app.route("/b/<board>/<post>")
|
||||
def viewpost(board, post):
|
||||
uname = request.cookies.get('username')
|
||||
token = request.cookies.get('token')
|
||||
loggedin = tokenchk(uname, token)
|
||||
post_json = getPostById(board, post)
|
||||
if "replies" in post_json:
|
||||
return render_template('postdetails.html', post=post_json, id=post, board=board, loggedin=loggedin, name=uname, hasreplies=True, replies=post_json["replies"])
|
||||
else:
|
||||
return render_template('postdetails.html', post=post_json, id=post, board=board, loggedin=loggedin, name=uname)
|
||||
|
||||
@app.route("/b/<board>/<post>/reply", methods=["POST"])
|
||||
def replypost(board, post):
|
||||
uname = request.cookies.get('username')
|
||||
token = request.cookies.get('token')
|
||||
loggedin = tokenchk(uname, token)
|
||||
if not loggedin:
|
||||
return render_template('err.html', err="You have to log in or register to reply to posts.", refer=f"/b/{board}/{post}")
|
||||
elif "postbody" not in request.form:
|
||||
return render_template('err.html', err="...did you just try to reply without content?", refer=f"/b/{board}/{post}")
|
||||
elif request.form["postbody"].strip() == "":
|
||||
return render_template('err.html', err="Empty replies are generally discouraged.", refer=f"/b/{board}/{post}")
|
||||
else:
|
||||
if addreply(uname, board, post, escape(request.form["postbody"])) == True:
|
||||
return redirect(f"/b/{board}/{post}", 303)
|
||||
else:
|
||||
return render_template('err.html', err="An error occurred while leaving a reply. Try again later. :(", refer=f"/b/{board}/{post}")
|
||||
return "Fatal error. (009)"
|
|
@ -0,0 +1,41 @@
|
|||
from flask import request
|
||||
from flask import render_template
|
||||
from main import app
|
||||
from morsel_util import *
|
||||
|
||||
@app.route('/', methods=['POST', 'GET'])
|
||||
def homepage():
|
||||
# login check
|
||||
uname = request.cookies.get('username')
|
||||
token = request.cookies.get('token')
|
||||
loggedin = tokenchk(uname, token)
|
||||
if loggedin == True:
|
||||
# get most recent posts in chronological order
|
||||
feed = get_feed(uname)
|
||||
# show subscribe notification if no posts are in feed
|
||||
if len(feed) == 0:
|
||||
noposts = True
|
||||
else: noposts = False
|
||||
# serve timeline
|
||||
return render_template('home.html', name=uname, feed=feed, noposts=noposts)
|
||||
elif loggedin == False:
|
||||
# serve the logged out error page otherwise
|
||||
return render_template('loggedout.html')
|
||||
elif loggedin == None:
|
||||
# if the user isn't logged in, serve the landing page
|
||||
return render_template('landing.html')
|
||||
|
||||
def get_feed(uname):
|
||||
boards = json_read("boards.json")
|
||||
subbedboardnames = getSubbedArray(uname, boards)
|
||||
allposts = []
|
||||
for i in subbedboardnames:
|
||||
posts = getRecentPosts(i, 20)
|
||||
for post in posts:
|
||||
post["ownerblog"] = (config["owner"] == post["author"])
|
||||
post["board"] = i
|
||||
post["credate"] = int(post["credate"])
|
||||
allposts.append(post)
|
||||
# Excuse me op but what the fuck is this
|
||||
allposts = sorted(allposts, key=lambda x: x["credate"], reverse=True)
|
||||
return allposts
|
|
@ -0,0 +1,43 @@
|
|||
from flask import request
|
||||
from flask import render_template
|
||||
from flask import redirect
|
||||
from main import app
|
||||
from morsel_util import *
|
||||
|
||||
def session_start(form):
|
||||
resp = redirect("/", code=303)
|
||||
resp.set_cookie('username', form['username'])
|
||||
resp.set_cookie('token', crypt(form['password'], str(int(time()))))
|
||||
return resp
|
||||
|
||||
@app.route('/login', methods=['POST', 'GET'])
|
||||
def login():
|
||||
error = None
|
||||
if request.method == 'POST':
|
||||
if passchk(request.form['username'], request.form['password']):
|
||||
return session_start(request.form)
|
||||
else:
|
||||
error = 'Invalid username/password'
|
||||
|
||||
return render_template('login.html', error=error)
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
resp = redirect("/login", code=303)
|
||||
resp.delete_cookie('username')
|
||||
resp.delete_cookie('token')
|
||||
return resp
|
||||
|
||||
@app.route('/reg', methods=['POST', 'GET'])
|
||||
def register():
|
||||
error = None
|
||||
if request.method == 'POST':
|
||||
if (not uexist(request.form['username'])) and len(request.form['password']) >= 8:
|
||||
newuser(request.form['username'], request.form['password'])
|
||||
return redirect('/', 303)
|
||||
elif len(request.form['password']) < 8:
|
||||
error = "Password must be at least 8 characters."
|
||||
else:
|
||||
error = "Username already taken."
|
||||
|
||||
return render_template('register.html', error=error)
|
|
@ -0,0 +1,87 @@
|
|||
from flask import request
|
||||
from flask import render_template
|
||||
from flask import redirect
|
||||
from main import app
|
||||
from morsel_util import *
|
||||
|
||||
@app.route('/u/<user>')
|
||||
def viewuser(user):
|
||||
# check if usre is logged in
|
||||
uname = request.cookies.get('username')
|
||||
token = request.cookies.get('token')
|
||||
loggedin = tokenchk(uname, token)
|
||||
if loggedin == True and uexist(user):
|
||||
# get the user's information
|
||||
u = getUser(user)
|
||||
if u == None:
|
||||
# if they don't exist, serve an error page
|
||||
return render_template('err.html', err="This user doesn't exist.", refer="/")
|
||||
|
||||
args = request.args.to_dict()
|
||||
|
||||
# if "follow" is in the URL
|
||||
if args.get("follow") != None:
|
||||
if user == uname:
|
||||
# ensure it isn't a self-follow
|
||||
return render_template('err.html', err="You can't follow yourself.", refer=f"/u/{user}")
|
||||
else:
|
||||
# and follow the target user.
|
||||
toggle_follow(uname, user)
|
||||
if not (args.get("goto") == None):
|
||||
return redirect(args["goto"])
|
||||
else:
|
||||
return redirect(f"/u/{user}", 303)
|
||||
|
||||
# load user follow data here
|
||||
followdata = json_read("follows.json")[user]
|
||||
myfollowdata = json_read("follows.json")[uname]
|
||||
|
||||
# also get the users' subscriptions
|
||||
subdata = getUser(user)["subscriptions"]
|
||||
mysubdata = getUser(uname)["subscriptions"]
|
||||
|
||||
# serve the user infosheet
|
||||
return render_template(
|
||||
'user.html', name=uname,
|
||||
bname=u["name"],
|
||||
bdesc=u["bio"],
|
||||
following=isFollowing(uname,user),
|
||||
mutual=isFollowing(user,uname),
|
||||
followings=followdata["following"],
|
||||
myfollowings=myfollowdata["following"],
|
||||
followers=followdata["followers"],
|
||||
myfollowers=myfollowdata["followers"],
|
||||
subs=subdata,
|
||||
mysubs=mysubdata
|
||||
)
|
||||
elif loggedin == None:
|
||||
# same song and dance, serve login page
|
||||
return render_template('landing.html')
|
||||
else:
|
||||
# serve error if user doesn't exist
|
||||
return render_template('err.html', err='This user does not exist.', refer='/')
|
||||
|
||||
@app.route('/settings')
|
||||
def servesettings():
|
||||
uname = request.cookies.get('username')
|
||||
token = request.cookies.get('token')
|
||||
loggedin = tokenchk(uname, token)
|
||||
if loggedin == True:
|
||||
# serve settings page with filled in details
|
||||
user = getUser(uname)
|
||||
libra = user["avatar"]
|
||||
bio = user["bio"]
|
||||
return render_template('settings.html', libra=libra, bio=bio, name=uname)
|
||||
else:
|
||||
# serve landing if not logged in
|
||||
return render_template('landing.html')
|
||||
|
||||
@app.route('/apply_settings', methods=['POST'])
|
||||
def applysettings():
|
||||
uname = request.cookies.get('username')
|
||||
token = request.cookies.get('token')
|
||||
loggedin = tokenchk(uname, token)
|
||||
if loggedin == True:
|
||||
# apply POST'ed user settings
|
||||
applyToUser(uname, request.form["bio"], request.form["libravatar"])
|
||||
return redirect("/", 303)
|
|
@ -0,0 +1,325 @@
|
|||
import json
|
||||
from crypt import crypt
|
||||
from time import time
|
||||
from time import sleep
|
||||
from os.path import exists
|
||||
from os import unlink
|
||||
from hashlib import md5
|
||||
from markdown2 import Markdown
|
||||
|
||||
def safechars(string):
|
||||
string_safe = ""
|
||||
for i in string:
|
||||
if i.lower() in "abcdefghijklmnopqrstuvwxyz1234567890-_ ":
|
||||
string_safe += i
|
||||
return string_safe
|
||||
|
||||
def json_read(file):
|
||||
with open(file, "r") as file_io:
|
||||
json_bits = json.load(file_io)
|
||||
file_io.close()
|
||||
return json_bits
|
||||
|
||||
def json_sync(file, content, create = False):
|
||||
mode = "w"
|
||||
if create: mode = "x"
|
||||
with open(file, mode) as file_io:
|
||||
file_io.write(json.dumps(content, indent=2))
|
||||
file_io.close()
|
||||
return True
|
||||
|
||||
config = json_read("config.json")
|
||||
|
||||
def permcheck(permission, uname="joe"):
|
||||
if uname == config["owner"] or config["permissions"][permission]:
|
||||
return True
|
||||
else: return False
|
||||
|
||||
def passchk(name, pwd):
|
||||
for i in json_read("users.json")["users"]:
|
||||
if i["name"] == name:
|
||||
if crypt(pwd, str(int(time()))) == i["token"]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return None
|
||||
|
||||
def tokenchk(name, token):
|
||||
for i in json_read("users.json")["users"]:
|
||||
if i["name"] == name:
|
||||
if token == i["token"]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return None
|
||||
|
||||
def uexist(name):
|
||||
for i in json_read("users.json")["users"]:
|
||||
if i["name"].lower() == name.lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
def getUser(name):
|
||||
users = json_read("users.json")["users"]
|
||||
for user in users:
|
||||
if user["name"].lower() == name.lower(): return user
|
||||
return None
|
||||
|
||||
def getUserPos(name):
|
||||
try:
|
||||
users = json_read("users.json")["users"]
|
||||
i = 0
|
||||
for user in users:
|
||||
if user["name"] == name: return i
|
||||
i += 1
|
||||
except:
|
||||
return None
|
||||
|
||||
def bexist(name):
|
||||
for i in json_read("boards.json")["boards"]:
|
||||
if i["name"] == name:
|
||||
return i
|
||||
return False
|
||||
|
||||
def newuser(name, password):
|
||||
if permcheck("can_create_accounts"):
|
||||
pwd = crypt(password, str(int(time())))
|
||||
users = json_read("users.json")
|
||||
users["users"].append({
|
||||
"name": name,
|
||||
"token": pwd,
|
||||
"credate": int(time()),
|
||||
"avatar": None,
|
||||
"subscriptions": [],
|
||||
"bio": "A new user on Morsel!"
|
||||
})
|
||||
json_sync("users.json", users)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def newboard(name, mod):
|
||||
if permcheck("can_create_boards", mod):
|
||||
boards = json_read("boards.json")
|
||||
for i in boards["boards"]:
|
||||
if i["name"] == name:
|
||||
return False
|
||||
boards["boards"].append({
|
||||
"name": name,
|
||||
"founder": mod,
|
||||
"description": "A board on Morsel!",
|
||||
"moderators": [mod],
|
||||
"credate": int(time()),
|
||||
"posts": f"boards/{name}.json"
|
||||
})
|
||||
json_sync("boards.json", boards)
|
||||
json_sync(f"boards/{name}.json",
|
||||
{
|
||||
"posts": {
|
||||
0: {
|
||||
"author": mod,
|
||||
"credate": int(time()),
|
||||
"content": f"Hello, {name}!"
|
||||
}
|
||||
}
|
||||
}, True
|
||||
)
|
||||
subscribe(mod, name)
|
||||
return True
|
||||
|
||||
def bsetdesc(board, description):
|
||||
if bexist(board):
|
||||
boards = json_read("boards.json")
|
||||
for i in boards["boards"]:
|
||||
if i["name"] == board:
|
||||
i["description"] = description
|
||||
json_sync("boards.json", boards)
|
||||
return True
|
||||
return False
|
||||
|
||||
def bknight(board, user):
|
||||
if bexist(board):
|
||||
boards = json_read("boards.json")
|
||||
for i in boards["boards"]:
|
||||
if i["name"] == board:
|
||||
i["moderators"].append(user)
|
||||
json_sync("boards.json", boards)
|
||||
return True
|
||||
return False
|
||||
|
||||
def subscribe(user, board):
|
||||
if bexist(board) and uexist(user) and permcheck("can_subscribe", user):
|
||||
users=json_read("users.json")
|
||||
users["users"][getUserPos(user)]["subscriptions"].append(board)
|
||||
json_sync("users.json", users)
|
||||
return True
|
||||
return None
|
||||
|
||||
def unsubscribe(user, board):
|
||||
if bexist(board) and uexist(user) and is_subbed(user, board) and permcheck("can_subscribe", user):
|
||||
users=json_read("users.json")
|
||||
users["users"][getUserPos(user)]["subscriptions"].remove(board)
|
||||
json_sync("users.json", users)
|
||||
return True
|
||||
return None
|
||||
|
||||
def is_subbed(user, board):
|
||||
if bexist(board) and uexist(user):
|
||||
user = getUser(user)
|
||||
subs = user["subscriptions"]
|
||||
for i in subs:
|
||||
if i == board:
|
||||
return True
|
||||
return False
|
||||
else: print("Lmao no")
|
||||
return None
|
||||
|
||||
def add_post(user, board, content):
|
||||
if bexist(board) and uexist(user) and len(content) < 1000 and permcheck("can_post", user):
|
||||
b = json_read("boards.json")["boards"]
|
||||
for candidate in b:
|
||||
print(candidate["name"], board)
|
||||
if candidate["name"] == board:
|
||||
posts = json_read(candidate["posts"])
|
||||
# this gets the new post ID
|
||||
postid = len(posts["posts"])
|
||||
posts["posts"][str(postid)] = {
|
||||
"author": user,
|
||||
"credate": int(time()),
|
||||
"content": content
|
||||
}
|
||||
json_sync(candidate["posts"], posts)
|
||||
return True
|
||||
print("nope")
|
||||
|
||||
def libravatar_geturl(user):
|
||||
if uexist(user):
|
||||
u = getUser(user)
|
||||
if u["avatar"] == None:
|
||||
return None
|
||||
elif len(u["avatar"].split("@")) != 2:
|
||||
return u["avatar"]
|
||||
else:
|
||||
hash = md5(u["avatar"].encode()).hexdigest()
|
||||
return f"https://seccdn.libravatar.org/avatar/{hash}?s=64"
|
||||
|
||||
def getRecentPosts(board, count):
|
||||
if bexist(board):
|
||||
posts = {}
|
||||
b = json_read("boards.json")["boards"]
|
||||
for candidate in b:
|
||||
print(candidate["name"], board)
|
||||
if candidate["name"] == board:
|
||||
posts = json_read(candidate["posts"])["posts"]
|
||||
if posts == {}:
|
||||
return {}
|
||||
else:
|
||||
array_posts = []
|
||||
for post in posts:
|
||||
tmp_post = posts[post]
|
||||
tmp_post["author_avatar"] = libravatar_geturl(tmp_post["author"])
|
||||
tmp_post["content"] = Markdown().convert(tmp_post["content"])
|
||||
tmp_post["id"] = post
|
||||
array_posts.append(tmp_post)
|
||||
|
||||
array_posts = array_posts[::-1]
|
||||
if len(array_posts) > 20: return array_posts[:20]
|
||||
else: return array_posts
|
||||
|
||||
def applyToUser(user, bio, avatar):
|
||||
if uexist(user):
|
||||
users=json_read("users.json")
|
||||
users["users"][getUserPos(user)]["bio"] = bio
|
||||
if avatar.lower().strip() != "none": users["users"][getUserPos(user)]["avatar"] = avatar
|
||||
else: users["users"][getUserPos(user)]["avatar"] = None
|
||||
json_sync("users.json", users)
|
||||
return
|
||||
|
||||
def toggle_follow(follower, followee):
|
||||
if permcheck("can_follow", follower):
|
||||
follows_json = json_read("follows.json")
|
||||
|
||||
follower_json = follows_json.get(follower)
|
||||
if follower_json == None:
|
||||
follows_json[follower] = {}
|
||||
follower_json = follows_json[follower]
|
||||
follower_json["following"] = []
|
||||
follower_json["followers"] = []
|
||||
|
||||
followee_json = follows_json.get(followee)
|
||||
if followee_json == None:
|
||||
follows_json[followee] = {}
|
||||
followee_json = follows_json[followee]
|
||||
followee_json["following"] = []
|
||||
followee_json["followers"] = []
|
||||
|
||||
if follower in followee_json["followers"]:
|
||||
follower_json["following"].remove(followee)
|
||||
followee_json["followers"].remove(follower)
|
||||
else:
|
||||
follower_json["following"].append(followee)
|
||||
followee_json["followers"].append(follower)
|
||||
|
||||
json_sync("follows.json", follows_json)
|
||||
|
||||
def isFollowing(follower, followee):
|
||||
follows_json = json_read("follows.json")
|
||||
|
||||
try:
|
||||
if follower in follows_json[followee]["followers"]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
def getPostById(board, id):
|
||||
boards_json = json_read("boards.json")
|
||||
posts = None
|
||||
for i in boards_json["boards"]:
|
||||
if i["name"] == board:
|
||||
posts = i["posts"]
|
||||
break
|
||||
if posts == None: return
|
||||
matchpost = json_read(posts)["posts"][str(id)]
|
||||
matchpost["content"] = Markdown().convert(matchpost["content"])
|
||||
return matchpost
|
||||
|
||||
def delPostById(board, id):
|
||||
boards_json = json_read("boards.json")
|
||||
postsf = None
|
||||
for i in boards_json["boards"]:
|
||||
if i["name"] == board:
|
||||
postsf = i["posts"]
|
||||
break
|
||||
if postsf == None: return
|
||||
posts_json = json_read(postsf)
|
||||
del posts_json["posts"][id]
|
||||
json_sync(postsf, posts_json)
|
||||
|
||||
def getSubbedArray(user, boards):
|
||||
subbed = []
|
||||
for i in boards["boards"]:
|
||||
if is_subbed(user, i["name"]):
|
||||
subbed.append(i["name"])
|
||||
return subbed
|
||||
|
||||
def addreply(uname, board, post, message):
|
||||
if permcheck("can_reply", uname):
|
||||
boards_json = json_read("boards.json")
|
||||
jboard = None
|
||||
for i in boards_json["boards"]:
|
||||
if i["name"] == board:
|
||||
jboard = i
|
||||
break
|
||||
if jboard == None: return jboard
|
||||
posts_json = json_read(jboard["posts"])
|
||||
if not "replies" in posts_json["posts"][post]:
|
||||
posts_json["posts"][post]["replies"] = []
|
||||
posts_json["posts"][post]["replies"].append({
|
||||
"author": uname,
|
||||
"credate": int(time()),
|
||||
"content": Markdown().convert(message)
|
||||
})
|
||||
json_sync(jboard["posts"], posts_json)
|
||||
return True
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1,164 @@
|
|||
body {
|
||||
background: #1c1c1c;
|
||||
color: #dfdfdf;
|
||||
/* stolen system font stack, for kicks */
|
||||
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
|
||||
max-width: 1024px; /* rather aesthetically pleasing, i might add! */
|
||||
margin: auto;
|
||||
}
|
||||
.panel {
|
||||
border: 1px solid #3c3c3c;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: inset 1px 1px #4c4c4c,
|
||||
inset -1px -1px #0c0c0c;
|
||||
}
|
||||
.feed_panel {
|
||||
background: url("/static/background.png");
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.login_panel {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
margin-top: 12.5%;
|
||||
}
|
||||
input[type="text"], input[type="password"], textarea {
|
||||
color: #ececec;
|
||||
background: #3c3c3c;
|
||||
border: 1px solid #3c3c3c;
|
||||
margin: 3px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
box-shadow: inset 1px 1px #1c1c1c, inset -1px -1px #4c4c4c;
|
||||
padding: 3px;
|
||||
}
|
||||
.abtn {
|
||||
text-decoration: none;
|
||||
}
|
||||
input[type="submit"], .abtn {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
cursor: pointer;
|
||||
background: #3c3c3c;
|
||||
color: inherit;
|
||||
border: 1px solid #5c5c5c;
|
||||
box-shadow: inset 1px 1px #4c4c4c, inset -1px -1px #2c2c2c;
|
||||
padding: 1px 8px 1px 8px;
|
||||
margin: 3px;
|
||||
}
|
||||
input[type="submit"]:active, .abtn:active {
|
||||
background: #2c2c2c;
|
||||
color: transparent;
|
||||
text-shadow: 1px 1px white;
|
||||
border: 1px solid #5c5c5c;
|
||||
box-shadow: inset -1px -1px #4c4c4c;
|
||||
}
|
||||
.buttons {
|
||||
border-top: 1px solid #000;
|
||||
text-align: center;
|
||||
box-shadow: inset -0px 1px #3c3c3c;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.action_panel {
|
||||
padding: 8px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.action_panel:nth-of-type(4) { text-align: right; }
|
||||
.error {
|
||||
background: maroon;
|
||||
color: white;
|
||||
border: 1px solid red;
|
||||
padding: 6px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
table tr td { margin: 0px; padding: 8px; }
|
||||
.boardlist li { margin: 0px; list-style: none; padding: 0px; }
|
||||
.boardlist h3 { margin: 0px; font-size: xxlarge; border-bottom: 1px solid transparent; }
|
||||
.boardlist a { text-decoration: none; color: inherit; }
|
||||
.boardlist a:hover { text-decoration: underline; border-bottom: 1px dotted white; }
|
||||
.subbtn {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: white;
|
||||
background: inherit;
|
||||
border: 1px solid #5c5c5c;
|
||||
background: #4c4c4c;
|
||||
text-decoration: none !important;
|
||||
margin: auto;
|
||||
box-shadow: inset 1px 1px #4c4c4c, inset -1px -1px #2c2c2c;
|
||||
line-height: 24px;
|
||||
}
|
||||
.extra-room {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.subbtn:active, .subbtn.remove {
|
||||
box-shadow: inset -1px -1px #4c4c4c, inset 1px 1px #2c2c2c;
|
||||
text-shadow: 1px 1px lime, 1px 1px 8px lime;
|
||||
color: transparent;
|
||||
}
|
||||
.subbtn.remove:active {
|
||||
text-shadow: 1px 1px red, 1px 1px 8px red;
|
||||
}
|
||||
.board a { color: inherit; text-decoration: none; margin: none; }
|
||||
.board a:hover { text-decoration: underline; text-decoration-style: dotted; }
|
||||
.board { padding-top: 8px; }
|
||||
.mods { color: #aaff33; }
|
||||
h1 { margin-top: 24px; }
|
||||
h2, h3, h4, p { margin: 0px; }
|
||||
hr { border-left: none; border-right: none; border-top: 1px solid #000; border-bottom: 1px solid #4c4c4c; }
|
||||
p { color: #8c8c8c; padding-bottom: 4px; }
|
||||
tr:nth-child(odd) { background: #4f4f4f88; }
|
||||
.red { color: #9b33ff; background: #2c2c2c; box-shadow: inset -1px -1px #4c4c4c, inset 1px 1px #2c2c2c; }
|
||||
td { vertical-align: top; }
|
||||
td .buttons { padding: 4px; text-align: right; }
|
||||
td .buttons input[type=submit] {
|
||||
color: #aaff33;
|
||||
padding: 16px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
border: 1px solid green;
|
||||
}
|
||||
td .buttons input[type=submit]:active { color: transparent; }
|
||||
textarea.entry {
|
||||
display: block;
|
||||
width: 99.5%;
|
||||
margin: auto;
|
||||
margin-bottom: 4px;
|
||||
height: 100px;
|
||||
resize: none;
|
||||
}
|
||||
td h3 a { color: #aaff33; text-decoration: none; font-weight: 300; }
|
||||
td h3 a:hover { text-decoration: underline; text-decoration-style: dotted; }
|
||||
span.mutual {
|
||||
background: #4c4c5e;
|
||||
color: white;
|
||||
font-size: small;
|
||||
padding: 3px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
border-radius: 3px;
|
||||
font-style: italic;
|
||||
}
|
||||
img { max-width: 100%; max-height: 480px; }
|
||||
div.action_panel table tr { background: none; }
|
||||
div.action_panel table td { padding: 0px; }
|
||||
.logobox {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.username {
|
||||
color: #b05eff;
|
||||
font-weight: bold;
|
||||
text-shadow: 1px 1px 2px black;
|
||||
}
|
||||
.notsoobviouslink { text-decoration: none; color: inherit; }
|
||||
.notsoobviouslink:hover * { color: #efefef; }
|
||||
.userpage_col tr td { vertical-align: middle; }
|
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
|
@ -0,0 +1,57 @@
|
|||
/* This theme had to be put down. */
|
||||
/*
|
||||
* { box-shadow: none !important; border: none !important; }
|
||||
body {
|
||||
background: #dcdcdc;
|
||||
color: black;
|
||||
}
|
||||
.action_panel.panel {
|
||||
background: #1c3c1c;
|
||||
color: white;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
text-align: center;
|
||||
margin: 0px;
|
||||
}
|
||||
.action_panel.panel:nth-of-type(1) {
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
.action_panel.panel:nth-of-type(3) {
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
div.panel {
|
||||
background: #efefef;
|
||||
color: black;
|
||||
}
|
||||
tr:nth-of-type(odd) {
|
||||
background: #cddecd;
|
||||
}
|
||||
a.abtn, input[type=submit], .subbtn {
|
||||
padding: 4px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
background: #0caf0c;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.subbtn { padding: 0px; }
|
||||
.subbtn.remove { color: white; }
|
||||
a.abtn:hover, input[type=submit]:hover, .subbtn:hover { background: #2ccf2c; transition: 0.1s; }
|
||||
.mods {
|
||||
color: green;
|
||||
}
|
||||
p { color: #3c3c3c; }
|
||||
.input[type=text], .input[type=password], textarea {
|
||||
background: inherit;
|
||||
padding: 4px;
|
||||
color: inherit;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.input[type=text]:focus, .input[type=password]:focus, textarea:focus {
|
||||
border: 1px solid #1c1c1c !important;
|
||||
outline: none;
|
||||
}
|
||||
*/
|
||||
.owner { color: orange; }
|
|
@ -0,0 +1,64 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Morsel - {{ name }}</title>
|
||||
<link rel="stylesheet" href="/static/morsel.css" />
|
||||
<link rel="stylesheet" href="/static/theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
{% include "logo.html" %}
|
||||
{% include "nav.html" %}
|
||||
<div class="feed_panel panel">
|
||||
<h2>{{ bname }}</h2>
|
||||
<p class="extra-room">Moderated by: <span class="mods">{{ bmods }}</span></p>
|
||||
<p class="extra-room">{{ bdesc }}</p>
|
||||
{% if subbed -%}
|
||||
<a href="/b/{{ bname }}?unsubscribe" class="red abtn">Unsubscribe from this board</a>
|
||||
{% else -%}
|
||||
<a href="/b/{{ bname }}?subscribe" class="mods abtn">Subscribe to this board</a>
|
||||
{% endif -%}
|
||||
<hr/>
|
||||
{% if subbed -%}
|
||||
<form action="/postto/{{ bname }}" method="POST">
|
||||
<table width="100%"><tr>
|
||||
<td width="64px">
|
||||
<img src="/avatar/{{ name }}">
|
||||
</td>
|
||||
<td>
|
||||
<p>Posts now support Markdown. <a href="https://www.markdownguide.org/" class="mods">Here's a guide on that.</a></p>
|
||||
<textarea
|
||||
class="entry"
|
||||
placeholder="Share your thoughts! Type here."
|
||||
name="postbody"
|
||||
></textarea>
|
||||
<div class="buttons">
|
||||
<input type="submit" value="Post!" />
|
||||
</div>
|
||||
</td>
|
||||
</tr></table>
|
||||
</form>
|
||||
<hr/>
|
||||
{% endif -%}
|
||||
<h2>Recent Posts</h2>
|
||||
<table width="100%" cellspacing="0px">
|
||||
{% for post in posts -%}
|
||||
<tr class="post">
|
||||
<td width="80px" align="right">
|
||||
<img src="/avatar/{{ post['author'] }}" /><br/>
|
||||
</td>
|
||||
<td>
|
||||
<h3><a href="/u/{{ post['author'] }}" class="username {% if post['ownerblog'] %}owner{% endif %}">{{ post['author'] }}</a> says...</h3>
|
||||
<a href="/b/{{bname}}/{{post['id']}}" class="notsoobviouslink"><p>{{ post['content'] | safe }}</p></a>
|
||||
</td>
|
||||
<td width="24px">
|
||||
{% if post['author'] == name or ismod -%}
|
||||
<a class="abtn red" href="/del/{{bname}}/{{post['id']}}">X</a>
|
||||
{% endif -%}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor -%}
|
||||
</table>
|
||||
</div>
|
||||
{% include "nav.html" %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Morsel - {{ name }}</title>
|
||||
<link rel="stylesheet" href="/static/morsel.css" />
|
||||
<link rel="stylesheet" href="/static/theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
{% include "logo.html" %}
|
||||
{% include "nav.html" %}
|
||||
<div class="feed_panel panel">
|
||||
<table width="100%" cellspacing="0px">
|
||||
<tr class="board">
|
||||
<td width="32px"> </td>
|
||||
<td style="padding: 0px; padding-top: 6px; padding-left: 10px;">
|
||||
<a style="color:#aaff33;" href="/new/b">Create New Board</a>
|
||||
<p style="margin-bottom: 0px;"><small><i>You know you want to.</i></small></p>
|
||||
</td>
|
||||
</tr>
|
||||
{% for board in boards -%}
|
||||
<tr class="board">
|
||||
{% if board['name'] in subbed -%}
|
||||
<td width="32px"><a href="/b/{{ board['name'] }}?unsubscribe" class="subbtn remove">+</a></td>
|
||||
{% else -%}
|
||||
<td width="32px"><a href="/b/{{ board['name'] }}?subscribe" class="subbtn">+</a></td>
|
||||
{% endif -%}
|
||||
<td>
|
||||
<h3><a href="/b/{{ board['name'] }}">{{ board['name'] }}</a></h3>
|
||||
{{ board['description'] }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor -%}
|
||||
</table>
|
||||
</div>
|
||||
{% include "nav.html" %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Morsel</title>
|
||||
<link rel="stylesheet" href="/static/morsel.css" />
|
||||
<link rel="stylesheet" href="/static/theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
{% include "logo.html" %}
|
||||
<div class="panel">
|
||||
<p class="red" style="padding: 16px;">
|
||||
Are you <b><u>CERTAIN</u></b> you want to delete this post? It will be permanently lost.
|
||||
</p>
|
||||
<table width="100%" cellspacing="0px"><tr class="post">
|
||||
<td width="80px" align="right">
|
||||
<img src="/avatar/{{name}}" />
|
||||
</td>
|
||||
<td>
|
||||
<h3>{{name}}</h3>
|
||||
<p>{{post['content']}}</p>
|
||||
</td>
|
||||
</tr></table>
|
||||
<div class="buttons">
|
||||
<a class="abtn" href="/b/{{bname}}">Nevermind</a>
|
||||
<a class="abtn red" href="/del/{{bname}}/{{id}}?confirm">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Morsel</title>
|
||||
<link rel="stylesheet" href="/static/morsel.css" />
|
||||
<link rel="stylesheet" href="/static/theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
{% include "logo.html" %}
|
||||
<div class="panel">
|
||||
<p>
|
||||
{{ err }}
|
||||
</p>
|
||||
<div class="buttons">
|
||||
<a class="abtn" href="{{ refer }}">OK</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,58 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Morsel - {{ name }}</title>
|
||||
<link rel="stylesheet" href="/static/morsel.css" />
|
||||
<link rel="stylesheet" href="/static/theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
{% include "logo.html" %}
|
||||
{% include "nav.html" %}
|
||||
<div class="feed_panel panel">
|
||||
<h3>Welcome to your <span class="mods">Timeline</span>!</h3>
|
||||
<hr/>
|
||||
<table width="100%" cellspacing="0px">
|
||||
{% if noposts %}
|
||||
<tr class="post">
|
||||
<td width="80px" align="right"><img src="/static/noti_avatar.png" alt="System" /></td>
|
||||
<td>
|
||||
<h3 class="username">System</h3>
|
||||
<p>You aren't subscribed to any boards. <a href="/b/" class="mods">Click here to fix that.</a></p>
|
||||
<hr/>
|
||||
<h4>Tips for getting set up:</h4>
|
||||
<ul>
|
||||
<li>You have to subscribe to a board to post in it. Click the green plus next to a board you're interested in to subscribe.<br/><br/></li>
|
||||
<li>To unsubscribe, just hit the same button you used to subscribe again.<br/><br/></li>
|
||||
<li>If you can't find a board that piques your interest, feel free to create your own! Hit the "Create New Board" link at the top of the boards list
|
||||
to get started.<br/><br/></li>
|
||||
<li>Morsel posts are written in <a href="https://daringfireball.net/projects/markdown/" class="mods">Markdown</a>.
|
||||
<a href="https://www.markdownguide.org/basic-syntax/" class="mods">Here's a tutorial on writing posts in Markdown.</a><br/><br/></li>
|
||||
<li>You can set your avatar and bio in <a href="/settings" class="mods">user settings</a>.<br/><br/></li>
|
||||
<li>Your avatar can be either a <a href="//libravatar.org" class="mods">Libravatar</a> e-mail, or the URL of an image hosted on another site.<br/><br/></li>
|
||||
<li>Timeline posts are sorted in <i>chronological order</i>, which basically means they're ordered from newest to oldest.<br/><br/></li>
|
||||
<li>Only posts from boards you subscribe to will appear in your timeline.<br/><br/></li>
|
||||
<li>Check out the boards list (linked above) and subscribe to some interesting boards. This will populate your timeline.<br/><br/></li>
|
||||
<li>If you ever want to see this message again, just unsubscribe from all boards.</li>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% for post in feed %}
|
||||
<tr class="post">
|
||||
<td width="80px" align="right"><img src="/avatar/{{post['author']}}" width="64px" alt="{{post['author']}}"/></td>
|
||||
<td>
|
||||
<h3>
|
||||
<a class="username {% if post['ownerblog'] %}owner{% endif %}" href="/u/{{post['author']}}">
|
||||
{{post['author']}}
|
||||
</a>
|
||||
»
|
||||
<a class="mods" href="/b/{{post['board']}}">{{post['board']}}</a></h3>
|
||||
<a class="notsoobviouslink" href="/b/{{post['board']}}/{{post['id']}}"><p>{{post['content']|safe}}</p></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% include "nav.html" %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,47 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Morsel</title>
|
||||
<link rel="stylesheet" href="/static/morsel.css" />
|
||||
<link rel="stylesheet" href="/static/theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
<center class="logobox">
|
||||
<img src="/static/logo.png" alt="bigger morsel logo"/>
|
||||
</center>
|
||||
<div class="feed_panel panel">
|
||||
<h1 style="margin-bottom: 0px; text-align: center;">Morsel</h1>
|
||||
<p><center>
|
||||
is a combination of a microblogging service and forum. It encourages <big class="mods"><b>humanity</b></big> over all else.
|
||||
</center></p>
|
||||
<ul>
|
||||
<li>
|
||||
<b class="mods">Designed for people with fingers and brains.</b><br/>
|
||||
Morsel is designed with people in mind, intended to be easy to use and simple to navigate.<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
<b class="username">It looks pretty nice, too.</b><br/>
|
||||
Morsel's also designed to look very nice, at least according to
|
||||
<a href="//morsel.videotoblin.me/u/videotoblin" class="mods">my tastes.</a><br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
<b class="mods">It's open source.</b><br/>
|
||||
It's pretty easy to get set up for debugging and development, pretty much as soon as you get your hands on
|
||||
<a href="//git.catvibers.me/videotoaster/morsel" class="mods">the freely available source code</a>, licensed under the MIT
|
||||
license for ease of access and use.<br/><br/>
|
||||
</li>
|
||||
<li>
|
||||
<b class="username">It's customizable.</b><br/>
|
||||
As a Morsel admin, you can completely uproot the UI design of the webapp to your tastes. You can make it flat,
|
||||
you can change the logo, the background, the proportions, you can make it look <i>terrible</i>, or you can make
|
||||
it look <b>stylish.</b> It's all up to you!<br/><br/>
|
||||
</li>
|
||||
</ul>
|
||||
<h3><center>Ready to get started?</center></h3>
|
||||
<div class="buttons">
|
||||
<a class="abtn" href="/login">Log into Morsel</a>
|
||||
<a class="abtn" href="/login">Create an Account</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,27 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Login</title>
|
||||
<link rel="stylesheet" href="/static/morsel.css" />
|
||||
<link rel="stylesheet" href="/static/theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="login_panel panel">
|
||||
<h3>Welcome back!</h3>
|
||||
<hr/>
|
||||
{% if error %}
|
||||
<div class="error">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="/login" method="POST">
|
||||
<input type="text" name="username" placeholder="Username">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<div class="buttons">
|
||||
<a href="/reg" class="abtn">Register</a>
|
||||
<input type="submit" value="Log In" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,3 @@
|
|||
<div class="logobox">
|
||||
<img src="/static/logo.png" alt="Morsel logo" />
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
<div class="action_panel panel top">
|
||||
<table width="100%">
|
||||
<tr><td align="left">
|
||||
<a class="abtn" href="/">Timeline</a>
|
||||
<a class="abtn" href="/subs/">Subscriptions</a>
|
||||
<a class="abtn" href="/b/">Boards</a>
|
||||
</td><td align="right">
|
||||
<a class="abtn" href="/settings">Settings</a>
|
||||
<a class="abtn" href="/u/{{ name }}">{{name}}</a>
|
||||
<a class="abtn red" href="/logout">Sign Out</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Morsel - {{ name }}</title>
|
||||
<link rel="stylesheet" href="/static/morsel.css" />
|
||||
<link rel="stylesheet" href="/static/theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
{% include "nav.html" %}
|
||||
<div class="feed_panel panel">
|
||||
<h2>Settings</h2>
|
||||
<form action="/new/b" method="POST">
|
||||
<table width="75%" style="border: 1px solid #4c4c4c; margin: auto;">
|
||||
<tr>
|
||||
<td width="25%">
|
||||
Board Name
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="bname" value="newboard" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="25%">Board Description</td>
|
||||
<td>
|
||||
<textarea name="bdesc">A new board on Morsel!</textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="25%">
|
||||
Board Moderators
|
||||
<p><small>This should be a comma separated list of usernames.</small></p>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="bmods" value="{{name}}" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td |