mirror of
https://gitlab.com/octospacc/Snippets.git
synced 2025-02-02 08:56:45 +01:00
530 lines
22 KiB
Python
Executable File
530 lines
22 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# *----------------------------------------------------------------------* #
|
|
# | [ ShioriFeed 🔖 (OctoSpacc) ] | #
|
|
# | Simple service for getting an Atom/RSS feed from your Shiori profile | #
|
|
# *----------------------------------------------------------------------* #
|
|
Version = '2023-02-28'
|
|
# *----------------------------------------------------------------------* #
|
|
|
|
# *-------------------------------------------* #
|
|
# | Configuration | #
|
|
# *-------------------------------------------* #
|
|
Host = ('localhost', 8176)
|
|
Debug = False
|
|
UserAgent = f'ShioriFeed v{Version} at {Host[0]}'
|
|
DefFeedType = 'atom'
|
|
# *-------------------------------------------* #
|
|
|
|
# External Requirements: urllib3
|
|
|
|
# TODO:
|
|
# - Cheking if Content mode content is actually present, otherwise fall back to Archive mode or original link (using API data is unreliable it seems)
|
|
# - HTML proxy (direct access to web UI, without JS)
|
|
# - Actually valid RSS
|
|
# - XML stylesheet
|
|
# - Filtering (tags, etc.)
|
|
# - Write privacy policy
|
|
# - Fix the URL copy thing
|
|
# - Minification (?)
|
|
|
|
# *-------------------------------------------------------------------------* #
|
|
|
|
import json
|
|
import threading
|
|
import traceback
|
|
from base64 import urlsafe_b64decode as b64UrlDecode, urlsafe_b64encode as b64UrlEncode, standard_b64encode as b64Encode
|
|
from html import escape as HtmlEscape
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from socketserver import ThreadingMixIn
|
|
from urllib.parse import unquote as UrlUnquote
|
|
from urllib.request import urlopen, Request
|
|
|
|
HtmlHead = '''
|
|
<meta charset="utf-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<!--
|
|
"bookmark" Emoji icon - Copyright 2021 Google Inc. All Rights Reserved.
|
|
<https://fonts.google.com/noto/specimen/Noto+Color+Emoji/about>
|
|
<https://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web>
|
|
-->
|
|
<link rel="shortcut icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAACgCAMAAAC8EZcfAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAMAUExURUdwTPRBM/FLNfRBM/NEM+5QNvRBM85ONfRBM/RBM/VBM/RBM+dCM4hMM/RBM/RBM/RBM/RBM+1UN/RBM/VBM/NFNPRBM/RBM/RBM/VBM/RBM/RBM/RBM+xXN/RBM990PvRBM/RBM/RBM/RBM/RBM/RBM/RBM/FJNPRBM/RBM/RBM/RBM/RBM/RBM/RBM/RBM35HMYhMM4hMM4hMM/RBM/RBM/RBM/RBM/RBM/RBM/RBM/RBM/RBM/RBM/RBM/RBM/RBM/RBM/JHNIhMM4hMM/RBM/RBM/RBM91zPvRBM/RBM/RBM4hMM5JQNIhMM/RBM/RBM/RBM/RBM/RBM/RBM/RBM/RBM/RBM/RBM990PohMM4dLM990PodLM4hMM4pNM+B0Pt90PolMM4ZMM4hMM4hMM4hMM4hMM990PvRBM95zPfRBM4dMM990PohMM4hMM990PohMM990PvRBM990PvRBM990PohMM990PqdJM41OM990Pt90Pt90PvRBM5VRNIhMM990PohMM4hMM4hMM4hMM45MM4tNM990PohMM4hMM9JtPbFfOE0xKt90Pn5HMs5rPN90PthxPd50Pt90PrljOU0xKt90Ps1FM0wxKrxjOYRJMtVuPUwxKk0xKlQ0K7tjOV84LK9JM99MzvRBM990PohMM/zszodMM/Q/MeB0Pvzw0fzu0PZBM4lMM00xKtpyPfQ+MZtUNd1zPuB1Po5NM9BsPItNM/d9avRFNu1BM5dSNfzmyfVYSPrIrvVeTfZlU/VQQKJXNqZZNrpjOZFQNPiLd/zqzPzhxPzrzZROM8lpOvZqWfvWuvvdwPVSQvq4n/idh6pJM7xGM1MzK9ZvPdRuPbRgOL9lOcxrPPvQtPq9pPVLPPRIOfmslZ5WNXtFMd5DM8VnOveDb/iSfWg7LfmzmviXgvrHraNJM+ZCM8pqPNlEM/Z1Yviii/rLsLFHM81EM9JCMq5eN+F1Pl44LPmkjvztz7M6MPmjjJtKM4g4LZ04Lm5AMK1dN0dwTGi1UKgAAAEAdFJOUwBICTUPBPgB/d8T/gL8B/o/7w312QXnKX9r6vIjCjH6xdbMFsLtixu9ZE3PLaxYvwQlB+4rtnE6j6bT5JSdHBj89yAUVVzcojGbYVLzDNt2hEOXZ8m5JlWw5kikRDizGcP04z6HejS5qLNwn54T6myc9SaoznvvXDzijmF7jnrqxtbTyKcb+a9baW+uZzC22fYt+eChs/lIh+rWHJB6TqtP7u7//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wAl04dNAAAKGUlEQVR42u2cd1AbVx7HVxI6kBDIBiyLJjC9Y7rBgA0xxhSbXkzcu+MWx44vPZfitOu95Pos0i6WqQKBAZtiA3ZccAvujnEj8SW55JLT1cytyorFaFcr6a20M8fvD8OInXmfeb/2fb99FgTN2Iyxznates6DzXzL1q1f/4PnnFnL98y6esxWPsXWTfR/ul5nAdueYSfgO+vrDbZ/FRvdvGudCgesX/niHPYBrvrR+OUGnHD9S/6sA/xjd2PrlWYcMeBlJ7YBJqyQI+rBDiPh07vYRrhkg1yOnLrS1sBWQuctW92wTRxrNhKyzstPbEqSy+WDeK4EvMy6TJm/ZAXm5tt4IAa8xL5qs/o7GGErTnjgRfaVw4Q1RMKVq9hHePBJLFVaPzQQ7mdhX35CSziA5/I2DzYSYrn8tzZDojzFQt1wEItD9XFDS/npMhYSJrwhR058YHDyd5ezkHD1islE+cn3TD4iyi6riUp2dxTht4vlyGCzfgt/k2DigWDpvBxXsSSdZzfBv2wZsbHN3+SGh2HDoxfmT3s8JsUF1pkgyk4Hum0HDmwLJ6byVjnSrg/Dy6+ufty9nAwYt4y5dsBzf+c1LcnPiHu4cTcehm0DOxOnPl4hgSdtB5dxvrW/XW+oeYTDkmizGx6Gg0kbiY8LwzwJfHBOIeOF+e1HKhOtdy2mGxrH6hsa2m4jOwlR6Be7GJ5iXjFMZ6zbGC709/+YKLH3yuXtx5vbrrTLd09GYVol/LjFMqxs30O6cW1Qv44gDpa/jbU8devtduzHCwbn8/ME0/hgcSqz5+iNu5Fv2vAD5zpCZ6vTCmwEwf6Rr9DXQg/pbNiEScqY1dGb3NRXjEfi134pwv+Q+KYcN7d3tR9k7xDDJi0wm9ks2TrZebXTI2PzrSs2Em7FPpTFu8Ik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"/>
|
|
<title>ShioriFeed 🔖</title>
|
|
<meta name="description" content="Simple service for getting an Atom/RSS feed from your Shiori profile"/>
|
|
<meta property="og:title" content="ShioriFeed 🔖"/>
|
|
<meta property="og:description" content="Simple service for getting an Atom/RSS feed from your Shiori profile"/>
|
|
'''
|
|
|
|
HomeTemplate = '''\
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
{{HtmlHead}}
|
|
<style>
|
|
:root {
|
|
--cFore0: #232323;
|
|
--cFore1: #292929;
|
|
--cAccent: #f44336;
|
|
--cBack0: #e9e9e9;
|
|
--cBack1: #ffffff;
|
|
/*--cGray: #c9c9c9;*/
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--cFore0: #ffffff;
|
|
--cFore1: #eeeeee;
|
|
--cBack0: #292929;
|
|
--cBack1: #1f1f1f;
|
|
--cGray: #606060;
|
|
}
|
|
}
|
|
* { box-sizing: border-box; }
|
|
.Underline { text-decoration: underline; }
|
|
.NoSelect {
|
|
user-select: none;
|
|
-ms-user-select: none;
|
|
-moz-user-select: none;
|
|
-khtml-user-select: none;
|
|
-webkit-user-select: none;
|
|
-webkit-touch-callout: none;
|
|
}
|
|
body {
|
|
color: var(--cFore0);
|
|
background: var(--cBack0);
|
|
font-family: "Source Sans Pro", sans-serif;
|
|
margin: 0px;
|
|
padding-top: 24px;
|
|
padding-bottom: 24px;
|
|
padding-left: 10%;
|
|
padding-right: 10%;
|
|
word-break: break-word;
|
|
}
|
|
a { color: var(--cAccent); }
|
|
form > label { padding: 8px; }
|
|
form > label > span { padding-bottom: 4px; }
|
|
form > label, form > label > span {
|
|
display: inline-block;
|
|
width: 100%;
|
|
}
|
|
textarea {
|
|
width: 100%;
|
|
height: 5em;
|
|
font-size: large;
|
|
resize: none;
|
|
}
|
|
input { height: 2em; }
|
|
input[type="submit"], button { font-size: large; }
|
|
input, textarea, details {
|
|
width: 100%;
|
|
border-radius: 2px;
|
|
}
|
|
input, textarea, button {
|
|
color: var(--cFore1);
|
|
background: var(--cBack1);
|
|
border: none;
|
|
}
|
|
details {
|
|
background: var(--cBack1)/*var(--cGray)*/;
|
|
padding: 8px;
|
|
}
|
|
details > summary > h4 { display: inline; }
|
|
span.Separator {
|
|
display: inline-block;
|
|
width: 0.25em;
|
|
height: 0.25em;
|
|
margin: 0.25em;
|
|
vertical-align: middle;
|
|
background: var(--cFore1);
|
|
}
|
|
/* {{PostCss}} */
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="NoSelect">
|
|
<h2>ShioriFeed 🔖</h2>
|
|
<p class="PostObscure">
|
|
Enter the details of your account on a
|
|
<a href="https://github.com/go-shiori/">Shiori</a>
|
|
server to get an Atom/RSS feed link.
|
|
</p>
|
|
<p class="PostObscure">
|
|
<small>Note: still a work-in-progress!</small>
|
|
</p>
|
|
<br />
|
|
<!-- {{PostResult}} -->
|
|
<p class="PostObscure">
|
|
<form action="./" method="POST">
|
|
<label class="PostObscure">
|
|
<span>Server <small>(must start with protocol prefix)</small>:</span>
|
|
<input type="text" name="Remote" placeholder="http[s]://..."/>
|
|
</label>
|
|
<br />
|
|
<label class="PostObscure">
|
|
<span>Username:</span>
|
|
<input type="text" name="Username" placeholder="erre"/>
|
|
</label>
|
|
<br />
|
|
<label class="PostObscure">
|
|
<span>Password:</span>
|
|
<input type="password" name="Password" placeholder="**********"/>
|
|
</label>
|
|
<br />
|
|
<label class="PostObscure">
|
|
<span> </span>
|
|
<input type="submit" value="Submit"/>
|
|
</label>
|
|
</form>
|
|
</p>
|
|
<br />
|
|
</div>
|
|
<!--
|
|
NOTE TO SELF-HOSTERS:
|
|
You should probably either adjust or remove this :)
|
|
For sure you should at least write your own domain.
|
|
-->
|
|
<p>
|
|
<details>
|
|
<summary class="NoSelect">
|
|
<!-- Change the domain if self-hosting! -->
|
|
<h4>Privacy Policy</h4>
|
|
(applies to <em class="Underline">ShioriFeed.Octt.eu.org</em>)
|
|
</summary>
|
|
<p><!--
|
|
By using this service
|
|
(doing any action that sends/requests data to/from the server),
|
|
you understand and agree to the following:
|
|
<ul>
|
|
<li>
|
|
</li>
|
|
</ul>-->
|
|
<!--<ul>
|
|
<li>-->
|
|
I still have to write this... tough luck.
|
|
I'm not yet actively inviting anyone to use this instance right now,
|
|
if you're worried about your security then just host the software yourself.
|
|
<!-- </li>
|
|
</ul>-->
|
|
</p>
|
|
</details>
|
|
</p>
|
|
<p class="NoSelect">
|
|
<span>v. {{Version}}</span>
|
|
<span class="Separator"></span>
|
|
<a href="https://gitlab.com/octospacc/Snippets/-/blob/main/ShioriFeed.py">Source Code</a>
|
|
</p>
|
|
<script>
|
|
var Box = document.querySelector('textarea');
|
|
if (Box) {
|
|
//BoxFocused = false;
|
|
Box.value = location.origin + Box.value.substring('http[s]://<THIS SHIORIFEED SERVER ADDRESS>'.length);
|
|
//Box.onfocusout = function() { console.log(1); BoxFocused = false; };
|
|
//Box.onfocusin = function() { console.log(2); BoxFocused = true; };
|
|
Box.onclick = function() {
|
|
try {
|
|
//if (BoxFocused) {
|
|
navigator.clipboard.writeText(Box.value);
|
|
alert('Copied to clipboard!');
|
|
//};
|
|
} catch(e) {};
|
|
};
|
|
BtnChangeFeed.onclick = function() {
|
|
var CurType = Box.value.split('/')[-1].split('?')[0].split('.')[0];
|
|
if (CurType == 'atom') {
|
|
|
|
} else
|
|
if (CurType == 'rss') {
|
|
|
|
};
|
|
};
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|
|
'''.replace('{{HtmlHead}}', HtmlHead).replace('{{Version}}', Version)
|
|
|
|
XmlHead = '''\
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<?xml-stylesheet type="text/xsl" href="#stylesheet"?>
|
|
<!DOCTYPE xml [<!ATTLIST xsl:stylesheet id ID #REQUIRED>]>
|
|
'''
|
|
|
|
XmlStyle='''
|
|
<!-- Partially derived from https://gist.github.com/andrewstiefel/57a0a400aa2deb6c9fe18c6da4e16e0f -->
|
|
<xsl:stylesheet
|
|
id="stylesheet"
|
|
version="1.0"
|
|
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
|
xmlns:atom="http://www.w3.org/2005/Atom"
|
|
exclude-result-prefixes="atom"
|
|
>
|
|
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
|
|
<xsl:template match="/">
|
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
<head>
|
|
{{HtmlHead}}
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
<style type="text/css">
|
|
* { max-width: 100%; height: auto; box-sizing: border-box; }
|
|
body { overflow-wrap: break-word; background: #ffffff; color: #000000; }
|
|
details { border: 2px solid gray; margin: 16px; }
|
|
details > summary { padding: 8px; background: #dddddd; }
|
|
details > summary > h2 { display: inline; }
|
|
details > summary img { max-height: 50vh; }
|
|
details > div { padding: 8px; }
|
|
</style>
|
|
<script type="application/javascript">
|
|
// TODO: Output escaping doesn't work on Firefox so we must parse XML via scripts to properly display it
|
|
//var Req = new XMLHttpRequest();
|
|
//Req.open('GET', window.location, false);
|
|
//Req.send();
|
|
//var Xml = Req.responseXML;
|
|
//var XsltProc = new XSLTProcessor(); // Get only the stylesheet from the XML
|
|
//XsltProc.importStylesheet(Xsl);
|
|
//var Result = XsltProc.transformToFragment(Xml, document);
|
|
//body.innerHTML = '';
|
|
//document.body.appendChild(Result);
|
|
//alert(1);
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<section>
|
|
<xsl:apply-templates select="atom:feed" />
|
|
</section>
|
|
<section>
|
|
<xsl:apply-templates select="atom:feed/atom:entry" />
|
|
</section>
|
|
</body>
|
|
</html>
|
|
</xsl:template>
|
|
<xsl:template match="atom:feed"></xsl:template>
|
|
<xsl:template match="atom:entry">
|
|
<details class="entry">
|
|
<summary>
|
|
<h2>
|
|
<a>
|
|
<xsl:attribute name="href">
|
|
<xsl:value-of select="atom:id"/>
|
|
</xsl:attribute>
|
|
<xsl:value-of select="atom:title"/>
|
|
</a>
|
|
</h2>
|
|
<p>
|
|
<small>
|
|
Date: <xsl:value-of select="atom:updated"/>
|
|
</small>
|
|
<xsl:value-of select="atom:summary" disable-output-escaping="yes"/>
|
|
</p>
|
|
</summary>
|
|
<div>
|
|
<p>
|
|
<xsl:value-of select="atom:content" disable-output-escaping="yes"/>
|
|
</p>
|
|
</div>
|
|
</details>
|
|
</xsl:template>
|
|
</xsl:stylesheet>
|
|
'''.replace('{{HtmlHead}}', HtmlHead)
|
|
|
|
def RetDebugIf():
|
|
return f'\n\n{traceback.format_exc()}' if Debug else ''
|
|
|
|
def SessionHash(Remote, Username, Password):
|
|
return f'{hash(Remote)}{hash(Username)}{hash(Password)}'
|
|
|
|
def MkFeed(Data, Remote, Username, Session, Type=DefFeedType):
|
|
Feed = ''
|
|
FeedTitle = f'<title>ShioriFeed ({HtmlEscape(Username)}) 🔖</title>'
|
|
Generator = f'<generator uri="https://gitlab.com/octospacc/Snippets/-/blob/main/ShioriFeed.py" version="{Version}">ShioriFeed</generator>'
|
|
FeedDate = Data['bookmarks'][0]['modified'] if Data['bookmarks'] else ''
|
|
for Mark in Data['bookmarks']:
|
|
Id = Mark['id']
|
|
EntryTitle = f'<title>{HtmlEscape(Mark["title"])}</title>'
|
|
EntryAuthor = f'<author>{HtmlEscape(Mark["author"])}</author>' if Mark['author'] else ''
|
|
EntryLink = f'{Remote}/bookmark/{Id}/content'
|
|
# NOTE: when shiori issue #578 is fixed, this should use a thumb URL from the original article HTML to cope with private bookmarks
|
|
EntryCover = f'<p><a href="{EntryLink}"><img src="{Remote}/bookmark/{Id}/thumb"/></a></p>' if Mark['imageURL'] else ''
|
|
# Not so sure about this chief, downloading and embedding EVERY cover image into the XML is slow (~8s per 1 req) and traffic-hungry (~10 simultaneous requests are enough to temporarily DoS the Raspi)
|
|
#ImgData = GetContent(Remote, f'bookmark/{Id}/thumb', Session) if Mark['imageURL'] else None
|
|
#Cover = f'<![CDATA[<a href="{Link}"><img src="data:{ImgData["Content-Type"]};base64,{b64Encode(ImgData["Body"]).decode()}"/></a><br /><br />]]>' if ImgData else ''
|
|
EntryPreview = f'<![CDATA[{EntryCover}<p>{HtmlEscape(Mark["excerpt"])}</p>]]>'
|
|
EntryContent = f'{HtmlEscape(GetContent(Remote, f"bookmark/{Id}/content", Session)["Body"].decode())}'
|
|
if Type == 'atom':
|
|
Feed += f'''
|
|
<entry>
|
|
{EntryTitle}
|
|
{EntryAuthor}
|
|
<summary>{EntryPreview}</summary>
|
|
<content type="text/html">{EntryContent}</content>
|
|
<link rel="alternate" href="{EntryLink}"/>
|
|
<published>{Mark['modified']}</published>
|
|
<updated>{Mark['modified']}</updated>
|
|
<id>{EntryLink}</id>
|
|
</entry>
|
|
'''
|
|
elif Type == 'rss':
|
|
Feed += f'''
|
|
<item>
|
|
{EntryTitle}
|
|
{EntryAuthor}
|
|
<description>{EntryPreview}</description>
|
|
<content:encoded type="text/html">{EntryContent}</content:encoded>
|
|
<link>{EntryLink}</link>
|
|
<pubDate>{Mark['modified']}</pubDate>
|
|
<guid isPermaLink="false">{EntryLink}</guid>
|
|
</item>
|
|
'''
|
|
if Type == 'atom':
|
|
return f'''\
|
|
{XmlHead}
|
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
{XmlStyle}
|
|
{FeedTitle}
|
|
{Generator}
|
|
<updated>{FeedDate}</updated>
|
|
{Feed}
|
|
</feed>
|
|
'''
|
|
elif Type == 'rss':
|
|
return f'''\
|
|
{XmlHead}
|
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
|
|
{XmlStyle}
|
|
<channel>
|
|
{FeedTitle}
|
|
{Generator}
|
|
<pubDate>{FeedDate}</pubDate>
|
|
<lastBuildDate>{FeedDate}</lastBuildDate>
|
|
{Feed}
|
|
</channel>
|
|
</rss>
|
|
'''
|
|
|
|
def MkUrl(Post, Type=DefFeedType):
|
|
Args = {}
|
|
for Arg in Post.split('&'):
|
|
Arg = Arg.split('=')
|
|
Args.update({Arg[0]: UrlUnquote(Arg[1])})
|
|
return f'''\
|
|
http[s]://<THIS SHIORIFEED SERVER ADDRESS>\
|
|
/{Args['Remote']}\
|
|
/{b64UrlEncode(Args['Username'].encode()).decode()}\
|
|
/{b64UrlEncode(Args['Password'].encode()).decode()}\
|
|
/{Type}.xml'''
|
|
|
|
def GetSession(Remote, Username, Password):
|
|
try:
|
|
Rq = urlopen(Request(f'{Remote}/api/login',
|
|
data=json.dumps({'username': Username, 'password': Password, 'remember': True, 'owner': True}).encode(),
|
|
headers={'User-Agent': UserAgent}))
|
|
if Rq.code == 200:
|
|
Data = {SessionHash(Remote, Username, Password): json.loads(Rq.read().decode())['session']}
|
|
Sessions.update(Data)
|
|
return {'Code': 200, 'Body': Data}
|
|
else:
|
|
return {'Code': Rq.code, 'Body': f'[{Rq.code}] External Server Error\n\n{Rq.read().decode()}'}
|
|
except Exception:
|
|
return {'Code': 500, 'Body': f'[500] Internal Server Error{RetDebugIf()}'}
|
|
|
|
def GetContent(Remote, Path, Session):
|
|
try:
|
|
Rq = urlopen(Request(f'{Remote}/{Path}', headers={'X-Session-Id': Session, 'User-Agent': UserAgent}))
|
|
if Rq.code == 200:
|
|
return {'Code': 200, 'Body': Rq.read(), 'Content-Type': Rq.headers['Content-Type']}
|
|
else:
|
|
return {'Code': Rq.code, 'Body': f'[{Rq.code}] External Server Error\n\n{Rq.read().decode()}'.encode()}
|
|
except Exception:
|
|
return {'Code': 500, 'Body': f'[500] Internal Server Error{RetDebugIf()}'.encode()}
|
|
|
|
def RqHandle(Path, Attempt=0):
|
|
try:
|
|
Rs = {}
|
|
Args = Path.strip().removeprefix('/').removesuffix('/').strip().split('/')
|
|
if Args[0] == '':
|
|
return {'Code': 200, 'Body': HomeTemplate, 'Content-Type': 'text/html'}
|
|
else:
|
|
TypeCheck = Args[-1].lower().replace('?', '&').split('&')[0]
|
|
#Shift = 1 if TypeCheck in ('atom.xml', 'rss.xml', 'atom', 'rss') else 0
|
|
#Type = Args[-1].lower().split('&')[0] if Shift == 1 else
|
|
if TypeCheck in ('atom.xml', 'rss.xml', 'atom', 'rss'):
|
|
Shift = 1
|
|
FeedType = TypeCheck.split('.')[0]
|
|
else:
|
|
Shift = 0
|
|
FeedType = DefFeedType
|
|
Remote = '/'.join(Args[:-(2+Shift)]).removesuffix('/')
|
|
Username = b64UrlDecode(Args[-(2+Shift)]).decode()
|
|
Password = b64UrlDecode(Args[-(1+Shift)]).decode()
|
|
if not SessionHash(Remote, Username, Password) in Sessions:
|
|
TrySession = GetSession(Remote, Username, Password)
|
|
if TrySession['Code'] != 200:
|
|
return TrySession
|
|
Session = Sessions[SessionHash(Remote, Username, Password)]
|
|
Rq = urlopen(Request(f'{Remote}/api/bookmarks', headers={
|
|
'X-Session-Id': Session,
|
|
'User-Agent': UserAgent}))
|
|
Rs['Code'] = Rq.code
|
|
if Rq.code == 200:
|
|
# Shiori got us JSON data, parse it and return our result
|
|
Rs['Body'] = MkFeed(json.loads(Rq.read().decode()), Remote, Username, Session, FeedType)
|
|
Rs['Content-Type'] = 'application/xml'
|
|
elif Rq.code == 500 and Attempt < 1:
|
|
# We probably got an expired Session-Id, let's renew it and retry
|
|
TrySession = GetSession(Remote, Username, Password)
|
|
if TrySession['Code'] != 200:
|
|
return TrySession
|
|
return ReqHandle(Path, Attempt+1)
|
|
else:
|
|
Rs['Body'] = f'[{Rq.code}] External Server Error\n\n{Rq.read().decode()}'
|
|
return Rs
|
|
except Exception:
|
|
return {'Code': 500, 'Body': f'[500] Internal Server Error{RetDebugIf()}'}
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
Rs = RqHandle(self.path)
|
|
self.send_response(Rs['Code'])
|
|
self.send_header('Content-Type', Rs['Content-Type'] if 'Content-Type' in Rs else 'text/plain')
|
|
self.end_headers()
|
|
self.wfile.write(Rs['Body'].encode())
|
|
def do_POST(self):
|
|
try:
|
|
if self.path == '/':
|
|
Post = self.rfile.read(int(self.headers['Content-Length'])).decode()
|
|
Body = HomeTemplate.replace('<!-- {{PostResult}} -->', f'''
|
|
<p>
|
|
Here's your <button id="BtnChangeFeed">Atom</button> feed:
|
|
<textarea class="Visible" readonly="true">{MkUrl(Post, 'atom')}</textarea>
|
|
<textarea class="Hidden" hidden="true" readonly="true">{MkUrl(Post, 'rss')}</textarea>
|
|
</p>
|
|
<br />
|
|
''').replace('/* {{PostCss}} */', '.PostObscure { opacity: 0.5; }')
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(Body.encode())
|
|
else:
|
|
self.send_response(400)
|
|
self.send_header('Content-Type', 'text/plain')
|
|
self.end_headers()
|
|
self.wfile.write(b'[400] Bad Request')
|
|
except Exception:
|
|
self.send_response(500)
|
|
self.send_header('Content-Type', 'text/plain')
|
|
self.end_headers()
|
|
self.wfile.write((f'[500] Internal Server Error{RetDebugIf()}').encode())
|
|
##Prevent logging | https://stackoverflow.com/a/3389505
|
|
#def log_message(self, format, *args):
|
|
# return
|
|
# https://stackoverflow.com/a/51559006
|
|
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
|
pass
|
|
def Serve():
|
|
ThreadedHTTPServer(Host, Handler).serve_forever()
|
|
|
|
if __name__ == '__main__':
|
|
Sessions = {}
|
|
try:
|
|
Serve()
|
|
except KeyboardInterrupt:
|
|
pass
|