init commit

This commit is contained in:
Toaster 2023-01-08 00:20:36 -06:00
commit e5f74e88d7
37 changed files with 1569 additions and 0 deletions

21
LICENSE Normal file
View File

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

12
README.md Normal file
View File

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

1
boards.json Normal file
View File

@ -0,0 +1 @@
{"boards":[]}

1
boards/.dontdeleteme Normal file
View File

@ -0,0 +1 @@

11
config.json Normal file
View File

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

1
follows.json Normal file
View File

@ -0,0 +1 @@
{}

12
main.py Normal file
View File

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

22
morsel_avatar.py Normal file
View File

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

148
morsel_boards.py Normal file
View File

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

41
morsel_home.py Normal file
View File

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

43
morsel_login.py Normal file
View File

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

87
morsel_users.py Normal file
View File

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

325
morsel_util.py Normal file
View File

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

BIN
static/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/default_avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
static/logo-tiny.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

164
static/morsel.css Normal file
View File

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

BIN
static/noti_avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

57
static/theme.css Normal file
View File

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

64
templates/board.html Normal file
View File

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

37
templates/boards.html Normal file
View File

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

29
templates/del.html Normal file
View File

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

19
templates/err.html Normal file
View File

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

58
templates/home.html Normal file
View File

@ -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>
&raquo;
<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>

47
templates/landing.html Normal file
View File

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

27
templates/login.html Normal file
View File

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

3
templates/logo.html Normal file
View File

@ -0,0 +1,3 @@
<div class="logobox">
<img src="/static/logo.png" alt="Morsel logo" />
</div>

13
templates/nav.html Normal file
View File

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

49
templates/nboard.html Normal file
View File

@ -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>&nbsp;</td