diff --git a/ShioriFeed.py b/ShioriFeed.py index 6049b65..2c1c641 100755 --- a/ShioriFeed.py +++ b/ShioriFeed.py @@ -4,7 +4,7 @@ # | [ ShioriFeed 🔖 (OctoSpacc) ] | # # | Simple service for getting an Atom/RSS feed from your Shiori profile | # # *----------------------------------------------------------------------* # -Version = '2023-02-15' +Version = '2023-02-16' # *----------------------------------------------------------------------* # # *-------------------------------------------* # @@ -13,30 +13,31 @@ Version = '2023-02-15' 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) -# - Atom feed # - Actually valid RSS # - XML stylesheet # - Filtering (tags, etc.) # - Write privacy policy # - Fix the URL copy thing -# - Minification +# - Minification (?) # *-------------------------------------------------------------------------* # -import traceback 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 -import threading HomeTemplate = '''\ @@ -107,10 +108,12 @@ HomeTemplate = '''\ resize: none; } input { height: 2em; } - input[type="submit"] { font-size: large; } - input, textarea, details { border-radius: 2px; } - input, textarea { + 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; @@ -181,6 +184,14 @@ HomeTemplate = '''\

Privacy Policy

(applies to ShioriFeed.Octt.eu.org) +

I still have to write this... tough luck. @@ -188,6 +199,7 @@ HomeTemplate = '''\ if you're worried about your security then just host the software yourself. +

@@ -222,57 +234,77 @@ def RetDebugIf(): def SessionHash(Remote, Username, Password): return f'{hash(Remote)}{hash(Username)}{hash(Password)}' -def MkFeed(Data, Remote, Username, Session, Type='RSS'): +def MkFeed(Data, Remote, Username, Session, Type=DefFeedType): Feed = '' - Date = Data['bookmarks'][0]['modified'] if Data['bookmarks'] else '' + FeedTitle = f'ShioriFeed ({HtmlEscape(Username)}) 🔖' + Generator = f'ShioriFeed' + FeedDate = Data['bookmarks'][0]['modified'] if Data['bookmarks'] else '' for Mark in Data['bookmarks']: Id = Mark['id'] - Link = f'{Remote}/bookmark/{Id}/content' + EntryTitle = f'{HtmlEscape(Mark["title"])}' + EntryAuthor = f'{HtmlEscape(Mark["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 - Cover = f']]>' if Mark['imageURL'] else '' + EntryCover = f'

' 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'

]]>' if ImgData else '' - Content = f'{HtmlEscape(GetContent(Remote, f"bookmark/{Id}/content", Session)["Body"].decode())}' - if Type == 'Atom': + EntryPreview = f'{HtmlEscape(Mark["excerpt"])}

]]>' + EntryContent = f'{HtmlEscape(GetContent(Remote, f"bookmark/{Id}/content", Session)["Body"].decode())}' + if Type == 'atom': Feed += f''' - + + {EntryTitle} + {EntryAuthor} + {EntryPreview} + {EntryContent} + + {Mark['modified']} + {Mark['modified']} + {EntryLink} + ''' - elif Type == 'RSS': + elif Type == 'rss': Feed += f''' - {HtmlEscape(Mark['title'])} - {Cover}{HtmlEscape(Mark['excerpt'])} - {Mark['author']} - {Content} - {Link} + {EntryTitle} + {EntryAuthor} + {EntryPreview} + {EntryContent} + {EntryLink} {Mark['modified']} - {Link} + {EntryLink} ''' - if Type == 'Atom': + if Type == 'atom': return f'''\ - + + + {FeedTitle} + {Generator} + {FeedDate} + {Feed} + ''' - elif Type == 'RSS': + elif Type == 'rss': return f'''\ - ShioriFeed ({HtmlEscape(Username)}) 🔖 - {Date} - {Date} + {FeedTitle} + {Generator} + {FeedDate} + {FeedDate} {Feed} ''' -def MkUrl(Post, Type='RSS'): +def MkUrl(Post, Type=DefFeedType): Args = {} - #Args = Post.split('&') for Arg in Post.split('&'): Arg = Arg.split('=') - Args.update({Arg[0]: Arg[1]}) + Args.update({Arg[0]: UrlUnquote(Arg[1])}) return f'''\ http[s]://\ /{Args['Remote']}\ @@ -311,8 +343,16 @@ def RqHandle(Path, Attempt=0): if Args[0] == '': return {'Code': 200, 'Body': HomeTemplate, 'Content-Type': 'text/html'} else: - Shift = 1 if Args[-1].lower().startswith(('atom.xml', 'rss.xml')) else 0 - Remote = '/'.join(Args[:-(2+Shift)]) + 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: @@ -326,7 +366,7 @@ def RqHandle(Path, Attempt=0): 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) + 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 @@ -350,10 +390,12 @@ class Handler(BaseHTTPRequestHandler): def do_POST(self): try: if self.path == '/': + Post = self.rfile.read(int(self.headers['Content-Length'])).decode() Body = HomeTemplate.replace('', f'''

- Here's your RSS feed: - + Here's your feed: + +


''').replace('/* {{PostCss}} */', '.PostObscure { opacity: 0.5; }') @@ -371,9 +413,9 @@ class Handler(BaseHTTPRequestHandler): self.send_header('Content-Type', 'text/plain') self.end_headers() self.wfile.write((f'[500] Internal Server Error{RetDebugIf()}').encode()) - # https://stackoverflow.com/a/3389505 - def log_message(self, format, *args): - return + ##Prevent logging | https://stackoverflow.com/a/3389505 + #def log_message(self, format, *args): + # return # https://stackoverflow.com/a/51559006 class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): pass