commit 9e1f209f02713da741a3d9f749486ea716573a64 Author: spla Date: Thu Dec 24 14:13:31 2020 +0100 First Budget commit! diff --git a/README.md b/README.md new file mode 100644 index 0000000..4097d71 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Budget +Budget helps you control your Mastodon's server bills and donations and share current financial status with your users by posting them via this Budget bot. + +### Dependencies + +- **Python 3** +- Postgresql server +- Mastodon's bot account + +### Usage: + +Within Python Virtual Environment: + +1. Run `pip install -r requirements.txt` to install needed Python libraries. + +2. Run `python db-setup.py` to setup and create new Postgresql database and needed tables in it. + +3. Run `python setup.py` to get your Mastodon's bot account tokens and to be able to post your server's financial status. + +4. Run `python addincome.py` to add your donations. + +5. Run `python addbill.py` to add your server or domain bills. + +6. Use your favourite scheduling method to set `python budget.py` to run regularly. It will post your finances to your fediverse server. + diff --git a/addbill.py b/addbill.py new file mode 100644 index 0000000..fe9ca06 --- /dev/null +++ b/addbill.py @@ -0,0 +1,145 @@ +from datetime import datetime, timezone, timedelta +import time +import threading +import os +import sys +import os.path +import psycopg2 +import pytz +import dateutil +from dateutil.parser import parse +from decimal import * +getcontext().prec = 2 + +############################################################################### +# get bills rows +############################################################################### + +def print_bills(): + + try: + + conn = psycopg2.connect(database = budget_db, user = budget_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute("SELECT datetime, domain, server, backup, fileserver, setup FROM bills") + + rows = cur.fetchall() + + for row in rows: + + date = row[0].date().strftime('%d.%m.%Y') + print(date, ', domain: ' + str(row[1]), ', server: ' + str(row[2]), 'backup: ' + str(row[3]), ', fileserver: ' + str(row[4]), ' setup: ' + str(row[5])) + + cur.close() + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + +################################################################################### +# add bills to database +################################################################################### + +def insert_bills(billdate, domainbill, serverbill, backupbill, fileserverbill, setupbill): + + sql = "INSERT INTO bills(datetime, domain, server, backup, fileserver, setup) VALUES(%s,%s,%s,%s,%s,%s)" + + try: + + conn = psycopg2.connect(database = budget_db, user = budget_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute(sql, (billdate, domainbill, serverbill, backupbill, fileserverbill, setupbill)) + print("\n") + print("Updating bills...") + + conn.commit() + + cur.close() + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + +############################################################################### +# INITIALISATION +############################################################################### + +# Returns the parameter from the specified file +def get_parameter( parameter, file_path ): + # Check if db_config.txt file exists + if not os.path.isfile(file_path): + if file_path == "config/db_config.txt": + print("File %s not found, exiting. Run db-setup.py."%file_path) + sys.exit(0) + + # Find parameter in file + with open( file_path ) as f: + for line in f: + if line.startswith( parameter ): + return line.replace(parameter + ":", "").strip() + + # Cannot find parameter, exit + print(file_path + " Missing parameter %s "%parameter) + print("Run setup.py") + sys.exit(0) + +# Load configuration from config file +config_filepath = "config/db_config.txt" +budget_db = get_parameter("budget_db", config_filepath) # E.g., budget +budget_db_user = get_parameter("budget_db_user", config_filepath) # E.g., mastodon + +############################################################################### + +while True: + + domainbill = input("Domain bill? (q to quit) ") + if domainbill == '': + domainbill = '0.00' + elif domainbill == 'q': + sys.exit("Bye") + domainbill = round(float(domainbill),2) + + serverbill = input("Server bill? ") + if serverbill == '': + serverbill = '0.00' + serverbill = round(float(serverbill),2) + + backupbill = input("Backup bill? ") + if backupbill == '': + backupbill = '0.00' + backupbill = round(float(backupbill),2) + + fileserverbill = input("Fileserver bill? ") + if fileserverbill == '': + fileserverbill = 0.00 + fileserverbill = round(float(fileserverbill),2) + + setupbill = input("Setup bill? ") + if setupbill == '': + setupbill = 0.00 + setupbill = round(float(setupbill),2) + + billdate = datetime.strptime(input('Enter Bill date in the format d.m.yyyy '), '%d.%m.%Y') + + now = datetime.now() + + billdate = billdate.replace(hour=now.hour, minute=now.minute, second=now.second) + + insert_bills(billdate, domainbill, serverbill, backupbill, fileserverbill, setupbill) + print_bills() diff --git a/addincome.py b/addincome.py new file mode 100644 index 0000000..c96b803 --- /dev/null +++ b/addincome.py @@ -0,0 +1,132 @@ +from datetime import datetime, timezone, timedelta +import time +import os +import sys +import os.path +import psycopg2 +import dateutil +from dateutil.parser import parse +from decimal import * +getcontext().prec = 2 + +############################################################################### +# get income rows +############################################################################### + +def print_incomes(): + + conn = None + + try: + + conn = psycopg2.connect(database = budget_db, user = budget_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute("SELECT datetime, donations, owner FROM incomes") + + rows = cur.fetchall() + + for row in rows: + + date = row[0].date().strftime('%d.%m.%Y') + print(date,' donation: '+ str(row[1]), 'server owner: ' + str(row[2])) + + cur.close() + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + +################################################################################### +# add incomes to database +################################################################################### + +def insert_incomes(incomedate, donationincome, ownerincome): + + sql = "INSERT INTO incomes(datetime, donations, owner) VALUES(%s,%s,%s)" + + conn = None + + try: + + conn = psycopg2.connect(database = budget_db, user = budget_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute(sql, (incomedate, donationincome, ownerincome)) + print("\n") + print("Updating incomes...") + + conn.commit() + + cur.close() + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + +############################################################################### +# INITIALISATION +############################################################################### + +# Returns the parameter from the specified file +def get_parameter( parameter, file_path ): + # Check if db_config.txt file exists + if not os.path.isfile(file_path): + if file_path == "config/db_config.txt": + print("File %s not found, exiting. Run db-setup.py."%file_path) + sys.exit(0) + + # Find parameter in file + with open( file_path ) as f: + for line in f: + if line.startswith( parameter ): + return line.replace(parameter + ":", "").strip() + + # Cannot find parameter, exit + print(file_path + " Missing parameter %s "%parameter) + print("Run setup.py") + sys.exit(0) + +# Load configuration from config file +config_filepath = "config/db_config.txt" +budget_db = get_parameter("budget_db", config_filepath) # E.g., budget +budget_db_user = get_parameter("budget_db_user", config_filepath) # E.g., mastodon + +############################################################################### + +while True: + + donationincome = input("Donation income? (q to quit) ") + if donationincome == '': + donationincome = '0.00' + elif donationincome == 'q': + sys.exit("Bye") + donationincome = round(float(donationincome),2) + + ownerincome = input("Server owner income? ") + if ownerincome == '': + ownerincome = '0.00' + ownerincome = round(float(ownerincome),2) + + incomedate = datetime.strptime(input('Income date in the format dd.mm.yyyy? '), '%d.%m.%Y') + + now = datetime.now() + + incomedate = incomedate.replace(hour=now.hour, minute=now.minute, second=now.second) + + insert_incomes(incomedate, donationincome, ownerincome) + print_incomes() diff --git a/budget.py b/budget.py new file mode 100644 index 0000000..2d01025 --- /dev/null +++ b/budget.py @@ -0,0 +1,167 @@ +import sys +import os +import os.path +import re +from datetime import datetime, timedelta +from mastodon import Mastodon +import psycopg2 + +def get_bills(): + + current_year = now.year + + bills = 0 + + try: + + conn = None + + conn = psycopg2.connect(database = budget_db, user = budget_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute("select sum (domain) + sum(server) + sum(backup) + sum(fileserver) +sum(setup) from bills where date_part('year', datetime) = (%s)", (current_year,)) + + row = cur.fetchone() + + if row[0] != None: + + bills = row[0] + + cur.close() + + return bills + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() + +def get_donations(): + + current_year = now.year + + donations = 0 + + try: + + conn = None + + conn = psycopg2.connect(database = budget_db, user = budget_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + cur = conn.cursor() + + cur.execute("select sum (donations) + sum(owner) from incomes where date_part('year', datetime) = (%s)", (current_year,)) + + row = cur.fetchone() + + if row[0] != None: + + donations = row[0] + + else: + + donations = 0 + + cur.close() + + return donations + + except (Exception, psycopg2.DatabaseError) as error: + + sys.exit(error) + + finally: + + if conn is not None: + + conn.close() + +def mastodon(): + + # Load secrets from secrets file + secrets_filepath = "secrets/secrets.txt" + uc_client_id = get_parameter("uc_client_id", secrets_filepath) + uc_client_secret = get_parameter("uc_client_secret", secrets_filepath) + uc_access_token = get_parameter("uc_access_token", secrets_filepath) + + # Load configuration from config file + config_filepath = "config/config.txt" + mastodon_hostname = get_parameter("mastodon_hostname", config_filepath) + bot_username = get_parameter("bot_username", config_filepath) + + # Initialise Mastodon API + mastodon = Mastodon( + client_id = uc_client_id, + client_secret = uc_client_secret, + access_token = uc_access_token, + api_base_url = 'https://' + mastodon_hostname, + ) + + # Initialise access headers + headers={ 'Authorization': 'Bearer %s'%uc_access_token } + + return (mastodon, mastodon_hostname, bot_username) + +def db_config(): + + # Load db configuration from config file + config_filepath = "config/db_config.txt" + budget_db = get_parameter("budget_db", config_filepath) + budget_db_user = get_parameter("budget_db_user", config_filepath) + + return (budget_db, budget_db_user) + +def get_parameter( parameter, file_path ): + + if not os.path.isfile(file_path): + print("File %s not found, exiting."%file_path) + sys.exit(0) + + with open( file_path ) as f: + for line in f: + if line.startswith( parameter ): + return line.replace(parameter + ":", "").strip() + + print(file_path + " Missing parameter %s "%parameter) + sys.exit(0) + +############################################################################### +# main + +if __name__ == '__main__': + + mastodon, mastodon_hostname, bot_username = mastodon() + + budget_db, budget_db_user = db_config() + + now = datetime.now() + + donations = get_donations() + + bills = get_bills() + + toot_text = 'Finances of ' + mastodon_hostname + '\n\n' + + toot_text += 'Year: ' + str(now.year) + '\n' + + toot_text += '\n' + + toot_text += 'Server bills: ' + str(bills) + '€' + '\n' + + toot_text += 'Donations: ' + str(donations) + '€' + '\n\n' + + if donations != 0 and bills != 0: + + toot_text += '%: ' + str(round((donations * 100)/bills,2)) + '\n\n' + + print(toot_text) + + #mastodon.status_post(toot_text, in_reply_to_id=None) + + diff --git a/db-setup.py b/db-setup.py new file mode 100644 index 0000000..a28f7fb --- /dev/null +++ b/db-setup.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import getpass +import os +import sys +from mastodon import Mastodon +from mastodon.Mastodon import MastodonMalformedEventError, MastodonNetworkError, MastodonReadTimeout, MastodonAPIError +import psycopg2 +from psycopg2 import sql +from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + +# Returns the parameter from the specified file +def get_parameter( parameter, file_path ): + # Check if secrets file exists + if not os.path.isfile(file_path): + print("File %s not found, asking."%file_path) + write_parameter( parameter, file_path ) + #sys.exit(0) + + # Find parameter in file + with open( file_path ) as f: + for line in f: + if line.startswith( parameter ): + return line.replace(parameter + ":", "").strip() + + # Cannot find parameter, exit + print(file_path + " Missing parameter %s "%parameter) + sys.exit(0) + +def write_parameter( parameter, file_path ): + if not os.path.exists('config'): + os.makedirs('config') + print("Setting up finances parameters...") + print("\n") + budget_db = input("budget db name: ") + budget_db_user = input("budget db user: ") + + with open(file_path, "w") as text_file: + print("budget_db: {}".format(budget_db), file=text_file) + print("budget_db_user: {}".format(budget_db_user), file=text_file) + +def create_table(db, db_user, table, sql): + + conn = None + + try: + + conn = psycopg2.connect(database = db, user = db_user, password = "", host = "/var/run/postgresql", port = "5432") + cur = conn.cursor() + + print("Creating table.. "+table) + # Create the table in PostgreSQL database + cur.execute(sql) + + conn.commit() + print("Table "+table+" created!") + print("\n") + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + +############################################################################### +# main + +if __name__ == '__main__': + + # Load configuration from config file + config_filepath = "config/db_config.txt" + budget_db = get_parameter("budget_db", config_filepath) + budget_db_user = get_parameter("budget_db_user", config_filepath) + + ############################################################ + # create database + ############################################################ + + conn = None + + try: + + conn = psycopg2.connect(dbname='postgres', + user=budget_db_user, host='', + password='') + + conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + + cur = conn.cursor() + + print("Creating database " + budget_db + ". Please wait...") + + cur.execute(sql.SQL("CREATE DATABASE {}").format( + sql.Identifier(budget_db)) + ) + print("Database " + budget_db + " created!") + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + finally: + + if conn is not None: + + conn.close() + + ############################################################ + + try: + + conn = None + + conn = psycopg2.connect(database = budget_db, user =budget_db_user, password = "", host = "/var/run/postgresql", port = "5432") + + except (Exception, psycopg2.DatabaseError) as error: + + print(error) + + # Load configuration from config file + os.remove("config/db_config.txt") + + print("Exiting. Run db-setup again with right parameters") + sys.exit(0) + + finally: + + if conn is not None: + + conn.close() + + print("\n") + print("budget parameters saved to db-config.txt!") + print("\n") + + ############################################################ + # Create needed tables + ############################################################ + + db = budget_db + db_user = budget_db_user + table = 'bills' + sql = "create table " + table + " (datetime timestamptz primary key, domain decimal(7,2), server decimal(7,2), backup decimal(7,2), fileserver decimal(7,2)," + sql += "setup decimal(7,2))" + create_table(db, db_user, table, sql) + + ##################################### + + db = budget_db + db_user = budget_db_user + table = 'incomes' + sql = "create table " + table + " (datetime timestamptz primary key, donations decimal(7,2), owner decimal(7,2))" + create_table(db, db_user, table, sql) + + ############################################################ + ############################################################ + + print("Done!") + print("Now you can run setup.py!") + print("\n") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4581e20 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Mastodon.py>=1.5.1 +psycopg2-binary>=2.8.6