mirror of
https://gitlab.com/octospacc/Snippets.git
synced 2025-04-16 00:07:22 +02:00
ShioriFeed v2
This commit is contained in:
parent
8a2c2ae67d
commit
5b9caf9417
0
InvidiousFeedProxy.py
Normal file → Executable file
0
InvidiousFeedProxy.py
Normal file → Executable file
235
ShioriFeed.py
Normal file → Executable file
235
ShioriFeed.py
Normal file → Executable file
@ -1,25 +1,46 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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 traceback
|
||||||
import json
|
import json
|
||||||
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
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 http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
from socketserver import ThreadingMixIn
|
from socketserver import ThreadingMixIn
|
||||||
from urllib.request import urlopen, Request
|
from urllib.request import urlopen, Request
|
||||||
from urllib.error import HTTPError, URLError
|
from urllib.error import HTTPError, URLError
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
# Requirements: urllib3
|
|
||||||
# Usage: http[s]://<This Server>/http[s]://<Shiori Server>/<Shiori Username (in base64url)>/<Shiori Password (in base64url)>
|
# Usage: http[s]://<This Server>/http[s]://<Shiori Server>/<Shiori Username (in base64url)>/<Shiori Password (in base64url)>
|
||||||
Host = ('localhost', 8176)
|
|
||||||
|
|
||||||
HomeTemplate = '''
|
HomeTemplate = '''\
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>ShioriFeed</title>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>ShioriFeed 🔖</title>
|
||||||
|
<meta name="description" content="Simple service for getting an Atom/RSS feed from your Shiori profile"/>
|
||||||
|
<meta property="og:title" content="ShioriFeed 🔖"/>
|
||||||
|
<meta property="og:description" content="Simple service for getting an Atom/RSS feed from your Shiori profile"/>
|
||||||
<style>
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
body {
|
body {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
@ -27,7 +48,7 @@ HomeTemplate = '''
|
|||||||
padding-left: 10%;
|
padding-left: 10%;
|
||||||
padding-right: 10%;
|
padding-right: 10%;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
ford-break: break-word;
|
word-break: break-word;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
@ -35,73 +56,71 @@ HomeTemplate = '''
|
|||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
}
|
}
|
||||||
details {
|
form > label { padding: 8px; }
|
||||||
background: lightgray;
|
form > label > span { padding-bottom: 4px; }
|
||||||
padding: 8px;
|
form > label, form > label > span {
|
||||||
}
|
|
||||||
label > span {
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding-bottom: 4px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
}
|
}
|
||||||
input[type="submit"] {
|
input[type="submit"] { font-size: large; }
|
||||||
width: calc(100% + 8px);
|
|
||||||
font-size: large;
|
|
||||||
}
|
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 5em;
|
height: 5em;
|
||||||
|
font-size: large;
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
details {
|
||||||
|
background: lightgray;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
details > summary > h4 { display: inline; }
|
||||||
|
/* {{PostCss}} */
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>ShioriFeed</h2>
|
<h2>ShioriFeed 🔖</h2>
|
||||||
<p>
|
<p class="PostObscure">
|
||||||
Enter details about your account on a
|
Enter the details of your account on a
|
||||||
<a href="https://github.com/go-shiori/">Shiori</a>
|
<a href="https://github.com/go-shiori/">Shiori</a>
|
||||||
server to get an RSS feed link.
|
server to get an Aotm/RSS feed link.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
{{PostResult}}
|
<!-- {{PostResult}} -->
|
||||||
<p>
|
<p class="PostObscure">
|
||||||
<form action="/" method="POST">
|
<form action="./" method="POST">
|
||||||
<label>
|
<label class="PostObscure">
|
||||||
<span>
|
<span>Server <small>(must start with protocol prefix)</small>:</span>
|
||||||
Server <small>(must start with protocol prefix)</small>:
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
<input type="text" name="Remote" placeholder="http[s]://..."/>
|
<input type="text" name="Remote" placeholder="http[s]://..."/>
|
||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
<label>
|
<label class="PostObscure">
|
||||||
<span>
|
<span>Username:</span>
|
||||||
Username:
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
<input type="text" name="Username" placeholder="erre"/>
|
<input type="text" name="Username" placeholder="erre"/>
|
||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
<label>
|
<label class="PostObscure">
|
||||||
<span>
|
<span>Password:</span>
|
||||||
Password:
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
<input type="password" name="Password" placeholder="**********"/>
|
<input type="password" name="Password" placeholder="**********"/>
|
||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
|
<label class="PostObscure">
|
||||||
|
<span> </span>
|
||||||
<input type="submit" value="Submit"/>
|
<input type="submit" value="Submit"/>
|
||||||
|
</label>
|
||||||
</form>
|
</form>
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
|
<!--
|
||||||
<p>
|
<p>
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
Privacy Policy
|
<h4>Privacy Policy</h4>
|
||||||
</summary>
|
</summary>
|
||||||
|
<p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
|
||||||
@ -109,23 +128,39 @@ HomeTemplate = '''
|
|||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</p>
|
</p>
|
||||||
|
-->
|
||||||
<p>
|
<p>
|
||||||
|
<span>v. 2023-02-13</span>
|
||||||
|
▪️
|
||||||
<a href="https://gitlab.com/octospacc/Snippets/-/blob/main/ShioriFeed.py">Source Code</a>
|
<a href="https://gitlab.com/octospacc/Snippets/-/blob/main/ShioriFeed.py">Source Code</a>
|
||||||
</p>
|
</p>
|
||||||
|
<script>
|
||||||
|
var Box = document.querySelector('textarea');
|
||||||
|
if (Box) {
|
||||||
|
Box.value = location.origin + Box.value.substring('http[s]://<THIS SHIORIFEED SERVER ADDRESS>'.length);
|
||||||
|
};
|
||||||
|
Box.onclick = function() {
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(Box.value);
|
||||||
|
alert('Copied to clipboard!');
|
||||||
|
} catch(e) {};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
def SessionHash(Remote, Username, Password):
|
||||||
|
return f'{hash(Remote)}{hash(Username)}{hash(Password)}'
|
||||||
|
|
||||||
def MkFeed(Data, Remote, Username):
|
def MkFeed(Data, Remote, Username):
|
||||||
Feed = ''
|
Feed = ''
|
||||||
if not Data['bookmarks']:
|
Date = Data['bookmarks'][0]['modified'] if Data['bookmarks'] else ''
|
||||||
return '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"></rss>'
|
|
||||||
Date = Data['bookmarks'][0]['modified']
|
|
||||||
for Mark in Data['bookmarks']:
|
for Mark in Data['bookmarks']:
|
||||||
Feed += f'''
|
Feed += f'''
|
||||||
<item>
|
<item>
|
||||||
<title>{HTMLEscape(Mark['title'])}</title>
|
<title>{HtmlEscape(Mark['title'])}</title>
|
||||||
<description>{HTMLEscape(Mark['excerpt'])}</description>
|
<description>{HtmlEscape(Mark['excerpt'])}</description>
|
||||||
<link>{Remote}/bookmark/{Mark['id']}/content</link>
|
<link>{Remote}/bookmark/{Mark['id']}/content</link>
|
||||||
<pubDate>{Mark['modified']}</pubDate>
|
<pubDate>{Mark['modified']}</pubDate>
|
||||||
<guid isPermaLink="false">{Mark['id']}</guid>
|
<guid isPermaLink="false">{Mark['id']}</guid>
|
||||||
@ -135,7 +170,7 @@ def MkFeed(Data, Remote, Username):
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
|
||||||
<channel>
|
<channel>
|
||||||
<title>ShioriFeed ({HTMLEscape(Username)})</title>
|
<title>ShioriFeed ({HtmlEscape(Username)}) 🔖</title>
|
||||||
<pubDate>{Date}</pubDate>
|
<pubDate>{Date}</pubDate>
|
||||||
<lastBuildDate>{Date}</lastBuildDate>
|
<lastBuildDate>{Date}</lastBuildDate>
|
||||||
{Feed}
|
{Feed}
|
||||||
@ -143,82 +178,118 @@ def MkFeed(Data, Remote, Username):
|
|||||||
</rss>
|
</rss>
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
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]://<THIS SHIORIFEED SERVER ADDRESS>\
|
||||||
|
/{Args['Remote']}\
|
||||||
|
/{urlsafe_b64encode(Args['Username'].encode()).decode()}\
|
||||||
|
/{urlsafe_b64encode(Args['Password'].encode()).decode()}/'''
|
||||||
|
|
||||||
def GetSession(Remote, Username, Password):
|
def GetSession(Remote, Username, Password):
|
||||||
try:
|
try:
|
||||||
Rq = urlopen(Request(f'{Remote}/api/login',
|
Rq = urlopen(Request(f'{Remote}/api/login',
|
||||||
data=json.dumps({'username': Username, 'password': Password, 'remember': True, 'owner': True}).encode(),
|
data=json.dumps({'username': Username, 'password': Password, 'remember': True, 'owner': True}).encode(),
|
||||||
headers={'User-Agent': f'ShioriFeed at {Host[0]}'}))
|
headers={'User-Agent': f'ShioriFeed at {Host[0]}'}))
|
||||||
if Rq.code == 200:
|
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)
|
Sessions.update(Data)
|
||||||
return Data
|
return {
|
||||||
except Exception as Ex: #(HTTPError, URLError) as Ex:
|
'Code': 200,
|
||||||
print(traceback.format_exc())
|
'Body': Data}
|
||||||
return False
|
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):
|
def RqHandle(Path, Attempt=0):
|
||||||
Rs = {}
|
|
||||||
try:
|
try:
|
||||||
RqItems = Path.strip().removeprefix('/').removesuffix('/').strip().split('/')
|
Rs = {}
|
||||||
if RqItems[0] == '':
|
Args = Path.strip().removeprefix('/').removesuffix('/').strip().split('/')
|
||||||
Rs['Code'] = 200
|
if Args[0] == '':
|
||||||
Rs['Body'] = HomeTemplate.replace('{{PostResult}}', '')
|
return {
|
||||||
Rs['Content-Type'] = 'text/html'
|
'Code': 200,
|
||||||
|
'Body': HomeTemplate,
|
||||||
|
'Content-Type': 'text/html'}
|
||||||
else:
|
else:
|
||||||
Remote = '/'.join(RqItems[:-2])
|
Remote = '/'.join(Args[:-2])
|
||||||
Username = urlsafe_b64decode(RqItems[-2]).decode()
|
Username = urlsafe_b64decode(Args[-2]).decode()
|
||||||
Password = urlsafe_b64decode(RqItems[-1]).decode()
|
Password = urlsafe_b64decode(Args[-1]).decode()
|
||||||
if not f'{Remote}/{Username}/{Password}' in Sessions:
|
if not SessionHash(Remote, Username, Password) in Sessions:
|
||||||
GetSession(Remote, Username, Password)
|
TrySession = GetSession(Remote, Username, Password)
|
||||||
|
if TrySession['Code'] != 200:
|
||||||
|
return TrySession
|
||||||
Rq = urlopen(Request(f'{Remote}/api/bookmarks', headers={
|
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]}'}))
|
'User-Agent': f'ShioriFeed at {Host[0]}'}))
|
||||||
Rs['Code'] = Rq.code
|
Rs['Code'] = Rq.code
|
||||||
|
# Shiori got us JSON data, parse it and return our result
|
||||||
if Rq.code == 200:
|
if Rq.code == 200:
|
||||||
Rs['Body'] = MkFeed(json.loads(Rq.read().decode()), Remote, Username)
|
Rs['Body'] = MkFeed(json.loads(Rq.read().decode()), Remote, Username)
|
||||||
Rs['Content-Type'] = 'application/xml'
|
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:
|
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)
|
return ReqHandle(Path, Attempt+1)
|
||||||
else:
|
else:
|
||||||
Rs['Body'] = f'[{Rq.code}] External Server Error\n\n{Rq.read().decode()}'
|
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
|
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 '')}
|
||||||
|
|
||||||
# https://stackoverflow.com/a/51559006
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
Rs = RqHandle(self.path)
|
Rs = RqHandle(self.path)
|
||||||
self.send_response(Rs['Code'])
|
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.end_headers()
|
||||||
self.wfile.write(Rs['Body'].encode())
|
self.wfile.write(Rs['Body'].encode())
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
|
try:
|
||||||
if self.path == '/':
|
if self.path == '/':
|
||||||
Params = self.rfile.read(int(self.headers['Content-Length']))
|
Body = HomeTemplate.replace('<!-- {{PostResult}} -->', f'''
|
||||||
|
<p>
|
||||||
|
Here's your Atom feed:
|
||||||
|
<textarea readonly="true">{MkUrl(self.rfile.read(int(self.headers['Content-Length'])).decode())}</textarea>
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
''').replace('/* {{PostCss}} */', '.PostObscure { opacity: 0.5; }')
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header('Content-Type', 'text/html')
|
self.send_header('Content-Type', 'text/html')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(HomeTemplate.replace('{{PostResult}}', f'''
|
self.wfile.write(Body.encode())
|
||||||
<p>
|
|
||||||
Here's your RSS feed:
|
|
||||||
<textarea readonly="true"> </textarea>
|
|
||||||
</p>
|
|
||||||
<br />
|
|
||||||
''').encode())
|
|
||||||
else:
|
else:
|
||||||
self.send_response(400)
|
self.send_response(400)
|
||||||
self.send_header('Content-Type', 'text/plain')
|
self.send_header('Content-Type', 'text/plain')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(b'[400] Bad Request')
|
self.wfile.write(b'[400] Bad Request')
|
||||||
def log_request(self, code='-', size='-'):
|
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
|
return
|
||||||
|
# https://stackoverflow.com/a/51559006
|
||||||
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
||||||
pass
|
pass
|
||||||
def Serve():
|
def Serve():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user