Set logging process
* Sets logging method and replaces basic "print" with logs * Replaces /HOME/USER/.config/ directory with /HOME/USER/.inopy directory * Moves config directory in new /HOME/USER/.inopy directory and creates a new "logs" directory * Adapts README filemaster
							parent
							
								
									f4fe6e717e
								
							
						
					
					
						commit
						7c3c922529
					
				| 
						 | 
				
			
			@ -11,7 +11,7 @@ Inopy is a Python application that retrieves unread articles from the [Inoreader
 | 
			
		|||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
It uses OAuth authentication for accessing the [API](https://www.inoreader.com/developers) and stores the authentication data in a JSON configuration file located at `/HOME/USER/.config/inopy/config.json`. The program is divided into multiple modules and functions for making API requests, parsing response data, refreshing tokens, and sending notifications.
 | 
			
		||||
It uses OAuth authentication for accessing the [API](https://www.inoreader.com/developers) and stores the authentication data in a JSON configuration file located at `/HOME/USER/.inopy/config/config.json`. The program is divided into multiple modules and functions for making API requests, parsing response data, refreshing tokens, sending notifications and logging the processes.
 | 
			
		||||
 | 
			
		||||
The code is organized into the following modules:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ The code is organized into the following modules:
 | 
			
		|||
- `oauth.py`: Implements the OAuth authentication flow using Flask.
 | 
			
		||||
- `refresh.py`: Contains a `refresh` function for refreshing OAuth access and refresh tokens and updating the configuration file.
 | 
			
		||||
- `notif.py`: Provides a function for sending notifications using D-Bus (only tested with Cinnamon desktop environment).
 | 
			
		||||
- `logs.py`: Defines logging options.
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -39,7 +40,7 @@ pip install requests pydbus flask waitress
 | 
			
		|||
 | 
			
		||||
## Usage
 | 
			
		||||
 | 
			
		||||
The first time `ino.py` module is run, it will check if `config.json` exists in `/HOME/USER/.config/inopy/`. If not it will prompt user for configuration details and create the `config.json` file. The file should contain the OAuth endpoint, client ID, client secret, callback URL, scope, CSRF value and home URL.
 | 
			
		||||
The first time `ino.py` module is run, it will check if `config.json` exists in `/HOME/USER/.inopy/config/`. If not it will prompt user for configuration details and create the `config.json` file. The file should contain the OAuth endpoint, client ID, client secret, callback URL, scope, CSRF value and home URL.
 | 
			
		||||
 | 
			
		||||
It should also contain the Inoreader API endpoints, notification labels, production status, browser path, host and port.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										15
									
								
								config.py
								
								
								
								
							
							
						
						
									
										15
									
								
								config.py
								
								
								
								
							| 
						 | 
				
			
			@ -32,6 +32,11 @@
 | 
			
		|||
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
import logging
 | 
			
		||||
from logs import LogFile
 | 
			
		||||
 | 
			
		||||
# Set logs file
 | 
			
		||||
log_file = LogFile()
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
================================================================
 | 
			
		||||
| 
						 | 
				
			
			@ -51,8 +56,10 @@ import os
 | 
			
		|||
def get_config(config_path, config_file_path):
 | 
			
		||||
	
 | 
			
		||||
	if os.path.exists(config_path):
 | 
			
		||||
		logging.info(f'Found config directory at {config_path}')
 | 
			
		||||
		
 | 
			
		||||
		if os.path.exists(config_file_path):
 | 
			
		||||
			logging.info(f'Found config file at {config_file_path}!')
 | 
			
		||||
			pass
 | 
			
		||||
		
 | 
			
		||||
		else:
 | 
			
		||||
| 
						 | 
				
			
			@ -64,7 +71,8 @@ def get_config(config_path, config_file_path):
 | 
			
		|||
 | 
			
		||||
		# If the config directory doesn't exist, create it
 | 
			
		||||
		os.mkdir(config_path)
 | 
			
		||||
		print(f'{config_path} created!')
 | 
			
		||||
		logging.info(f'{config_path} created!')
 | 
			
		||||
 | 
			
		||||
		create_file(config_file_path)
 | 
			
		||||
	
 | 
			
		||||
	# load config content
 | 
			
		||||
| 
						 | 
				
			
			@ -167,7 +175,8 @@ def create_file(config_file_path):
 | 
			
		|||
	with open(config_file_path, "w") as file:
 | 
			
		||||
		json.dump(config, file, indent=4)
 | 
			
		||||
 | 
			
		||||
	print(f"{config_file_path} created successfully!")
 | 
			
		||||
	#print(f"{config_file_path} created successfully!")
 | 
			
		||||
	logging.info(f'Created config file at {config_file_path}!')
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
==============================================
 | 
			
		||||
| 
						 | 
				
			
			@ -182,7 +191,7 @@ def config():
 | 
			
		|||
	
 | 
			
		||||
	# Set the path of config file to
 | 
			
		||||
	# /HOME/USER/.config/inopy/config.json
 | 
			
		||||
	config_path = os.path.join(os.environ['HOME'], '.config/inopy')
 | 
			
		||||
	config_path = os.path.join(os.environ['HOME'], '.inopy/config')
 | 
			
		||||
	config_file = 'config.json'
 | 
			
		||||
	config_file_path = os.path.join(config_path, config_file)
 | 
			
		||||
	
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										81
									
								
								ino.py
								
								
								
								
							
							
						
						
									
										81
									
								
								ino.py
								
								
								
								
							| 
						 | 
				
			
			@ -25,7 +25,7 @@
 | 
			
		|||
 | 
			
		||||
	It performs token refreshing process if the API request returns an unauthorized status code.
 | 
			
		||||
 | 
			
		||||
	Inopy is structured into functions and modules for making API requests, parsing response data, refreshing tokens and sending notifications.
 | 
			
		||||
	Inopy is structured into functions and modules for making API requests, parsing response data, refreshing tokens, sending notifications and logging the processes.
 | 
			
		||||
 | 
			
		||||
	For more information about OAuth authentication, plase see <https://www.inoreader.com/developers/oauth>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -35,9 +35,14 @@
 | 
			
		|||
import requests
 | 
			
		||||
import json
 | 
			
		||||
import notif
 | 
			
		||||
import logging
 | 
			
		||||
from config import config
 | 
			
		||||
from oauth import app, run_app
 | 
			
		||||
from oauth import run_app
 | 
			
		||||
from refresh import refresh
 | 
			
		||||
from logs import LogFile
 | 
			
		||||
 | 
			
		||||
# Set logs file
 | 
			
		||||
log_file = LogFile()
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
===========================================
 | 
			
		||||
| 
						 | 
				
			
			@ -100,7 +105,12 @@ subscriptions = {}
 | 
			
		|||
categories = []
 | 
			
		||||
 | 
			
		||||
# Make API request to get unread counts
 | 
			
		||||
unread_response = APIrequest(unread_counts_url, bearer)
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
	unread_response = APIrequest(unread_counts_url, bearer)
 | 
			
		||||
 | 
			
		||||
except Exception as e:
 | 
			
		||||
	logging.debug(e)
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
===================================================
 | 
			
		||||
| 
						 | 
				
			
			@ -138,34 +148,47 @@ unread_response = APIrequest(unread_counts_url, bearer)
 | 
			
		|||
============================================
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
# Check for 403 error case
 | 
			
		||||
if unread_response.status_code == 403:
 | 
			
		||||
	run_app()	
 | 
			
		||||
try:
 | 
			
		||||
 | 
			
		||||
	with open(config_path) as config_file:
 | 
			
		||||
		new_config = json.load(config_file)
 | 
			
		||||
	# Check for 403 error case
 | 
			
		||||
	if unread_response.status_code == 403:
 | 
			
		||||
		logging.info('Token not available: starting oauth process...')
 | 
			
		||||
		run_app()
 | 
			
		||||
 | 
			
		||||
	bearer = new_config['oauth']['bearer']
 | 
			
		||||
		with open(config_path) as config_file:
 | 
			
		||||
			new_config = json.load(config_file)
 | 
			
		||||
 | 
			
		||||
	unread_response = APIrequest(unread_counts_url, bearer)
 | 
			
		||||
		bearer = new_config['oauth']['bearer']
 | 
			
		||||
 | 
			
		||||
# Check for 401 error case
 | 
			
		||||
elif unread_response.status_code == 401:
 | 
			
		||||
	refresh(config_path, endpoint, client_id, client_secret, refresh_token)
 | 
			
		||||
		unread_response = APIrequest(unread_counts_url, bearer)
 | 
			
		||||
 | 
			
		||||
	with open(config_path) as config_file:
 | 
			
		||||
		new_config = json.load(config_file)
 | 
			
		||||
	# Check for 401 error case
 | 
			
		||||
	elif unread_response.status_code == 401:
 | 
			
		||||
		logging.info('Token expired: starting refresh process...')
 | 
			
		||||
		refresh(config_path, endpoint, client_id, client_secret, refresh_token)
 | 
			
		||||
 | 
			
		||||
	bearer = new_config['oauth']['bearer']
 | 
			
		||||
	
 | 
			
		||||
	unread_response = APIrequest(unread_counts_url, bearer)
 | 
			
		||||
		with open(config_path) as config_file:
 | 
			
		||||
			new_config = json.load(config_file)
 | 
			
		||||
 | 
			
		||||
# Proceed with the code execution
 | 
			
		||||
elif unread_response.status_code == 200:
 | 
			
		||||
	pass
 | 
			
		||||
		bearer = new_config['oauth']['bearer']
 | 
			
		||||
		
 | 
			
		||||
		unread_response = APIrequest(unread_counts_url, bearer)
 | 
			
		||||
 | 
			
		||||
	# Proceed with the code execution
 | 
			
		||||
	elif unread_response.status_code == 200:
 | 
			
		||||
		logging.info('API request ok: retrieving data...')
 | 
			
		||||
		pass
 | 
			
		||||
 | 
			
		||||
except Exception as e:
 | 
			
		||||
	logging.debug(e)
 | 
			
		||||
 | 
			
		||||
# Make API request to get feeds list
 | 
			
		||||
feeds_list_response = APIrequest(feeds_list_url, bearer)
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
	feeds_list_response = APIrequest(feeds_list_url, bearer)
 | 
			
		||||
 | 
			
		||||
except Exception as e:
 | 
			
		||||
	logging.debug(e)
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
=======================================
 | 
			
		||||
| 
						 | 
				
			
			@ -255,8 +278,14 @@ for unread_id, count in unreadcounts.items():
 | 
			
		|||
====================================
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
if message != "":
 | 
			
		||||
	notif.send_notification(summary, message)
 | 
			
		||||
try:
 | 
			
		||||
	if message != "":
 | 
			
		||||
		notif.send_notification(summary, message)
 | 
			
		||||
		logging.info('Notification successfully sent!')
 | 
			
		||||
 | 
			
		||||
else:
 | 
			
		||||
	pass
 | 
			
		||||
	else:
 | 
			
		||||
		logging.info('No unread articles. Notification not sent.')
 | 
			
		||||
		pass
 | 
			
		||||
 | 
			
		||||
except Exception as e:
 | 
			
		||||
	logging.debug(e)
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
"""
 | 
			
		||||
=========================================================================================
 | 
			
		||||
    
 | 
			
		||||
    Copyright © 2023 Alexandre Racine <https://alex-racine.ch>
 | 
			
		||||
 | 
			
		||||
    This file is part of Inopy.
 | 
			
		||||
 | 
			
		||||
    Inopy is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 | 
			
		||||
 | 
			
		||||
    Inopy is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 | 
			
		||||
 | 
			
		||||
    You should have received a copy of the GNU General Public License along with Inopy. If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
=========================================================================================
 | 
			
		||||
 | 
			
		||||
    DISCLAIMER: parts of this code and comments blocks were created
 | 
			
		||||
    with the help of ChatGPT developped by OpenAI <https://openai.com/>
 | 
			
		||||
    Followed by human reviewing, refactoring and fine-tuning.
 | 
			
		||||
 | 
			
		||||
=========================================================================================
 | 
			
		||||
 | 
			
		||||
    This module aims to log the program processes.
 | 
			
		||||
 | 
			
		||||
=========================================================================================
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
# Setting the logs file
 | 
			
		||||
def LogFile():
 | 
			
		||||
    
 | 
			
		||||
    # Determine the user's home directory
 | 
			
		||||
    logs_dir = os.path.join(os.environ['HOME'], '.inopy/logs')
 | 
			
		||||
    os.makedirs(logs_dir, exist_ok=True)
 | 
			
		||||
 | 
			
		||||
    # Configure logging
 | 
			
		||||
    log_file = os.path.join(logs_dir, "inopy.log")
 | 
			
		||||
 | 
			
		||||
    logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', 
 | 
			
		||||
datefmt='%m/%d/%Y %I:%M:%S %p', filename=log_file, level=logging.DEBUG)
 | 
			
		||||
							
								
								
									
										456
									
								
								oauth.py
								
								
								
								
							
							
						
						
									
										456
									
								
								oauth.py
								
								
								
								
							| 
						 | 
				
			
			@ -34,225 +34,255 @@ import time
 | 
			
		|||
import json
 | 
			
		||||
import subprocess
 | 
			
		||||
import threading
 | 
			
		||||
import logging
 | 
			
		||||
from flask import Flask, request, redirect, render_template
 | 
			
		||||
from config import config
 | 
			
		||||
from waitress import serve
 | 
			
		||||
from logs import LogFile
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
===================================
 | 
			
		||||
	Load configuration values
 | 
			
		||||
	
 | 
			
		||||
	Extract values from the config
 | 
			
		||||
	dictionary
 | 
			
		||||
	
 | 
			
		||||
	Build the URL for authorization
 | 
			
		||||
===================================
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
config = config()
 | 
			
		||||
 | 
			
		||||
endpoint = config['endpoint']
 | 
			
		||||
client_id = config['client_id']
 | 
			
		||||
client_secret = config['client_secret']
 | 
			
		||||
callback = config['callback']
 | 
			
		||||
scope = config['scope']
 | 
			
		||||
CSRF = config['csrf']
 | 
			
		||||
home_url = config['home_url']
 | 
			
		||||
prod_status = config['prod_status']
 | 
			
		||||
browser_path = config['browser_path']
 | 
			
		||||
host = config['host']
 | 
			
		||||
port = config['port']
 | 
			
		||||
config_file_path = config['config_file_path']
 | 
			
		||||
 | 
			
		||||
url = 'https://www.inoreader.com/oauth2/auth?client_id={}&redirect_uri={}&response_type=code&scope={}&state={}'.format(client_id, callback, scope, CSRF)
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
==========================
 | 
			
		||||
	Initiate the Flask app
 | 
			
		||||
==========================
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
app = Flask(__name__)
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
==================================
 | 
			
		||||
	When the home url is accessed:
 | 
			
		||||
	
 | 
			
		||||
	*	Redirect the user to the
 | 
			
		||||
		authorization URL
 | 
			
		||||
==================================
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
@app.route('/')
 | 
			
		||||
def index():
 | 
			
		||||
	return redirect(url)
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
=====================================================
 | 
			
		||||
	When the oauth-callback url is accessed:
 | 
			
		||||
 | 
			
		||||
	*	Get the authorization code and other
 | 
			
		||||
		parameters from the callback URL
 | 
			
		||||
	
 | 
			
		||||
	*	Check CSRF validation token and if there
 | 
			
		||||
		is an error parameter in the URL
 | 
			
		||||
	
 | 
			
		||||
	*	Request bearer token and refresh token
 | 
			
		||||
 | 
			
		||||
	*	Save the bearer token and refresh token
 | 
			
		||||
		to the config file
 | 
			
		||||
	
 | 
			
		||||
	*	If process is successfull:
 | 
			
		||||
		
 | 
			
		||||
		*	render the success template with the
 | 
			
		||||
			response
 | 
			
		||||
	
 | 
			
		||||
	*	If CSRF failed:
 | 
			
		||||
		
 | 
			
		||||
		*	render the CSRF failure template
 | 
			
		||||
	
 | 
			
		||||
	*	If an error parameter is present in the URL:
 | 
			
		||||
 | 
			
		||||
		*	Render the OAuth error template
 | 
			
		||||
=====================================================
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
@app.route('/oauth-callback')
 | 
			
		||||
 | 
			
		||||
def oauth_callback():
 | 
			
		||||
	authorization_code = request.args.get('code')
 | 
			
		||||
	csrf_check = request.args.get('state')
 | 
			
		||||
	error_param = request.args.get('error')
 | 
			
		||||
 | 
			
		||||
	csrf = True if csrf_check == CSRF else False
 | 
			
		||||
	error = True if error_param is not None else False
 | 
			
		||||
 | 
			
		||||
	if csrf and not error:
 | 
			
		||||
		access_token_url = endpoint
 | 
			
		||||
		
 | 
			
		||||
		# Prepare data to request bearer token
 | 
			
		||||
		payload = {
 | 
			
		||||
			'grant_type': 'authorization_code',
 | 
			
		||||
			'code': authorization_code,
 | 
			
		||||
			'client_id': client_id,
 | 
			
		||||
			'client_secret': client_secret,
 | 
			
		||||
			'redirect_uri': callback
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		# Request bearer token and refresh token
 | 
			
		||||
		response = requests.post(access_token_url, data=payload)
 | 
			
		||||
		
 | 
			
		||||
		if response.status_code == 200:
 | 
			
		||||
	
 | 
			
		||||
			access_token = response.json()['access_token']
 | 
			
		||||
			refresh_token = response.json()['refresh_token']
 | 
			
		||||
 | 
			
		||||
			with open(config_file_path, 'r+') as config_file:
 | 
			
		||||
				config = json.load(config_file)
 | 
			
		||||
				
 | 
			
		||||
				# Save the bearer token and refresh token to the config file
 | 
			
		||||
				config['oauth']['bearer'] = access_token
 | 
			
		||||
				config['oauth']['refresh_token'] = refresh_token
 | 
			
		||||
				
 | 
			
		||||
				config_file.seek(0)
 | 
			
		||||
				
 | 
			
		||||
				json.dump(config, config_file, indent=4)
 | 
			
		||||
				config_file.truncate()
 | 
			
		||||
 | 
			
		||||
		return render_template('success.html', response=(access_token, refresh_token))
 | 
			
		||||
	
 | 
			
		||||
	else:
 | 
			
		||||
		if not csrf:
 | 
			
		||||
			return render_template('csrf-failed.html', response=(CSRF, csrf_check))
 | 
			
		||||
		
 | 
			
		||||
		elif error:
 | 
			
		||||
			error_content = request.args.get('error_description')
 | 
			
		||||
			return render_template('oauth-error.html', response=(error_param, error_content))
 | 
			
		||||
		else:
 | 
			
		||||
			pass
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
======================================
 | 
			
		||||
	When the shutdown url is accessed:
 | 
			
		||||
 | 
			
		||||
	*	Shut down the Flask server
 | 
			
		||||
		gracefully
 | 
			
		||||
======================================
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
@app.route('/shutdown')
 | 
			
		||||
def shutdown():
 | 
			
		||||
	request.environ.get('werkzeug.server.shutdown')
 | 
			
		||||
	return 'Close this browser to terminate the process!'
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
======================================
 | 
			
		||||
	Define a function to start the
 | 
			
		||||
	Flask server using Waitress in
 | 
			
		||||
	production mode
 | 
			
		||||
======================================
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
def start_server():
 | 
			
		||||
	serve(app, host=host, port=port)
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
======================================
 | 
			
		||||
	Define a function to start the
 | 
			
		||||
	production server inside a new
 | 
			
		||||
	thread.
 | 
			
		||||
 | 
			
		||||
	This is to ensure the server is
 | 
			
		||||
	actually already running before
 | 
			
		||||
	opening the web browser
 | 
			
		||||
======================================
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
def run_prod():
 | 
			
		||||
	server_thread = threading.Thread(target=start_server)
 | 
			
		||||
	server_thread.start()
 | 
			
		||||
	time.sleep(2)
 | 
			
		||||
 | 
			
		||||
	# Launch a separate browser process with a new profile
 | 
			
		||||
	subprocess.run([browser_path, "-CreateProfile", "new_profile", "-no-remote"])
 | 
			
		||||
	subprocess.run([browser_path, "-P", "new_profile", "-no-remote", home_url])
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
======================================
 | 
			
		||||
	Define a function to start the
 | 
			
		||||
	development server inside a new
 | 
			
		||||
	thread.
 | 
			
		||||
 | 
			
		||||
	This is to ensure the server is
 | 
			
		||||
	actually already running before
 | 
			
		||||
	opening the web browser
 | 
			
		||||
======================================
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
def run_dev():
 | 
			
		||||
	server_thread = threading.Thread(target=start_server)
 | 
			
		||||
	server_thread.start()
 | 
			
		||||
	time.sleep(2)
 | 
			
		||||
 | 
			
		||||
	# Open the home URL in the default web browser
 | 
			
		||||
	webbrowser.open(home_url)
 | 
			
		||||
	app.run()
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
======================================
 | 
			
		||||
	Determine whether to run the
 | 
			
		||||
	production or development server
 | 
			
		||||
	based on the config
 | 
			
		||||
======================================
 | 
			
		||||
'''
 | 
			
		||||
# Set logs file
 | 
			
		||||
log_file = LogFile()
 | 
			
		||||
 | 
			
		||||
def run_app():
 | 
			
		||||
	
 | 
			
		||||
	if prod_status == "true":
 | 
			
		||||
		run_prod()
 | 
			
		||||
	
 | 
			
		||||
	else:
 | 
			
		||||
		run_dev()
 | 
			
		||||
 | 
			
		||||
	'''
 | 
			
		||||
	===================================
 | 
			
		||||
		Load configuration values
 | 
			
		||||
		
 | 
			
		||||
		Extract values from the config
 | 
			
		||||
		dictionary
 | 
			
		||||
		
 | 
			
		||||
		Build the URL for authorization
 | 
			
		||||
	===================================
 | 
			
		||||
	'''
 | 
			
		||||
 | 
			
		||||
	conf = config()
 | 
			
		||||
 | 
			
		||||
	endpoint = conf['endpoint']
 | 
			
		||||
	client_id = conf['client_id']
 | 
			
		||||
	client_secret = conf['client_secret']
 | 
			
		||||
	callback = conf['callback']
 | 
			
		||||
	scope = conf['scope']
 | 
			
		||||
	CSRF = conf['csrf']
 | 
			
		||||
	home_url = conf['home_url']
 | 
			
		||||
	prod_status = conf['prod_status']
 | 
			
		||||
	browser_path = conf['browser_path']
 | 
			
		||||
	host = conf['host']
 | 
			
		||||
	port = conf['port']
 | 
			
		||||
	config_file_path = conf['config_file_path']
 | 
			
		||||
 | 
			
		||||
	url = 'https://www.inoreader.com/oauth2/auth?client_id={}&redirect_uri={}&response_type=code&scope={}&state={}'.format(client_id, callback, scope, CSRF)
 | 
			
		||||
 | 
			
		||||
	'''
 | 
			
		||||
	==========================
 | 
			
		||||
		Initiate the Flask app
 | 
			
		||||
	==========================
 | 
			
		||||
	'''
 | 
			
		||||
 | 
			
		||||
	app = Flask(__name__)
 | 
			
		||||
 | 
			
		||||
	'''
 | 
			
		||||
	==================================
 | 
			
		||||
		When the home url is accessed:
 | 
			
		||||
		
 | 
			
		||||
		*	Redirect the user to the
 | 
			
		||||
			authorization URL
 | 
			
		||||
	==================================
 | 
			
		||||
	'''
 | 
			
		||||
 | 
			
		||||
	try:
 | 
			
		||||
		@app.route('/')
 | 
			
		||||
		def index():
 | 
			
		||||
			return redirect(url)
 | 
			
		||||
 | 
			
		||||
		logging.info('Redirecting to authorization URL...')
 | 
			
		||||
 | 
			
		||||
	except Exception as e:
 | 
			
		||||
		logging.debug(e)
 | 
			
		||||
 | 
			
		||||
	'''
 | 
			
		||||
	=====================================================
 | 
			
		||||
		When the oauth-callback url is accessed:
 | 
			
		||||
 | 
			
		||||
		*	Get the authorization code and other
 | 
			
		||||
			parameters from the callback URL
 | 
			
		||||
		
 | 
			
		||||
		*	Check CSRF validation token and if there
 | 
			
		||||
			is an error parameter in the URL
 | 
			
		||||
		
 | 
			
		||||
		*	Request bearer token and refresh token
 | 
			
		||||
 | 
			
		||||
		*	Save the bearer token and refresh token
 | 
			
		||||
			to the config file
 | 
			
		||||
		
 | 
			
		||||
		*	If process is successfull:
 | 
			
		||||
			
 | 
			
		||||
			*	render the success template with the
 | 
			
		||||
				response
 | 
			
		||||
		
 | 
			
		||||
		*	If CSRF failed:
 | 
			
		||||
			
 | 
			
		||||
			*	render the CSRF failure template
 | 
			
		||||
		
 | 
			
		||||
		*	If an error parameter is present in the URL:
 | 
			
		||||
 | 
			
		||||
			*	Render the OAuth error template
 | 
			
		||||
	=====================================================
 | 
			
		||||
	'''
 | 
			
		||||
 | 
			
		||||
	try:
 | 
			
		||||
		@app.route('/oauth-callback')
 | 
			
		||||
 | 
			
		||||
		def oauth_callback():
 | 
			
		||||
			authorization_code = request.args.get('code')
 | 
			
		||||
			csrf_check = request.args.get('state')
 | 
			
		||||
			error_param = request.args.get('error')
 | 
			
		||||
 | 
			
		||||
			csrf = True if csrf_check == CSRF else False
 | 
			
		||||
			error = True if error_param is not None else False
 | 
			
		||||
 | 
			
		||||
			if csrf and not error:
 | 
			
		||||
				access_token_url = endpoint
 | 
			
		||||
				
 | 
			
		||||
				# Prepare data to request bearer token
 | 
			
		||||
				payload = {
 | 
			
		||||
					'grant_type': 'authorization_code',
 | 
			
		||||
					'code': authorization_code,
 | 
			
		||||
					'client_id': client_id,
 | 
			
		||||
					'client_secret': client_secret,
 | 
			
		||||
					'redirect_uri': callback
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				# Request bearer token and refresh token
 | 
			
		||||
				response = requests.post(access_token_url, data=payload)
 | 
			
		||||
				
 | 
			
		||||
				if response.status_code == 200:
 | 
			
		||||
			
 | 
			
		||||
					access_token = response.json()['access_token']
 | 
			
		||||
					refresh_token = response.json()['refresh_token']
 | 
			
		||||
 | 
			
		||||
					with open(config_file_path, 'r+') as config_file:
 | 
			
		||||
						config = json.load(config_file)
 | 
			
		||||
						
 | 
			
		||||
						# Save the bearer token and refresh token to the config file
 | 
			
		||||
						config['oauth']['bearer'] = access_token
 | 
			
		||||
						config['oauth']['refresh_token'] = refresh_token
 | 
			
		||||
						
 | 
			
		||||
						config_file.seek(0)
 | 
			
		||||
						
 | 
			
		||||
						json.dump(config, config_file, indent=4)
 | 
			
		||||
						config_file.truncate()
 | 
			
		||||
 | 
			
		||||
				logging.info('New token obtained successfully...')
 | 
			
		||||
				return render_template('success.html', response=(access_token, refresh_token))
 | 
			
		||||
			
 | 
			
		||||
			else:
 | 
			
		||||
				if not csrf:
 | 
			
		||||
					logging.warning('CRSF validation failed...')
 | 
			
		||||
					return render_template('csrf-failed.html', response=(CSRF, csrf_check))
 | 
			
		||||
				
 | 
			
		||||
				elif error:
 | 
			
		||||
					error_content = request.args.get('error_description')
 | 
			
		||||
					logging.debug(error_content)
 | 
			
		||||
					return render_template('oauth-error.html', response=(error_param, error_content))
 | 
			
		||||
				
 | 
			
		||||
				else:
 | 
			
		||||
					pass
 | 
			
		||||
 | 
			
		||||
	except Exception as e:
 | 
			
		||||
		logging.debug(e)
 | 
			
		||||
 | 
			
		||||
	'''
 | 
			
		||||
	======================================
 | 
			
		||||
		When the shutdown url is accessed:
 | 
			
		||||
 | 
			
		||||
		*	Shut down the Flask server
 | 
			
		||||
			gracefully
 | 
			
		||||
	======================================
 | 
			
		||||
	'''
 | 
			
		||||
 | 
			
		||||
	try:
 | 
			
		||||
		@app.route('/shutdown')
 | 
			
		||||
		def shutdown():
 | 
			
		||||
			request.environ.get('werkzeug.server.shutdown')
 | 
			
		||||
			logging.info('Shutting down Flask server...')
 | 
			
		||||
			return 'Close this browser to terminate the process!'
 | 
			
		||||
 | 
			
		||||
	except Exception as e:
 | 
			
		||||
		logging.debug(e)
 | 
			
		||||
 | 
			
		||||
	'''
 | 
			
		||||
	======================================
 | 
			
		||||
		Define a function to start the
 | 
			
		||||
		Flask server using Waitress in
 | 
			
		||||
		production mode
 | 
			
		||||
	======================================
 | 
			
		||||
	'''
 | 
			
		||||
 | 
			
		||||
	def start_server():
 | 
			
		||||
		serve(app, host=host, port=port)
 | 
			
		||||
 | 
			
		||||
	'''
 | 
			
		||||
	======================================
 | 
			
		||||
		Define a function to start the
 | 
			
		||||
		production server inside a new
 | 
			
		||||
		thread.
 | 
			
		||||
 | 
			
		||||
		This is to ensure the server is
 | 
			
		||||
		actually already running before
 | 
			
		||||
		opening the web browser
 | 
			
		||||
	======================================
 | 
			
		||||
	'''
 | 
			
		||||
 | 
			
		||||
	def run_prod():
 | 
			
		||||
		logging.info('Running program in production mode...')
 | 
			
		||||
		server_thread = threading.Thread(target=start_server)
 | 
			
		||||
		server_thread.start()
 | 
			
		||||
		time.sleep(2)
 | 
			
		||||
 | 
			
		||||
		# Launch a separate browser process with a new profile
 | 
			
		||||
		subprocess.run([browser_path, "-CreateProfile", "new_profile", "-no-remote"])
 | 
			
		||||
		subprocess.run([browser_path, "-P", "new_profile", "-no-remote", home_url])
 | 
			
		||||
 | 
			
		||||
	'''
 | 
			
		||||
	======================================
 | 
			
		||||
		Define a function to start the
 | 
			
		||||
		development server inside a new
 | 
			
		||||
		thread.
 | 
			
		||||
 | 
			
		||||
		This is to ensure the server is
 | 
			
		||||
		actually already running before
 | 
			
		||||
		opening the web browser
 | 
			
		||||
	======================================
 | 
			
		||||
	'''
 | 
			
		||||
 | 
			
		||||
	def run_dev():
 | 
			
		||||
		logging.info('Running program in development mode...')
 | 
			
		||||
		server_thread = threading.Thread(target=start_server)
 | 
			
		||||
		server_thread.start()
 | 
			
		||||
		time.sleep(2)
 | 
			
		||||
 | 
			
		||||
		# Open the home URL in the default web browser
 | 
			
		||||
		webbrowser.open(home_url)
 | 
			
		||||
		app.run()
 | 
			
		||||
 | 
			
		||||
	'''
 | 
			
		||||
	======================================
 | 
			
		||||
		Determine whether to run the
 | 
			
		||||
		production or development server
 | 
			
		||||
		based on the config
 | 
			
		||||
	======================================
 | 
			
		||||
	'''
 | 
			
		||||
		
 | 
			
		||||
	try:
 | 
			
		||||
		if prod_status == "true":
 | 
			
		||||
			run_prod()
 | 
			
		||||
 | 
			
		||||
		else:
 | 
			
		||||
			run_dev()
 | 
			
		||||
 | 
			
		||||
	except Exception as e:
 | 
			
		||||
		logging.debug(e)
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
======================================
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,6 +58,11 @@
 | 
			
		|||
 | 
			
		||||
import requests
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
from logs import LogFile
 | 
			
		||||
 | 
			
		||||
# Set logs file
 | 
			
		||||
log_file = LogFile()
 | 
			
		||||
 | 
			
		||||
def refresh(config_path, endpoint, client_id, client_secret, refresh_token):
 | 
			
		||||
	headers = {"Content-type": "application/x-www-form-urlencoded"}
 | 
			
		||||
| 
						 | 
				
			
			@ -72,10 +77,10 @@ def refresh(config_path, endpoint, client_id, client_secret, refresh_token):
 | 
			
		|||
	response = requests.post(endpoint, data=payload, headers=headers)
 | 
			
		||||
	
 | 
			
		||||
	if response.status_code == 200:
 | 
			
		||||
		print("Request was successful.")
 | 
			
		||||
		logging.info("Request was successful. Token was refreshed...")
 | 
			
		||||
	
 | 
			
		||||
	else:
 | 
			
		||||
		print("Request failed with status code:", response.status_code)
 | 
			
		||||
		logging.debug(f'Request failed with status code: {response.status_code}')
 | 
			
		||||
	
 | 
			
		||||
	data = json.loads(response.text)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue