diff --git a/ShioriFeed.py b/ShioriFeed.py new file mode 100644 index 0000000..5acef5d --- /dev/null +++ b/ShioriFeed.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +import traceback +import json +from base64 import urlsafe_b64decode, urlsafe_b64encode +from html import escape as HTMLEscape +from http.server import HTTPServer, BaseHTTPRequestHandler +from socketserver import ThreadingMixIn +from urllib.request import urlopen, Request +from urllib.error import HTTPError, URLError +import threading + +# Requirements: urllib3 +# Usage: http[s]:///http[s]://// +Host = ('localhost', 8176) + +HomeTemplate = ''' + + + + ShioriFeed + + + + +

ShioriFeed

+

+ Enter details about your account on a + Shiori + server to get an RSS feed link. +

+
+ {{PostResult}} +

+

+ +
+ +
+ +
+ +
+

+
+

+

+ + Privacy Policy + +
    +
  • + +
  • +
+
+

+

+ Source Code +

+ + +''' + +def MkFeed(Data, Remote, Username): + Feed = '' + if not Data['bookmarks']: + return '' + Date = Data['bookmarks'][0]['modified'] + for Mark in Data['bookmarks']: + Feed += f''' + + {HTMLEscape(Mark['title'])} + {HTMLEscape(Mark['excerpt'])} + {Remote}/bookmark/{Mark['id']}/content + {Mark['modified']} + {Mark['id']} + + ''' + return f'''\ + + + + ShioriFeed ({HTMLEscape(Username)}) + {Date} + {Date} + {Feed} + + +''' + +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': f'ShioriFeed at {Host[0]}'})) + if Rq.code == 200: + Data = {f'{Remote}/{Username}/{Password}': json.loads(Rq.read().decode())['session']} + Sessions.update(Data) + return Data + except Exception as Ex: #(HTTPError, URLError) as Ex: + print(traceback.format_exc()) + return False + +def RqHandle(Path, Attempt=0): + Rs = {} + try: + RqItems = Path.strip().removeprefix('/').removesuffix('/').strip().split('/') + if RqItems[0] == '': + Rs['Code'] = 200 + Rs['Body'] = HomeTemplate.replace('{{PostResult}}', '') + Rs['Content-Type'] = 'text/html' + else: + Remote = '/'.join(RqItems[:-2]) + Username = urlsafe_b64decode(RqItems[-2]).decode() + Password = urlsafe_b64decode(RqItems[-1]).decode() + if not f'{Remote}/{Username}/{Password}' in Sessions: + GetSession(Remote, Username, Password) + Rq = urlopen(Request(f'{Remote}/api/bookmarks', headers={ + 'X-Session-Id': Sessions[f'{Remote}/{Username}/{Password}'], + 'User-Agent': f'ShioriFeed at {Host[0]}'})) + Rs['Code'] = Rq.code + if Rq.code == 200: + Rs['Body'] = MkFeed(json.loads(Rq.read().decode()), Remote, Username) + Rs['Content-Type'] = 'application/xml' + elif Rq.code == 500 and Attempt < 1: + GetSession(Remote, Username, Password) + return ReqHandle(Path, Attempt+1) + else: + Rs['Body'] = f'[{Rq.code}] External Server Error\n\n{Rq.read().decode()}' + Rs['Content-Type'] = 'text/plain' + except Exception as Ex: #(HTTPError, URLError) as Ex: + print(traceback.format_exc()) + Rs['Code'] = 500 + #Rs['Body'] = f'[500] Internal Server Error\n\n{traceback.format_exc()}' + Rs['Body'] = f'[500] Internal Server Error' + Rs['Content-Type'] = 'text/plain' + return Rs + +# https://stackoverflow.com/a/51559006 +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + Rs = RqHandle(self.path) + self.send_response(Rs['Code']) + self.send_header('Content-Type', Rs['Content-Type']) + self.end_headers() + self.wfile.write(Rs['Body'].encode()) + def do_POST(self): + if self.path == '/': + Params = self.rfile.read(int(self.headers['Content-Length'])) + self.send_response(200) + self.send_header('Content-Type', 'text/html') + self.end_headers() + self.wfile.write(HomeTemplate.replace('{{PostResult}}', f''' +

+ Here's your RSS feed: + +

+
+ ''').encode()) + else: + self.send_response(400) + self.send_header('Content-Type', 'text/plain') + self.end_headers() + self.wfile.write(b'[400] Bad Request') + def log_request(self, code='-', size='-'): + return +class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): + pass +def Serve(): + ThreadedHTTPServer(Host, Handler).serve_forever() + +if __name__ == '__main__': + Sessions = {} + try: + Serve() + except KeyboardInterrupt: + pass