mirror of
				https://gitlab.com/octospacc/Snippets.git
				synced 2025-06-27 09:02:56 +02:00 
			
		
		
		
	ShioriFeed v2
This commit is contained in:
		
							
								
								
									
										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(): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user