From 2edb3ceba7b4e0df480412915039f30bbfa010ab Mon Sep 17 00:00:00 2001
From: Tanner Collin
Date: Fri, 8 Nov 2019 05:55:30 +0000
Subject: [PATCH] Allow manual submission of articles
---
apiserver/feed.py | 4 ++-
apiserver/feeds/manual.py | 46 +++++++++++++++++++++++++++
apiserver/server.py | 58 ++++++++++++++++++++++++++---------
webclient/src/App.js | 2 ++
webclient/src/Search.js | 22 ++++++-------
webclient/src/Style-light.css | 5 +++
webclient/src/Submit.js | 54 ++++++++++++++++++++++++++++++++
7 files changed, 163 insertions(+), 28 deletions(-)
create mode 100644 apiserver/feeds/manual.py
create mode 100644 webclient/src/Submit.js
diff --git a/apiserver/feed.py b/apiserver/feed.py
index a56e685..0cb8e42 100644
--- a/apiserver/feed.py
+++ b/apiserver/feed.py
@@ -7,7 +7,7 @@ import requests
import time
from bs4 import BeautifulSoup
-from feeds import hackernews, reddit, tildes
+from feeds import hackernews, reddit, tildes, manual
OUTLINE_API = 'https://outlineapi.com/article'
ARCHIVE_API = 'https://archive.fo/submit/'
@@ -99,6 +99,8 @@ def update_story(story):
res = reddit.story(story['ref'])
elif story['source'] == 'tildes':
res = tildes.story(story['ref'])
+ elif story['source'] == 'manual':
+ res = manual.story(story['ref'])
if res:
story.update(res) # join dicts
diff --git a/apiserver/feeds/manual.py b/apiserver/feeds/manual.py
new file mode 100644
index 0000000..fef3cd0
--- /dev/null
+++ b/apiserver/feeds/manual.py
@@ -0,0 +1,46 @@
+import logging
+logging.basicConfig(
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ level=logging.DEBUG)
+
+import requests
+import time
+from bs4 import BeautifulSoup
+
+USER_AGENT = 'Twitterbot/1.0'
+
+def api(route):
+ try:
+ headers = {'User-Agent': USER_AGENT}
+ r = requests.get(route, headers=headers, timeout=5)
+ if r.status_code != 200:
+ raise Exception('Bad response code ' + str(r.status_code))
+ return r.text
+ except KeyboardInterrupt:
+ raise
+ except BaseException as e:
+ logging.error('Problem hitting manual website: {}'.format(str(e)))
+ return False
+
+def story(ref):
+ html = api(ref)
+ if not html: return False
+
+ soup = BeautifulSoup(html, features='html.parser')
+
+ s = {}
+ s['author'] = 'manual submission'
+ s['author_link'] = 'https://news.t0.vc'
+ s['score'] = 0
+ s['date'] = int(time.time())
+ s['title'] = str(soup.title.string)
+ s['link'] = ref
+ s['url'] = ref
+ s['comments'] = []
+ s['num_comments'] = 0
+
+ return s
+
+# scratchpad so I can quickly develop the parser
+if __name__ == '__main__':
+ print(story('https://www.backblaze.com/blog/what-smart-stats-indicate-hard-drive-failures/'))
diff --git a/apiserver/server.py b/apiserver/server.py
index 44db571..0d7b254 100644
--- a/apiserver/server.py
+++ b/apiserver/server.py
@@ -13,7 +13,7 @@ import archive
import feed
from utils import gen_rand_id
-from flask import abort, Flask, request, render_template
+from flask import abort, Flask, request, render_template, stream_with_context, Response
from werkzeug.exceptions import NotFound
from flask_cors import CORS
@@ -36,12 +36,37 @@ with shelve.open(DATA_FILE) as db:
news_ref_to_id = db.get('news_ref_to_id', {})
news_cache = db.get('news_cache', {})
+ # clean cache if broken
+ try:
+ for ref in news_list:
+ nid = news_ref_to_id[ref]
+ _ = news_cache[nid]
+ except KeyError as e:
+ logging.error('Unable to find key: ' + str(e))
+ logging.info('Clearing caches...')
+ news_list = []
+ news_ref_to_id = {}
+ news_cache = {}
+
def get_story(sid):
if sid in news_cache:
return news_cache[sid]
else:
return archive.get_story(sid)
+def new_id():
+ nid = gen_rand_id()
+ while nid in news_cache or archive.get_story(nid):
+ nid = gen_rand_id()
+ return nid
+
+def remove_ref(old_ref):
+ while old_ref in news_list:
+ news_list.remove(old_ref)
+ old_story = news_cache.pop(news_ref_to_id[old_ref])
+ old_id = news_ref_to_id.pop(old_ref)
+ logging.info('Removed ref {} id {}.'.format(old_ref, old_id))
+
build_folder = '../webclient/build'
flask_app = Flask(__name__, template_folder=build_folder, static_folder=build_folder, static_url_path='')
cors = CORS(flask_app)
@@ -66,6 +91,20 @@ def search():
res = []
return {'results': res}
+@flask_app.route('/api/submit', methods=['POST'], strict_slashes=False)
+def submit():
+ url = request.form['url']
+ nid = new_id()
+ news_story = dict(id=nid, ref=url, source='manual')
+ news_cache[nid] = news_story
+ valid = feed.update_story(news_story)
+ if valid:
+ archive.update(news_story)
+ return {'nid': nid}
+ else:
+ news_cache.pop(nid, '')
+ abort(400)
+
@flask_app.route('/api/')
def story(sid):
story = get_story(sid)
@@ -105,19 +144,6 @@ def static_story(sid):
url=url,
description=description)
-def new_id():
- nid = gen_rand_id()
- while nid in news_cache or archive.get_story(nid):
- nid = gen_rand_id()
- return nid
-
-def remove_ref(old_ref):
- while old_ref in news_list:
- news_list.remove(old_ref)
- old_story = news_cache.pop(news_ref_to_id[old_ref])
- old_id = news_ref_to_id.pop(old_ref)
- logging.info('Removed ref {} id {}.'.format(old_ref, old_id))
-
http_server = WSGIServer(('', 33842), flask_app)
def feed_thread():
@@ -159,7 +185,9 @@ def feed_thread():
news_index += 1
if news_index == CACHE_LENGTH: news_index = 0
- except BaseException as e:
+ except KeyboardInterrupt:
+ logging.info('Ending feed thread...')
+ except ValueError as e:
logging.error('feed_thread error: {} {}'.format(e.__class__.__name__, e))
http_server.stop()
diff --git a/webclient/src/App.js b/webclient/src/App.js
index 7b4d8da..f977f31 100644
--- a/webclient/src/App.js
+++ b/webclient/src/App.js
@@ -8,6 +8,7 @@ import Feed from './Feed.js';
import Article from './Article.js';
import Comments from './Comments.js';
import Search from './Search.js';
+import Submit from './Submit.js';
import Results from './Results.js';
import ScrollToTop from './ScrollToTop.js';
@@ -60,6 +61,7 @@ class App extends React.Component {
Reddit, Hacker News, and Tildes combined, then pre-rendered in reader mode.
+
} />
diff --git a/webclient/src/Search.js b/webclient/src/Search.js
index 52cbbe4..80396c0 100644
--- a/webclient/src/Search.js
+++ b/webclient/src/Search.js
@@ -34,18 +34,16 @@ class Search extends Component {
const search = this.state.search;
return (
-
+
+
+
);
}
}
diff --git a/webclient/src/Style-light.css b/webclient/src/Style-light.css
index b00011e..1c91c5b 100644
--- a/webclient/src/Style-light.css
+++ b/webclient/src/Style-light.css
@@ -15,6 +15,7 @@ input {
font-size: 1.05rem;
background-color: transparent;
border: 1px solid #828282;
+ margin: 0.25rem;
padding: 6px;
border-radius: 4px;
}
@@ -173,3 +174,7 @@ span.source {
top: 0.1rem;
left: 0.1rem;
}
+
+.search form {
+ display: inline;
+}
diff --git a/webclient/src/Submit.js b/webclient/src/Submit.js
new file mode 100644
index 0000000..eed87d7
--- /dev/null
+++ b/webclient/src/Submit.js
@@ -0,0 +1,54 @@
+import React, { Component } from 'react';
+import { withRouter } from 'react-router-dom';
+
+class Submit extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ progress: null,
+ };
+
+ this.inputRef = React.createRef();
+ }
+
+ submitArticle = (event) => {
+ event.preventDefault();
+ const url = event.target[0].value;
+ this.inputRef.current.blur();
+
+ this.setState({ progress: 'Submitting...' });
+
+ let data = new FormData();
+ data.append('url', url);
+
+ fetch('/api/submit', { method: 'POST', body: data })
+ .then(res => res.json())
+ .then(
+ (result) => {
+ this.props.history.replace('/' + result.nid);
+ },
+ (error) => {
+ this.setState({ progress: 'Error' });
+ }
+ );
+ }
+
+ render() {
+ const progress = this.state.progress;
+
+ return (
+
+
+ {progress ? progress : ''}
+
+ );
+ }
+}
+
+export default withRouter(Submit);