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