Skip to content

Commit 88d6849

Browse files
Created web frontend launched via --web flag (#1967)
Author: overcuriousity Co-authored-by: Soxoj <[email protected]>
1 parent cb01535 commit 88d6849

File tree

13 files changed

+530
-13
lines changed

13 files changed

+530
-13
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ settings.json
4242

4343
# other
4444
*.egg-info
45-
build
45+
build

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
LINT_FILES=maigret wizard.py tests
22

33
test:
4-
coverage run --source=./maigret -m pytest tests
4+
coverage run --source=./maigret,./maigret/web -m pytest tests
55
coverage report -m
66
coverage html
77

maigret/maigret.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,15 @@ def setup_arguments_parser(settings: Settings):
324324
default=False,
325325
help="Show database statistics (most frequent sites engines and tags).",
326326
)
327-
327+
modes_group.add_argument(
328+
"--web",
329+
metavar='PORT',
330+
type=int,
331+
nargs='?', # Optional PORT value
332+
const=5000, # Default PORT if `--web` is provided without a value
333+
default=None, # Explicitly set default to None
334+
help="Launch the web interface on the specified port (default: 5000 if no PORT is provided).",
335+
)
328336
output_group = parser.add_argument_group(
329337
'Output options', 'Options to change verbosity and view of the console output'
330338
)
@@ -485,6 +493,13 @@ async def main():
485493
log_level = logging.WARNING
486494
logger.setLevel(log_level)
487495

496+
if args.web is not None:
497+
from maigret.web.app import app
498+
499+
port = args.web if args.web else 5000 # args.web is either the specified port or 5000 by default
500+
app.run(port=port)
501+
return
502+
488503
# Usernames initial list
489504
usernames = {
490505
u: args.id_type

maigret/resources/settings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,6 @@
5353
"xmind_report": false,
5454
"graph_report": false,
5555
"pdf_report": false,
56-
"html_report": false
56+
"html_report": false,
57+
"web_interface_port": 5000
5758
}

maigret/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class Settings:
4242
pdf_report: bool
4343
html_report: bool
4444
graph_report: bool
45+
web_interface_port: int
4546

4647
# submit mode settings
4748
presence_strings: list

maigret/web/app.py

+280
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
# app.py
2+
from flask import (
3+
Flask,
4+
render_template,
5+
request,
6+
send_file,
7+
Response,
8+
flash,
9+
redirect,
10+
url_for,
11+
)
12+
import logging
13+
import os
14+
import asyncio
15+
from datetime import datetime
16+
from threading import Thread
17+
import maigret
18+
import maigret.settings
19+
from maigret.sites import MaigretDatabase
20+
from maigret.report import generate_report_context
21+
22+
app = Flask(__name__)
23+
app.secret_key = 'your-secret-key-here'
24+
25+
# Add background job tracking
26+
background_jobs = {}
27+
job_results = {}
28+
29+
# Configuration
30+
MAIGRET_DB_FILE = os.path.join('maigret', 'resources', 'data.json')
31+
COOKIES_FILE = "cookies.txt"
32+
UPLOAD_FOLDER = 'uploads'
33+
REPORTS_FOLDER = os.path.abspath('/tmp/maigret_reports')
34+
35+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
36+
os.makedirs(REPORTS_FOLDER, exist_ok=True)
37+
38+
39+
def setup_logger(log_level, name):
40+
logger = logging.getLogger(name)
41+
logger.setLevel(log_level)
42+
return logger
43+
44+
45+
async def maigret_search(username, options):
46+
logger = setup_logger(logging.WARNING, 'maigret')
47+
try:
48+
db = MaigretDatabase().load_from_path(MAIGRET_DB_FILE)
49+
sites = db.ranked_sites_dict(top=int(options.get('top_sites', 500)))
50+
51+
results = await maigret.search(
52+
username=username,
53+
site_dict=sites,
54+
timeout=int(options.get('timeout', 30)),
55+
logger=logger,
56+
id_type=options.get('id_type', 'username'),
57+
cookies=COOKIES_FILE if options.get('use_cookies') else None,
58+
)
59+
return results
60+
except Exception as e:
61+
logger.error(f"Error during search: {str(e)}")
62+
raise
63+
64+
65+
async def search_multiple_usernames(usernames, options):
66+
results = []
67+
for username in usernames:
68+
try:
69+
search_results = await maigret_search(username.strip(), options)
70+
results.append((username.strip(), options['id_type'], search_results))
71+
except Exception as e:
72+
logging.error(f"Error searching username {username}: {str(e)}")
73+
return results
74+
75+
76+
def process_search_task(usernames, options, timestamp):
77+
try:
78+
# Setup event loop for async operations
79+
loop = asyncio.new_event_loop()
80+
asyncio.set_event_loop(loop)
81+
82+
# Run the search
83+
general_results = loop.run_until_complete(
84+
search_multiple_usernames(usernames, options)
85+
)
86+
87+
# Create session folder
88+
session_folder = os.path.join(REPORTS_FOLDER, f"search_{timestamp}")
89+
os.makedirs(session_folder, exist_ok=True)
90+
91+
# Save the combined graph
92+
graph_path = os.path.join(session_folder, "combined_graph.html")
93+
maigret.report.save_graph_report(
94+
graph_path,
95+
general_results,
96+
MaigretDatabase().load_from_path(MAIGRET_DB_FILE),
97+
)
98+
99+
# Save individual reports
100+
individual_reports = []
101+
for username, id_type, results in general_results:
102+
report_base = os.path.join(session_folder, f"report_{username}")
103+
104+
csv_path = f"{report_base}.csv"
105+
json_path = f"{report_base}.json"
106+
pdf_path = f"{report_base}.pdf"
107+
html_path = f"{report_base}.html"
108+
109+
context = generate_report_context(general_results)
110+
111+
maigret.report.save_csv_report(csv_path, username, results)
112+
maigret.report.save_json_report(
113+
json_path, username, results, report_type='ndjson'
114+
)
115+
maigret.report.save_pdf_report(pdf_path, context)
116+
maigret.report.save_html_report(html_path, context)
117+
118+
claimed_profiles = []
119+
for site_name, site_data in results.items():
120+
if (
121+
site_data.get('status')
122+
and site_data['status'].status
123+
== maigret.result.MaigretCheckStatus.CLAIMED
124+
):
125+
claimed_profiles.append(
126+
{
127+
'site_name': site_name,
128+
'url': site_data.get('url_user', ''),
129+
'tags': (
130+
site_data.get('status').tags
131+
if site_data.get('status')
132+
else []
133+
),
134+
}
135+
)
136+
137+
individual_reports.append(
138+
{
139+
'username': username,
140+
'csv_file': os.path.join(
141+
f"search_{timestamp}", f"report_{username}.csv"
142+
),
143+
'json_file': os.path.join(
144+
f"search_{timestamp}", f"report_{username}.json"
145+
),
146+
'pdf_file': os.path.join(
147+
f"search_{timestamp}", f"report_{username}.pdf"
148+
),
149+
'html_file': os.path.join(
150+
f"search_{timestamp}", f"report_{username}.html"
151+
),
152+
'claimed_profiles': claimed_profiles,
153+
}
154+
)
155+
156+
# Save results and mark job as complete
157+
job_results[timestamp] = {
158+
'status': 'completed',
159+
'session_folder': f"search_{timestamp}",
160+
'graph_file': os.path.join(f"search_{timestamp}", "combined_graph.html"),
161+
'usernames': usernames,
162+
'individual_reports': individual_reports,
163+
}
164+
except Exception as e:
165+
job_results[timestamp] = {'status': 'failed', 'error': str(e)}
166+
finally:
167+
background_jobs[timestamp]['completed'] = True
168+
169+
170+
@app.route('/')
171+
def index():
172+
return render_template('index.html')
173+
174+
175+
@app.route('/search', methods=['POST'])
176+
def search():
177+
usernames_input = request.form.get('usernames', '').strip()
178+
if not usernames_input:
179+
flash('At least one username is required', 'danger')
180+
return redirect(url_for('index'))
181+
182+
usernames = [
183+
u.strip() for u in usernames_input.replace(',', ' ').split() if u.strip()
184+
]
185+
186+
# Create timestamp for this search session
187+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
188+
189+
logging.info(f"Starting search for usernames: {usernames}")
190+
191+
options = {
192+
'top_sites': request.form.get('top_sites', '500'),
193+
'timeout': request.form.get('timeout', '30'),
194+
'id_type': 'username', # fixed as username
195+
'use_cookies': 'use_cookies' in request.form,
196+
}
197+
198+
# Start background job
199+
background_jobs[timestamp] = {
200+
'completed': False,
201+
'thread': Thread(
202+
target=process_search_task, args=(usernames, options, timestamp)
203+
),
204+
}
205+
background_jobs[timestamp]['thread'].start()
206+
207+
logging.info(f"Search job started with timestamp: {timestamp}")
208+
209+
# Redirect to status page
210+
return redirect(url_for('status', timestamp=timestamp))
211+
212+
213+
@app.route('/status/<timestamp>')
214+
def status(timestamp):
215+
logging.info(f"Status check for timestamp: {timestamp}")
216+
217+
# Validate timestamp
218+
if timestamp not in background_jobs:
219+
flash('Invalid search session', 'danger')
220+
return redirect(url_for('index'))
221+
222+
# Check if job is completed
223+
if background_jobs[timestamp]['completed']:
224+
result = job_results.get(timestamp)
225+
if not result:
226+
flash('No results found for this search session', 'warning')
227+
return redirect(url_for('index'))
228+
229+
if result['status'] == 'completed':
230+
# Redirect to results page once done
231+
return redirect(url_for('results', session_id=result['session_folder']))
232+
else:
233+
error_msg = result.get('error', 'Unknown error occurred')
234+
flash(f'Search failed: {error_msg}', 'danger')
235+
return redirect(url_for('index'))
236+
237+
# If job is still running, show status page with a simple spinner
238+
return render_template('status.html', timestamp=timestamp)
239+
240+
241+
@app.route('/results/<session_id>')
242+
def results(session_id):
243+
if not session_id.startswith('search_'):
244+
flash('Invalid results session format', 'danger')
245+
return redirect(url_for('index'))
246+
247+
result_data = next(
248+
(
249+
r
250+
for r in job_results.values()
251+
if r.get('status') == 'completed' and r['session_folder'] == session_id
252+
),
253+
None,
254+
)
255+
256+
return render_template(
257+
'results.html',
258+
usernames=result_data['usernames'],
259+
graph_file=result_data['graph_file'],
260+
individual_reports=result_data['individual_reports'],
261+
timestamp=session_id.replace('search_', ''),
262+
)
263+
264+
265+
@app.route('/reports/<path:filename>')
266+
def download_report(filename):
267+
try:
268+
file_path = os.path.join(REPORTS_FOLDER, filename)
269+
return send_file(file_path)
270+
except Exception as e:
271+
logging.error(f"Error serving file {filename}: {str(e)}")
272+
return "File not found", 404
273+
274+
275+
if __name__ == '__main__':
276+
logging.basicConfig(
277+
level=logging.INFO,
278+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
279+
)
280+
app.run(debug=True)

maigret/web/templates/base.html

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!-- templates/base.html -->
2+
<!DOCTYPE html>
3+
<html lang="en" data-bs-theme="dark">
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Maigret Web Interface</title>
8+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
9+
<style>
10+
body {
11+
padding-top: 2rem;
12+
}
13+
.form-container {
14+
max-width: auto;
15+
margin: auto;
16+
}
17+
[data-bs-theme="dark"] {
18+
--bs-body-bg: #212529;
19+
--bs-body-color: #dee2e6;
20+
}
21+
</style>
22+
</head>
23+
<body>
24+
<div class="container">
25+
<div class="mb-3">
26+
<button class="btn btn-outline-secondary" id="theme-toggle">
27+
Toggle Dark/Light Mode
28+
</button>
29+
</div>
30+
{% block content %}{% endblock %}
31+
</div>
32+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
33+
<script>
34+
document.getElementById('theme-toggle').addEventListener('click', function() {
35+
const html = document.documentElement;
36+
if (html.getAttribute('data-bs-theme') === 'dark') {
37+
html.setAttribute('data-bs-theme', 'light');
38+
} else {
39+
html.setAttribute('data-bs-theme', 'dark');
40+
}
41+
});
42+
</script>
43+
</body>
44+
</html>

0 commit comments

Comments
 (0)