diff --git a/InvidiousFeedProxy.py b/InvidiousFeedProxy.py
old mode 100644
new mode 100755
diff --git a/ShioriFeed.py b/ShioriFeed.py
old mode 100644
new mode 100755
index 5acef5d..75353f0
--- a/ShioriFeed.py
+++ b/ShioriFeed.py
@@ -1,25 +1,46 @@
#!/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 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 = '''
+HomeTemplate = '''\
-
+
- ShioriFeed
+
+ ShioriFeed 🔖
+
+
+
- ShioriFeed
-
- Enter details about your account on a
+
ShioriFeed 🔖
+
+ Enter the details of your account on a
Shiori
- server to get an RSS feed link.
+ server to get an Aotm/RSS feed link.
- {{PostResult}}
-
-
+
+
+
+ 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 = ''
- if not Data['bookmarks']:
- return ''
- Date = Data['bookmarks'][0]['modified']
+ Date = Data['bookmarks'][0]['modified'] if Data['bookmarks'] else ''
for Mark in Data['bookmarks']:
Feed += f'''
-
- {HTMLEscape(Mark['title'])}
- {HTMLEscape(Mark['excerpt'])}
+ {HtmlEscape(Mark['title'])}
+ {HtmlEscape(Mark['excerpt'])}
{Remote}/bookmark/{Mark['id']}/content
{Mark['modified']}
{Mark['id']}
@@ -135,7 +170,7 @@ def MkFeed(Data, Remote, Username):
- ShioriFeed ({HTMLEscape(Username)})
+ ShioriFeed ({HtmlEscape(Username)}) 🔖
{Date}
{Date}
{Feed}
@@ -143,82 +178,118 @@ def MkFeed(Data, Remote, Username):
'''
+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 = {f'{Remote}/{Username}/{Password}': json.loads(Rq.read().decode())['session']}
+ Data = {SessionHash(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
+ 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):
- 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'
+ Rs = {}
+ Args = Path.strip().removeprefix('/').removesuffix('/').strip().split('/')
+ if Args[0] == '':
+ return {
+ 'Code': 200,
+ 'Body': HomeTemplate,
+ '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)
+ 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[f'{Remote}/{Username}/{Password}'],
+ '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:
- GetSession(Remote, Username, Password)
+ 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()}'
- Rs['Content-Type'] = 'text/plain'
- except Exception as Ex: #(HTTPError, URLError) as Ex:
- print(traceback.format_exc())
- Rs['Code'] = 500
+ 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 Rs
+ #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 '')}
-# 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.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):
- 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'''
+ try:
+ if self.path == '/':
+ Body = HomeTemplate.replace('', f'''
- Here's your RSS feed:
-
+ Here's your Atom feed:
+
- ''').encode())
- else:
- self.send_response(400)
+ ''').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(b'[400] Bad Request')
- def log_request(self, code='-', size='-'):
+ 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():