Initial commit

This commit is contained in:
Drew DeVault 2017-12-03 11:56:28 -05:00
commit 5a1778b7e6
17 changed files with 497 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
*.pyc
bin/
config.ini
alembic.ini
include/
local/
lib/
static/
*.swp
*.rdb
storage/
pip-selfcheck.json
.sass-cache/
overrides/
.pgp
build

3
Makefile Normal file
View File

@ -0,0 +1,3 @@
SRHT_PATH?=/usr/lib/python3.6/site-packages/srht
MODULE=mansrht/
include ${SRHT_PATH}/Makefile

58
alembic.ini.example Normal file
View File

@ -0,0 +1,58 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = mansrht/alembic
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
sqlalchemy.url = postgres://postgres@localhost/man.sr.ht
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

48
config.ini.example Normal file
View File

@ -0,0 +1,48 @@
#
# todo.sr.ht config
[server]
#
# Specifies the protocol (usually http or https) meta.sr.ht runs with.
protocol=http
#
# Specifies the domain name meta.sr.ht is running on.
domain=man.sr.ht.local
#
# A secret key to encrypt session cookies with.
secret-key=CHANGEME
[debug]
#
# Address and port to bind the debug server to.
debug-host=0.0.0.0
debug-port=5004
[sr.ht]
#
# Configures the SQLAlchemy connection string for the database.
connection-string=postgresql://postgres@localhost/man.sr.ht
#
# The name of your network of sr.ht-based sites
site-name=sr.ht
[network]
#
# Location of other sites in your network
#
# This isn't a hardcoded list, add or remove entries as you like. The upstream
# sites do know about each other and will omit integrations if you leave out
# the relevant site. Only meta is required.
meta=http://meta.sr.ht.local
git=http://git.sr.ht.local
builds=http://builds.sr.ht.local
todo=http://todo.sr.ht.local
man=http://man.sr.ht.local
[man.sr.ht]
git-user=man:man
repo-path=/var/lib/git/man
[meta.sr.ht]
oauth-client-id=CHANGEME
oauth-client-secret=CHANGEME

62
mansrht/app.py Normal file
View File

@ -0,0 +1,62 @@
from flask import render_template, request
from flask_login import LoginManager, current_user
from jinja2 import Markup
from datetime import datetime
import locale
import urllib
from srht.config import cfg, cfgi, load_config
load_config("man")
from srht.database import DbSession
db = DbSession(cfg("sr.ht", "connection-string"))
from mansrht.types import User
db.init()
from srht.flask import SrhtFlask
app = SrhtFlask("man", __name__)
app.secret_key = cfg("server", "secret-key")
login_manager = LoginManager()
login_manager.init_app(app)
@login_manager.user_loader
def load_user(username):
return User.query.filter(User.username == username).first()
login_manager.anonymous_user = lambda: None
try:
locale.setlocale(locale.LC_ALL, 'en_US')
except:
pass
def oauth_url(return_to):
return "{}/oauth/authorize?client_id={}&scopes=profile,keys&state={}".format(
meta_sr_ht, meta_client_id, urllib.parse.quote_plus(return_to))
from mansrht.blueprints.auth import auth
from mansrht.blueprints.html import html
app.register_blueprint(auth)
app.register_blueprint(html)
meta_sr_ht = cfg("network", "meta")
meta_client_id = cfg("meta.sr.ht", "oauth-client-id")
git_user = cfg("man.sr.ht", "git-user")
domain = cfg("server", "domain")
@app.context_processor
def inject():
return {
"oauth_url": oauth_url(request.full_path),
"current_user": (
User.query.filter(User.id == current_user.id).first()
) if current_user else None,
"repo_uri": lambda user=None, wiki=None: (
"{}@{}/{}".format(
git_user.split(":")[0],
domain,
"~{}/{}".format(user, wiki) if user and wiki else "root.git"
)
),
"now": datetime.now
}

View File

@ -0,0 +1,82 @@
from flask import Blueprint, request, render_template, redirect
from flask_login import login_user, logout_user
from sqlalchemy import or_
from srht.config import cfg
from srht.flask import DATE_FORMAT
from srht.oauth import OAuthScope
from srht.database import db
from mansrht.types import User
from datetime import datetime
import urllib.parse
import requests
auth = Blueprint('auth', __name__)
meta_uri = cfg("network", "meta")
client_id = cfg("meta.sr.ht", "oauth-client-id")
client_secret = cfg("meta.sr.ht", "oauth-client-secret")
@auth.route("/oauth/callback")
def oauth_callback():
error = request.args.get("error")
if error:
details = request.args.get("details")
return render_template("oauth-error.html", details=details)
exchange = request.args.get("exchange")
scopes = request.args.get("scopes")
state = request.args.get("state")
_scopes = [OAuthScope(s) for s in scopes.split(",")]
if not OAuthScope("profile:read") in _scopes or not OAuthScope("keys:read") in _scopes:
return render_template("oauth-error.html",
details="man.sr.ht requires profile and key access at a mininum to function correctly. " +
"To use man.sr.ht, try again and do not untick these permissions.")
if not exchange:
return render_template("oauth-error.html",
details="Expected an exchange token from meta.sr.ht. Something odd has happened, try again.")
r = requests.post(meta_uri + "/oauth/exchange", json={
"client_id": client_id,
"client_secret": client_secret,
"exchange": exchange,
})
if r.status_code != 200:
print(r.text)
return render_template("oauth-error.html",
details="Error occured retrieving OAuth token. Try again.")
json = r.json()
token = json.get("token")
expires = json.get("expires")
if not token or not expires:
return render_template("oauth-error.html",
details="Error occured retrieving OAuth token. Try again.")
expires = datetime.strptime(expires, DATE_FORMAT)
r = requests.get(meta_uri + "/api/user/profile", headers={
"Authorization": "token " + token
})
if r.status_code != 200:
return render_template("oauth-error.html",
details="Error occured retrieving account info. Try again.")
json = r.json()
user = User.query.filter(or_(User.oauth_token == token,
User.username == json["username"])).first()
if not user:
user = User()
db.session.add(user)
user.username = json.get("username")
user.email = json.get("email")
user.oauth_token = token
user.oauth_token_expires = expires
user.oauth_token_scopes = scopes
db.session.commit()
login_user(user, remember=True)
if not state or not state.startswith("/"):
return redirect("/")
else:
return redirect(urllib.parse.unquote(state))
@auth.route("/logout")
def logout():
logout_user()
return redirect(request.headers.get("Referer") or "/")

View File

@ -0,0 +1,47 @@
from flask import Blueprint, render_template, abort
from srht.config import cfg
from srht.markdown import markdown, extract_toc
from datetime import datetime
import pygit2 as git
import os
html = Blueprint('html', __name__)
repo_path = cfg("man.sr.ht", "repo-path")
def content(repo, path):
master = repo.branches.get("master")
if not master:
# TODO: show reason maybe
abort(404)
commit = repo.get(master.target)
tree = commit.tree
path = os.path.split(path) if path else tuple()
for entry in path:
if tree.type != "tree":
abort(404)
if not entry in tree:
abort(404)
tree = tree["path"]
if tree.type != "blob":
if "index.md" in tree:
tree = tree["index.md"]
else:
abort(404)
blob = repo.get(tree.id)
md = blob.data.decode()
html = markdown(md, ["h1", "h2", "h3", "h4", "h5"], baselevel=3)
title = os.path.splitext(path[-1])[0] if path else "index"
ctime = datetime.fromtimestamp(commit.commit_time)
toc = extract_toc(html)
return render_template("content.html",
content=html, title=title, commit=commit, ctime=ctime, toc=toc)
@html.route("/")
@html.route("/<path:path>")
def root_content(path=None):
try:
repo = git.Repository(os.path.join(repo_path, "root"))
except:
# Fallback page
return render_template("index.html")
return content(repo, path)

View File

@ -0,0 +1,65 @@
{% extends "man.html" %}
{% block title %}
{% if title %}
<title>{{ title }} - man.sr.ht</title>
{% else %}
<title>man.sr.ht</title>
{% endif %}
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-7">
<h1>{{ title }}</h1>
{{ content }}
</div>
<div class="col-md-5">
<h3>Table of Contents</h3>
<ul style="list-style: none; padding-left: 0;">
{% macro toc_entry(entry, depth) %}
<li>
<a href="#{{ entry.id }}">{{ entry.name }}</a>
{% if len(entry.children) > 0 %}
<ul style="list-style: none; padding-left: {{ depth + 1 }}rem">
{% for child in entry.children %}
{{ toc_entry(child, depth + 1) }}
{% endfor %}
</ul>
{% endif %}
</li>
{% endmacro %}
{% for entry in toc %}
{{ toc_entry(entry, 0) }}
{% endfor %}
</ul>
<div class="pull-right" style="margin-bottom: 1rem">
<a href="#">Edit page</a>
</div>
<h4>This commit</h4>
<div class="clearfix"></div>
<div class="card" style="padding: 0.25rem; border: none; border-radius: 0; margin-bottom: 0.5rem">
<pre class="card-text">commit {{commit.id}}
Author: {{commit.author.name}} &lt;{{ commit.author.email }}&gt;
Date: {{ctime.isoformat()}}
{{commit.message.rstrip("\n")}}</pre>
</div>
<dl>
<dt>Clone this wiki</dt>
<dd>
<a href="#">{{repo_uri()}}</a><br />
<a href="#">https://man.sr.ht/root.git</a><br />
<a href="#">git://man.sr.ht/root.git</a>
</dd>
</dl>
<!--
<div class="row">
<div class="col-8 offset-2">
<a href="#" class="btn btn-default btn-block">Manage your wiki</a>
</div>
</div>
-->
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends "man.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-6">
<h1>man</h1>
<p>
You're seeing this page because the admin has not configured the
default wiki yet. If you are an admin, ensure your SSH keys are
configured for your account on meta.sr.ht, then push to
<code>{{repo_uri()}}</code>.
</p>
<p>Set the contents of the home page by writing <code>index.md</code>.</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends "layout.html" %}
{% block login_nav %}
<a href="/wiki/create">Create wiki</a>
&mdash;
{% endblock %}
{% block body %}
{% block content %}{% endblock %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "man.html" %}
{% block content %}
<div class="container">
<h2>Error logging in</h2>
<p>{{ details }}</p>
</div>
{% endblock %}

View File

@ -0,0 +1 @@
from mansrht.types.user import User

26
mansrht/types/user.py Normal file
View File

@ -0,0 +1,26 @@
import sqlalchemy as sa
from srht.database import Base
class User(Base):
__tablename__ = 'user'
id = sa.Column(sa.Integer, primary_key=True)
username = sa.Column(sa.Unicode(256))
created = sa.Column(sa.DateTime, nullable=False)
updated = sa.Column(sa.DateTime, nullable=False)
oauth_token = sa.Column(sa.String(256), nullable=False)
oauth_token_expires = sa.Column(sa.DateTime, nullable=False)
oauth_token_scopes = sa.Column(sa.String, nullable=False, default="")
email = sa.Column(sa.String(256), nullable=False)
def __repr__(self):
return '<User {} {}>'.format(self.id, self.username)
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return self.username

11
run.py Normal file
View File

@ -0,0 +1,11 @@
from mansrht.app import app
from srht.config import cfg, cfgi
import os
app.static_folder = os.path.join(os.getcwd(), "static")
if __name__ == '__main__':
app.run(host=cfg("debug", "debug-host"),
port=cfgi("debug", "debug-port"),
debug=True)

11
scss/main.scss Normal file
View File

@ -0,0 +1,11 @@
@import "base";
@import "font-awesome.min";
code {
color: black;
border-radius: 0;
padding-top: 0;
padding-bottom: 0;
margin: 0 0.25rem;
background: #e0e0e0;
}

34
setup.py Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
from distutils.core import setup
import subprocess
import glob
import os
subprocess.call(["make"])
ver = os.environ.get("PKGVER") or subprocess.run(['git', 'describe', '--tags'],
stdout=subprocess.PIPE).stdout.decode().strip()
setup(
name = 'mansrht',
packages = [
'mansrht',
'mansrht.types',
'mansrht.blueprints',
'mansrht.alembic',
'mansrht.alembic.versions'
],
version = ver,
description = 'man.sr.ht website',
author = 'Drew DeVault',
author_email = 'sir@cmpwn.com',
url = 'https://git.sr.ht/~sircmpwn/man.sr.ht',
install_requires = ['srht', 'flask-login', 'alembic'],
license = 'AGPL-3.0',
package_data={
'mansrht': [
'templates/*.html',
'static/*'
]
}
)

1
static Symbolic link
View File

@ -0,0 +1 @@
mansrht/static/