From 7e6d28b39ec3f706d86280804011f7436df90851 Mon Sep 17 00:00:00 2001 From: Raghuram Subramani <raghus2247@gmail.com> Date: Sat, 10 May 2025 09:05:48 +0530 Subject: [PATCH] add webapp --- web/db.json | 1 + web/run.py | 6 ++++++ web/app/__init__.py | 24 ++++++++++++++++++++++++ web/app/auth.py | 21 +++++++++++++++++++++ web/app/dump.rdb | 0 web/app/job_manager.py | 16 ++++++++++++++++ web/app/main.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ web/app/models.py | 36 ++++++++++++++++++++++++++++++++++++ web/app/jobs/scrape_cases.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ web/app/modules/encryption.py | 33 +++++++++++++++++++++++++++++++++ web/app/modules/interface.py | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ web/app/templates/base.html | 26 ++++++++++++++++++++++++++ web/app/templates/home.html | 34 ++++++++++++++++++++++++++++++++++ web/app/templates/login.html | 16 ++++++++++++++++ 14 files changed, 458 insertions(+) diff --git a/web/db.json b/web/db.json new file mode 100644 index 0000000..13140a4 100644 --- /dev/null +++ a/web/db.json @@ -1,0 +1,1 @@ +{"users": {"1": {"username": "admin", "password": "dontguessthisplzahh", "admin": true}}}diff --git a/web/run.py b/web/run.py new file mode 100644 index 0000000..a3fdaf3 100644 --- /dev/null +++ a/web/run.py @@ -1,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True) diff --git a/web/app/__init__.py b/web/app/__init__.py new file mode 100644 index 0000000..0ed5a9e 100644 --- /dev/null +++ a/web/app/__init__.py @@ -1,0 +1,24 @@ +from flask import Flask +from flask_login import LoginManager +from .models import User + +login_manager = LoginManager() + +def create_app(): + app = Flask(__name__) + app.secret_key = 'your_secret_key' + + login_manager.init_app(app) + login_manager.login_view = 'auth.login' + + from .auth import auth as auth_blueprint + from .main import main as main_blueprint + + app.register_blueprint(auth_blueprint) + app.register_blueprint(main_blueprint) + + return app + +@login_manager.user_loader +def load_user(user_id): + return User.get(user_id) diff --git a/web/app/auth.py b/web/app/auth.py new file mode 100644 index 0000000..88bc181 100644 --- /dev/null +++ a/web/app/auth.py @@ -1,0 +1,21 @@ +from flask import Blueprint, render_template, request, redirect, url_for +from flask_login import login_user +from .models import User + +auth = Blueprint('auth', __name__) + +@auth.route('/login', methods=['GET', 'POST']) +def login(): + error = None + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + + user = User.validate_login(username, password) + if user: + login_user(user) + return redirect(url_for('main.home')) + + error = "Invalid credentials" + + return render_template('login.html', error=error) diff --git a/web/app/dump.rdb b/web/app/dump.rdb new file mode 100644 index 0000000000000000000000000000000000000000..b0cbf446a965e3c019a666ccce1e342af4f8524b 100644 Binary files /dev/null and a/web/app/dump.rdb differ diff --git a/web/app/job_manager.py b/web/app/job_manager.py new file mode 100644 index 0000000..60c4635 100644 --- /dev/null +++ a/web/app/job_manager.py @@ -1,0 +1,16 @@ +from rq import Queue +from redis import Redis +from jobs.scrape_cases import scrape_cases + +class JobManager: + def __init__(self): + redis = Redis() + self.q = Queue(connection=redis) + + def enqueue_scrape(self, act, section, state_code): + return self.q.enqueue( + scrape_cases, + act, + section, + state_code + ) diff --git a/web/app/main.py b/web/app/main.py new file mode 100644 index 0000000..bd817b2 100644 --- /dev/null +++ a/web/app/main.py @@ -1,0 +1,61 @@ +from flask import request, flash +from flask import Blueprint, render_template, redirect, url_for +from flask_login import login_required, logout_user, current_user +from .models import User + +from .modules.interface import Interface + +states = Interface().get_states() + +main = Blueprint('main', __name__) + +@main.route('/') +@login_required +def home(): + return render_template('home.html', user=current_user, states=states) + +@main.route('/logout') +@login_required +def logout(): + logout_user() + return redirect(url_for('auth.login')) + + +@main.route('/create_user', methods=['POST']) +@login_required +def create_user(): + username = request.form.get('username') + password = request.form.get('password') + + if current_user.admin != True: + flash('Only admin can create new users.', 'error') + return redirect(url_for('main.home')) + + if not username or not password: + flash('Username and password required.', 'error') + return redirect(url_for('main.home')) + + user = User.create(username, password) + if user: + flash(f'User {username} created successfully.', 'success') + else: + flash(f'User {username} already exists.', 'error') + + return redirect(url_for('main.home')) + +@main.route('/enqueue_job', methods=['POST']) +@login_required +def enqueue_job(): + act = request.form.get('act') + section = request.form.get('section') + state_code = request.form.get('state_code') + + if not act or not state_code: + flash('All fields must be filled.', 'error') + return redirect(url_for('main.home')) + + if not section: + section = '' + + flash('Job created.', 'info') + return redirect(url_for('main.home')) diff --git a/web/app/models.py b/web/app/models.py new file mode 100644 index 0000000..e4bbb00 100644 --- /dev/null +++ a/web/app/models.py @@ -1,0 +1,36 @@ +from flask_login import UserMixin +from tinydb import TinyDB, Query + +db = TinyDB('db.json') +users_table = db.table('users') +UserQuery = Query() + +class User(UserMixin): + def __init__(self, username, admin): + self.id = username + self.admin = admin + + @staticmethod + def get(username): + result = users_table.get(UserQuery.username == username) + if result: + return User(username, result['admin']) + return None + + @staticmethod + def validate_login(username, password): + user = users_table.get((UserQuery.username == username) & (UserQuery.password == password)) + if user: + return User(username, user['admin']) + + return None + + @staticmethod + def create(username, password, admin=False): + if users_table.get(UserQuery.username == username): + return None + + users_table.insert({'username': username, 'password': password, 'admin': admin}) + return User(username, admin) + +User.create('admin', 'dontguessthisplzahh', admin=True) diff --git a/web/app/jobs/scrape_cases.py b/web/app/jobs/scrape_cases.py new file mode 100644 index 0000000..ec31f8a 100644 --- /dev/null +++ a/web/app/jobs/scrape_cases.py @@ -1,0 +1,68 @@ +from modules.interface import Interface +from tinydb import TinyDB +import time + +def scrape_cases(act, section, state_code, name=time.time_ns()): + db = TinyDB(f'{name}.json') + interface = Interface() + + def get_act_number(acts): + for act_code, act_name in acts: + if act_name == act: + return act_code + return None + try: + districts = interface.get_districts(state_code) + except Exception as e: + print(f"[ERROR] Failed to scrape districts: {e}") + districts = [] + + for dist_code, dist_name in districts: + print(f'DISTRICT: {dist_name}') + + try: + complexes = interface.get_complexes(state_code, dist_code) + except Exception as e: + print(f"[ERROR] Failed to scrape complexes for {dist_name}: {e}") + continue + + for complex_code, complex_name in complexes: + print(f'COMPLEX: {complex_name}') + + court_establishments = str(complex_code).split(',') + for i, court_establishment in enumerate(court_establishments, 1): + print(f'ESTABLISHMENT: {i}/{len(court_establishments)}') + + try: + acts = interface.get_acts(state_code, dist_code, court_establishment) + act_number = get_act_number(acts) + except Exception as e: + print(f"[ERROR] Failed to scrape acts for complex {complex_name}: {e}") + continue + + if not act_number: + continue + + try: + cases = interface.search_by_act(state_code, dist_code, court_establishment, act_number, section) + except Exception as e: + print(f"[ERROR] Failed to scrape cases in complex {complex_name}: {e}") + continue + + for j, case in enumerate(cases, 1): + print(f'CASE: {j}/{len(cases)}') + + try: + case_no = case['case_no'] + case_history = interface.case_history(state_code, dist_code, court_establishment, case_no) + except Exception as e: + print(f"[ERROR] Failed to get history for case {case.get('case_no', 'UNKNOWN')}: {e}") + continue + + try: + case_history['case_no'] = case_no + case_history['complex_name'] = complex_name + db.insert(case_history) + + except Exception as e: + print(f"[ERROR] Failed to parse orders for case {case_no}: {e}") diff --git a/web/app/modules/encryption.py b/web/app/modules/encryption.py new file mode 100644 index 0000000..47f9f29 100644 --- /dev/null +++ a/web/app/modules/encryption.py @@ -1,0 +1,33 @@ +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad +import base64 +import os +import json + +REQUEST_KEY = bytes.fromhex('4D6251655468576D5A7134743677397A') +RESPONSE_KEY = bytes.fromhex('3273357638782F413F4428472B4B6250') +GLOBAL_IV = "556A586E32723575" +IV_INDEX = '0' +RANDOMIV = os.urandom(8).hex() +IV = bytes.fromhex(GLOBAL_IV + RANDOMIV) + +class Encryption: + @staticmethod + def encrypt(data): + cipher = AES.new(REQUEST_KEY, AES.MODE_CBC, IV) + padded_data = pad(json.dumps(data).encode(), 16) + ct = cipher.encrypt(padded_data) + ct_b64 = base64.b64encode(ct).decode() + return RANDOMIV + str(IV_INDEX) + ct_b64 + + @staticmethod + def decrypt(data): + data = data.strip() + iv_hex = data[:32] + ct_b64 = data[32:] + + iv = bytes.fromhex(iv_hex) + ct = base64.b64decode(ct_b64) + cipher = AES.new(RESPONSE_KEY, AES.MODE_CBC, iv) + pt = unpad(cipher.decrypt(ct), 16) + return json.loads(pt.decode(errors="ignore")) diff --git a/web/app/modules/interface.py b/web/app/modules/interface.py new file mode 100644 index 0000000..929c5ab 100644 --- /dev/null +++ a/web/app/modules/interface.py @@ -1,0 +1,116 @@ +import requests + +import os + +from .encryption import Encryption + +BASE_URL = "https://app.ecourts.gov.in/ecourt_mobile_DC" +RETRY_ATTEMPTS = 10 +TIMEOUT = 5 + +class Interface: + def __init__(self): + self.token = self.fetch_token() + + def fetch_token(self): + uid = os.urandom(8).hex() + ':in.gov.ecourts.eCourtsServices' + payload = Encryption.encrypt({"version": "3.0", "uid": uid}) + r1 = requests.get(f"{BASE_URL}/appReleaseWebService.php", params={'params': payload}) + token = Encryption.decrypt(r1.text)['token'] + token = Encryption.encrypt(token) + if not token: + raise Exception + + return token + + def get(self, endpoint, data): + for _ in range(RETRY_ATTEMPTS): + try: + resp = requests.get( + f"{BASE_URL}/{endpoint}", + params={'params': data}, + headers={"Authorization": f"Bearer {self.token}"}, + timeout=TIMEOUT + ) + + return Encryption.decrypt(resp.text) + except: + continue + + raise Exception + + def get_states(self): + try: + data = Encryption.encrypt({'action_code': 'fillState'}) + states_list = self.get('stateWebService.php', data)['states'] + return list(map(lambda x: (x['state_code'], x['state_name']), states_list)) + except RuntimeError: + raise Exception("Failed to scrape states") + + def get_districts(self, state_code): + try: + data = Encryption.encrypt({"state_code": str(state_code)}) + districts_list = self.get('districtWebService.php', data)['districts'] + return list(map(lambda x: (x['dist_code'], x['dist_name']), districts_list)) + except RuntimeError: + raise Exception("Failed to scrape districts") + + def get_complexes(self, state_code, dist_code): + try: + data = Encryption.encrypt({ + "action_code": "fillCourtComplex", + "state_code": str(state_code), + "dist_code": str(dist_code) + }) + complexes_list = self.get('courtEstWebService.php', data)['courtComplex'] + if complexes_list is None: + return [] + return list(map(lambda x: (x['njdg_est_code'], x['court_complex_name']), complexes_list)) + except RuntimeError: + raise Exception("Failed to scrape court complexes") + + def get_acts(self, state_code, dist_code, complex_code): + try: + data = Encryption.encrypt({ + "state_code": str(state_code), + "dist_code": str(dist_code), + "court_code": str(complex_code), + "searchText": "", + "language_flag": "english", + "bilingual_flag": "0" + }) + acts_list = self.get('actWebService.php', data)['actsList'][0]['acts'].split('#') + return list(map(lambda x: (x.split('~')[0], x.split('~')[1]) if '~' in x else (x, None), acts_list)) + except RuntimeError: + raise Exception("Failed to scrape acts") + + def search_by_act(self, state_code, dist_code, complex_code, act_number, section=""): + try: + data = Encryption.encrypt({ + "state_code": str(state_code), + "dist_code": str(dist_code), + "court_code_arr": str(complex_code), + "language_flag": "english", + "bilingual_flag": "0", + "selectActTypeText": str(act_number), + "underSectionText": section, + "pendingDisposed": "Disposed" + }) + cases_list = self.get('searchByActWebService.php', data) + return cases_list['0']['caseNos'] + except RuntimeError: + raise Exception("Failed to scrape cases by act") + + def case_history(self, state_code, dist_code, complex_code, case_no): + try: + data = Encryption.encrypt({ + "state_code": str(state_code), + "dist_code": str(dist_code), + "court_code": str(complex_code), + "language_flag": "english", + "bilingual_flag": "0", + "case_no": case_no + }) + return self.get('caseHistoryWebService.php', data)['history'] + except RuntimeError: + raise Exception("Failed to scrape case history") diff --git a/web/app/templates/base.html b/web/app/templates/base.html new file mode 100644 index 0000000..190ed56 100644 --- /dev/null +++ a/web/app/templates/base.html @@ -1,0 +1,26 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>{% block title %}App{% endblock %}</title> + <link + rel="stylesheet" + href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css" + > +</head> +<body> +<main class="container"> + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + <ul class="flashes"> + {% for category, message in messages %} + <li class="{{ category }}">{{ message }}</li> + {% endfor %} + </ul> + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +</main> +</body> +</html> diff --git a/web/app/templates/home.html b/web/app/templates/home.html new file mode 100644 index 0000000..6bf2bc3 100644 --- /dev/null +++ a/web/app/templates/home.html @@ -1,0 +1,34 @@ +{% extends 'base.html' %} +{% block title %}Home{% endblock %} + +{% block content %} +<h2>Welcome, {{ user.id }}!</h2> +<p>You are logged in.</p> + +<a href="{{ url_for('main.logout') }}" role="button" class="secondary">Logout</a> + +{% if user.admin == True %} +<details name="example" style="margin-top: 40px;"> + <summary>Create a New User</summary> + <form method="post" action="{{ url_for('main.create_user') }}"> + <input type="text" name="username" placeholder="New Username" required> + <input type="password" name="password" placeholder="New Password" required> + <button type="submit">Create User</button> + </form> + +</details> +{% endif %} + +<h3>Create a New Job</h3> +<form method="post" action="{{ url_for('main.enqueue_job') }}"> + <input type="text" name="act" placeholder="Act Name*" required> + <input type="text" name="section" placeholder="Section"> + <select name="state" id="state"> + {% for code, name in states %} + <option value="{{ code }}">{{ name }}</option> + {% endfor %} + </select> + <button type="submit">Create User</button> +</form> + +{% endblock %} diff --git a/web/app/templates/login.html b/web/app/templates/login.html new file mode 100644 index 0000000..7855267 100644 --- /dev/null +++ a/web/app/templates/login.html @@ -1,0 +1,16 @@ +{% extends 'base.html' %} +{% block title %}Login{% endblock %} + +{% block content %} +<h2>Login</h2> +{% if error %} +<article class="grid"> + <p style="color: red">{{ error }}</p> +</article> +{% endif %} +<form method="post"> + <input type="text" name="username" placeholder="Username" required> + <input type="password" name="password" placeholder="Password" required> + <button type="submit">Login</button> +</form> +{% endblock %} -- rgit 0.1.5