Comparar commits

...

15 commits

S'han modificat 7 arxius amb 814 adicions i 675 eliminacions

Veure arxiu

@ -2,7 +2,7 @@
Mail your Mastodon server inactive users and track their feedback.
This code written in Python get all more than a year inactive users from Mastodon's database and email them with the subject and message of your choice.
This code written in Python get all more than six months inactive users from your Mastodon's database and email them with the subject and message of your choice.
Then, inactive users data is stored into new created Postgresql database to track feedback and status.
Run mailing.py periodically to catch 'new' inactive users and update the elapsed days of the already emailed ones.
@ -11,7 +11,6 @@ Run mailing.py periodically to catch 'new' inactive users and update the elapsed
- **Python 3**
- Postgresql server
- Mastodon server (admin)
- Everything else at the top of `mailing.py`!
### Usage:
@ -21,11 +20,11 @@ Within Python Virtual Environment:
2. Run `python setup.py` to set your SMTP parameters and desired email subject. Also set your Mastodon's full path. They will be saved to `secrets/secrets.txt` for further use.
3. Run `python mailing.py` to start emailing your inactive users (`last_sign_in_at` column older than a year). Their username, account_id, email, delivery status (True if successful) and delivery date will be written to Postgresql database. There is another column, `deleted`, False by default. Will be useful to track deleted/not deleted inactive users if you choose to do so.
3. Run `python mailing.py` to start emailing your inactive users (`current_sign_in_at` column older than six months). Their username, account_id, email, delivery status (True if successful) and delivery date will be written to Postgresql database. There is another column, `deleted`, False by default. Will be useful to track deleted/not deleted inactive users if you choose to do so.
4. Use your favourite scheduling method to set mailing.py to run regularly. Column 'elapsed_days' of mailing's database will be updated so you can decide actions after some time.
5. Run `python delete_inactives.py` to delete all inactive users after 30 days period from the warning email.
5. Run `python delete_inactives.py` to delete all inactive users who replied yes to deletion and all the rest with no feedback after 31 days period from the warning email.
6. Run `python edit_status.py` to set True or False any of following `mailing_db_table` columns: `to_be_deleted`, `feedback` and `recipient_error`. Useful after emailed user's feedback.

Veure arxiu

@ -8,7 +8,19 @@ import psycopg2
from psycopg2 import sql
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
# Returns the parameter from the specified file
def get_config():
# Load configuration from config file
config_filepath = "config/db_config.txt"
mastodon_db = get_parameter("mastodon_db", config_filepath)
mastodon_db_user = get_parameter("mastodon_db_user", config_filepath)
mailing_db = get_parameter("mailing_db", config_filepath)
mailing_db_user = get_parameter("mailing_db_user", config_filepath)
mailing_db_table = get_parameter("mailing_db_table", config_filepath)
inactive_days = get_parameter("inactive_days", config_filepath)
return (mastodon_db, mastodon_db_user, mailing_db, mailing_db_user, mailing_db_table)
def get_parameter( parameter, file_path ):
# Check if secrets file exists
if not os.path.isfile(file_path):
@ -27,28 +39,84 @@ def get_parameter( parameter, file_path ):
sys.exit(0)
def write_parameter( parameter, file_path ):
print("Setting up mailing DB parameters...")
print("\n")
mastodon_db = input("Mastodon db name: ")
mastodon_db_user = input("Mastodon db user: ")
mailing_db = input("Mailing db name: ")
mailing_db_user = input("Mailing db user: ")
mailing_db_table = input("Mailing db table: ")
with open(file_path, "w") as text_file:
print("mastodon_db: {}".format(mastodon_db), file=text_file)
print("mastodon_db_user: {}".format(mastodon_db_user), file=text_file)
print("mailing_db: {}".format(mailing_db), file=text_file)
print("mailing_db_user: {}".format(mailing_db_user), file=text_file)
print("mailing_db_table: {}".format(mailing_db_table), file=text_file)
if not os.path.exists('config'):
os.makedirs('config')
print("Setting up mailing DB parameters...")
print("\n")
mastodon_db = input("Mastodon db name: ")
mastodon_db_user = input("Mastodon db user: ")
mailing_db = input("Mailing db name: ")
mailing_db_user = input("Mailing db user: ")
mailing_db_table = input("Mailing db table: ")
inactive_days = input("Inactive days: ")
with open(file_path, "w") as text_file:
print("mastodon_db: {}".format(mastodon_db), file=text_file)
print("mastodon_db_user: {}".format(mastodon_db_user), file=text_file)
print("mailing_db: {}".format(mailing_db), file=text_file)
print("mailing_db_user: {}".format(mailing_db_user), file=text_file)
print("mailing_db_table: {}".format(mailing_db_table), file=text_file)
print("inactive_days: {}".format(inactive_days), file=text_file)
def create_database():
try:
conn = psycopg2.connect(dbname='postgres',
user=mailing_db_user, host='',
password='')
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
cur = conn.cursor()
print("Creating database " + mailing_db + ". Please wait...")
cur.execute(sql.SQL("CREATE DATABASE {}").format(
sql.Identifier(mailing_db))
)
print("Database " + mailing_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 = mailing_db, user = mailing_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 setup again with right parameters")
sys.exit(0)
if conn is not None:
print("\n")
print("Mailing db parameters saved to config.txt!")
print("\n")
def create_table(db, db_user, table, sql):
try:
conn = None
conn = psycopg2.connect(database = db, user = db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
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
@ -68,85 +136,25 @@ def create_table(db, db_user, table, sql):
conn.close()
#############################################################################################
# main
# Load configuration from config file
config_filepath = "config.txt"
mastodon_db = get_parameter("mastodon_db", config_filepath) # E.g., mastodon_production
mastodon_db_user = get_parameter("mastodon_db_user", config_filepath) # E.g., mastodon
mailing_db = get_parameter("mailing_db", config_filepath) # E.g., inactive
mailing_db_user = get_parameter("mailing_db_user", config_filepath) # E.g., mastodon
mailing_db_table = get_parameter("mailing_db_table", config_filepath) # E.g., inactive_users
if __name__ == '__main__':
############################################################
# create database
############################################################
mastodon_db, mastodon_db_user, mailing_db, mailing_db_user, mailing_db_table = get_config()
try:
create_database()
conn = psycopg2.connect(dbname='postgres',
user=mailing_db_user, host='',
password='')
########################################
# create mailing DB table
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
db = mailing_db
db_user = mailing_db_user
table = mailing_db_table
sql = "create table " + table + "(datetime timestamptz, account_id bigint primary key, username varchar(30), email varchar(60), emailed_at timestamptz,"
sql += "emailed boolean default False, deleted boolean default False, elapsed_days varchar(30), to_be_deleted boolean default False,"
sql += "recipient_error boolean default False, feedback boolean default False, current_sign_in_at timestamptz)"
create_table(db, db_user, table, sql)
cur = conn.cursor()
#####################################
print("Creating database " + mailing_db + ". Please wait...")
cur.execute(sql.SQL("CREATE DATABASE {}").format(
sql.Identifier(mailing_db))
)
print("Database " + mailing_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 = mailing_db, user = mailing_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.txt")
print("Exiting. Run setup again with right parameters")
sys.exit(0)
if conn is not None:
print("\n")
print("Mailing db parameters saved to config.txt!")
print("\n")
############################################################
# Create needed tables
############################################################
print("Creating table...")
########################################
db = mailing_db
db_user = mailing_db_user
table = mailing_db_table
sql = "create table " + table + "(datetime timestamptz, account_id int primary key, username varchar(30), email varchar(50), emailed_at timestamptz,"
sql += "emailed boolean default False, deleted boolean default False, elapsed_days varchar(30), to_be_deleted boolean default False,"
sql += "recipient_error boolean default False, feedback boolean default False)"
create_table(db, db_user, table, sql)
#####################################
print("Done!")
print("Now you can run setup.py!")
print("\n")
print("Done!\nNow you can run setup.py!\n")

Veure arxiu

@ -3,150 +3,173 @@
from datetime import datetime, timezone, timedelta
import time
import threading
import os
import sys
import os.path
import psycopg2
def delete_inactives(mailing_db, mailing_db_user, mailing_db_table, deletion_accepted, query, id_array, username_array):
def delete_inactives(deletion_accepted, query):
conn = None
id_array = []
try:
username_array = []
conn = psycopg2.connect(database = mailing_db, user = mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
conn = None
cur = conn.cursor()
try:
cur.execute(query)
conn = psycopg2.connect(database = mailing_db, user = mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
for row in cur:
cur = conn.cursor()
id_array.append(row[0])
username_array.append(row[1])
cur.execute(query)
i = 0
if len(id_array) == 0:
if deletion_accepted == True:
print("None inactive users who accepted to be deleted found!")
elif deletion_accepted == False:
print("None inactive users to be deleted!")
return
for row in cur:
while i < len(id_array):
id_array.append(row[0])
if deletion_accepted == True:
print("Deleting inactive users who accepted to be deleted...")
elif deletion_accepted == False:
print("Deleting inactive users who do not reply our email...")
username_array.append(row[1])
print("\n")
print("Deleting user " + str(i) + " of " + str(len(id_array)) + " users")
print("Deleting user with id " + str(id_array[i]) + ", username: " + username_array[i])
os.system("RAILS_ENV=production " + rvm_ruby + " " + mastodon_full_path + "/bin/tootctl accounts delete " + username_array[i])
delete_user(id_array[i], username_array[i])
i += 1
i = 0
cur.close()
if len(id_array) == 0:
except (Exception, psycopg2.DatabaseError) as error:
if deletion_accepted == True:
print(error)
print("None inactive users who accepted to be deleted found!")
finally:
elif deletion_accepted == False:
if conn is not None:
print("None inactive users to be deleted!")
conn.close()
return
###################################################################################
# update user as deleted
###################################################################################
while i < len(id_array):
if deletion_accepted == True:
print("Deleting inactive users who accepted to be deleted...")
elif deletion_accepted == False:
print("Deleting inactive users who do not reply our email...")
print(f"\nDeleting user {str(i)} of {str(len(id_array))} users")
print(f"Deleting user with id {str(id_array[i])}, username: {username_array[i]}")
os.system(f"RAILS_ENV=production {rvm_ruby} {mastodon_full_path}/bin/tootctl accounts delete {username_array[i]}")
delete_user(id_array[i], username_array[i])
i += 1
cur.close()
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
def delete_user(id, username):
conn = None
conn = None
try:
try:
conn = psycopg2.connect(database = mailing_db, user = mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
conn = psycopg2.connect(database = mailing_db, user = mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur = conn.cursor()
cur.execute("DELETE FROM " + mailing_db_table + " where account_id=(%s)", (str(id),))
print("Deleting user " + str(id) + ", username " + str(username))
cur.execute("DELETE FROM " + mailing_db_table + " where account_id=(%s)", (str(id),))
conn.commit()
print(f"Deleting user {str(id)}, username {str(username)}")
cur.close()
conn.commit()
except (Exception, psycopg2.DatabaseError) as error:
cur.close()
print(error)
except (Exception, psycopg2.DatabaseError) as error:
finally:
print(error)
if conn is not None:
finally:
conn.close()
if conn is not None:
###############################################################################
# INITIALISATION
###############################################################################
conn.close()
# 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):
if file_path == "secrets/secrets.txt":
print("File %s not found, exiting. Run setup.py."%file_path)
print("File %s not found, exiting. Run setup.py."%file_path)
elif file_path == "config.txt":
print("File %s not found, exiting. Run db-setup.py."%file_path)
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(f"{file_path} Missing parameter {parameter}")
print("Run setup.py")
sys.exit(0)
# Load secrets from secrets file
secrets_filepath = "secrets/secrets.txt"
mastodon_full_path = get_parameter("mastodon_full_path", secrets_filepath)
def config():
# Load configuration from config file
config_filepath = "config.txt"
mailing_db = get_parameter("mailing_db", config_filepath)
mailing_db_user = get_parameter("mailing_db_user", config_filepath)
mailing_db_table = get_parameter("mailing_db_table", config_filepath)
secrets_filepath = "secrets/secrets.txt"
mastodon_full_path = get_parameter("mastodon_full_path", secrets_filepath)
# Load configuration from config file
config_filepath = "config.txt"
mailing_db = get_parameter("mailing_db", config_filepath)
mailing_db_user = get_parameter("mailing_db_user", config_filepath)
mailing_db_table = get_parameter("mailing_db_table", config_filepath)
return (mastodon_full_path, mailing_db, mailing_db_user, mailing_db_table)
###############################################################################
# main
global rvm_ruby
rvm_ruby = os.environ['HOME'] + "/.rbenv/shims/ruby"
if __name__ == '__main__':
###############################################################################
# select users who replied the warning email saying yes to deletion
###############################################################################
mastodon_full_path, mailing_db, mailing_db_user, mailing_db_table = config()
deletion_accepted = True
query = "select account_id, username, email, to_be_deleted, feedback, recipient_error, elapsed_days from " + mailing_db_table + " where to_be_deleted = 't' and feedback = 't' and recipient_error = 'f' and emailed_at < now() - interval '31 days'"
id_array = []
username_array = []
delete_inactives(mailing_db, mailing_db_user, mailing_db_table, deletion_accepted, query, id_array, username_array)
global rvm_ruby
rvm_ruby = os.environ['HOME'] + "/.rbenv/shims/ruby"
###############################################################################
# select users who don't replied to email after 30 days
###############################################################################
###############################################################################
# select and delete users who replied the warning email saying yes to deletion
deletion_accepted = False
query = "select account_id, username, email, to_be_deleted, feedback, recipient_error, elapsed_days from " + mailing_db_table + " where to_be_deleted = 'f' and feedback = 'f' and recipient_error = 'f' and emailed_at < now() - interval '31 days'"
id_array = []
username_array = []
delete_inactives(mailing_db, mailing_db_user, mailing_db_table, deletion_accepted, query, id_array, username_array)
deletion_accepted = True
query = "select account_id, username, email, to_be_deleted, feedback, recipient_error, elapsed_days from " + mailing_db_table + " where to_be_deleted and feedback"
delete_inactives(deletion_accepted, query)
###############################################################################
# select and delete users who don't replied the email after 31 days
deletion_accepted = False
query = "select account_id, username, email, to_be_deleted, feedback, recipient_error, elapsed_days from " + mailing_db_table + " where not feedback and elapsed_days = '31'"
delete_inactives(deletion_accepted, query)

Veure arxiu

@ -8,123 +8,101 @@ import os
import sys
import os.path
import psycopg2
##############################################################################
# ask what user to be edit
##############################################################################
def ask_what_user():
try:
useremail = input("Enter user email address: (press q to quit) ")
if useremail == 'q':
sys.exit()
else:
user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback = get_user(useremail)
return (user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback)
except:
if useremail != 'q':
print("Enter an existent email address!")
sys.exit("Good bye!")
###############################################################################
# get user row
###############################################################################
try:
from prettytable import PrettyTable
except ModuleNotFoundError as mod_not_found:
print(f"{mod_not_found}. Run 'pip install -r requirements.txt' and try again")
def get_user(email):
try:
found_it = False
conn = psycopg2.connect(database = mailing_db, user = mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
try:
cur = conn.cursor()
conn = psycopg2.connect(database = mailing_db, user = mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur.execute("SELECT account_id, username, email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, feedback FROM " + mailing_db_table + " where email=(%s)", (email,))
cur = conn.cursor()
row = cur.fetchone()
cur.execute("SELECT account_id, username, email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, feedback FROM " + mailing_db_table + " where email=(%s)", (email,))
user_id = row[0]
user_name = row[1]
user_email = row[2]
emailed_at = row[3].replace(tzinfo=None)
deleted = row[4]
elapsed_days = row[5]
to_be_deleted = row[6]
recipient_error = row[7]
user_feedback = row[8]
row = cur.fetchone()
cur.close()
return (user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback)
if row != None:
except (Exception, psycopg2.DatabaseError) as error:
found_it = True
print(error)
user_id = row[0]
user_name = row[1]
user_email = row[2]
emailed_at = row[3].replace(tzinfo=None)
deleted = row[4]
elapsed_days = row[5]
to_be_deleted = row[6]
recipient_error = row[7]
user_feedback = row[8]
finally:
else:
if conn is not None:
user_id = ''
user_name = ''
user_email = ''
emailed_at = ''
deleted = False
elapsed_days = '0'
to_be_deleted = False
recipient_error = False
user_feedback = False
conn.close()
cur.close()
###################################################################################
# write to database mailing status of inactive users
###################################################################################
return (found_it, user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback)
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
def update_user(will_be_deleted, recip_error, user_feedback, id):
conn = None
conn = None
try:
try:
conn = psycopg2.connect(database = mailing_db, user = mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
conn = psycopg2.connect(database = mailing_db, user = mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur = conn.cursor()
cur.execute("UPDATE " + mailing_db_table + " SET to_be_deleted=(%s), recipient_error=(%s), feedback=(%s) where account_id=(%s)", (will_be_deleted, recip_error, user_feedback, id))
print("\n")
print("Updating user " + str(id))
cur.execute("UPDATE " + mailing_db_table + " SET to_be_deleted=(%s), recipient_error=(%s), feedback=(%s) where account_id=(%s)", (will_be_deleted, recip_error, user_feedback, id))
conn.commit()
print(f"\nUpdating user {str(id)}")
cur.close()
conn.commit()
except (Exception, psycopg2.DatabaseError) as error:
cur.close()
print(error)
except (Exception, psycopg2.DatabaseError) as error:
finally:
print(error)
if conn is not None:
finally:
conn.close()
if conn is not None:
##################################################################################
# print user data
##################################################################################
conn.close()
def print_user(user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback):
print("\n")
print("--------------------------------------------------------------------------------------------------------------------------------")
print("| account_id: " + str(user_id) + " | username: " + str(user_name) + " | email: " + str(user_email) + " | emailed at: " + str(emailed_at) + " |")
print("| deleted: " + str(deleted) + " | elapsed days: " + str(elapsed_days) + " | to be deleted: " + str(to_be_deleted) + " | recipient error: " + str(recipient_error) + " | user feedback: " +str(user_feedback) + " |")
print("--------------------------------------------------------------------------------------------------------------------------------")
print("\n")
###############################################################################
# INITIALISATION
###############################################################################
# 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):
if file_path == "secrets/secrets.txt":
print("File %s not found, exiting. Run setup.py."%file_path)
print(f"File {file_path} not found, exiting. Run setup.py.")
elif file_path == "config.txt":
print("File %s not found, exiting. Run db-setup.py."%file_path)
print(f"File {file_path} not found, exiting. Run db-setup.py.")
sys.exit(0)
# Find parameter in file
@ -134,44 +112,81 @@ def get_parameter( parameter, file_path ):
return line.replace(parameter + ":", "").strip()
# Cannot find parameter, exit
print(file_path + " Missing parameter %s "%parameter)
print("Run setup.py")
print(f"{file_path} Missing parameter {parameter}\nRun setup.py")
sys.exit(0)
# Load configuration from config file
config_filepath = "config.txt"
mastodon_db = get_parameter("mastodon_db", config_filepath)
mastodon_db_user = get_parameter("mastodon_db_user", config_filepath)
mailing_db = get_parameter("mailing_db", config_filepath)
mailing_db_user = get_parameter("mailing_db_user", config_filepath)
mailing_db_table = get_parameter("mailing_db_table", config_filepath)
def db_config():
###############################################################################
# Load configuration from config file
config_filepath = "config.txt"
mastodon_db = get_parameter("mastodon_db", config_filepath)
mastodon_db_user = get_parameter("mastodon_db_user", config_filepath)
mailing_db = get_parameter("mailing_db", config_filepath)
mailing_db_user = get_parameter("mailing_db_user", config_filepath)
mailing_db_table = get_parameter("mailing_db_table", config_filepath)
while True:
return (mastodon_db, mastodon_db_user, mailing_db, mailing_db_user, mailing_db_table)
user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback = ask_what_user()
# main
print_user(user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback)
if __name__ == '__main__':
willdeleteit = input("Do you want to mark user to be deleted? (press t for True or f for False) ")
if willdeleteit == 'f':
willdeleteit = 'False'
elif willdeleteit == 't':
willdeleteit = 'True'
mastodon_db, mastodon_db_user, mailing_db, mailing_db_user, mailing_db_table = db_config()
recip_error = input("Was the email refused? (press t for True or f for False) ")
if recip_error == 'f':
recip_error = 'False'
elif recip_error == 't':
recip_error = 'True'
while True:
useremail = input("Enter user email address: (press q to quit) ")
if useremail == 'q':
sys.exit('Bye.')
else:
found_it, user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback = get_user(useremail)
if found_it:
print_table = PrettyTable()
print_table.field_names = ['id', 'username', 'email', 'emailed_at', 'deleted', 'elapsed_days', 'to_be_deleted', 'recipient_error', 'user_feedback']
print_table.add_row([user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback])
print(f'\n{print_table}\n')
willdeleteit = input("Do you want to mark user to be deleted? (press t for True or f for False) ")
if willdeleteit == 'f':
willdeleteit = 'False'
elif willdeleteit == 't':
willdeleteit = 'True'
recip_error = input("Was the email refused? (press t for True or f for False) ")
if recip_error == 'f':
recip_error = 'False'
elif recip_error == 't':
recip_error = 'True'
user_feed = input("Have the user replied? (press t for True or f for False) ")
if user_feed == 'f':
user_feed = 'False'
elif user_feed == 't':
user_feed = 'True'
update_user(willdeleteit, recip_error, user_feed, user_id)
found_it, user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback = get_user(useremail)
print_table = PrettyTable()
print_table.field_names = ['id', 'username', 'email', 'emailed_at', 'deleted', 'elapsed_days', 'to_be_deleted', 'recipient_error', 'user_feedback']
print_table.add_row([user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback])
print(f'\n{print_table}\n')
else:
print(f'email {useremail} not found!')
user_feed = input("Have the user replied? (press t for True or f for False) ")
if user_feed == 'f':
user_feed = 'False'
elif user_feed == 't':
user_feed = 'True'
update_user(willdeleteit, recip_error, user_feed, user_id)
user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback = get_user(user_email)
print_user(user_id, user_name, user_email, emailed_at, deleted, elapsed_days, to_be_deleted, recipient_error, user_feedback)

468
inactives.py Normal file
Veure arxiu

@ -0,0 +1,468 @@
import os
import sys
from datetime import datetime, timezone, timedelta
import time
try:
import psycopg2
except ModuleNotFoundError as mod_not_found:
print(f"{mod_not_found}'. Run 'pip install -r requirements.txt' and try again")
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib
from smtplib import SMTPException, SMTPAuthenticationError, SMTPConnectError, SMTPRecipientsRefused
import socket
from socket import gaierror
def smtp_config():
secrets_filepath = "secrets/secrets.txt"
smtp_host = get_parameter("smtp_host", secrets_filepath)
smtp_user_login = get_parameter("smtp_user_login", secrets_filepath)
smtp_user_password = get_parameter("smtp_user_password", secrets_filepath)
email_subject = get_parameter("email_subject", secrets_filepath)
return (smtp_host, smtp_user_login, smtp_user_password, email_subject)
def db_config():
config_filepath = "config/db_config.txt"
mastodon_db = get_parameter("mastodon_db", config_filepath)
mastodon_db_user = get_parameter("mastodon_db_user", config_filepath)
mailing_db = get_parameter("mailing_db", config_filepath)
mailing_db_user = get_parameter("mailing_db_user", config_filepath)
mailing_db_table = get_parameter("mailing_db_table", config_filepath)
inactive_days = get_parameter("inactive_days", config_filepath)
return (mastodon_db, mastodon_db_user, mailing_db, mailing_db_user, mailing_db_table, inactive_days)
def get_parameter( parameter, file_path ):
# Check if secrets file exists
if not os.path.isfile(file_path):
if file_path == "secrets/secrets.txt":
print("File %s not found, exiting. Run setup.py."%file_path)
elif 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)
class Inactives:
name = "Inactives"
def __init__(self):
smtp_host, smtp_user_login, smtp_user_password, email_subject = smtp_config()
mastodon_db, mastodon_db_user, mailing_db, mailing_db_user, mailing_db_table, inactive_days = db_config()
self.smtp_host = smtp_host
self.smtp_user_login = smtp_user_login
self.smtp_user_password = smtp_user_password
self.email_subject = email_subject
self.mastodon_db = mastodon_db
self.mastodon_db_user = mastodon_db_user
self.mailing_db = mailing_db
self.mailing_db_user = mailing_db_user
self.mailing_db_table = mailing_db_table
self.inactive_days = inactive_days
self.now = datetime.now(timezone.utc)
def id(self):
###############################################################################
# get id of inactive users from inactive database
conn = None
try:
conn = psycopg2.connect(database = self.mailing_db, user = self.mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur.execute("SELECT account_id FROM " + self.mailing_db_table)
rows = cur.fetchall()
if rows != []:
inactive_users_id = []
for row in rows:
inactive_users_id.append(row[0])
else:
inactive_users_id = []
cur.close()
return inactive_users_id
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
def delete(self,ids_lst):
print(f'\nChecking {len(ids_lst)} ids to delete reactivated or deleted accounts...')
i = 0
while i < len(ids_lst):
seen = self.check_alive(self, ids_lst[i])
try:
conn = psycopg2.connect(database = self.mailing_db, user = self.mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur.execute("SELECT emailed_at FROM " + self.mailing_db_table + " where account_id=(%s)", (ids_lst[i],))
row = cur.fetchone()
email_datetime = row[0]
email_datetime = email_datetime.replace(tzinfo=None)
if seen != None:
reactivated = email_datetime < seen
last_year = datetime.today() - timedelta(days=int(self.inactive_days))
if reactivated == True or seen == None or seen > last_year: #if inactive user had reactivated its account or had deleted it we must delete related row from 'mailing_db_table'
cur.execute("DELETE FROM " + self.mailing_db_table + " where account_id=(%s)", (ids_lst[i],))
print(f"Deleting user {ids_lst[i]}")
conn.commit()
cur.close()
i += 1
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
def get_inactives(self):
###############################################################################
# Connect to Mastodon's Postgres DB to get last six months inactive users
###############################################################################
conn = None
try:
conn = psycopg2.connect(database = self.mastodon_db, user = self.mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur.execute("select account_id, email, current_sign_in_at from users where current_sign_in_at < now() - interval '" + self.inactive_days + " days' and disabled=False and approved=True order by current_sign_in_at desc")
rows = cur.fetchall()
inactive_account_id = []
inactive_email = []
current_sign_in_at = []
for row in rows:
inactive_account_id.append(row[0])
inactive_email.append(row[1])
current_sign_in_at.append(row[2])
cur.close()
print(f'inactive accounts found: {len(inactive_account_id)}')
return (inactive_account_id, inactive_email, current_sign_in_at)
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
def usernames(self, account_ids):
########################################################################################################
# get accounts usernames #
########################################################################################################
inactive_usernames = []
i = 0
while i < len(account_ids):
conn = None
try:
conn = psycopg2.connect(database = self.mastodon_db, user = self.mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
inactive_id = account_ids[i]
cur.execute("select username from accounts where id = '%s';", [inactive_id])
row = cur.fetchone()
new_username = row[0]
inactive_usernames.append(new_username)
i += 1
cur.close()
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
return inactive_usernames
def mailing(self, account_ids, account_emails, account_usernames, account_current_sign_in_ats):
###########################################################################################################
# email inactive users
try:
fp = open('message.txt')
text = fp.read()
message = MIMEText(text)
fp.close()
except:
print("message.txt file not found! Create it and write in the message you want for your inactive users.")
sys.exit(0)
i = 0
while i < len(account_emails):
been_emailed = self.email_sent(self, account_ids[i])
if been_emailed == False:
# Create message object instance
msg = MIMEMultipart()
# Declare message elements
msg['From'] = self.smtp_user_login
msg['To'] = account_emails[i]
msg['Subject'] = account_usernames[i] + " " + self.email_subject
# Add the message body to the object instance
msg.attach(message)
try:
# Create the server connection
server = smtplib.SMTP(self.smtp_host)
# Switch the connection over to TLS encryption
server.starttls()
# Authenticate with the server
server.login(self.smtp_user_login, self.smtp_user_password)
# Send the message
server.sendmail(msg['From'], msg['To'], msg.as_string())
# Disconnect
server.quit()
print("Successfully sent email message to %s" % msg['To'])
emailed = True
self.write_db(self, self.now, account_ids[i], account_usernames[i], account_emails[i], self.now, emailed, account_current_sign_in_ats[i])
i += 1
time.sleep(5)
except SMTPAuthenticationError as auth_error:
print(auth_error)
sys.exit(":-(")
except socket.gaierror as socket_error:
print(socket_error)
print("Unknown SMTP server")
sys.exit(":-(")
except SMTPRecipientsRefused as recip_error:
print(recip_error)
emailed = False
self.write_db(self, self.now, account_ids[i], account_usernames[i], account_emails[i], self.now, emailed, account_current_sign_in_ats[i])
i += 1
else:
emailed = True
self.write_db(self, self.now, account_ids[i], account_usernames[i], account_emails[i], self.now, emailed, account_current_sign_in_ats[i])
i += 1
@staticmethod
def write_db(self, now, id, username, email, emailed_at, emailed, current_sign_in_at):
###################################################################################
# write to mailing database the status of inactive users
insert_line = "INSERT INTO " + self.mailing_db_table + "(datetime, account_id, username, email, emailed_at, emailed, current_sign_in_at) VALUES(%s,%s,%s,%s,%s,%s,%s) ON CONFLICT DO NOTHING"
conn = None
try:
conn = psycopg2.connect(database = self.mailing_db, user = self.mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur.execute("SELECT account_id FROM " + self.mailing_db_table + " where account_id=(%s)", (id,))
row = cur.fetchone()
if row == None:
cur.execute(insert_line, (now, id, username, email, now, emailed, current_sign_in_at))
else:
if emailed == True:
cur.execute("SELECT datetime FROM " + self.mailing_db_table + " where account_id=(%s)", (id,))
row = cur.fetchone()
delta = now-row[0]
cur.execute("UPDATE " + self.mailing_db_table + " SET elapsed_days=(%s), email=(%s), emailed=(%s), current_sign_in_at=(%s) where account_id=(%s)", (delta.days, email, emailed, current_sign_in_at, id))
print(f"Updating user {str(id)}")
conn.commit()
cur.close()
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
@staticmethod
def email_sent(self, account_id):
###############################################################################
# check if inactive user had been already emailed
been_emailed = False
conn = None
try:
conn = psycopg2.connect(database = self.mailing_db, user = self.mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur.execute("SELECT emailed FROM " + self.mailing_db_table + " where account_id=(%s)", (account_id,))
row = cur.fetchone()
if row != None:
been_emailed = row[0]
cur.close()
return been_emailed
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
@staticmethod
def check_alive(self, id):
conn = None
try:
conn = psycopg2.connect(database = self.mastodon_db, user = self.mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur.execute("select current_sign_in_at from users where account_id=(%s)", (id,))
row = cur.fetchone()
if row != None:
seen = row[0]
else:
seen = None
cur.close()
return seen
except (Exception, psycopg2.DatabaseError) as error:
print(error)

Veure arxiu

@ -1,396 +1,21 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from datetime import datetime, timezone, timedelta
import time
import threading
import os
import sys
import os.path
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib
from smtplib import SMTPException, SMTPAuthenticationError, SMTPConnectError, SMTPRecipientsRefused
import psycopg2
import socket
from socket import gaierror
from inactives import Inactives
###################################################################################
# write to database mailing status of inactive users
###################################################################################
# main
def write_db(now, id, username, email, emailed_at, emailed):
if __name__ == '__main__':
insert_line = "INSERT INTO " + mailing_db_table + "(datetime, account_id, username, email, emailed_at, emailed) VALUES(%s,%s,%s,%s,%s,%s) ON CONFLICT DO NOTHING"
inactives = Inactives()
conn = None
inactives_ids = inactives.id()
try:
inactives.delete(inactives_ids)
conn = psycopg2.connect(database = mailing_db, user = mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
account_ids, account_emails, account_last_sign_in_ats = inactives.get_inactives()
cur = conn.cursor()
account_usernames = inactives.usernames(account_ids)
cur.execute("SELECT account_id FROM " + mailing_db_table + " where account_id=(%s)", (id,))
inactives.mailing(account_ids, account_emails, account_usernames, account_last_sign_in_ats)
row = cur.fetchone()
if row == None:
cur.execute(insert_line, (now, id, username, email, now, emailed))
else:
if emailed == True:
cur.execute("SELECT datetime FROM " + mailing_db_table + " where account_id=(%s)", (id,))
row = cur.fetchone()
delta = now-row[0]
cur.execute("UPDATE " + mailing_db_table + " SET elapsed_days=(%s), email=(%s), emailed=(%s) where account_id=(%s)", (delta, email, emailed, id))
print("Updating user " + str(id))
conn.commit()
cur.close()
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
###############################################################################
# check if inactive user had been already emailed
###############################################################################
def email_sent(id):
try:
conn = psycopg2.connect(database = mailing_db, user = mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur.execute("SELECT emailed FROM " + mailing_db_table + " where account_id=(%s)", (id,))
row = cur.fetchone()
if row == None:
been_emailed = False
else:
been_emailed = row[0]
cur.close()
return been_emailed
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
###############################################################################
# Connect to Mastodon's Postgres DB to check if any inactive user is back online
###############################################################################
def check_alive(id):
try:
conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur.execute("select last_sign_in_at from users where account_id=(%s)", (id,))
row = cur.fetchone()
if row != None:
seen = row[0]
else:
seen = None
cur.close()
return seen
except (Exception, psycopg2.DatabaseError) as error:
print(error)
###############################################################################
# INITIALISATION
###############################################################################
# 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):
if file_path == "secrets/secrets.txt":
print("File %s not found, exiting. Run setup.py."%file_path)
elif file_path == "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 secrets from secrets file
secrets_filepath = "secrets/secrets.txt"
smtp_host = get_parameter("smtp_host", secrets_filepath)
smtp_user_login = get_parameter("smtp_user_login", secrets_filepath)
smtp_user_password = get_parameter("smtp_user_password", secrets_filepath)
email_subject = get_parameter("email_subject", secrets_filepath)
# Load configuration from config file
config_filepath = "config.txt"
mastodon_db = get_parameter("mastodon_db", config_filepath)
mastodon_db_user = get_parameter("mastodon_db_user", config_filepath)
mailing_db = get_parameter("mailing_db", config_filepath)
mailing_db_user = get_parameter("mailing_db_user", config_filepath)
mailing_db_table = get_parameter("mailing_db_table", config_filepath)
###############################################################################
# check if inactive user is back online
###############################################################################
try:
conn = psycopg2.connect(database = mailing_db, user = mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur.execute("SELECT account_id FROM " + mailing_db_table)
rows = cur.fetchall()
if rows != []:
inactive_users_id = []
for row in rows:
inactive_users_id.append(row[0])
else:
inactive_users_id = []
cur.close()
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
i = 0
while i < len(inactive_users_id):
seen = check_alive(inactive_users_id[i])
try:
conn = psycopg2.connect(database = mailing_db, user = mailing_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur.execute("SELECT emailed_at FROM " + mailing_db_table + " where account_id=(%s)", (inactive_users_id[i],))
row = cur.fetchone()
email_datetime = row[0]
email_datetime = email_datetime.replace(tzinfo=None)
if seen != None:
reactivated = email_datetime < seen
last_year = datetime.today() - timedelta(days=365)
if reactivated == True or seen == None or seen > last_year: #if inactive user had reactivated its account or had deleted it we must delete related row from 'mailing_db_table'
cur.execute("DELETE FROM " + mailing_db_table + " where account_id=(%s)", (inactive_users_id[i],))
print("Deleting user " + str(inactive_users_id[i]))
conn.commit()
cur.close()
i += 1
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
###############################################################################
now = datetime.now(timezone.utc)
###############################################################################
# Connect to Mastodon's Postgres DB to get last year inactive users
###############################################################################
try:
conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
cur.execute("select account_id, email from users where last_sign_in_at < now() - interval '365 days' and disabled=False and approved=True order by last_sign_in_at desc;")
rows = cur.fetchall()
inactive_account_id = []
inactive_email = []
for row in rows:
inactive_account_id.append(row[0])
inactive_email.append(row[1])
cur.close()
except (Exception, psycopg2.DatabaseError) as error:
print(error)
########################################################################################################
# get related usernames #
########################################################################################################
inactive_usernames = []
i = 0
while i < len(inactive_account_id):
try:
conn = psycopg2.connect(database = mastodon_db, user = mastodon_db_user, password = "", host = "/var/run/postgresql", port = "5432")
cur = conn.cursor()
inactive_id = inactive_account_id[i]
cur.execute("select username from accounts where id = '%s';", [inactive_id])
row = cur.fetchone()
new_username = row[0]
inactive_usernames.append(new_username)
i += 1
cur.close()
except (Exception, psycopg2.DatabaseError) as error:
print(error)
finally:
if conn is not None:
conn.close()
###########################################################################################################
# email inactive users
###########################################################################################################
try:
fp = open('message.txt')
text = fp.read()
message = MIMEText(text)
fp.close()
except:
print("message.txt file not found! Create it and write in the message you want for your inactive users.")
sys.exit(0)
i = 0
while i < len(inactive_email):
been_emailed = email_sent(inactive_account_id[i])
if been_emailed == False:
# Create message object instance
msg = MIMEMultipart()
# Declare message elements
msg['From'] = smtp_user_login
msg['To'] = inactive_email[i]
msg['Subject'] = inactive_usernames[i] + " " + email_subject
# Add the message body to the object instance
msg.attach(message)
try:
# Create the server connection
server = smtplib.SMTP(smtp_host)
# Switch the connection over to TLS encryption
server.starttls()
# Authenticate with the server
server.login(smtp_user_login, smtp_user_password)
# Send the message
server.sendmail(msg['From'], msg['To'], msg.as_string())
# Disconnect
server.quit()
print("Successfully sent email message to %s" % msg['To'])
emailed = True
write_db(now, inactive_account_id[i], inactive_usernames[i], inactive_email[i], now, emailed)
i += 1
time.sleep(5)
except SMTPAuthenticationError as auth_error:
print(auth_error)
sys.exit(":-(")
except socket.gaierror as socket_error:
print(socket_error)
print("Unknown SMTP server")
sys.exit(":-(")
except SMTPRecipientsRefused as recip_error:
print(recip_error)
emailed = False
write_db(now, inactive_account_id[i], inactive_usernames[i], inactive_email[i], now, emailed)
i += 1
else:
emailed = True
write_db(now, inactive_account_id[i], inactive_usernames[i], inactive_email[i], now, emailed)
i += 1

Veure arxiu

@ -1 +1,2 @@
psycopg2-binary>=2.8.4
psycopg2-binary
prettytable