commit a5bba8e8ca8044a426af51ef9f05a793b8bfedbf Author: Daniel Muckerman Date: Sat Oct 31 19:55:57 2020 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b235d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +.envrc +.DS_Store +.vscode/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a6d698f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.7 + +LABEL maintainer="Dan Muckerman " + +WORKDIR /project +ADD . /project +RUN rm /project/.envrc +RUN rm -rf /project/env + +RUN apt-get update && apt-get install -y python3-dev libldap2-dev libsasl2-dev libssl-dev + +RUN pip install -r requirements.txt +CMD ["flask","run","--host=0.0.0.0"] \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..74553b1 --- /dev/null +++ b/app.py @@ -0,0 +1,218 @@ +from utils import clean_articles +import ldap as l +from ldap3 import Server, Connection, ALL, MODIFY_REPLACE +from flask import Flask, g, request, session, redirect, url_for, render_template +from flask_simpleldap import LDAP +from flask_bootstrap import Bootstrap +from readability import Document +from readabilipy import simple_json_from_html_string +import os +import sqlite3 +import requests +from requests.api import head +from utils import clean_articles + +app = Flask(__name__) +Bootstrap(app) +app.secret_key = 'asdf' +app.debug = True + +# Base +app.config['LDAP_REALM_NAME'] = 'OpenLDAP Authentication' +app.config['LDAP_HOST'] = os.environ.get('LDAP_HOST') +app.config['LDAP_BASE_DN'] = os.environ.get('LDAP_BASE_DN') +app.config['LDAP_USERNAME'] = os.environ.get('LDAP_USERNAME') +app.config['LDAP_PASSWORD'] = os.environ.get('LDAP_PASSWORD') + +# OpenLDAP +app.config['LDAP_OBJECTS_DN'] = 'dn' +app.config['LDAP_OPENLDAP'] = True +app.config['LDAP_USER_OBJECT_FILTER'] = '(&(objectclass=posixAccount)(uid=%s))' + +short_domain = os.environ.get('SHORT_DOMAIN') + +ldap = LDAP(app) + +server = Server(app.config['LDAP_HOST']) +conn = Connection(server, app.config['LDAP_USERNAME'], app.config['LDAP_PASSWORD'], auto_bind=True) + +@app.before_request +def before_request(): + g.user = None + if 'user_id' in session: + # This is where you'd query your database to get the user info. + g.user = {} + + +@app.route('/') +@ldap.login_required +def index(): + user_dict = ldap.get_object_details(session['user_id']) + + if 'user_id' in session: + user = {'dn': 'cn={},cn=usergroup,ou=users,dc=technicalincompetence,dc=club'.format(user_dict['cn'][0].decode('ascii')), + 'firstName': user_dict['givenName'][0].decode('ascii'), + 'lastName': user_dict['sn'][0].decode('ascii'), + 'email': user_dict['mail'][0].decode('ascii'), + 'userName': user_dict['uid'][0].decode('ascii'), + } + + conn = sqlite3.connect('pocket/readitlater.db') + c = conn.cursor() + + c.execute("SELECT article_id, url, title, byline FROM saved_articles INNER JOIN articles on saved_articles.article_id = articles.id WHERE user=? AND read=0 OR read IS NULL", (session['user_id'], )) + rows = c.fetchall() + + conn.commit() + conn.close() + + return render_template('list.j2', articles = clean_articles(rows)) + + +@app.route('/archived') +@ldap.login_required +def archived(): + conn = sqlite3.connect('pocket/readitlater.db') + c = conn.cursor() + + c.execute("SELECT article_id, url, title, byline FROM saved_articles INNER JOIN articles on saved_articles.article_id = articles.id WHERE user=? AND read=1", (session['user_id'], )) + rows = c.fetchall() + print(rows) + + conn.commit() + conn.close() + + return render_template('list.j2', articles = clean_articles(rows)) + + +@app.route('/save') +@ldap.login_required +def save(): + return render_template('save.j2') + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if g.user: + return redirect(url_for('index')) + if request.method == 'POST': + user = request.form['user'] + passwd = request.form['passwd'] + test = ldap.bind_user(user, passwd) + if test is None or passwd == '': + return render_template('login.j2', error='Invalid credentials') + else: + session['user_id'] = request.form['user'] + session['passwd'] = request.form['passwd'] + + if session['next']: + next = session['next'] + session['next'] = '' + return redirect(next) + return redirect('/') + return render_template('login.j2') + + +@ldap.login_required +@app.route('/article/') +def read_article(article_id): + conn = sqlite3.connect('pocket/readitlater.db') + c = conn.cursor() + + c.execute("SELECT * FROM articles where id=?", (article_id,)) + rows = c.fetchall() + conn.commit() + conn.close() + + if (len(rows) > 0): + return render_template('article.j2', article=rows[0]) + + return render_template('article.j2', article=()) + + +@ldap.login_required +@app.route('/add', methods=['GET', 'POST']) +def add_url(): + if not 'user_id' in session: + session['next'] = request.url + return redirect(url_for('login')) + if request.method == 'POST': + url = request.form['url'] + close = None + else: + url = request.args.get('url') + close = request.args.get('close') + conn = sqlite3.connect('pocket/readitlater.db') + c = conn.cursor() + + if url is not None and len(url) > 0: + headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36'} + response = requests.get(url, headers=headers) + + article = simple_json_from_html_string(response.text, use_readability=True) + + c.execute("SELECT * FROM articles WHERE url=?", (url,)) + rows = c.fetchall() + + if (len(rows) == 0): + c.execute("INSERT INTO articles (url, content, title, byline) VALUES (?, ?, ?, ?)", (url, article['content'], article['title'], article['byline'])) + c.execute("SELECT * FROM articles WHERE url=?", (url,)) + rows = c.fetchall() + + article_id = rows[0][0] + + c.execute("SELECT * FROM saved_articles WHERE user=? AND article_id=?", (session['user_id'], article_id)) + rows = c.fetchall() + + if (len(rows) == 0): + c.execute("INSERT INTO saved_articles (user, article_id) VALUES (?, ?)", (session['user_id'], article_id)) + conn.commit() + conn.close() + + if close is not None and close == '1': + return render_template('close.j2') + return 'Saved' + conn.commit() + conn.close() + return 'Error' + +@ldap.login_required +@app.route('/delete/') +def delete_article(article_id): + conn = sqlite3.connect('pocket/readitlater.db') + c = conn.cursor() + + c.execute("DELETE FROM saved_articles WHERE user=? AND article_id=?", (session['user_id'], article_id)) + c.execute("SELECT * FROM saved_articles WHERE article_id=?", (article_id, )) + rows = c.fetchall() + + if (len(rows) == 0): + c.execute("DELETE FROM articles WHERE id=?", (article_id,)) + + conn.commit() + conn.close() + + return redirect(url_for('index')) + +@ldap.login_required +@app.route('/archive/') +def archive_article(article_id): + conn = sqlite3.connect('pocket/readitlater.db') + c = conn.cursor() + + c.execute("UPDATE saved_articles SET read=1 WHERE user=? AND article_id=?", (session['user_id'], article_id)) + + conn.commit() + conn.close() + + return redirect(url_for('index')) + + +@app.route('/logout') +def logout(): + session.pop('user_id', None) + return redirect(url_for('index')) + + +if __name__ == '__main__': + app.run() \ No newline at end of file diff --git a/blurb.txt b/blurb.txt new file mode 100644 index 0000000..785ff02 --- /dev/null +++ b/blurb.txt @@ -0,0 +1,4 @@ +Instapaper outdated +Pocket overrated +Long have we waited +Read TI Later _activated_ \ No newline at end of file diff --git a/bookmarklet.txt b/bookmarklet.txt new file mode 100644 index 0000000..e678410 --- /dev/null +++ b/bookmarklet.txt @@ -0,0 +1 @@ +javascript:(function(){var%20url%20=%20location.href;var%20otherWindow=window.open('about:blank','_blank');otherWindow.opener=null;otherWindow.location='http://127.0.0.1:5000/add?close=1&url='+encodeURIComponent(url);})(); \ No newline at end of file diff --git a/pocket/readitlater.db b/pocket/readitlater.db new file mode 100644 index 0000000..52dcc42 Binary files /dev/null and b/pocket/readitlater.db differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2e91653 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +Flask==1.1.2 +Flask-Bootstrap4==4.0.2 +Flask-Login==0.5.0 +Flask-SimpleLDAP==1.4.0 +python-ldap==3.2.0 +ldap3==2.7 +readability-lxml==0.8.1 +requests==2.24.0 \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..04a4796 --- /dev/null +++ b/static/style.css @@ -0,0 +1,197 @@ +.wrapper { + display: flex; + width: 100%; + align-items: stretch; +} + +.toolbar-button { + padding-left: 2rem; + padding-top: .375rem; + line-height: 1.5; + color: white; +} + +.back-chevron { + line-height: 1.5 !important; + vertical-align: middle !important; + margin-bottom: 4px !important; +} + +#sidebar { + padding-left: 20px; + background-color: #ecedee; + height: 100vh; + padding-top: 20px; + border-right: 1px solid darkgray; +} + +#sidebar ul > li { + padding-bottom: 10px; +} + +#sidebar ul > li > a { + color: rgba(0,0,0,.5); +} + +#sidebar ul > li.active > a { + color: black; +} + +#content { + width: 100%; + padding: 20px; +} + +@media (prefers-color-scheme: dark) { + #sidebar { + padding-left: 20px; + background-color: #1a1d20; + height: 100vh; + padding-top: 20px; + border-right: 1px solid black; + } + + #sidebar ul > li { + padding-bottom: 10px; + } + + #sidebar ul > li > a { + color: rgba(255,255,255,.5); + } + + #sidebar ul > li.active > a { + color: white; + } + + body { + background-color: #111 !important; + color: #eee; + } + + .jumbotron { + background-color: #333 !important; + } + + .modal-content { + background-color: #111 !important; + color: #eee; + } + + .modal-header { + border-bottom: 1px solid #555 !important; + } + + .modal-header .close { + color: #eee !important; + text-shadow: 0 1px 0 #555 !important; + } + + .modal-footer { + border-top: 1px solid #555 !important; + } + + .bg-light { + background-color: #333 !important; + } + + .bg-white { + background-color: #000 !important; + } + + .bg-black { + background-color: #eee !important; + } + + .form-control { + display: block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #dee2e6; + background-color: #000; + background-clip: padding-box; + border: 1px solid #6c757d; + border-radius: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + } + + @media (prefers-reduced-motion: reduce) { + .form-control { + transition: none; + } + } + + .form-control::-ms-expand { + background-color: transparent; + border: 0; + } + + .form-control:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #dee2e6; + } + + .form-control:focus { + color: #dee2e6; + background-color: #191d21; + border-color: #b3d7ff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); + } + + .form-control::-webkit-input-placeholder { + color: #6c757d; + opacity: 1; + } + + .form-control::-moz-placeholder { + color: #6c757d; + opacity: 1; + } + + .form-control::-ms-input-placeholder { + color: #6c757d; + opacity: 1; + } + + .form-control::placeholder { + color: #6c757d; + opacity: 1; + } + + .form-control:disabled, + .form-control[readonly] { + background-color: #343a40; + opacity: 1; + } + + .card { + background-color: #000; + border: 1px solid #6c757d; + } +} + +@media (max-width: 991.98px) { + #sidebar { + display: none !important; + } +} + +@media (min-width: 991.98px) { + .navbar-nav { + display: none !important; + } + + #sidebar { + min-width: 250px; + max-width: 250px; + display: block !important; + } + + #sidebar.active { + margin-left: -250px; + } +} \ No newline at end of file diff --git a/templates/article.j2 b/templates/article.j2 new file mode 100644 index 0000000..8643876 --- /dev/null +++ b/templates/article.j2 @@ -0,0 +1,60 @@ +{% extends "bootstrap/base.html" %} +{% block title %}Read TI Later{% endblock %} + +{% block styles %} +{{super()}} + +{% endblock %} + +{% block navbar %} + +{% endblock %} + +{% block content %} +
+ +
+
+ {#
+ + +
+ +
+
+
#} +
+
+

{{ article[3] }}

+ View Original + {{ article[2] | safe}} +
+
+
+
+
+{% endblock %} + +{% block scripts %} +{{ super() }} + + +{% endblock %} \ No newline at end of file diff --git a/templates/close.j2 b/templates/close.j2 new file mode 100644 index 0000000..f40c222 --- /dev/null +++ b/templates/close.j2 @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/templates/list.j2 b/templates/list.j2 new file mode 100644 index 0000000..049cc7a --- /dev/null +++ b/templates/list.j2 @@ -0,0 +1,93 @@ +{% extends "bootstrap/base.html" %} +{% block title %}Read TI Later{% endblock %} + +{% block styles %} +{{super()}} + +{% endblock %} + +{% block navbar %} + +{% endblock %} + +{% block content %} +
+ + + + +
+
+
+
+ {% for article in articles|reverse %} +
+ {{ article[2] }} + {{ article[4] }} +
+ + Archive +      + Delete + +
+ {% endfor %} +
+
+
+
+
+{% endblock %} + +{% block scripts %} +{{ super() }} + + +{% endblock %} \ No newline at end of file diff --git a/templates/login.j2 b/templates/login.j2 new file mode 100644 index 0000000..e6919ce --- /dev/null +++ b/templates/login.j2 @@ -0,0 +1,37 @@ +{% extends "bootstrap/base.html" %} +{% block title %}Technical Incompetence Link Shortener{% endblock %} + +{% block styles %} +{{super()}} + +{% endblock %} + +{% block navbar %} + +{% endblock %} + +{% block content %} +
+
+

Sign in for our awesome service

+

Forgot your password? Too bad! We don't have emails working yet!

+
+
+ + {% if error is defined %} + + {% endif %} + +
+ +
+ +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/save.j2 b/templates/save.j2 new file mode 100644 index 0000000..ccb2990 --- /dev/null +++ b/templates/save.j2 @@ -0,0 +1,118 @@ +{% extends "bootstrap/base.html" %} +{% block title %}Read TI Later{% endblock %} + +{% block styles %} +{{super()}} + +{% endblock %} + +{% block navbar %} + +{% endblock %} + +{% block content %} +
+ + + + +
+ +
+
+
+ + +
+ +
+
+
+
+

Would you rather save on the go? Try our bookmarklet!

+ + javascript:(function(){var%20url%20=%20location.href;var%20otherWindow=window.open('about:blank','_blank');otherWindow.opener=null;otherWindow.location='{{ request.url_root }}add?close=1&url='+encodeURIComponent(url);})(); + +
+
+
+
+
+{% endblock %} + +{% block scripts %} +{{ super() }} + +{% endblock %} \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..7fb6d1e --- /dev/null +++ b/utils.py @@ -0,0 +1,12 @@ +from urllib.parse import urlparse + +def clean_articles(rows): + #article_id, url, title, byline + out = [] + + for row in rows: + parsed_uri = urlparse(row[1]) + result = '{uri.netloc}'.format(uri=parsed_uri) + out.append([row[0], row[1], row[2], row[3], result]) + + return out \ No newline at end of file