summaryrefslogtreecommitdiff
path: root/app.py
diff options
context:
space:
mode:
authordoc <doc@filenotfound.org>2025-06-28 21:03:28 +0000
committerdoc <doc@filenotfound.org>2025-06-28 21:03:28 +0000
commit86ee174c9d81c0ed5672113fcd8e76cf30c671ec (patch)
tree78336d6aee604dad9d385b275fff7016699bd33b /app.py
inital commitHEADmaster
Diffstat (limited to 'app.py')
-rw-r--r--app.py287
1 files changed, 287 insertions, 0 deletions
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..36dff89
--- /dev/null
+++ b/app.py
@@ -0,0 +1,287 @@
+import os
+import uuid
+import threading
+import logging
+from flask import Flask, request, render_template, redirect, url_for, flash
+from flask_sqlalchemy import SQLAlchemy
+from flask_migrate import Migrate
+from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
+from werkzeug.security import generate_password_hash, check_password_hash
+from dotenv import load_dotenv
+import psycopg2
+from mastodon import Mastodon
+import schedule as sch
+import time as t
+from forms import LoginForm, RegistrationForm
+from models import db, User, Toot
+from sqlalchemy.orm import Session
+from flask_wtf import CSRFProtect
+
+# Load env from /etc/radiotoot.env unless overridden
+env_path = os.getenv("ENV_PATH", "/etc/radiotoot.env")
+load_dotenv(dotenv_path=env_path)
+
+# Environment validation
+def validate_env():
+ required_vars = {
+ "SECRET_KEY": "used to secure session cookies and forms",
+ "DATABASE_URL": "PostgreSQL connection string",
+ "MASTODON_ACCESS_TOKEN": "Token for posting to Mastodon"
+ }
+ missing = []
+ for var, reason in required_vars.items():
+ if not os.getenv(var):
+ logging.error(f"Missing required environment variable: {var} — {reason}")
+ missing.append(var)
+ if missing:
+ raise RuntimeError(f"Missing environment variables: {', '.join(missing)}")
+
+validate_env()
+
+# Initialize logging
+logging.basicConfig(level=logging.DEBUG)
+logger = logging.getLogger(__name__)
+
+app = Flask(__name__)
+
+# Securely configure the app secret key
+app.secret_key = os.getenv("SECRET_KEY")
+
+# Initialize CSRF Protection
+csrf = CSRFProtect()
+csrf.init_app(app)
+
+# Configure app SERVER_NAME to support url_for outside requests
+app.config['SERVER_NAME'] = 'toot.themediahub.org:5010'
+app.config['APPLICATION_ROOT'] = '/'
+app.config['PREFERRED_URL_SCHEME'] = 'http'
+
+# Database configuration
+DATABASE_URL = os.getenv("DATABASE_URL")
+logger.debug(f"Using database: {DATABASE_URL}")
+
+def create_db_session():
+ app.config['SQLALCHEMY_DATABASE_URI'] = DATABASE_URL
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
+ try:
+ db.init_app(app)
+ conn = psycopg2.connect(DATABASE_URL)
+ conn.close()
+ return db
+ except Exception as error:
+ logger.error(f"Database connection failed at {DATABASE_URL}: {error}")
+ raise
+
+db = create_db_session()
+migrate = Migrate(app, db)
+
+# Flask-Login configuration
+login_manager = LoginManager()
+login_manager.init_app(app)
+login_manager.login_view = 'login'
+
+# Mastodon instance URL and access token
+api_base_url = 'https://chatwithus.live'
+access_token = os.getenv('MASTODON_ACCESS_TOKEN')
+logger.info(f"Using Mastodon access token: {access_token[:6]}...")
+
+# Initialize Mastodon API
+mastodon = Mastodon(
+ access_token=access_token,
+ api_base_url=api_base_url
+)
+
+@login_manager.user_loader
+def load_user(user_id):
+ with app.app_context():
+ session = db.session
+ logger.debug(f"Loading user with ID: {user_id}")
+ return session.get(User, user_id)
+
+def post_toot(toot):
+ try:
+ if toot.suspended:
+ logger.info(f"Toot '{toot.message}' is suspended. Skipping post.")
+ return
+
+ logger.info(f"Attempting to post toot: {toot.message}")
+ mastodon.status_post(toot.message)
+ logger.info(f"Successfully posted toot: {toot.message}")
+ except Exception as e:
+ logger.error(f"Failed to post toot: {toot.message} due to {e}")
+
+@app.route('/')
+@login_required
+def index():
+ logger.debug("Rendering index page")
+ toots = Toot.query.all()
+ logger.debug(f"Retrieved {len(toots)} toots from the database")
+ return render_template('index.html', toots=toots)
+
+@app.route('/add', methods=['POST'])
+@login_required
+def add_toot():
+ message = request.form['message']
+ toot_time = request.form['toot_time']
+ day = request.form['day'].lower()
+
+ logger.debug(f"Adding new toot with message: {message}, time: {toot_time}, day: {day}")
+
+ new_toot = Toot(
+ id=str(uuid.uuid4()),
+ message=message,
+ toot_time=toot_time,
+ day=day
+ )
+
+ db.session.add(new_toot)
+ db.session.commit()
+
+ schedule_toot(new_toot)
+
+ return redirect(url_for('index'))
+
+@app.route('/delete/<toot_id>', methods=['POST'])
+@login_required
+def delete_toot(toot_id):
+ logger.debug(f"Deleting toot with ID: {toot_id}")
+ toot = Toot.query.get(toot_id)
+ if toot:
+ db.session.delete(toot)
+ db.session.commit()
+ sch.clear(toot_id)
+ logger.info(f"Deleted toot with ID: {toot_id}")
+ else:
+ logger.warning(f"Toot with ID {toot_id} not found")
+
+ return redirect(url_for('index'))
+
+@app.route('/suspend/<toot_id>', methods=['POST'])
+@login_required
+def suspend_toot(toot_id):
+ logger.debug(f"Suspending toot with ID: {toot_id}")
+ toot = Toot.query.get(toot_id)
+ if toot:
+ toot.suspended = True
+ db.session.commit()
+ sch.clear(toot_id)
+ flash(f"Toot '{toot.message}' has been suspended.")
+ logger.info(f"Suspended toot with ID: {toot_id}")
+ else:
+ flash("Toot not found.")
+ logger.warning(f"Toot with ID {toot_id} not found")
+ return redirect(url_for('index'))
+
+@app.route('/resume/<toot_id>', methods=['POST'])
+@login_required
+def resume_toot(toot_id):
+ logger.debug(f"Resuming toot with ID: {toot_id}")
+ toot = Toot.query.get(toot_id)
+ if toot and toot.suspended:
+ toot.suspended = False
+ db.session.commit()
+ schedule_toot(toot)
+ flash(f"Toot '{toot.message}' has been resumed.")
+ logger.info(f"Resumed toot with ID: {toot_id}")
+ else:
+ flash("Toot not found or already active.")
+ logger.warning(f"Toot with ID {toot_id} not found or not suspended")
+ return redirect(url_for('index'))
+
+@app.route('/logout', methods=['POST'])
+@login_required
+def logout():
+ logger.debug("Logging out user")
+ logout_user()
+ return redirect(url_for('login'))
+
+@app.route('/login', methods=['GET', 'POST'])
+def login():
+ logger.debug("Rendering login page")
+ form = LoginForm()
+ logger.debug(f"CSRF token: {form.csrf_token.data}")
+ if form.validate_on_submit():
+ logger.debug(f"Login form submitted with username: {form.username.data}")
+ user = User.query.filter_by(username=form.username.data).first()
+ if user and user.check_password(form.password.data):
+ logger.info(f"User {form.username.data} authenticated successfully")
+ login_user(user)
+ return redirect(url_for('index'))
+ logger.warning(f"Authentication failed for user {form.username.data}")
+ flash('Invalid username or password')
+ return render_template('login.html', form=form)
+
+@app.route('/register', methods=['GET', 'POST'])
+def register():
+ form = RegistrationForm()
+ if form.validate_on_submit():
+ username = form.username.data
+ email = form.email.data
+ password = form.password.data
+ hashed_password = generate_password_hash(password)
+
+ new_user = User(
+ username=username,
+ email=email,
+ password=hashed_password
+ )
+
+ db.session.add(new_user)
+ db.session.commit()
+
+ flash('Your account has been created! You can now log in.', 'success')
+ return redirect(url_for('login'))
+
+ return render_template('register.html', form=form)
+
+scheduler_lock = threading.Lock()
+
+def schedule_toot(toot):
+ try:
+ if toot.suspended:
+ logger.info(f"Toot '{toot.message}' is suspended. Skipping scheduling.")
+ return
+
+ with scheduler_lock:
+ sch.clear(toot.id)
+ day_schedule = {
+ 'monday': sch.every().monday,
+ 'tuesday': sch.every().tuesday,
+ 'wednesday': sch.every().wednesday,
+ 'thursday': sch.every().thursday,
+ 'friday': sch.every().friday,
+ 'saturday': sch.every().saturday,
+ 'sunday': sch.every().sunday,
+ 'everyday': sch.every().day
+ }
+
+ if toot.day in day_schedule:
+ logger.info(f"Scheduling toot: {toot.message} for {toot.day} at {toot.toot_time}")
+ day_schedule[toot.day].at(toot.toot_time).do(post_toot, toot).tag(toot.id)
+ else:
+ logger.error(f"Unknown day: {toot.day}. Unable to schedule toot.")
+ except Exception as e:
+ logger.error(f"Error scheduling toot: {str(e)}")
+
+def run_scheduler():
+ try:
+ while True:
+ sch.run_pending()
+ t.sleep(1)
+ except Exception as e:
+ logger.error(f"Scheduler error: {str(e)}")
+
+def initialize_scheduler():
+ with app.app_context():
+ db.create_all()
+ sch.clear()
+ for toot in Toot.query.all():
+ schedule_toot(toot)
+
+if __name__ == '__main__':
+ if os.getenv("FLASK_ENV") != "development" or os.environ.get("WERKZEUG_RUN_MAIN") == "true":
+ initialize_scheduler()
+ scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
+ scheduler_thread.start()
+
+ app.run(debug=False, host='0.0.0.0', port=5010)