Program documentation

* Comments the code

* Includes a README file
master
Alexandre Racine 2023-06-16 22:51:47 +02:00
parent 442a89b81b
commit f4fe6e717e
8 changed files with 659 additions and 304 deletions

1
.gitignore vendored
View File

@ -1,2 +1 @@
tokens.txt
__pycache__/

62
README.md Normal file
View File

@ -0,0 +1,62 @@
# Inopy
Inopy is a Python application that retrieves unread articles from the [Inoreader](https://www.inoreader.com) API and sends a notification if there are any.
## Table of Contents
- [Overview](#overview)
- [Installation](#installation)
- [Usage](#usage)
- [License](#license)
## 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.
The code is organized into the following modules:
- `ino.py`: The main module that retrieves unread articles, handles token refreshing, and sends notifications.
- `config.py`: Contains configuration settings used by other 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).
## Installation
To use Inopy, you need to follow these steps:
1. Install the required dependencies by running the following command:
```bash
pip install requests pydbus flask waitress
```
2. Clone or download this repository.
3. Run the `ino.py` module the first time and set up the configuration file by providing the necessary OAuth, API endpoint and notifications details.
4. Run the `ino.py` module to retrieve unread articles and receive notifications.
## 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.
It should also contain the Inoreader API endpoints, notification labels, production status, browser path, host and port.
Once the configuration is set up, you can adapt some of the default values. Typically, check and if necessary adapt the `prod` section of the file. It defines whether the program is run in production or development mode.
To set a cron in Linux triggering the program for a notification, create a bash script containing the following code
```bash
#!/bin/bash
export DISPLAY=:0.0
export XAUTHORITY=/home/user/.Xauthority # adapt with your username
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
python path_to_your_ino.py # adapt with the path to your program directory
```
and define the cron job pointing to the bash script created.
## License
Inopy is released under the GNU General Public License version 3 or later. You can redistribute it and/or modify it under the terms of the license. For more details, please refer to the [GNU General Public License](https://www.gnu.org/licenses/).

257
config.py
View File

@ -1,115 +1,194 @@
"""
=========================================================================================
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 code consists of two main functions: get_config and create_file.
The get_config function check if the config directory or the config file exist. If not it creates config directory and/or config file and then gets the configuration data.
The create_file function is called if the configuration file should be created. It prompts the user to enter configuration details and save them in the config file.
Finally the config function serves as a wrapper function that sets the paths and calls get_config to retrieve the configuration data.
=========================================================================================
"""
import json
import os
'''
================================================================
Create a function to check if the config directory
and the config file exist.
If the config file or the config directory don't exist
create them.
Read the config file and load its contents into a dictionary.
Extract the necessary values from the config dictionary,
create a dictionary of local variables and return it.
================================================================
'''
def get_config(config_path, config_file_path):
if os.path.exists(config_path):
if os.path.exists(config_file_path):
pass
else:
if os.path.exists(config_path):
# If the config file doesn't exist, create it.
create_file(config_file_path)
else:
if os.path.exists(config_file_path):
pass
else:
create_file(config_file_path)
else:
os.mkdir(config_path)
print(f'{config_path} created!')
create_file(config_file_path)
# If the config directory doesn't exist, create it
os.mkdir(config_path)
print(f'{config_path} created!')
create_file(config_file_path)
# load config content
with open(config_file_path) as config_file:
config = json.load(config_file)
# Extract the necessary values from the config dictionary
bearer = config['oauth']['bearer']
refresh_token = config['oauth']['refresh_token']
endpoint = config['oauth']['endpoint']
client_id = config['oauth']['client_id']
client_secret = config['oauth']['client_secret']
callback = config['oauth']['callback']
scope = config['oauth']['scope']
csrf = config['oauth']['csrf']
home_url = config['oauth']['home_url']
# Load the config file
with open(config_file_path) as config_file:
config = json.load(config_file)
unread_counts_url = config['inoapi']['unread_counts_url']
feeds_list_url = config['inoapi']['feeds_list_url']
bearer = config['oauth']['bearer']
refresh_token = config['oauth']['refresh_token']
endpoint = config['oauth']['endpoint']
client_id = config['oauth']['client_id']
client_secret = config['oauth']['client_secret']
callback = config['oauth']['callback']
scope = config['oauth']['scope']
csrf = config['oauth']['csrf']
home_url = config['oauth']['home_url']
summary = config['notification']['summary']
singular_article = config['notification']['singular_article']
plural_articles = config['notification']['plural_articles']
unread_counts_url = config['inoapi']['unread_counts_url']
feeds_list_url = config['inoapi']['feeds_list_url']
prod_status = config['prod']['status']
browser_path = config['prod']['browser_path']
host = config['prod']['host']
port = config['prod']['port']
variables = locals()
return variables
summary = config['notification']['summary']
singular_article = config['notification']['singular_article']
plural_articles = config['notification']['plural_articles']
'''
==========================================================
Create a function to prompt the user to enter details
for the configuration file.
prod_status = config['prod']['status']
browser_path = config['prod']['browser_path']
host = config['prod']['host']
port = config['prod']['port']
variables = locals()
return variables
Create the configuration dictionary.
Write the configuration dictionary to the config file.
==========================================================
'''
def create_file(config_file_path):
config = {}
# Ask user for input
print("\nEnter details about OAuth authentication: \n")
config = {}
endpoint = input("Enter OAuth endpoint: ")
client_id = input("Enter your client id: ")
client_secret = input("Enter your client secret: ")
callback = input("Enter your callback URL: ")
scope = input("Enter the API scope (e.g. read OR read write): ")
# prompt user for configuration data
print("\nEnter details about OAuth authentication: \n")
print("\nEnter details about Inoreader API: \n")
unread_counts_url = input("Enter URL for unread articles: ")
feeds_list_url = input("Enter URL for feeds lists: ")
endpoint = input("Enter OAuth endpoint: ")
client_id = input("Enter your client id: ")
client_secret = input("Enter your client secret: ")
callback = input("Enter your callback URL: ")
scope = input("Enter the API scope (e.g. read OR read write): ")
print("\nEnter details about notification message: \n")
print("\nEnter details about Inoreader API: \n")
summary = input("Enter summary (title) for notification: ")
singular_article = input("Enter singular label if there is only one unread article (e.g. new article in feed): ")
plural_articles = input("Enter plural label if there are many unread articles (e.g. new articles in feed): ")
unread_counts_url = input("Enter URL for unread articles: ")
feeds_list_url = input("Enter URL for feeds lists: ")
print("\nEnter details about notification message: \n")
# Create nested JSON structure
config["oauth"] = {
"bearer": "",
"refresh_token": "",
"endpoint": endpoint,
"client_id": client_id,
"client_secret": client_secret,
"callback": callback,
"scope": scope,
"csrf": "4902358490258",
"home_url": "http://localhost:5000"
}
summary = input("Enter summary (title) for notification: ")
singular_article = input("Enter singular label if there is only one unread article (e.g. new article in feed): ")
plural_articles = input("Enter plural label if there are many unread articles (e.g. new articles in feed): ")
config["inoapi"] = {
"unread_counts_url": unread_counts_url,
"feeds_list_url": feeds_list_url
}
# Create the configuration dictionary
config["oauth"] = {
"bearer": "",
"refresh_token": "",
"endpoint": endpoint,
"client_id": client_id,
"client_secret": client_secret,
"callback": callback,
"scope": scope,
"csrf": "4902358490258",
"home_url": "http://localhost:5000"
}
config["notification"] = {
"summary": summary,
"singular_article": singular_article,
"plural_articles": plural_articles
}
config["inoapi"] = {
"unread_counts_url": unread_counts_url,
"feeds_list_url": feeds_list_url
}
config["prod"] = {
"status": "true",
"browser_path": "/usr/bin/firefox",
"host": "0.0.0.0",
"port": "5000"
}
config["notification"] = {
"summary": summary,
"singular_article": singular_article,
"plural_articles": plural_articles
}
# Save config to a file
with open(config_file_path, "w") as file:
json.dump(config, file, indent=4)
config["prod"] = {
"status": "true",
"browser_path": "/usr/bin/firefox",
"host": "0.0.0.0",
"port": "5000"
}
print(f"{config_file_path} created successfully!")
# Write the config data to the config file
with open(config_file_path, "w") as file:
json.dump(config, file, indent=4)
print(f"{config_file_path} created successfully!")
'''
==============================================
Create a function to set the paths for the
config directory and file.
Get the configuration data and return it.
==============================================
'''
def config():
config_path = os.path.join(os.environ['HOME'], '.config/inopy')
config_file = 'config.json'
config_file_path = os.path.join(config_path, config_file)
data = get_config(config_path, config_file_path)
return data
# Set the path of config file to
# /HOME/USER/.config/inopy/config.json
config_path = os.path.join(os.environ['HOME'], '.config/inopy')
config_file = 'config.json'
config_file_path = os.path.join(config_path, config_file)
# Get and return the configuration data
data = get_config(config_path, config_file_path)
return data
if __name__ == '__main__':
config = config()

BIN
icons/inoreader.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

212
ino.py
View File

@ -1,4 +1,6 @@
"""
=========================================================================================
Copyright © 2023 Alexandre Racine <https://alex-racine.ch>
This file is part of Inopy.
@ -9,13 +11,13 @@
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.
-------------------------------------------------------------------------------------
=========================================================================================
Inopy retrieves unread articles from the Inoreader API <https://www.inoreader.com/developers> and sends a notification if there are any unread articles.
@ -26,48 +28,51 @@
Inopy is structured into functions and modules for making API requests, parsing response data, refreshing tokens and sending notifications.
For more information about OAuth authentication, plase see <https://www.inoreader.com/developers/oauth>
=========================================================================================
"""
import requests
import json
import notif
import time
import sys
import os
from config import config
from oauth import app, run_app
from refresh import refresh
"""
Read the configuration file.
Get the bearer token from the config
and set it accordingly to Inoreader API
specifications.
Get the API endpoint URL from the config
and send a GET request to the API.
"""
'''
===========================================
Define functions to make an API request
with bearer token and parse response
data as JSON
===========================================
'''
def APIrequest(url, bearer):
bearer_string = 'Bearer {}'.format(bearer)
headers = {'Authorization': bearer_string}
response = requests.get(url, headers=headers)
print(response.status_code)
return response
# Parse the response as JSON
def getData(response):
data = json.loads(response.text)
return data
"""
Refresh the bearer token if it expired.
Update the bearer and refresh token
in the config
"""
'''
===================================
Initialize an empty message
string for the notification
===================================
'''
message = ""
'''
===========================================
Load configuration settings and
retrieve necessary configuration values
===========================================
'''
config = config()
bearer = config['bearer']
@ -81,102 +86,177 @@ refresh_token = config['refresh_token']
summary = config['summary']
singular_article = config['singular_article']
plural_articles = config['plural_articles']
singular_article = config['singular_article']
plural_articles = config['plural_articles']
'''
=========================================
Create dictionaries and list to store
unread counts, subscriptions and feed
categories (folders)
=========================================
'''
unreadcounts = {}
subscriptions = {}
categories = []
# Make a request to get unread counts
# Make API request to get unread counts
unread_response = APIrequest(unread_counts_url, bearer)
"""
If unauthorized (401) status code
is received, refresh the bearer token
and make a new request with the updated token.
"""
'''
===================================================
If the response status code is 403 (Forbidden):
* Run the Flask app to get a bearer token
* Load the updated configuration file
* Update the bearer token with the new value
from the updated config and make a new API
request with the updated bearer token
===================================================
'''
'''
======================================================
If the response status code is 401 (Unauthorized):
* Refresh the bearer token
* Load the updated configuration file
* Update the bearer token with the new value
from the updated config and make a new API
request with the updated bearer token
======================================================
'''
'''
============================================
If the response status code is 200 (OK):
* proceed with the code execution
============================================
'''
# Check for 403 error case
if unread_response.status_code == 403:
print(unread_response.status_code)
run_app()
#new_config = config()
#bearer = new_config['bearer']
run_app()
with open(config_path) as config_file:
new_config = json.load(config_file)
bearer = new_config['oauth']['bearer']
print(bearer)
unread_response = APIrequest(unread_counts_url, bearer)
print(unread_response.text)
bearer = new_config['oauth']['bearer']
unread_response = APIrequest(unread_counts_url, bearer)
# Check for 401 error case
elif unread_response.status_code == 401:
refresh(config_path, endpoint, client_id, client_secret, refresh_token)
#new_config = config()
#bearer = new_config['bearer']
with open(config_path) as config_file:
new_config = json.load(config_file)
bearer = new_config['oauth']['bearer']
print(bearer)
unread_response = APIrequest(unread_counts_url, bearer)
# Proceed with the code execution
elif unread_response.status_code == 200:
pass
"""
Get the list of feeds
Parse the response data
Parse the unread counts data
"""
# Make API request to get feeds list
feeds_list_response = APIrequest(feeds_list_url, bearer)
print(feeds_list_response)
'''
=======================================
Parse the responses data as JSON
Iterate over the unread counts and
subscriptions and store them in the
respective dictionaries
If the subscription has categories
(is part of a folder) append the
category in the categories list
=======================================
'''
feeds_list_data = getData(feeds_list_response)
unread_data = getData(unread_response)
print(feeds_list_data)
print('\n\n')
print(unread_data)
for unread in unread_data['unreadcounts']:
unread['count'] = int(unread['count'])
if unread['count'] > 0:
unreadcounts[unread['id']] = unread['count']
for subscribed in feeds_list_data['subscriptions']:
if subscribed['categories']:
if subscribed['categories'][0]['id'] not in categories:
categories.append(subscribed['categories'][0]['id'])
subscriptions[subscribed['id']] = subscribed['title']
'''
==================================================
Iterate over the unreadcounts dictionary
Determine the appropriate singular or
plural notification label based on the
count (e.g. new article or new articles)
Include the unread feed in the notification
only if it is not in the categories list.
This is to avoid duplicates notifications
for the unread feed and the folder in which
the feed is.
Do not include the reading-list in the
notification
If the unread_id exists in the subscriptions
dictionary, get the title associated with it.
Else extract the title from the unread_id.
Finally append the count, new_articles label
and title to the message string
==================================================
'''
for unread_id, count in unreadcounts.items():
if count == 1:
new_articles = singular_article
else:
new_articles = plural_articles
# Determine singular or plural notification label
new_articles = singular_article if count == 1 else plural_articles
count = str(count)
# Do not include the categories and the reading-list in the notification
if not unread_id in categories:
if unread_id.split("/")[-1] == "reading-list":
pass
else:
# Get the clean feed title
if unread_id in (k for k,v in subscriptions.items()):
title = next(v for k, v in subscriptions.items() if k == unread_id)
title = next(v for k, v in subscriptions.items() if k == unread_id)
else:
title = unread_id.split("/")[-1]
# Build the final notification message
message = message + count + " " + new_articles + " " + title + "\n"
else:
pass
# Send notification if message is not empty.
'''
====================================
Send the notification for unread
feeds only if the message string
is set
====================================
'''
if message != "":
notif.send_notification(summary, message)
else:
pass

View File

@ -1,4 +1,6 @@
"""
=========================================================================================
Copyright © 2023 Alexandre Racine <https://alex-racine.ch>
This file is part of Inopy.
@ -9,26 +11,34 @@
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 code uses the pydbus library to send notifications. It defines a function send_notification() that takes summary and body as parameters.
The aim of this module is to send a notification using the Inopy application. It takes a summary and body as input and utilizes D-Bus (Desktop Bus) to establish a session bus connection.
Then, it calls the Notify method to send a notification with the specified parameters.
Note that this code assumes that the necessary dependencies are installed and that the D-Bus service for notifications is available on the system.
=========================================================================================
"""
import os
from pydbus import SessionBus
'''
Create a new session bus instance
Get the .Notifications interface object from the bus
=========================================
Define a function to send the
notification when a new unread
article is present in the feed
* Create a new session
bus instance
* Get the .Notifications interface
object from the bus
=========================================
'''
def send_notification(summary, body):
@ -36,15 +46,21 @@ def send_notification(summary, body):
notifications = bus.get('.Notifications')
'''
Call the Notify method on the notifications object to send a notification
==================================================================================
Call the Notify method on the notifications object to send a notification with
Parameters:
- 'MyApp': The name of the application sending the notification
- 0: The ID of the notification (0 means a new notification)
- '': An optional icon name or path for the notification
- summary: The summary text of the notification
- body: The body text of the notification
- []: A list of actions associated with the notification (empty in this case)
- {}: A dictionary of hints for the notification (empty in this case)
- 5000: The timeout duration in milliseconds for the notification (5000 ms = 5 seconds)
* 'MyApp': The name of the application sending the notification
* 0: The ID of the notification (0 means a new notification)
* '': An optional icon name or path for the notification
* summary: The summary text of the notification
* body: The body text of the notification
* []: A list of actions associated with the notification
(empty in this case)
* {}: A dictionary of hints for the notification (empty in this case)
* 5000: The timeout duration in milliseconds for the notification
(5000 ms = 5 seconds)
==================================================================================
'''
notifications.Notify('Inopy', 0, '/opt/chrome-apps-icons/inoreader.png', summary, body, [], {}, 5000)
icon = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icons/inoreader.png')
notifications.Notify('Inopy', 0, icon, summary, body, [], {}, 5000)

315
oauth.py
View File

@ -1,14 +1,53 @@
from flask import Flask, request, redirect, render_template
from config import config
from waitress import serve
"""
=========================================================================================
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 provide an OAuth authentication flow using Flask.
It sets up a web server that handles the authentication process, retrieves the access token and refresh token, and stores them in the configuration file.
The module can be run in either production or development mode, and it opens a web browser to complete the authentication process.
=========================================================================================
"""
import requests
import os
import signal
import webbrowser
import time
import json
import subprocess
import threading
from flask import Flask, request, redirect, render_template
from config import config
from waitress import serve
'''
===================================
Load configuration values
Extract values from the config
dictionary
Build the URL for authorization
===================================
'''
config = config()
@ -19,149 +58,209 @@ 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)
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():
# Get the authorization code from the request URL
authorization_code = request.args.get('code')
csrf_check = request.args.get('state')
error_param = request.args.get('error')
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 != None else False
csrf = True if csrf_check == CSRF else False
error = True if error_param is not None else False
if csrf == True and error != True:
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
}
# Exchange the authorization code for an access token
access_token_url = endpoint
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']
response = requests.post(access_token_url, data=payload)
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()
# Parse the response to get the access token
if response.status_code == 200:
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
access_token = response.json()['access_token']
refresh_token = response.json()['refresh_token']
'''
======================================
When the shutdown url is accessed:
with open(config_file_path, 'r+') as config_file:
# Load the JSON data from the file
config = json.load(config_file)
# Update the token value in the config data
config['oauth']['bearer'] = access_token
config['oauth']['refresh_token'] = refresh_token
# Move the file pointer back to the beginning of the file
config_file.seek(0)
# Write the updated config data to the file
json.dump(config, config_file, indent=4)
config_file.truncate()
return render_template('success.html', response=(access_token, refresh_token))
else:
# Redirect the user to a desired URL
if csrf != True:
return render_template('csrf-failed.html', response=(CSRF, csrf_check))
elif error == True:
error_content = request.args.get('error_description')
return render_template('oauth-error.html', response=(error_param, error_content))
else:
pass
* Shut down the Flask server
gracefully
======================================
'''
@app.route('/shutdown')
def shutdown():
# Shutting down the Flask app gracefully
#return ('proccess ended', time.sleep(5), os.kill(os.getpid(), signal.SIGINT))
request.environ.get('werkzeug.server.shutdown')
return 'Close this browser to terminate the process!'
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 run_prod():
# Create a new Firefox profile
subprocess.run([browser_path, "-CreateProfile", "new_profile", "-no-remote"])
# Launch Firefox with the new profile and open the URL
subprocess.run([browser_path, "-P", "new_profile", "-no-remote", home_url])
serve(app, host=host, port=port)'''
# Function to start the Flask server
def start_server():
serve(app, host=host, port=port)
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)
# Create a new thread for the Flask server
server_thread = threading.Thread(target=start_server)
# 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])
# Start the Flask server thread
server_thread.start()
'''
======================================
Define a function to start the
development server inside a new
thread.
# Wait for the Flask server to start (adjust the delay as needed)
time.sleep(2)
# Create a new Firefox profile
subprocess.run([browser_path, "-CreateProfile", "new_profile", "-no-remote"])
# Launch Firefox with the new profile and open the URL
subprocess.run([browser_path, "-P", "new_profile", "-no-remote", home_url])
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)
# Create a new thread for the Flask server
server_thread = threading.Thread(target=start_server)
# Open the home URL in the default web browser
webbrowser.open(home_url)
app.run()
# Start the Flask server thread
server_thread.start()
# Wait for the Flask server to start (adjust the delay as needed)
time.sleep(2)
webbrowser.open(home_url)
app.run()
'''
======================================
Determine whether to run the
production or development server
based on the config
======================================
'''
def run_app():
if prod_status == "true":
run_prod()
else:
run_dev()
if prod_status == "true":
print(prod_status)
run_prod()
else:
print(prod_status)
run_dev()
'''
======================================
Run the application standalone
if the script is executed but not
imported
======================================
'''
if __name__ == '__main__':
#app.run()
run_app()
run_app()

View File

@ -1,4 +1,6 @@
"""
=========================================================================================
Copyright © 2023 Alexandre Racine <https://alex-racine.ch>
This file is part of Inopy.
@ -9,31 +11,57 @@
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 code uses the values from the configuration file to construct a request payload.
The aim of this module is to refresh both OAuth 2.0 access and refresh tokens and update the configuration file with the refreshed tokens.
The refresh function sends a POST request to the Inoreader OAuth endpoint <https://www.inoreader.com/developers/oauth> using the payload and headers to handle the refresh token process.
Then it extracts the refreshed bearer token and the new refresh token in order to use it in the replace() function of the main ino.py module.
=========================================================================================
"""
'''
=====================================================
Create a refresh function to refresh the bearer
and refresh token in the config data
* Set the headers and prepare the payload
data for the HTTP request
* Send a POST request to the specified
endpoint with the payload and headers
* Check the response status code to determine
if the request was successful
* Parse the response data as JSON and extract
the refreshed bearer token and new refresh
token from the response data
* Open the config file. Load the existing
config data from the file and update the
bearer token and refresh token in the config
data
* Move the file pointer to the beginning of
the file, write the updated config data back
to the file, overwriting the existing content
* Truncate the file to remove any remaining
content after the updated data
=====================================================
'''
import requests
import json
# Define a function named 'refresh' that handles the token refresh logic
def refresh(config_path, endpoint, client_id, client_secret, refresh_token):
# Set the headers for the request
headers = {"Content-type": "application/x-www-form-urlencoded"}
# Prepare the payload for the request
payload = {
"client_id": client_id,
"client_secret": client_secret,
@ -41,33 +69,25 @@ def refresh(config_path, endpoint, client_id, client_secret, refresh_token):
"refresh_token": refresh_token
}
# Send a POST request to the specified URL with the payload and headers
response = requests.post(endpoint, data=payload, headers=headers)
# Check the response status code
if response.status_code == 200:
print("Request was successful.")
else:
print("Request failed with status code:", response.status_code)
# Parse the response data as JSON
data = json.loads(response.text)
# Extract the refreshed bearer token and new refresh token from the response data
refreshed_bearer = data['access_token']
new_refresh_token = data['refresh_token']
with open(config_path, 'r+') as config_file:
# Load the JSON data from the file
config = json.load(config_file)
# Update the token value in the config data
config['oauth']['bearer'] = refreshed_bearer
config['oauth']['refresh_token'] = new_refresh_token
# Move the file pointer back to the beginning of the file
config_file.seek(0)
# Write the updated config data to the file
json.dump(config, config_file, indent=4)
config_file.truncate()