inopy/ino.py

262 lines
7.6 KiB
Python

"""
=========================================================================================
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.
=========================================================================================
Inopy retrieves unread articles from the Inoreader API <https://www.inoreader.com/developers> and sends a notification if there are any unread articles.
It uses a config file to store OAuth authentication data, Inoreader API endpoints <https://www.inoreader.com/developers/api-endpoint> and notification data.
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.
For more information about OAuth authentication, plase see <https://www.inoreader.com/developers/oauth>
=========================================================================================
"""
import requests
import json
import notif
from config import config
from oauth import app, run_app
from refresh import refresh
'''
===========================================
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)
return response
def getData(response):
data = json.loads(response.text)
return data
'''
===================================
Initialize an empty message
string for the notification
===================================
'''
message = ""
'''
===========================================
Load configuration settings and
retrieve necessary configuration values
===========================================
'''
config = config()
bearer = config['bearer']
unread_counts_url = config['unread_counts_url']
feeds_list_url = config['feeds_list_url']
config_path = config['config_file_path']
endpoint = config['endpoint']
client_id = config['client_id']
client_secret = config['client_secret']
refresh_token = config['refresh_token']
summary = config['summary']
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 API request to get unread counts
unread_response = APIrequest(unread_counts_url, bearer)
'''
===================================================
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:
run_app()
with open(config_path) as config_file:
new_config = json.load(config_file)
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)
with open(config_path) as config_file:
new_config = json.load(config_file)
bearer = new_config['oauth']['bearer']
unread_response = APIrequest(unread_counts_url, bearer)
# Proceed with the code execution
elif unread_response.status_code == 200:
pass
# Make API request to get feeds list
feeds_list_response = APIrequest(feeds_list_url, bearer)
'''
=======================================
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)
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():
# 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)
else:
title = unread_id.split("/")[-1]
# Build the final notification message
message = message + count + " " + new_articles + " " + title + "\n"
else:
pass
'''
====================================
Send the notification for unread
feeds only if the message string
is set
====================================
'''
if message != "":
notif.send_notification(summary, message)
else:
pass