Improved modapp

This commit is contained in:
2024-10-11 19:36:58 +01:00
parent 79ed713a18
commit b8edf8a7f1
12 changed files with 378 additions and 27 deletions

135
app.py
View File

@@ -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/<int:id>/<action>')
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():
@@ -179,10 +195,10 @@ def modapp():
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()
total_quotes = Quote.query.filter_by(status=1).count() # Count only approved quotes
return render_template('modapp.html', all_quotes=all_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/<int:id>', 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)

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -32,7 +32,9 @@
<a href="/random">Random</a> /
<a href="/submit">Submit</a> /
<a href="/browse">Browse</a> /
<a href="/modapp">ModApp</a>
<a href="/modapp">ModApp</a> /
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
</table>
@@ -79,6 +81,9 @@
<a href="/random">Random</a> /
<a href="/submit">Submit</a> /
<a href="/browse">Browse</a> /
<a href="/modapp">ModApp</a> /
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
<tr>

123
templates/faq.html Normal file
View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ircquotes: FAQ</title>
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body bgcolor="#ffffff" text="#000000" link="#c08000" vlink="#c08000" alink="#c08000">
<!-- Navigation -->
<center>
<table cellpadding="2" cellspacing="0" width="80%" border="0">
<tr>
<td bgcolor="#c08000" align="left">
<font size="+1"><b><i>ircquotes</i></b></font>
</td>
<td bgcolor="#c08000" align="right">
<font face="arial" size="+1"><b>FAQ</b></font>
</td>
</tr>
</table>
<table cellpadding="2" cellspacing="0" width="80%" border="0">
<tr>
<td class="footertext" align="left" bgcolor="#f0f0f0"></td>
<td align="right" bgcolor="#f0f0f0" class="toplinks" colspan="2">
<a href="/">Home</a> /
<a href="/random">Random</a> /
<a href="/submit">Submit</a> /
<a href="/browse">Browse</a> /
<a href="/modapp">ModApp</a> /
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
</table>
</center>
<!-- FAQ Content -->
<center>
<table cellpadding="2" cellspacing="0" width="80%">
<tr>
<td class="bodytext">
<h2>Frequently Asked Questions (FAQ)</h2>
<h3>What is ircquotes?</h3>
<p>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.</p>
<h3>How does the API work?</h3>
<p>The ircquotes API allows users to retrieve quotes programmatically. It is designed for developers who want to integrate IRC quotes into their own applications.</p>
<h4>Available API Endpoints</h4>
<ul>
<li><strong>Get All Approved Quotes</strong>: <code>GET /api/quotes</code></li>
<li><strong>Get a Specific Quote by ID</strong>: <code>GET /api/quotes/&lt;id&gt;</code></li>
<li><strong>Get a Random Quote</strong>: <code>GET /api/random</code></li>
<li><strong>Get Top Quotes</strong>: <code>GET /api/top</code></li>
<li><strong>Search Quotes</strong>: <code>GET /api/search?q=&lt;search_term&gt;</code></li>
</ul>
<h4>Submitting Quotes via the API</h4>
<p>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.</p>
<ul>
<li><strong>Submit a Quote</strong>: <code>POST /api/submit</code></li>
<li><strong>Request Body</strong>: The request body should be in JSON format and contain the quote text like this:</li>
<pre><code>{
"text": "This is a memorable quote!"
}</code></pre>
<li><strong>Validation Rules</strong>: Quotes must be between 5 and 1000 characters.</li>
</ul>
<h3>Rules for Submitting Quotes</h3>
<p>To ensure that ircquotes remains a fun and enjoyable platform for everyone, we ask that you follow a few simple rules when submitting quotes:</p>
<ul>
<li><strong>No Offensive Content</strong>: Do not submit quotes that contain offensive language, hate speech, or other harmful content.</li>
<li><strong>No Spam</strong>: Please avoid submitting irrelevant or repetitive content. The site is for memorable IRC quotes, not spam.</li>
<li><strong>Stay On-Topic</strong>: Ensure that your submissions are actual quotes from IRC, not made-up content.</li>
<li><strong>Rate Limiting</strong>: The submission rate is limited to 5 quotes per minute to prevent spam.</li>
<li><strong>Moderation</strong>: All quotes are subject to approval by site moderators. Rejected quotes will not be publicly visible.</li>
</ul>
<h3>How are quotes moderated?</h3>
<p>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.</p>
<h3>How can I get the top 100 quotes?</h3>
<p>You can view the top 100 quotes by visiting the <a href="/top">Top 100 page</a>. The quotes are ranked by votes, with the highest-voted quotes appearing at the top.</p>
<h3>How do I search for quotes?</h3>
<p>Use the search bar on the site or the API endpoint <code>/api/search?q=&lt;search_term&gt;</code> to find specific quotes.</p>
<h3>Can I delete my submitted quotes?</h3>
<p>No, quotes are submitted anonymously, so there's no direct way to delete quotes after submission. However, site moderators can remove inappropriate content.</p>
<h3>Who can I contact for help?</h3>
<p>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.</p>
</td>
</tr>
</table>
</center>
<!-- Footer -->
<center>
<table border="0" cellpadding="2" cellspacing="0" width="80%" bgcolor="#c08000">
<tr>
<td bgcolor="#f0f0f0" class="toplinks" colspan="2">
<a href="/">Home</a> /
<a href="/random">Random</a> /
<a href="/submit">Submit</a> /
<a href="/browse">Browse</a> /
<a href="/modapp">ModApp</a> /
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
<tr>
<td class="footertext" align="left">&nbsp;</td>
<td class="footertext" align="right">{{ total_quotes }} quotes approved</td>
</tr>
</table>
</center>
</body>
</html>

View File

@@ -33,8 +33,8 @@
<a href="/submit">Submit</a> /
<a href="/browse">Browse</a> /
<a href="/modapp">ModApp</a> /
<!-- This link will now load search.html -->
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
</table>

View File

@@ -32,6 +32,7 @@
<a href="/browse">Browse</a> /
<a href="/modapp">ModApp</a> /
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
</table>
@@ -68,6 +69,9 @@
<a href="/random">Random</a> /
<a href="/submit">Submit</a> /
<a href="/browse">Browse</a> /
<a href="/modapp">ModApp</a> /
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
<tr>

View File

@@ -44,18 +44,22 @@
<!-- Check if there are any quotes -->
{% if all_quotes %}
<form method="POST" action="/modapp/bulk_action">
<table>
<table border="1" cellpadding="5" cellspacing="0" width="100%">
<tr>
<th>Select</th>
<th>Quote ID</th>
<th>Quote</th>
<th>Status</th>
<th>Submitted At</th>
<th>IP Address</th>
<th>User Agent</th>
<th>Actions</th>
</tr>
<!-- Loop through each quote -->
{% for quote in all_quotes %}
<tr style="background-color: '{% if quote.status == 1 %}#d4edda{% elif quote.status == 2 %}#f8d7da{% else %}#fff{% endif %}'">
<td><input type="checkbox" name="quote_ids" value="{{ quote.id }}"></td>
<tr style="background-color:
{% if quote.status == 1 %} #d4edda {% elif quote.status == 2 %} #f8d7da {% else %} #fff {% endif %}">
<td><input type="checkbox" name="quote_ids[]" value="{{ quote.id }}"></td>
<td>#{{ quote.id }}</td>
<td>{{ quote.text }}</td>
<td>
@@ -67,6 +71,15 @@
Rejected
{% endif %}
</td>
<td>
{% if quote.submitted_at %}
{{ quote.submitted_at.strftime('%Y-%m-%d %H:%M:%S') }}
{% else %}
N/A
{% endif %}
</td>
<td>{{ quote.ip_address }}</td>
<td>{{ quote.user_agent }}</td>
<td>
<a href="/approve/{{ quote.id }}">Approve</a> |
<a href="/reject/{{ quote.id }}">Reject</a> |
@@ -109,5 +122,11 @@
</tr>
</table>
</center>
<!-- Dark Mode Toggle -->
<center>
<button id="theme-toggle">Toggle Dark Mode</button>
</center>
</body>
</html>

View File

@@ -32,7 +32,9 @@
<a href="/random">Random</a> /
<a href="/submit">Submit</a> /
<a href="/browse">Browse</a> /
<a href="/top">Top 100</a>
<a href="/modapp">ModApp</a> /
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
</table>

View File

@@ -26,10 +26,12 @@
<td class="footertext" align="left" bgcolor="#f0f0f0"></td>
<td align="right" bgcolor="#f0f0f0" class="toplinks" colspan="2">
<a href="/">Home</a> /
<a href="/?latest">Latest</a> /
<a href="/?browse">Browse</a> /
<a href="/random" rel="nofollow">Random</a> /
<a href="/?top">Top 100</a>
<a href="/random">Random</a> /
<a href="/submit">Submit</a> /
<a href="/browse">Browse</a> /
<a href="/modapp">ModApp</a> /
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
</table>
@@ -57,10 +59,12 @@
<tr>
<td bgcolor="#f0f0f0" class="toplinks" colspan="2">
<a href="/">Home</a> /
<a href="/?latest">Latest</a> /
<a href="/?browse">Browse</a> /
<a href="/random" rel="nofollow">Random</a> /
<a href="/?top">Top 100</a>
<a href="/random">Random</a> /
<a href="/submit">Submit</a> /
<a href="/browse">Browse</a> /
<a href="/modapp">ModApp</a> /
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
<tr>

View File

@@ -32,6 +32,7 @@
<a href="/browse">Browse</a> /
<a href="/modapp">ModApp</a> /
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
</table>
@@ -83,6 +84,9 @@
<a href="/random">Random</a> /
<a href="/submit">Submit</a> /
<a href="/browse">Browse</a> /
<a href="/modapp">ModApp</a> /
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
<tr>

View File

@@ -29,9 +29,11 @@
<td align="right" bgcolor="#f0f0f0" class="toplinks" colspan="2">
<a href="/">Home</a> /
<a href="/random">Random</a> /
<a href="/submit"><b>Add Quote</b></a> /
<a href="/submit">Submit</a> /
<a href="/browse">Browse</a> /
<a href="/modapp">ModApp</a> /
<a href="/search">Search</a>
<a href="/faq">FAQ</a>
</td>
</tr>
</table>