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