mirror of
https://github.com/soxoj/maigret.git
synced 2026-05-07 06:24:35 +00:00
create flask frontend
This commit is contained in:
@@ -43,3 +43,8 @@ settings.json
|
|||||||
# other
|
# other
|
||||||
*.egg-info
|
*.egg-info
|
||||||
build
|
build
|
||||||
|
lib/vis-9.1.2/vis-network.min.js
|
||||||
|
lib/bindings/utils.js
|
||||||
|
lib/tom-select/tom-select.complete.min.js
|
||||||
|
lib/tom-select/tom-select.css
|
||||||
|
lib/vis-9.1.2/vis-network.css
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# app.py
|
||||||
|
from flask import Flask, render_template, request, send_file, Response, flash
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import maigret
|
||||||
|
from maigret.sites import MaigretDatabase
|
||||||
|
from maigret.report import generate_report_context
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = 'your-secret-key-here'
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
MAIGRET_DB_FILE = os.path.join('maigret', 'resources', 'data.json')
|
||||||
|
COOKIES_FILE = "cookies.txt"
|
||||||
|
UPLOAD_FOLDER = 'uploads'
|
||||||
|
REPORTS_FOLDER = 'reports'
|
||||||
|
|
||||||
|
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||||
|
os.makedirs(REPORTS_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
|
def setup_logger(log_level, name):
|
||||||
|
logger = logging.getLogger(name)
|
||||||
|
logger.setLevel(log_level)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
async def maigret_search(username, options):
|
||||||
|
logger = setup_logger(logging.WARNING, 'maigret')
|
||||||
|
|
||||||
|
try:
|
||||||
|
db = MaigretDatabase().load_from_path(MAIGRET_DB_FILE)
|
||||||
|
sites = db.ranked_sites_dict(top=int(options.get('top_sites', 500)))
|
||||||
|
|
||||||
|
results = await maigret.search(
|
||||||
|
username=username,
|
||||||
|
site_dict=sites,
|
||||||
|
timeout=int(options.get('timeout', 30)),
|
||||||
|
logger=logger,
|
||||||
|
id_type=options.get('id_type', 'username'),
|
||||||
|
cookies=COOKIES_FILE if options.get('use_cookies') else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during search: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def search_multiple_usernames(usernames, options):
|
||||||
|
results = []
|
||||||
|
for username in usernames:
|
||||||
|
try:
|
||||||
|
search_results = await maigret_search(username.strip(), options)
|
||||||
|
results.append((username.strip(), options['id_type'], search_results))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error searching username {username}: {str(e)}")
|
||||||
|
return results
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/search', methods=['POST'])
|
||||||
|
def search():
|
||||||
|
usernames_input = request.form.get('usernames', '').strip()
|
||||||
|
if not usernames_input:
|
||||||
|
return render_template('index.html', error="At least one username is required")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Split usernames by common separators
|
||||||
|
usernames = [u.strip() for u in usernames_input.replace(',', ' ').split() if u.strip()]
|
||||||
|
|
||||||
|
# Create timestamp for this search session
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
session_folder = os.path.join(REPORTS_FOLDER, f"search_{timestamp}")
|
||||||
|
os.makedirs(session_folder, exist_ok=True)
|
||||||
|
|
||||||
|
# Collect options from form
|
||||||
|
options = {
|
||||||
|
'top_sites': request.form.get('top_sites', '500'),
|
||||||
|
'timeout': request.form.get('timeout', '30'),
|
||||||
|
'id_type': request.form.get('id_type', 'username'),
|
||||||
|
'use_cookies': 'use_cookies' in request.form,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run search asynchronously for all usernames
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
general_results = loop.run_until_complete(search_multiple_usernames(usernames, options))
|
||||||
|
|
||||||
|
# Save the combined graph in the session folder
|
||||||
|
graph_path = os.path.join(session_folder, "combined_graph.html")
|
||||||
|
maigret.report.save_graph_report(graph_path, general_results, MaigretDatabase().load_from_path(MAIGRET_DB_FILE))
|
||||||
|
|
||||||
|
# Save individual reports for each username
|
||||||
|
individual_reports = []
|
||||||
|
for username, id_type, results in general_results:
|
||||||
|
report_base = os.path.join(session_folder, f"report_{username}")
|
||||||
|
|
||||||
|
# Save reports in different formats
|
||||||
|
csv_path = f"{report_base}.csv"
|
||||||
|
json_path = f"{report_base}.json"
|
||||||
|
pdf_path = f"{report_base}.pdf"
|
||||||
|
html_path = f"{report_base}.html"
|
||||||
|
|
||||||
|
context = generate_report_context(general_results)
|
||||||
|
|
||||||
|
maigret.report.save_csv_report(csv_path, username, results)
|
||||||
|
maigret.report.save_json_report(json_path, username, results, report_type='ndjson')
|
||||||
|
maigret.report.save_pdf_report(pdf_path, context)
|
||||||
|
maigret.report.save_html_report(html_path, context)
|
||||||
|
|
||||||
|
# Extract claimed profiles
|
||||||
|
claimed_profiles = []
|
||||||
|
for site_name, site_data in results.items():
|
||||||
|
if (site_data.get('status') and
|
||||||
|
site_data['status'].status == maigret.result.MaigretCheckStatus.CLAIMED):
|
||||||
|
claimed_profiles.append({
|
||||||
|
'site_name': site_name,
|
||||||
|
'url': site_data.get('url_user', ''),
|
||||||
|
'tags': site_data.get('status').tags if site_data.get('status') else []
|
||||||
|
})
|
||||||
|
|
||||||
|
individual_reports.append({
|
||||||
|
'username': username,
|
||||||
|
'csv_file': os.path.relpath(csv_path, REPORTS_FOLDER),
|
||||||
|
'json_file': os.path.relpath(json_path, REPORTS_FOLDER),
|
||||||
|
'pdf_file': os.path.relpath(pdf_path, REPORTS_FOLDER),
|
||||||
|
'html_file': os.path.relpath(html_path, REPORTS_FOLDER),
|
||||||
|
'claimed_profiles': claimed_profiles,
|
||||||
|
})
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'results.html',
|
||||||
|
usernames=usernames,
|
||||||
|
graph_file=os.path.relpath(graph_path, REPORTS_FOLDER),
|
||||||
|
individual_reports=individual_reports,
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error processing search: {str(e)}", exc_info=True)
|
||||||
|
return render_template('index.html', error=f"An error occurred: {str(e)}")
|
||||||
|
|
||||||
|
@app.route('/reports/<path:filename>')
|
||||||
|
def download_report(filename):
|
||||||
|
"""Serve report files"""
|
||||||
|
try:
|
||||||
|
return send_file(os.path.join(REPORTS_FOLDER, filename))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error serving file {filename}: {str(e)}")
|
||||||
|
return "File not found", 404
|
||||||
|
|
||||||
|
@app.route('/view_graph/<path:graph_path>')
|
||||||
|
def view_graph(graph_path):
|
||||||
|
"""Serve the graph HTML directly"""
|
||||||
|
graph_file = os.path.join(REPORTS_FOLDER, graph_path)
|
||||||
|
try:
|
||||||
|
with open(graph_file, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
return content
|
||||||
|
except FileNotFoundError:
|
||||||
|
logging.error(f"Graph file not found: {graph_file}")
|
||||||
|
return "Graph not found", 404
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error serving graph {graph_file}: {str(e)}")
|
||||||
|
return "Error loading graph", 500
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
app.run(debug=True)
|
||||||
@@ -17474,7 +17474,7 @@
|
|||||||
"method": "vimeo"
|
"method": "vimeo"
|
||||||
},
|
},
|
||||||
"headers": {
|
"headers": {
|
||||||
"Authorization": "jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzM5Njc3MjAsInVzZXJfaWQiOm51bGwsImFwcF9pZCI6NTg0NzksInNjb3BlcyI6InB1YmxpYyIsInRlYW1fdXNlcl9pZCI6bnVsbCwianRpIjoiNGJkNDE4NzktM2VhOS00ZWRiLWIzZDUtNjAyNjQ3YjMyNTVhIn0.kPbKREujSfYsisyF0pS_HskTapRlHBfVLRw4cis1ezk"
|
"Authorization": "jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzQwMzc5MjAsInVzZXJfaWQiOm51bGwsImFwcF9pZCI6NTg0NzksInNjb3BlcyI6InB1YmxpYyIsInRlYW1fdXNlcl9pZCI6bnVsbCwianRpIjoiM2U2ZWQ1MDYtZTU0OC00ZGIwLWI4YTMtMzdiZWMyYzRiYTJiIn0.vojHtXWsDNBtjQjoVm6DSV9XHhWzu-PUMwjOJouMkG8"
|
||||||
},
|
},
|
||||||
"urlProbe": "https://api.vimeo.com/users/{username}?fields=name%2Cgender%2Cbio%2Curi%2Clink%2Cbackground_video%2Clocation_details%2Cpictures%2Cverified%2Cmetadata.public_videos.total%2Cavailable_for_hire%2Ccan_work_remotely%2Cmetadata.connections.videos.total%2Cmetadata.connections.albums.total%2Cmetadata.connections.followers.total%2Cmetadata.connections.following.total%2Cmetadata.public_videos.total%2Cmetadata.connections.vimeo_experts.is_enrolled%2Ctotal_collection_count%2Ccreated_time%2Cprofile_preferences%2Cmembership%2Cclients%2Cskills%2Cproject_types%2Crates%2Ccategories%2Cis_expert%2Cprofile_discovery%2Cwebsites%2Ccontact_emails&fetch_user_profile=1",
|
"urlProbe": "https://api.vimeo.com/users/{username}?fields=name%2Cgender%2Cbio%2Curi%2Clink%2Cbackground_video%2Clocation_details%2Cpictures%2Cverified%2Cmetadata.public_videos.total%2Cavailable_for_hire%2Ccan_work_remotely%2Cmetadata.connections.videos.total%2Cmetadata.connections.albums.total%2Cmetadata.connections.followers.total%2Cmetadata.connections.following.total%2Cmetadata.public_videos.total%2Cmetadata.connections.vimeo_experts.is_enrolled%2Ctotal_collection_count%2Ccreated_time%2Cprofile_preferences%2Cmembership%2Cclients%2Cskills%2Cproject_types%2Crates%2Ccategories%2Cis_expert%2Cprofile_discovery%2Cwebsites%2Ccontact_emails&fetch_user_profile=1",
|
||||||
"checkType": "status_code",
|
"checkType": "status_code",
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<!-- templates/base.html -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-bs-theme="dark"></html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Maigret Web Interface</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
.form-container {
|
||||||
|
max-width: auto;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
[data-bs-theme="dark"] {
|
||||||
|
--bs-body-bg: #212529;
|
||||||
|
--bs-body-color: #dee2e6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="mb-3">
|
||||||
|
<button class="btn btn-outline-secondary" id="theme-toggle">
|
||||||
|
Toggle Dark/Light Mode
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.getElementById('theme-toggle').addEventListener('click', function() {
|
||||||
|
const html = document.documentElement;
|
||||||
|
if (html.getAttribute('data-bs-theme') === 'dark') {
|
||||||
|
html.setAttribute('data-bs-theme', 'light');
|
||||||
|
} else {
|
||||||
|
html.setAttribute('data-bs-theme', 'dark');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="form-container">
|
||||||
|
<h1 class="mb-4">Maigret Web Interface</h1>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('search') }}" class="mb-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="usernames" class="form-label">Usernames to Search</label>
|
||||||
|
<textarea class="form-control" id="usernames" name="usernames" rows="3" required
|
||||||
|
placeholder="Enter one or more usernames (separated by spaces or commas)"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="top_sites" class="form-label">Number of Top Sites to Check</label>
|
||||||
|
<input type="number" class="form-control" id="top_sites" name="top_sites" value="500" min="1" max="10000">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="timeout" class="form-label">Timeout (seconds)</label>
|
||||||
|
<input type="number" class="form-control" id="timeout" name="timeout" value="30" min="1" max="120">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="use_cookies" name="use_cookies">
|
||||||
|
<label class="form-check-label" for="use_cookies">Use Cookies File</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="form-container">
|
||||||
|
<h1 class="mb-4">Search Results</h1>
|
||||||
|
<p class="text-muted">Search session: {{ timestamp }}</p>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3>Combined Network Graph</h3>
|
||||||
|
<iframe src="{{ url_for('view_graph', graph_path=graph_file) }}" width="100%" height="600px" frameborder="0"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3>Individual Reports</h3>
|
||||||
|
<div class="accordion" id="reportsAccordion">
|
||||||
|
{% for report in individual_reports %}
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="heading{{ loop.index }}">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#collapse{{ loop.index }}" aria-expanded="false"
|
||||||
|
aria-controls="collapse{{ loop.index }}">
|
||||||
|
Results for "{{ report.username }}"
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapse{{ loop.index }}" class="accordion-collapse collapse"
|
||||||
|
aria-labelledby="heading{{ loop.index }}" data-bs-parent="#reportsAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="list-group mb-3">
|
||||||
|
<a href="{{ url_for('download_report', filename=report.csv_file) }}"
|
||||||
|
class="list-group-item list-group-item-action">
|
||||||
|
Download CSV Report
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('download_report', filename=report.json_file) }}"
|
||||||
|
class="list-group-item list-group-item-action">
|
||||||
|
Download JSON Report
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('download_report', filename=report.html_file) }}"
|
||||||
|
class="list-group-item list-group-item-action">
|
||||||
|
Download HTML Report
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('download_report', filename=report.pdf_file) }}"
|
||||||
|
class="list-group-item list-group-item-action">
|
||||||
|
Download PDF Report
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if report.claimed_profiles %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
Found Profiles
|
||||||
|
</div>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for profile in report.claimed_profiles %}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-secondary me-2">{{ profile.site_name }}</span>
|
||||||
|
<a href="{{ profile.url }}" target="_blank" rel="noopener noreferrer">
|
||||||
|
{{ profile.url }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% if profile.tags %}
|
||||||
|
<div>
|
||||||
|
{% for tag in profile.tags %}
|
||||||
|
<span class="badge bg-info">{{ tag }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ url_for('index') }}" class="btn btn-primary">New Search</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user