From 5a1778b7e6b1e9093374d2b2cad1b8dcf13b319b Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Sun, 3 Dec 2017 11:56:28 -0500 Subject: [PATCH] Initial commit --- .gitignore | 16 ++++++ Makefile | 3 ++ alembic.ini.example | 58 +++++++++++++++++++++ config.ini.example | 48 +++++++++++++++++ mansrht/app.py | 62 ++++++++++++++++++++++ mansrht/blueprints/auth.py | 82 ++++++++++++++++++++++++++++++ mansrht/blueprints/html.py | 47 +++++++++++++++++ mansrht/templates/content.html | 65 +++++++++++++++++++++++ mansrht/templates/index.html | 17 +++++++ mansrht/templates/man.html | 8 +++ mansrht/templates/oauth-error.html | 7 +++ mansrht/types/__init__.py | 1 + mansrht/types/user.py | 26 ++++++++++ run.py | 11 ++++ scss/main.scss | 11 ++++ setup.py | 34 +++++++++++++ static | 1 + 17 files changed, 497 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 alembic.ini.example create mode 100644 config.ini.example create mode 100644 mansrht/app.py create mode 100644 mansrht/blueprints/auth.py create mode 100644 mansrht/blueprints/html.py create mode 100644 mansrht/templates/content.html create mode 100644 mansrht/templates/index.html create mode 100644 mansrht/templates/man.html create mode 100644 mansrht/templates/oauth-error.html create mode 100644 mansrht/types/__init__.py create mode 100644 mansrht/types/user.py create mode 100644 run.py create mode 100644 scss/main.scss create mode 100755 setup.py create mode 120000 static diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2cb12a5 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..621ea56 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +SRHT_PATH?=/usr/lib/python3.6/site-packages/srht +MODULE=mansrht/ +include ${SRHT_PATH}/Makefile diff --git a/alembic.ini.example b/alembic.ini.example new file mode 100644 index 0000000..22b5bb4 --- /dev/null +++ b/alembic.ini.example @@ -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 diff --git a/config.ini.example b/config.ini.example new file mode 100644 index 0000000..c5b60dd --- /dev/null +++ b/config.ini.example @@ -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 diff --git a/mansrht/app.py b/mansrht/app.py new file mode 100644 index 0000000..b12075e --- /dev/null +++ b/mansrht/app.py @@ -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 + } diff --git a/mansrht/blueprints/auth.py b/mansrht/blueprints/auth.py new file mode 100644 index 0000000..e513197 --- /dev/null +++ b/mansrht/blueprints/auth.py @@ -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 "/") diff --git a/mansrht/blueprints/html.py b/mansrht/blueprints/html.py new file mode 100644 index 0000000..dbd662a --- /dev/null +++ b/mansrht/blueprints/html.py @@ -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("/") +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) diff --git a/mansrht/templates/content.html b/mansrht/templates/content.html new file mode 100644 index 0000000..d230c93 --- /dev/null +++ b/mansrht/templates/content.html @@ -0,0 +1,65 @@ +{% extends "man.html" %} +{% block title %} +{% if title %} +{{ title }} - man.sr.ht +{% else %} +man.sr.ht +{% endif %} +{% endblock %} +{% block content %} +
+
+
+

{{ title }}

+ {{ content }} +
+
+

Table of Contents

+
    + {% macro toc_entry(entry, depth) %} +
  • + {{ entry.name }} + {% if len(entry.children) > 0 %} +
      + {% for child in entry.children %} + {{ toc_entry(child, depth + 1) }} + {% endfor %} +
    + {% endif %} +
  • + {% endmacro %} + {% for entry in toc %} + {{ toc_entry(entry, 0) }} + {% endfor %} +
+
+ Edit page +
+

This commit

+
+
+
commit {{commit.id}}
+Author: {{commit.author.name}} <{{ commit.author.email }}>
+Date:   {{ctime.isoformat()}}
+
+{{commit.message.rstrip("\n")}}
+
+
+
Clone this wiki
+
+ {{repo_uri()}}
+ https://man.sr.ht/root.git
+ git://man.sr.ht/root.git +
+
+ +
+
+
+{% endblock %} diff --git a/mansrht/templates/index.html b/mansrht/templates/index.html new file mode 100644 index 0000000..3460c69 --- /dev/null +++ b/mansrht/templates/index.html @@ -0,0 +1,17 @@ +{% extends "man.html" %} +{% block content %} +
+
+
+

man

+

+ 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 + {{repo_uri()}}. +

+

Set the contents of the home page by writing index.md.

+
+
+
+{% endblock %} diff --git a/mansrht/templates/man.html b/mansrht/templates/man.html new file mode 100644 index 0000000..7f6bd6e --- /dev/null +++ b/mansrht/templates/man.html @@ -0,0 +1,8 @@ +{% extends "layout.html" %} +{% block login_nav %} +Create wiki +— +{% endblock %} +{% block body %} +{% block content %}{% endblock %} +{% endblock %} diff --git a/mansrht/templates/oauth-error.html b/mansrht/templates/oauth-error.html new file mode 100644 index 0000000..8cf00be --- /dev/null +++ b/mansrht/templates/oauth-error.html @@ -0,0 +1,7 @@ +{% extends "man.html" %} +{% block content %} +
+

Error logging in

+

{{ details }}

+
+{% endblock %} diff --git a/mansrht/types/__init__.py b/mansrht/types/__init__.py new file mode 100644 index 0000000..61eca10 --- /dev/null +++ b/mansrht/types/__init__.py @@ -0,0 +1 @@ +from mansrht.types.user import User diff --git a/mansrht/types/user.py b/mansrht/types/user.py new file mode 100644 index 0000000..d146d62 --- /dev/null +++ b/mansrht/types/user.py @@ -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 ''.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 + diff --git a/run.py b/run.py new file mode 100644 index 0000000..ff1b83b --- /dev/null +++ b/run.py @@ -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) diff --git a/scss/main.scss b/scss/main.scss new file mode 100644 index 0000000..0ec4aab --- /dev/null +++ b/scss/main.scss @@ -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; +} diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..cc3a177 --- /dev/null +++ b/setup.py @@ -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/*' + ] + } +) diff --git a/static b/static new file mode 120000 index 0000000..c81f6a1 --- /dev/null +++ b/static @@ -0,0 +1 @@ +mansrht/static/ \ No newline at end of file