#!/usr/bin/env python3 # *----------------------------------------------------------------------* # # | [ ShioriFeed 🔖 ] | # # | Simple service for getting an Atom/RSS feed from your Shiori profile | # # | v. 2023-02-13-r2, OctoSpacc | # # *----------------------------------------------------------------------* # # *---------------------------------* # # | Configuration | # # *---------------------------------* # Host = ('localhost', 8176) Debug = True # *---------------------------------* # # External Requirements: urllib3 # *-------------------------------------------------------------------------* # 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 # Usage: http[s]:///http[s]://// HomeTemplate = '''\ ShioriFeed 🔖

ShioriFeed 🔖

Enter the details of your account on a Shiori server to get an Aotm/RSS feed link.






v. 2023-02-13 ▪️ Source Code

''' def SessionHash(Remote, Username, Password): return f'{hash(Remote)}{hash(Username)}{hash(Password)}' def MkFeed(Data, Remote, Username): Feed = '' Date = Data['bookmarks'][0]['modified'] if Data['bookmarks'] else '' 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 MkUrl(Post): Args = {} #Args = Post.split('&') for Arg in Post.split('&'): Arg = Arg.split('=') Args.update({Arg[0]: Arg[1]}) return f'''\ http[s]://\ /{Args['Remote']}\ /{urlsafe_b64encode(Args['Username'].encode()).decode()}\ /{urlsafe_b64encode(Args['Password'].encode()).decode()}/''' 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 = {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: #as Ex: #(HTTPError, URLError) as Ex: #print(traceback.format_exc()) return { 'Code': 500, 'Body': '[500] Internal Server Error' + (f'\n\n{traceback.format_exc()}' if Debug else '')} 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: Remote = '/'.join(Args[:-2]) Username = urlsafe_b64decode(Args[-2]).decode() Password = urlsafe_b64decode(Args[-1]).decode() if not SessionHash(Remote, Username, Password) in Sessions: TrySession = GetSession(Remote, Username, Password) if TrySession['Code'] != 200: return TrySession Rq = urlopen(Request(f'{Remote}/api/bookmarks', headers={ 'X-Session-Id': Sessions[SessionHash(Remote, Username, Password)], 'User-Agent': f'ShioriFeed at {Host[0]}'})) Rs['Code'] = Rq.code # Shiori got us JSON data, parse it and return our result if Rq.code == 200: Rs['Body'] = MkFeed(json.loads(Rq.read().decode()), Remote, Username) Rs['Content-Type'] = 'application/xml' # We probably got an expired Session-Id, let's try to renew it elif Rq.code == 500 and Attempt < 1: 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: #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 { 'Code': 500, 'Body': '[500] Internal Server Error' + (f'\n\n{traceback.format_exc()}' if Debug else '')} 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 == '/': Body = HomeTemplate.replace('', f'''

Here's your Atom feed:


''').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(('[500] Internal Server Error' + (f'\n\n{traceback.format_exc()}' if Debug else '')).encode()) # 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