2023-02-13 13:46:26 +01:00
#!/usr/bin/env python3
2023-02-13 13:58:37 +01:00
# *----------------------------------------------------------------------* #
2023-02-15 17:54:25 +01:00
# | [ ShioriFeed 🔖 (OctoSpacc) ] | #
2023-02-13 13:58:37 +01:00
# | Simple service for getting an Atom/RSS feed from your Shiori profile | #
2023-02-15 17:54:25 +01:00
# *----------------------------------------------------------------------* #
2023-03-01 10:10:38 +01:00
Version = ' 2023-02-28 '
2023-02-13 13:58:37 +01:00
# *----------------------------------------------------------------------* #
2023-02-15 17:54:25 +01:00
# *-------------------------------------------* #
# | Configuration | #
# *-------------------------------------------* #
2023-02-13 13:58:37 +01:00
Host = ( ' localhost ' , 8176 )
2023-02-14 00:08:27 +01:00
Debug = False
2023-02-15 17:54:25 +01:00
UserAgent = f ' ShioriFeed v { Version } at { Host [ 0 ] } '
2023-02-16 11:24:54 +01:00
DefFeedType = ' atom '
2023-02-15 17:54:25 +01:00
# *-------------------------------------------* #
2023-02-13 13:58:37 +01:00
# External Requirements: urllib3
2023-02-14 00:08:27 +01:00
# TODO:
2023-02-15 17:54:25 +01:00
# - Cheking if Content mode content is actually present, otherwise fall back to Archive mode or original link (using API data is unreliable it seems)
2023-03-01 10:10:38 +01:00
# - HTML proxy (direct access to web UI, without JS)
2023-02-14 00:08:27 +01:00
# - Actually valid RSS
2023-02-15 17:54:25 +01:00
# - XML stylesheet
# - Filtering (tags, etc.)
2023-02-14 00:08:27 +01:00
# - Write privacy policy
# - Fix the URL copy thing
2023-02-16 11:24:54 +01:00
# - Minification (?)
2023-02-14 00:08:27 +01:00
2023-02-13 13:58:37 +01:00
# *-------------------------------------------------------------------------* #
2023-02-13 13:46:26 +01:00
import json
2023-02-16 11:24:54 +01:00
import threading
import traceback
2023-02-15 17:54:25 +01:00
from base64 import urlsafe_b64decode as b64UrlDecode , urlsafe_b64encode as b64UrlEncode , standard_b64encode as b64Encode
2023-02-13 13:58:37 +01:00
from html import escape as HtmlEscape
2023-02-13 13:46:26 +01:00
from http . server import HTTPServer , BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
2023-02-16 11:24:54 +01:00
from urllib . parse import unquote as UrlUnquote
2023-02-13 13:46:26 +01:00
from urllib . request import urlopen , Request
2023-03-01 10:10:38 +01:00
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+KYcN7d3tR9k7xDDJi0wm9ks2TrZebXTI2PzrSs2Em7FPpTFu8IkVs5srdmyAum+aiQM+PlGQ9Iu32oETHo3ocgLJjXPSIbPxEkEwoaOV3duSTQkuB4PaVS/teHPd8gBYUEas2H43l6kexwHbO5Gdr+55H1sG99/XYenvn3lasej44MXj5AThjAbhomb9yInjrepsF1s+OoDbWFx++Effvf772vFNfaHZpWqQaW6PHDhE3LCcn+GCYsR9eD4h83Nl8e7dZUFud/0i1//CcH4xg3OV11t/5icMIfhapi4Jwnz5YnW1m61jk/e+FFTU9OljxrVx/HgVHW0IxfIvSzh2GOsgOgLs24Lv8YI/ys/0fGVITbbvsE+vUju5GimldeWNW5yot3HAL9GGgfGmxuwTVQ1j6kx6va/kBPWCpnWNZs3EPj0PkYaEXXrWEdb26OBRt2+UjjZJYxpbSja8mQxAfBSU9O/v/jy4ikEOTEw0G7wPdUWenozPn5PrFtjREQuXfrPkcOH4TsX5MTQpIhCxhNF19/qdm7QxSKC3P+HftkjfyV4HrlAAQhnyOwxJkzYswZjRP71Jb4skRB5PiiHgjAiG7KHJSYs2fT8J4eNyx752Ohi+SZhXBAFYb6dJsfu3j7EZe+cwgm18r+6ihxQXGqXcYNwgefUdS8aAIv3aAVtcgZVKouY56uufezoBt9p1+/fHr1W9HYhJ/QpYhrPedb0INNFodsbdQYtWy2gEoezmOWbk7fQxKpfvFX8+uaD+DPcCKpiE83oUdlD6muyj/1q49rJPuG+nQoQjs9ljo8bRnJ0857yFOUOYof5YMYAo3xJ1iwgJqefgBrQtYCpcigqIA19otyjymJ9OWRK2fjHk66ZObkkVR3EjwDPMvMizaOSYkn8XORXBZs3X28+IzmSQrEp+TxsE52FeUEwHQstZEQdLqBa06dqQWkBpZqZ8nQcE4Q1YhiYSZgYsWdngAOEBUwobClAQHgpA205WQCS0As8oWgBzHJCnoTlhPxyoIBwxiyWbyH4PeSDjUIGCGMELCd0LgUMCC8FXLH95oEmFESB7cupi0ETLgSrHIJDQAPCPoVA9WGNJ3DCUG+QGpubDhwQ9n0F5DmFtxA8YU4YwLOes9QVPKG4AOB5ObwSPCDsEgtw5lDkywAhHA9ubuOeZSMLanqyBK7tzfWyBQiFj90wiSioAVYQI3Mswevv60InidBzDyd6UZKCCGpK7LHPEsDezx98esOAiN7sPKr8vB8lmRIDKzeypRYQ3rx+qOf63X4MEYX7HvQoD13vIi036bkOcDJ6XqlRDh8d6oXPPTyr1CiUtygeDgF0sc8/nz5gS+ewQqFQnrz29wmFRvvbeZRKwxaBeRkwl/6gAT12VsulUCqV2h8KzVALpbpJBXOJII52uUZvTOjJDDbc2ULdmcvDgcz8w+j35FtTAHtOo2bmxBFAXj0uiqcdhENEPs3ZY6jZw1QRiJqdRveM13J6REMAHLkLmyUMlYKoiJF01fW94SlbODLUZZZQvK8agLrOpBeGd4kbqCXsOX/TLCEczbHdzeERdJK48zE+jHD4Vr95Qp9XbHczz3zLQz89q1RMt9Fe84Ti2GTbq+Fsc3ynz5jiUygn+mjs/rwaW/UNX0o9W0f7rk3n02A9ZXjkwTnzewj7hi2y
< 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 " / >
'''
2023-02-13 13:58:37 +01:00
HomeTemplate = ''' \
2023-02-13 13:46:26 +01:00
< ! DOCTYPE html >
2023-02-13 13:58:37 +01:00
< html lang = " en " >
2023-02-13 13:46:26 +01:00
< head >
2023-03-01 10:10:38 +01:00
{ { HtmlHead } }
2023-02-13 13:46:26 +01:00
< style >
2023-02-15 17:54:25 +01:00
: 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;
}
}
2023-02-13 13:58:37 +01:00
* { box - sizing : border - box ; }
2023-02-15 17:54:25 +01:00
. 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 ;
}
2023-02-13 13:46:26 +01:00
body {
2023-02-15 17:54:25 +01:00
color : var ( - - cFore0 ) ;
background : var ( - - cBack0 ) ;
2023-02-14 00:08:27 +01:00
font - family : " Source Sans Pro " , sans - serif ;
2023-02-13 13:46:26 +01:00
margin : 0 px ;
padding - top : 24 px ;
padding - bottom : 24 px ;
padding - left : 10 % ;
padding - right : 10 % ;
2023-02-13 13:58:37 +01:00
word - break : break - word ;
2023-02-13 13:46:26 +01:00
}
2023-02-15 17:54:25 +01:00
a { color : var ( - - cAccent ) ; }
2023-02-13 13:58:37 +01:00
form > label { padding : 8 px ; }
form > label > span { padding - bottom : 4 px ; }
form > label , form > label > span {
2023-02-13 13:46:26 +01:00
display : inline - block ;
2023-02-13 13:58:37 +01:00
width : 100 % ;
2023-02-13 13:46:26 +01:00
}
textarea {
width : 100 % ;
height : 5 em ;
2023-02-13 13:58:37 +01:00
font - size : large ;
2023-02-13 13:46:26 +01:00
resize : none ;
}
2023-02-15 17:54:25 +01:00
input { height : 2 em ; }
2023-02-16 11:24:54 +01:00
input [ type = " submit " ] , button { font - size : large ; }
input , textarea , details {
2023-02-15 17:54:25 +01:00
width : 100 % ;
2023-02-16 11:24:54 +01:00
border - radius : 2 px ;
}
input , textarea , button {
2023-02-15 17:54:25 +01:00
color : var ( - - cFore1 ) ;
background : var ( - - cBack1 ) ;
border : none ;
}
2023-02-13 13:58:37 +01:00
details {
2023-02-15 17:54:25 +01:00
background : var ( - - cBack1 ) / * var ( - - cGray ) * / ;
2023-02-13 13:58:37 +01:00
padding : 8 px ;
}
details > summary > h4 { display : inline ; }
2023-02-14 00:08:27 +01:00
span . Separator {
display : inline - block ;
width : 0.25 em ;
height : 0.25 em ;
margin : 0.25 em ;
vertical - align : middle ;
2023-02-15 17:54:25 +01:00
background : var ( - - cFore1 ) ;
2023-02-14 00:08:27 +01:00
}
2023-02-13 13:58:37 +01:00
/ * { { PostCss } } * /
2023-02-13 13:46:26 +01:00
< / style >
< / head >
< body >
2023-02-15 17:54:25 +01:00
< 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 > & nbsp ; < / span >
< input type = " submit " value = " Submit " / >
< / label >
< / form >
< / p >
< br / >
< / div >
2023-02-13 13:58:37 +01:00
< ! - -
2023-02-15 17:54:25 +01:00
NOTE TO SELF - HOSTERS :
You should probably either adjust or remove this : )
For sure you should at least write your own domain .
- - >
2023-02-13 13:46:26 +01:00
< p >
< details >
2023-02-15 17:54:25 +01:00
< summary class = " NoSelect " >
< ! - - Change the domain if self - hosting ! - - >
2023-02-13 13:58:37 +01:00
< h4 > Privacy Policy < / h4 >
2023-02-15 17:54:25 +01:00
( applies to < em class = " Underline " > ShioriFeed . Octt . eu . org < / em > )
2023-02-13 13:46:26 +01:00
< / summary >
2023-02-16 11:24:54 +01:00
< 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 > - - >
2023-02-15 17:54:25 +01:00
< ! - - < 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 > - - >
2023-02-16 11:24:54 +01:00
< / p >
2023-02-13 13:46:26 +01:00
< / details >
< / p >
2023-02-15 17:54:25 +01:00
< p class = " NoSelect " >
< span > v . { { Version } } < / span >
2023-02-14 00:08:27 +01:00
< span class = " Separator " > < / span >
2023-02-13 13:46:26 +01:00
< a href = " https://gitlab.com/octospacc/Snippets/-/blob/main/ShioriFeed.py " > Source Code < / a >
< / p >
2023-02-13 13:58:37 +01:00
< script >
var Box = document . querySelector ( ' textarea ' ) ;
if ( Box ) {
2023-02-14 00:08:27 +01:00
/ / BoxFocused = false ;
2023-02-13 13:58:37 +01:00
Box . value = location . origin + Box . value . substring ( ' http[s]://<THIS SHIORIFEED SERVER ADDRESS> ' . length ) ;
2023-02-14 00:08:27 +01:00
/ / 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 ) { } ;
} ;
2023-03-01 10:10:38 +01:00
BtnChangeFeed . onclick = function ( ) {
var CurType = Box . value . split ( ' / ' ) [ - 1 ] . split ( ' ? ' ) [ 0 ] . split ( ' . ' ) [ 0 ] ;
if ( CurType == ' atom ' ) {
} else
if ( CurType == ' rss ' ) {
} ;
} ;
2023-02-13 13:58:37 +01:00
} ;
< / script >
2023-02-13 13:46:26 +01:00
< / body >
< / html >
2023-03-01 10:10:38 +01:00
''' .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 / 57 a0a400aa2deb6c9fe18c6da4e16e0f - - >
< 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 : 2 px solid gray ; margin : 16 px ; }
details > summary { padding : 8 px ; background : #dddddd; }
details > summary > h2 { display : inline ; }
details > summary img { max - height : 50 vh ; }
details > div { padding : 8 px ; }
< / 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)
2023-02-15 17:54:25 +01:00
def RetDebugIf ( ) :
return f ' \n \n { traceback . format_exc ( ) } ' if Debug else ' '
2023-02-13 13:46:26 +01:00
2023-02-13 13:58:37 +01:00
def SessionHash ( Remote , Username , Password ) :
return f ' { hash ( Remote ) } { hash ( Username ) } { hash ( Password ) } '
2023-02-16 11:24:54 +01:00
def MkFeed ( Data , Remote , Username , Session , Type = DefFeedType ) :
2023-02-13 13:46:26 +01:00
Feed = ' '
2023-02-16 11:24:54 +01:00
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 ' '
2023-02-13 13:46:26 +01:00
for Mark in Data [ ' bookmarks ' ] :
2023-02-15 17:54:25 +01:00
Id = Mark [ ' id ' ]
2023-02-16 11:24:54 +01:00
EntryTitle = f ' <title> { HtmlEscape ( Mark [ " title " ] ) } </title> '
EntryAuthor = f ' <author> { HtmlEscape ( Mark [ " author " ] ) } </author> ' if Mark [ ' author ' ] else ' '
EntryLink = f ' { Remote } /bookmark/ { Id } /content '
2023-02-15 17:54:25 +01:00
# NOTE: when shiori issue #578 is fixed, this should use a thumb URL from the original article HTML to cope with private bookmarks
2023-02-16 11:24:54 +01:00
EntryCover = f ' <p><a href= " { EntryLink } " ><img src= " { Remote } /bookmark/ { Id } /thumb " /></a></p> ' if Mark [ ' imageURL ' ] else ' '
2023-02-15 17:54:25 +01:00
# 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 ''
2023-02-16 11:24:54 +01:00
EntryPreview = f ' <![CDATA[ { EntryCover } <p> { HtmlEscape ( Mark [ " excerpt " ] ) } </p>]]> '
EntryContent = f ' { HtmlEscape ( GetContent ( Remote , f " bookmark/ { Id } /content " , Session ) [ " Body " ] . decode ( ) ) } '
if Type == ' atom ' :
2023-02-15 17:54:25 +01:00
Feed + = f '''
2023-02-16 11:24:54 +01:00
< 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 >
2023-02-15 17:54:25 +01:00
'''
2023-02-16 11:24:54 +01:00
elif Type == ' rss ' :
2023-02-15 17:54:25 +01:00
Feed + = f '''
2023-02-13 13:46:26 +01:00
< item >
2023-02-16 11:24:54 +01:00
{ EntryTitle }
{ EntryAuthor }
< description > { EntryPreview } < / description >
< content : encoded type = " text/html " > { EntryContent } < / content : encoded >
< link > { EntryLink } < / link >
2023-02-13 13:46:26 +01:00
< pubDate > { Mark [ ' modified ' ] } < / pubDate >
2023-02-16 11:24:54 +01:00
< guid isPermaLink = " false " > { EntryLink } < / guid >
2023-02-13 13:46:26 +01:00
< / item >
2023-02-15 17:54:25 +01:00
'''
2023-02-16 11:24:54 +01:00
if Type == ' atom ' :
2023-02-15 17:54:25 +01:00
return f ''' \
2023-03-01 10:10:38 +01:00
{ XmlHead }
2023-02-16 11:24:54 +01:00
< feed xmlns = " http://www.w3.org/2005/Atom " >
2023-03-01 10:10:38 +01:00
{ XmlStyle }
2023-02-16 11:24:54 +01:00
{ FeedTitle }
{ Generator }
< updated > { FeedDate } < / updated >
{ Feed }
< / feed >
2023-02-13 13:46:26 +01:00
'''
2023-02-16 11:24:54 +01:00
elif Type == ' rss ' :
2023-02-15 17:54:25 +01:00
return f ''' \
2023-03-01 10:10:38 +01:00
{ XmlHead }
2023-02-15 17:54:25 +01:00
< 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/ " >
2023-03-01 10:10:38 +01:00
{ XmlStyle }
2023-02-13 13:46:26 +01:00
< channel >
2023-02-16 11:24:54 +01:00
{ FeedTitle }
{ Generator }
< pubDate > { FeedDate } < / pubDate >
< lastBuildDate > { FeedDate } < / lastBuildDate >
2023-02-13 13:46:26 +01:00
{ Feed }
< / channel >
< / rss >
2023-02-15 17:54:25 +01:00
'''
2023-02-13 13:46:26 +01:00
2023-02-16 11:24:54 +01:00
def MkUrl ( Post , Type = DefFeedType ) :
2023-02-13 13:58:37 +01:00
Args = { }
for Arg in Post . split ( ' & ' ) :
Arg = Arg . split ( ' = ' )
2023-02-16 11:24:54 +01:00
Args . update ( { Arg [ 0 ] : UrlUnquote ( Arg [ 1 ] ) } )
2023-02-13 13:58:37 +01:00
return f ''' \
http [ s ] : / / < THIS SHIORIFEED SERVER ADDRESS > \
/ { Args [ ' Remote ' ] } \
2023-02-15 17:54:25 +01:00
/ { b64UrlEncode ( Args [ ' Username ' ] . encode ( ) ) . decode ( ) } \
/ { b64UrlEncode ( Args [ ' Password ' ] . encode ( ) ) . decode ( ) } \
2023-02-14 00:08:27 +01:00
/ { Type } . xml '''
2023-02-13 13:58:37 +01:00
2023-02-13 13:46:26 +01:00
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 ( ) ,
2023-02-15 17:54:25 +01:00
headers = { ' User-Agent ' : UserAgent } ) )
2023-02-13 13:46:26 +01:00
if Rq . code == 200 :
2023-02-13 13:58:37 +01:00
Data = { SessionHash ( Remote , Username , Password ) : json . loads ( Rq . read ( ) . decode ( ) ) [ ' session ' ] }
2023-02-13 13:46:26 +01:00
Sessions . update ( Data )
2023-02-15 17:54:25 +01:00
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 ' ] }
2023-02-13 13:58:37 +01:00
else :
2023-02-15 17:54:25 +01:00
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 ( ) }
2023-02-13 13:46:26 +01:00
def RqHandle ( Path , Attempt = 0 ) :
try :
2023-02-13 13:58:37 +01:00
Rs = { }
Args = Path . strip ( ) . removeprefix ( ' / ' ) . removesuffix ( ' / ' ) . strip ( ) . split ( ' / ' )
if Args [ 0 ] == ' ' :
2023-02-15 17:54:25 +01:00
return { ' Code ' : 200 , ' Body ' : HomeTemplate , ' Content-Type ' : ' text/html ' }
2023-02-13 13:46:26 +01:00
else :
2023-02-16 11:24:54 +01:00
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 ( ' / ' )
2023-02-15 17:54:25 +01:00
Username = b64UrlDecode ( Args [ - ( 2 + Shift ) ] ) . decode ( )
Password = b64UrlDecode ( Args [ - ( 1 + Shift ) ] ) . decode ( )
2023-02-13 13:58:37 +01:00
if not SessionHash ( Remote , Username , Password ) in Sessions :
TrySession = GetSession ( Remote , Username , Password )
if TrySession [ ' Code ' ] != 200 :
return TrySession
2023-02-15 17:54:25 +01:00
Session = Sessions [ SessionHash ( Remote , Username , Password ) ]
2023-02-13 13:46:26 +01:00
Rq = urlopen ( Request ( f ' { Remote } /api/bookmarks ' , headers = {
2023-02-15 17:54:25 +01:00
' X-Session-Id ' : Session ,
' User-Agent ' : UserAgent } ) )
2023-02-13 13:46:26 +01:00
Rs [ ' Code ' ] = Rq . code
if Rq . code == 200 :
2023-02-15 17:54:25 +01:00
# Shiori got us JSON data, parse it and return our result
2023-02-16 11:24:54 +01:00
Rs [ ' Body ' ] = MkFeed ( json . loads ( Rq . read ( ) . decode ( ) ) , Remote , Username , Session , FeedType )
2023-02-13 13:46:26 +01:00
Rs [ ' Content-Type ' ] = ' application/xml '
elif Rq . code == 500 and Attempt < 1 :
2023-02-15 17:54:25 +01:00
# We probably got an expired Session-Id, let's renew it and retry
2023-02-13 13:58:37 +01:00
TrySession = GetSession ( Remote , Username , Password )
if TrySession [ ' Code ' ] != 200 :
return TrySession
2023-02-13 13:46:26 +01:00
return ReqHandle ( Path , Attempt + 1 )
else :
Rs [ ' Body ' ] = f ' [ { Rq . code } ] External Server Error \n \n { Rq . read ( ) . decode ( ) } '
2023-02-13 13:58:37 +01:00
return Rs
2023-02-15 17:54:25 +01:00
except Exception :
return { ' Code ' : 500 , ' Body ' : f ' [500] Internal Server Error { RetDebugIf ( ) } ' }
2023-02-13 13:46:26 +01:00
class Handler ( BaseHTTPRequestHandler ) :
def do_GET ( self ) :
Rs = RqHandle ( self . path )
self . send_response ( Rs [ ' Code ' ] )
2023-02-13 13:58:37 +01:00
self . send_header ( ' Content-Type ' , Rs [ ' Content-Type ' ] if ' Content-Type ' in Rs else ' text/plain ' )
2023-02-13 13:46:26 +01:00
self . end_headers ( )
self . wfile . write ( Rs [ ' Body ' ] . encode ( ) )
def do_POST ( self ) :
2023-02-13 13:58:37 +01:00
try :
if self . path == ' / ' :
2023-02-16 11:24:54 +01:00
Post = self . rfile . read ( int ( self . headers [ ' Content-Length ' ] ) ) . decode ( )
2023-02-13 13:58:37 +01:00
Body = HomeTemplate . replace ( ' <!-- {{ PostResult}} --> ' , f '''
2023-02-13 13:46:26 +01:00
< p >
2023-03-01 10:10:38 +01:00
Here ' s your <button id= " BtnChangeFeed " >Atom</button> feed:
2023-02-16 11:24:54 +01:00
< textarea class = " Visible " readonly = " true " > { MkUrl ( Post , ' atom ' ) } < / textarea >
< textarea class = " Hidden " hidden = " true " readonly = " true " > { MkUrl ( Post , ' rss ' ) } < / textarea >
2023-02-13 13:46:26 +01:00
< / p >
< br / >
2023-02-13 13:58:37 +01:00
''' ).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 )
2023-02-13 13:46:26 +01:00
self . send_header ( ' Content-Type ' , ' text/plain ' )
self . end_headers ( )
2023-02-15 17:54:25 +01:00
self . wfile . write ( ( f ' [500] Internal Server Error { RetDebugIf ( ) } ' ) . encode ( ) )
2023-02-16 11:24:54 +01:00
##Prevent logging | https://stackoverflow.com/a/3389505
#def log_message(self, format, *args):
# return
2023-02-13 13:58:37 +01:00
# https://stackoverflow.com/a/51559006
2023-02-13 13:46:26 +01:00
class ThreadedHTTPServer ( ThreadingMixIn , HTTPServer ) :
pass
def Serve ( ) :
ThreadedHTTPServer ( Host , Handler ) . serve_forever ( )
if __name__ == ' __main__ ' :
Sessions = { }
try :
Serve ( )
except KeyboardInterrupt :
pass