From b8edf8a7f151987cd1a61e0232fcb35bbec7b781 Mon Sep 17 00:00:00 2001 From: ComputerTech312 Date: Fri, 11 Oct 2024 19:36:58 +0100 Subject: [PATCH] Improved modapp --- app.py | 139 +++++++++++++++++++++++++++++++++++++++--- requirements.txt | 7 ++- static/css/styles.css | 62 +++++++++++++++++++ templates/browse.html | 7 ++- templates/faq.html | 123 +++++++++++++++++++++++++++++++++++++ templates/index.html | 2 +- templates/login.html | 4 ++ templates/modapp.html | 29 +++++++-- templates/quote.html | 4 +- templates/random.html | 20 +++--- templates/search.html | 4 ++ templates/submit.html | 4 +- 12 files changed, 378 insertions(+), 27 deletions(-) create mode 100644 templates/faq.html diff --git a/app.py b/app.py index f9d3eb7..a6a2f7f 100644 --- a/app.py +++ b/app.py @@ -1,11 +1,15 @@ -from flask import Flask, render_template, request, redirect, url_for, flash, abort, make_response, session +from flask import Flask, render_template, request, redirect, url_for, flash, abort, make_response, session, jsonify from flask_sqlalchemy import SQLAlchemy +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address +from flask_cors import CORS import datetime import json import random from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError + app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///quotes.db' app.config['SECRET_KEY'] = 'your_secret_key' # Use environment variable in production @@ -21,13 +25,18 @@ ADMIN_CREDENTIALS = { 'password': '$argon2i$v=19$m=65536,t=4,p=1$cWZDc1pQaUJLTUJoaVI4cw$kn8XKz6AEZi8ebXfyyZuzommSypliVFrsGqzOyUEIHA' # Example hash } -# Define the Quote model +# Define the Quote modelclass Quote(db.Model): class Quote(db.Model): id = db.Column(db.Integer, primary_key=True) text = db.Column(db.Text, nullable=False) votes = db.Column(db.Integer, default=0) date = db.Column(db.DateTime, default=datetime.datetime.utcnow) status = db.Column(db.Integer, default=0) # 0 = pending, 1 = approved, 2 = rejected + ip_address = db.Column(db.String(45)) # Store IPv4 and IPv6 addresses + user_agent = db.Column(db.String(255)) # Store user-agent strings + submitted_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) + + # Home route to display quotes # Home route to display quotes @@ -51,7 +60,11 @@ def submit(): flash("Quote cannot be empty.", 'error') return redirect(url_for('submit')) - new_quote = Quote(text=quote_text) + ip_address = request.remote_addr # Get the user's IP address + user_agent = request.headers.get('User-Agent') # Get the user's browser info + + new_quote = Quote(text=quote_text, ip_address=ip_address, user_agent=user_agent) + try: db.session.add(new_quote) db.session.commit() @@ -68,7 +81,6 @@ def submit(): return render_template('submit.html', approved_count=approved_count, pending_count=pending_count) - # Route to handle voting (upvote/downvote) @app.route('/vote//') def vote(id, action): @@ -151,6 +163,10 @@ def quote(): quote = Quote.query.get_or_404(quote_id) return render_template('quote.html', quote=quote) +@app.route('/faq') +def faq(): + return render_template('faq.html') + # Admin login route @app.route('/login', methods=['GET', 'POST']) def login(): @@ -178,11 +194,11 @@ def modapp(): if not session.get('admin'): flash('You need to log in first.', 'danger') return redirect(url_for('login')) - - # Fetch all quotes (pending, approved, and rejected) - all_quotes = Quote.query.order_by(Quote.date.desc()).all() - return render_template('modapp.html', all_quotes=all_quotes) + all_quotes = Quote.query.order_by(Quote.date.desc()).all() + total_quotes = Quote.query.filter_by(status=1).count() # Count only approved quotes + + return render_template('modapp.html', all_quotes=all_quotes, total_quotes=total_quotes) @app.route('/modapp/bulk_action', methods=['POST']) def bulk_action(): @@ -298,6 +314,113 @@ def logout(): with app.app_context(): db.create_all() + +# Initialize rate limiter and CORS for cross-origin API access +limiter = Limiter(app, key_func=get_remote_address) +CORS(app) + +# API to get all approved quotes +@app.route('/api/quotes', methods=['GET']) +def get_all_quotes(): + quotes = Quote.query.filter_by(status=1).all() # Only approved quotes + quote_list = [{ + 'id': quote.id, + 'text': quote.text, + 'votes': quote.votes, + 'date': quote.date.strftime('%Y-%m-%d') + } for quote in quotes] + + return jsonify(quote_list), 200 + +# API to get a specific quote by ID +@app.route('/api/quotes/', methods=['GET']) +def get_quote(id): + quote = Quote.query.filter_by(id=id, status=1).first_or_404() # Only approved quotes + quote_data = { + 'id': quote.id, + 'text': quote.text, + 'votes': quote.votes, + 'date': quote.date.strftime('%Y-%m-%d') + } + return jsonify(quote_data), 200 + +# API to get a random approved quote +@app.route('/api/random', methods=['GET']) +def get_random_quote(): + count = Quote.query.filter_by(status=1).count() + if count == 0: + return jsonify({"error": "No approved quotes available"}), 404 + + random_offset = random.randint(0, count - 1) + random_quote = Quote.query.filter_by(status=1).offset(random_offset).first() + + quote_data = { + 'id': random_quote.id, + 'text': random_quote.text, + 'votes': random_quote.votes, + 'date': random_quote.date.strftime('%Y-%m-%d') + } + return jsonify(quote_data), 200 + +# API to get the top quotes by vote count +@app.route('/api/top', methods=['GET']) +def get_top_quotes(): + top_quotes = Quote.query.filter_by(status=1).order_by(Quote.votes.desc()).limit(10).all() # Limit to top 10 + quote_list = [{ + 'id': quote.id, + 'text': quote.text, + 'votes': quote.votes, + 'date': quote.date.strftime('%Y-%m-%d') + } for quote in top_quotes] + + return jsonify(quote_list), 200 + +# API to search for quotes +@app.route('/api/search', methods=['GET']) +def search_quotes(): + query = request.args.get('q', '').strip() + if not query: + return jsonify({"error": "No search term provided"}), 400 + + quotes = Quote.query.filter(Quote.text.ilike(f'%{query}%'), Quote.status == 1).all() # Search in approved quotes + if not quotes: + return jsonify({"error": "No quotes found for search term"}), 404 + + quote_list = [{ + 'id': quote.id, + 'text': quote.text, + 'votes': quote.votes, + 'date': quote.date.strftime('%Y-%m-%d') + } for quote in quotes] + + return jsonify(quote_list), 200 + +# API to submit a new quote +@app.route('/api/submit', methods=['POST']) +@limiter.limit("5 per minute") # Rate limiting to prevent abuse +def submit_quote(): + data = request.get_json() + + # Validate the input + if not data or not data.get('text'): + return jsonify({"error": "Quote text is required"}), 400 + + quote_text = data.get('text').strip() + + # Basic validation to prevent spam + if len(quote_text) < 5 or len(quote_text) > 1000: + return jsonify({"error": "Quote must be between 5 and 1000 characters"}), 400 + + new_quote = Quote(text=quote_text) + + try: + db.session.add(new_quote) + db.session.commit() + return jsonify({"success": "Quote submitted successfully!", "id": new_quote.id}), 201 + except Exception as e: + db.session.rollback() + return jsonify({"error": f"Error submitting quote: {str(e)}"}), 500 + # Run the Flask app if __name__ == '__main__': app.run(host='127.0.0.1', port=5050, debug=True) diff --git a/requirements.txt b/requirements.txt index 3c3b9fc..98a6834 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ -flask_sqlalchemy -argon2-cffi +Flask==2.3.2 +Flask-SQLAlchemy==3.0.5 +Flask-Limiter==2.4 +Flask-CORS==3.0.10 +argon2-cffi==21.3.0 diff --git a/static/css/styles.css b/static/css/styles.css index cd6003c..46e0bbb 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -5,12 +5,45 @@ body { font-family: Arial, Helvetica, sans-serif; margin: 0; padding: 0; + transition: background-color 0.3s, color 0.3s; +} + +/* Dark Mode */ +@media (prefers-color-scheme: dark) { + body { + background-color: #121212; + color: #ffffff; + } + + a { + color: #ffa500; + } + + table, td, input.text, textarea, select { + background-color: #1e1e1e; + color: #ffffff; + border-color: #ffa500; + } + + input.button { + background-color: #ffa500; + color: #000000; + } + + input.button:hover { + background-color: #ff8c00; + } + + .footertext, .toplinks, h2 { + color: #ffa500; + } } /* Links */ a { color: #c08000; text-decoration: none; + transition: color 0.3s; } a:visited, a:link, a:hover { @@ -78,6 +111,7 @@ input.text, textarea, select { padding: 5px; border: 1px solid #c08000; border-radius: 4px; + transition: background-color 0.3s, color 0.3s; } input.button { @@ -86,6 +120,7 @@ input.button { padding: 5px 10px; border: none; cursor: pointer; + transition: background-color 0.3s; } input.button:hover { @@ -141,6 +176,7 @@ footer { border: 1px solid #c08000; margin: 0 5px; border-radius: 4px; + transition: background-color 0.3s, color 0.3s; } #pagination a:hover { @@ -167,3 +203,29 @@ footer { .quote-id:hover { text-decoration: underline; } + +/* Dark Mode Specific */ +@media (prefers-color-scheme: dark) { + td.quote-box { + border-color: #ffa500; + } + + footer { + background-color: #ffa500; + color: #121212; + } + + #pagination a { + border-color: #ffa500; + color: #ffa500; + } + + #pagination a:hover { + background-color: #333333; + color: #ffffff; + } + + .quote-id { + color: #ffa500; + } +} diff --git a/templates/browse.html b/templates/browse.html index 9316734..4b580d2 100644 --- a/templates/browse.html +++ b/templates/browse.html @@ -32,7 +32,9 @@ Random / Submit / Browse / - ModApp + ModApp / + Search + FAQ @@ -79,6 +81,9 @@ Random / Submit / Browse / + ModApp / + Search + FAQ diff --git a/templates/faq.html b/templates/faq.html new file mode 100644 index 0000000..573a9f7 --- /dev/null +++ b/templates/faq.html @@ -0,0 +1,123 @@ + + + + + + ircquotes: FAQ + + + + + +
+ + + + + +
+ ircquotes + + FAQ +
+ + + + + +
+
+ + +
+ + + + +
+

Frequently Asked Questions (FAQ)

+ +

What is ircquotes?

+

ircquotes is a community-driven website where users can submit and browse memorable quotes from IRC (Internet Relay Chat). You can browse quotes, submit your own, and vote on others.

+ +

How does the API work?

+

The ircquotes API allows users to retrieve quotes programmatically. It is designed for developers who want to integrate IRC quotes into their own applications.

+ +

Available API Endpoints

+
    +
  • Get All Approved Quotes: GET /api/quotes
  • +
  • Get a Specific Quote by ID: GET /api/quotes/<id>
  • +
  • Get a Random Quote: GET /api/random
  • +
  • Get Top Quotes: GET /api/top
  • +
  • Search Quotes: GET /api/search?q=<search_term>
  • +
+ +

Submitting Quotes via the API

+

The API also allows you to submit quotes, but this feature is rate-limited to prevent abuse. Each user is allowed 5 submissions per minute.

+
    +
  • Submit a Quote: POST /api/submit
  • +
  • Request Body: The request body should be in JSON format and contain the quote text like this:
  • +
    {
    +    "text": "This is a memorable quote!"
    +}
    +
  • Validation Rules: Quotes must be between 5 and 1000 characters.
  • +
+ +

Rules for Submitting Quotes

+

To ensure that ircquotes remains a fun and enjoyable platform for everyone, we ask that you follow a few simple rules when submitting quotes:

+
    +
  • No Offensive Content: Do not submit quotes that contain offensive language, hate speech, or other harmful content.
  • +
  • No Spam: Please avoid submitting irrelevant or repetitive content. The site is for memorable IRC quotes, not spam.
  • +
  • Stay On-Topic: Ensure that your submissions are actual quotes from IRC, not made-up content.
  • +
  • Rate Limiting: The submission rate is limited to 5 quotes per minute to prevent spam.
  • +
  • Moderation: All quotes are subject to approval by site moderators. Rejected quotes will not be publicly visible.
  • +
+ +

How are quotes moderated?

+

Quotes go through a moderation process where they are either approved, rejected, or deleted. Approved quotes become visible to all users, while rejected ones remain in the system but aren't shown publicly.

+ +

How can I get the top 100 quotes?

+

You can view the top 100 quotes by visiting the Top 100 page. The quotes are ranked by votes, with the highest-voted quotes appearing at the top.

+ +

How do I search for quotes?

+

Use the search bar on the site or the API endpoint /api/search?q=<search_term> to find specific quotes.

+ +

Can I delete my submitted quotes?

+

No, quotes are submitted anonymously, so there's no direct way to delete quotes after submission. However, site moderators can remove inappropriate content.

+ +

Who can I contact for help?

+

If you encounter any issues or have questions, feel free to reach out to the admin via the contact page or submit a help request through the site's interface.

+
+
+ + +
+ + + + + + + + +
 {{ total_quotes }} quotes approved
+
+ + + diff --git a/templates/index.html b/templates/index.html index a4eeabd..fb2706d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -33,8 +33,8 @@ Submit / Browse / ModApp / - Search + FAQ diff --git a/templates/login.html b/templates/login.html index eb50b04..cf55f7c 100644 --- a/templates/login.html +++ b/templates/login.html @@ -32,6 +32,7 @@ Browse / ModApp / Search + FAQ @@ -68,6 +69,9 @@ Random / Submit / Browse / + ModApp / + Search + FAQ diff --git a/templates/modapp.html b/templates/modapp.html index 8fbb3bb..ee50523 100644 --- a/templates/modapp.html +++ b/templates/modapp.html @@ -40,22 +40,26 @@

All Quotes for Moderation

- + {% if all_quotes %}
- +
+ + + {% for quote in all_quotes %} - - + + + + +
Select Quote ID Quote StatusSubmitted AtIP AddressUser Agent Actions
#{{ quote.id }} {{ quote.text }} @@ -67,6 +71,15 @@ Rejected {% endif %} + {% if quote.submitted_at %} + {{ quote.submitted_at.strftime('%Y-%m-%d %H:%M:%S') }} + {% else %} + N/A + {% endif %} + {{ quote.ip_address }}{{ quote.user_agent }} Approve | Reject | @@ -109,5 +122,11 @@
+ + +
+ +
+ - + \ No newline at end of file diff --git a/templates/quote.html b/templates/quote.html index 0e0b37c..52a2dd2 100644 --- a/templates/quote.html +++ b/templates/quote.html @@ -32,7 +32,9 @@ Random / Submit / Browse / - Top 100 + ModApp / + Search + FAQ diff --git a/templates/random.html b/templates/random.html index c5f4d45..6fa8685 100644 --- a/templates/random.html +++ b/templates/random.html @@ -26,10 +26,12 @@ Home / - Latest / - Browse / - Random / - Top 100 + Random / + Submit / + Browse / + ModApp / + Search + FAQ @@ -57,10 +59,12 @@ Home / - Latest / - Browse / - Random / - Top 100 + Random / + Submit / + Browse / + ModApp / + Search + FAQ diff --git a/templates/search.html b/templates/search.html index 9c1d0d6..3cc19a1 100644 --- a/templates/search.html +++ b/templates/search.html @@ -32,6 +32,7 @@ Browse / ModApp / Search + FAQ @@ -83,6 +84,9 @@ Random / Submit / Browse / + ModApp / + Search + FAQ diff --git a/templates/submit.html b/templates/submit.html index 71c36bb..1cfd850 100644 --- a/templates/submit.html +++ b/templates/submit.html @@ -29,9 +29,11 @@ Home / Random / - Add Quote / + Submit / + Browse / ModApp / Search + FAQ