# Project export: ProtestLink

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2025
- Tagline: Secure, real-time protest coordination via encrypted mesh and AI insights
- Devpost: https://devpost.com/software/protestlink
- GitHub: https://github.com/wyang563/protest_app
- Demo: http://protest.morelos.dev/
- Video: https://www.youtube.com/embed/rx5fM67ezFQ?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Delve: Most Secure App (AirPods + Delve Founder Numbers + Onsite))
- Team: 2 GitHub contributor(s) — Alan Morelos (100 commits), wyang563 (41 commits)

## Devpost submission (written by the team)

### Inspiration

In times of social unrest, the ability to communicate is as vital as the cause itself. Protesters across the world face increasing challenges when it comes to coordination, safety, and information sharing. Governments have been known to shut down cellular networks, limit internet access, or monitor online communication, making it difficult for protesters to organize effectively. Even when networks remain online, they often become overloaded, rendering them useless at critical moments. Protesters struggle with locating medical aid, distributing supplies, and avoiding high-risk areas where clashes may occur. Beyond digital suppression, protesters also struggle with navigating volatile environments, distributing supplies efficiently, and requesting medical assistance when situations escalate. How can protesters stay connected and informed when traditional communication systems fail? That was the problem we set out to solve. We asked ourselves: How can we create a communication system that is secure, decentralized, and resilient enough to function even under extreme restrictions? With this goal in mind, we built ProtestLink, a Wi-Fi mesh-based protest coordination tool that provides secure messaging, real-time resource allocation, and risk assessment without relying on internet access or centralized servers. ProtestLink ensures that protesters stay connected, protected, and prepared, even when traditional communication methods fail.

### What it does

ProtestLink is a decentralized communication and coordination platform designed to keep protesters connected, informed, and protected even in the face of network disruptions, internet blackouts, and active surveillance. By leveraging Wi-Fi networking, live location sharing, and AI-driven speech analysis, ProtestLink provides an autonomous protest network that helps protesters navigate safely, request critical resources, and broadcast urgent alerts without relying on traditional service providers. With ProtestLink, protesters can: Share real-time location data with organizers and trusted participants to create an interactive protester map for coordination and safety tracking. Identify and avoid high-risk areas through real-time protest heatmaps that visualize areas of tension and clashes. Indicate an emergency alert status to notify others if they are in danger, being arrested, in need of medical aid, or require supplies. Broadcast emergency alerts to organizers and trusted participants. Automatically transcribe and analyze protest audio using Whisper AI, detecting keywords, sentiment, and potential escalation zones in real-time. Ensure trust-based access with a zero-trust authentication model, where protesters join securely via QR code, eliminating the need for personal data like phone numbers.

### How we built it

At the core of ProtestLink is a self-hosted web server that facilitates location sharing, encrypted communication, and live protest heatmaps. We built a custom Wi-Fi network to allow devices to connect and interact without relying on traditional cellular infrastructure. Protesters can open the web app, share their live location, and send alerts—all without requiring a constant internet connection. To enhance real-time protest intelligence, we integrated Whisper AI, an edge-based transcription and NLP system capable of converting live protest audio into actionable data. This means that if a protester yells for help, reports police movement, or requests medical assistance, the system can analyze and categorize the request, ensuring it reaches organizers or other protest members in real time. We also used sentiment analysis to detect potential conflict zones, allowing ProtestLink to generate a live map of risk areas. The protester map is powered by WebSockets and real-time geolocation tracking, ensuring fluid updates without relying on centralized data sources. Protesters can pin their locations on the map, mark areas as safe or dangerous, and request water, medical aid, or evacuation support—creating a distributed protest intelligence network that operates independently of government-controlled systems. Security was a critical concern throughout development. We implemented end-to-end encryption to ensure that messages, locations, and alerts remain private and tamper-proof. The zero-trust authentication model allows protesters to join the network anonymously via QR codes, eliminating the need for phone numbers, email, or personal data. This ensures that no centralized entity can track or compromise protesters through ProtestLink. By combining Wi-Fi-based communication, AI-powered speech processing, and encrypted location sharing, we created a robust and decentralized coordination system that empowers protesters to act swiftly, securely, and strategically.

### Challenges we ran into

One of the biggest technical challenges we faced was working with Bluetooth on iOS. Initially, we planned to leverage Bluetooth mesh networking for decentralized communication, but Apple's restrictive CoreBluetooth API made this extremely difficult to implement. Unlike Android, which offers flexible background BLE (Bluetooth Low Energy) support, iOS heavily limits background Bluetooth communication, making it nearly impossible to maintain persistent message relays between devices. After multiple failed optimizations, we ultimately pivoted to Wi-Fi-based networking, which provided greater reliability, better message throughput, and cross-platform compatibility. Navigating Apple’s strict development ecosystem also posed a challenge. Integrating Swift-based networking protocols with React Native required bridging complex native APIs while ensuring that location tracking and WebSockets functioned reliably across both iOS and Android. Debugging Bluetooth issues in Apple’s sandboxed environment further complicated development, as maintaining persistent connections in the background was heavily restricted. Despite these challenges, we successfully transitioned to a more scalable Wi-Fi-based approach, built a real-time protest tracking map, and integrated speech recognition AI into an autonomous protest coordination system. These obstacles forced us to rethink our architecture, leading to a more powerful and adaptable solution.

### Accomplishments we're proud of

Building ProtestLink required pushing beyond conventional networking and security models, and we are incredibly proud of what we accomplished. One of our biggest milestones was successfully implementing a live protester map that provides real-time protest intelligence without relying on external services. The ability to share live locations, report threats, and visualize protest dynamics makes ProtestLink a game-changer for secure protest coordination. We are also proud of integrating Whisper AI’s real-time transcription engine, which allows ProtestLink to analyze protest audio, detect emergency phrases, and generate dynamic risk assessments. Protesters can now transmit critical alerts without needing to type, ensuring faster responses in high-risk situations. Additionally, we successfully implemented a zero-trust authentication system, ensuring that protesters can join securely and anonymously without exposing personal information or metadata. By making ProtestLink fully decentralized and resistant to surveillance, we created a communication tool that protesters can truly rely on.

### What we learned

This project reinforced just how fragile centralized communication systems can be in times of crisis. Governments actively shut down networks, throttle internet speeds, and exploit surveillance laws to suppress protests. ProtestLink has shown us that truly decentralized, peer-to-peer communication is not just a convenience—it’s a necessity. We also gained extensive experience in Wi-Fi networking, encrypted communication protocols, and real-time AI processing on edge devices. Working with Whisper AI and NLP-driven event detection expanded our knowledge of how machine learning can be used to enhance real-world safety. Most importantly, we learned that technology has the power to protect human rights. ProtestLink is more than just an app—it’s a lifeline for those who need a secure way to communicate, mobilize, and protect themselves in uncertain times.

### What's next

ProtestLink has the potential to redefine protest communication, and we’re excited to take it even further: Enhancing AI-driven risk detection to provide smarter, more responsive alerts. Expanding to more languages for global accessibility. Exploring LoRa-based long-range communication to expand mesh connectivity even further. ProtestLink isn’t just a project—it’s a movement. We’re committed to building censorship-resistant, decentralized technology that empowers people to speak, organize, and protect their rights—no matter what.

## README (from the GitHub repository)

Visit http://protest.morelos.dev/ for main mapping and login

Visit http://protest.morelos.dev/radio for radio transcription and http://protest.morelos.dev/head_radio for protest leader broadcast acceptance (view the broadcasted messages by a protest leader)



Can run this using `python app.py` while ensuirng that you install from `requirements.txt`

Can run the frontend simply with `npm start` after installing with `npm install`


## Detected evidence (automated analysis)

Indexed codebase: 44 recognized source files, 101 KB.
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- Hugging Face (technology) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (57 of 57)

```
.gitignore
backend/app.py
backend/auth.py
backend/routes.py
backend/transcriptions.db
backend/users.db
backend/wsgi.py
frontend/.env.development
frontend/.env.production
frontend/package.json
frontend/postcss.config.js
frontend/public/index.html
frontend/public/manifest.json
frontend/public/robots.txt
frontend/README.md
frontend/src/App.css
frontend/src/App.test.tsx
frontend/src/App.tsx
frontend/src/components/BeaconMap.tsx
frontend/src/components/BluetoothScanner.tsx
frontend/src/components/Header.tsx
frontend/src/components/ProtectedRoute.tsx
frontend/src/components/Recorder.tsx
frontend/src/components/SupplyRequestForm.tsx
frontend/src/contexts/AuthContext.tsx
frontend/src/index.css
frontend/src/index.tsx
frontend/src/pages/Dashboard.tsx
frontend/src/pages/HeadRadio.tsx
frontend/src/pages/Home.tsx
frontend/src/pages/Login.tsx
frontend/src/pages/Map.tsx
frontend/src/pages/Radio.tsx
frontend/src/pages/Register.tsx
frontend/src/pages/Signup.tsx
frontend/src/react-app-env.d.ts
frontend/src/reportWebVitals.ts
frontend/src/services/api.ts
frontend/src/services/bluetoothService.ts
frontend/src/services/locationService.ts
frontend/src/setupTests.ts
frontend/src/types/bluetooth.d.ts
frontend/src/types/react-leaflet-heatmap-layer-v3.d.ts
frontend/src/types/react-leaflet.d.ts
frontend/src/utils.ts
frontend/src/utils/sentimentUtils.ts
frontend/tailwind.config.js
frontend/tsconfig.json
netlify.toml
radio_stream/__init__.py
radio_stream/radio_listener.py
radio_stream/run_listener.sh
README.md
requirements.txt
start_backend.sh
transcriptions.db
users.db
```

### Dependencies

- frontend/package.json: @emotion/react@^11.14.0, @emotion/styled@^11.14.0, @mui/icons-material@^6.4.4, @mui/material@^6.4.4, @testing-library/dom@^8.20.0, @testing-library/jest-dom@^5.16.5, @testing-library/react@^12.1.5, @testing-library/user-event@^13.5.0, @types/jest@^27.5.2, @types/leaflet@^1.9.16, @types/node@^16.18.126, @types/react@^17.0.0, @types/react-dom@^17.0.0, @types/react-router-dom@^5.3.3, ajv@^8.12.0, ajv-keywords@^5.1.0, autoprefixer@^10.4.20, axios@^1.7.9, leaflet@^1.9.4, lucide-react@^0.475.0, postcss@^8.5.2, react@^17.0.2, react-dom@^17.0.2, react-leaflet@^3.2.5, react-leaflet-heatmap-layer-v3@3.0.3-beta-1, react-router-dom@^6.8.0, react-scripts@5.0.1, tailwindcss@^3.4.17, typescript@^4.9.5, web-vitals@^2.1.4
- requirements.txt: flask, flask-cors, numpy@<2, openai-whisper, requests, sentence-transformers, transformers

### Recent commits (newest first)

- Update README.md
- Create README.md
- Merge branch 'main' of https://github.com/wyang563/protest_app
- netlify deployment crap attempt
- netlify deployment crap attempt
- Merge branch 'main' of https://github.com/wyang563/protest_app
- recording
- talk
- centered
- pls fix box
- all else
- hopefully fixed connections
- Merge branch 'main' of https://github.com/wyang563/protest_app
- added record button component
- map pls
- fix ugly pls
- more squaring
- padding again
- padding
- more zoomed in

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
openai-whisper
numpy<2
flask
flask-cors
requests
transformers
sentence-transformers
```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@emotion/react": "^11.14.0",
    "@emotion/styled": "^11.14.0",
    "@mui/icons-material": "^6.4.4",
    "@mui/material": "^6.4.4",
    "@testing-library/dom": "^8.20.0",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^12.1.5",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.18.126",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "ajv": "^8.12.0",
    "ajv-keywords": "^5.1.0",
    "axios": "^1.7.9",
    "leaflet": "^1.9.4",
    "lucide-react": "^0.475.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-leaflet": "^3.2.5",
    "react-leaflet-heatmap-layer-v3": "3.0.3-beta-1",
    "react-router-dom": "^6.8.0",
    "react-scripts": "5.0.1",
    "typescript": "^4.9.5",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@types/leaflet": "^1.9.16",
    "@types/react-router-dom": "^5.3.3",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.5.2",
    "tailwindcss": "^3.4.17"
  }
}

```

### backend/app.py

```python
from flask import Flask, request, jsonify
from flask_cors import CORS, cross_origin
import whisper
import os
from routes import routes_bp
from auth import auth_bp, init_auth_db
import sqlite3
from transformers import pipeline
from sentence_transformers import SentenceTransformer, util  # New import
from datetime import timedelta

app = Flask(__name__) # here

CORS(app,
    resources={
        r"/api/*": {
            "origins": ["https://protest.morelos.dev", "http://localhost:3000"],
            "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
            "allow_headers": ["Content-Type", "Authorization"],
            "supports_credentials": True,
            "expose_headers": ["Set-Cookie"]
        }
    },
    supports_credentials=True
)


# Basic session config without security
app.config.update(
    SECRET_KEY=os.environ.get('SECRET_KEY', 'dev-key-change-this'),
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE='Lax',
    SESSION_COOKIE_DOMAIN='protest.morelos.dev',  # Update for production
    PERMANENT_SESSION_LIFETIME=timedelta(days=7),
    SESSION_COOKIE_NAME='protest_session'
)

app.register_blueprint(auth_bp)  # Was: url_prefix='/api'
app.register_blueprint(routes_bp)  # Was: url_prefix='/api'

init_auth_db()

# Load the Whisper model once at startup to avoid reloading on every request.
# You can choose a model size: tiny, base, small, medium, large.
MODEL_TYPE = "base"
model = whisper.load_model(MODEL_TYPE)
classifier = pipeline("text-classification", model="martin-ha/toxic-comment-model", device=-1)
embedding_model = SentenceTransformer("all-MiniLM-L6-v2")

# Load the DistilBERT model once at startup to avoid reloading on every request.
@app.route('/sentiment_analysis', methods=['GET'])
@cross_origin(origin="https://protest.morelos.dev")
def sentiment_analysis():
    """
    Run the sentiment analysis model, then compute sentence embeddings for the 
    returned label using BERT (SentenceTransformers) and compare it to standard 
    candidate labels to output the one most similar.
    """
    text = request.args.get('text')
    if not text:
        return jsonify({"error": "Missing text parameter for input to ML model"}), 400

    # Run the sentiment analysis classifier
    result = classifier(text)[0]
    if not result:
        return jsonify({"error": "Failed to classify text"}), 500
    api_label = result.get("label", "")
    api_score = result.get("score", 0)

    # Define candidate labels
    candidate_labels = ["need supplies", "fleeing", "medical emergency", "advancing"]

    # Compute embeddings using the SentenceTransformer model
    api_embedding = embedding_model.encode(api_label, convert_to_tensor=True)
    candidate_embeddings = embedding_model.encode(candidate_labels, convert_to_tensor=True)

    # Compute cosine similarity scores and pick the best matching candidate
    cosine_scores = util.cos_sim(api_embedding, candidate_embeddings)
    best_idx = int(cosine_scores.argmax())
    best_candidate = candidate_labels[best_idx]
    
    # Optionally, include the similarity score (confidence) if desired:
    similarity_confidence = cosine_scores[0][best_idx].item()

    # Return the best matching candidate along with the original confidence score from the classifier
    return jsonify({
        "label": best_candidate,
        "score": api_score,
        "similarity_confidence": similarity_confidence
    })

@app.route('/query', methods=['GET'])
@cross_origin(origin="https://protest.morelos.dev")
def query_transcriptions_db():
    """
    SQL Query the transcriptions database for all transcriptions.
    """ 
    # Retrieve the SQL query from the GET request parameters
    sql_query = request.args.get('query')
    if not sql_query:
        return jsonify({"error": "Missing query parameter"}), 400

    # Define the path to the SQLite database file
    # Adjust this path as necessary to point to your transcriptions.db
    db_file = os.path.join("backend", "transcriptions.db")
    
    try:
        # Connect to the SQLite database
        conn = sqlite3.connect(db_file)
        cursor = conn.cursor()

        # Execute the query (WARNING: In production, never execute unsanitized SQL)
        cursor.execute(sql_query)
        rows = cursor.fetchall()

        # Retrieve column names for building dict results
        col_names = [description[0] for description in cursor.description]

        # Convert each row to a dictionary keyed by column name
        results = [dict(zip(col_names, row)) for row in rows]

        # Close the connection
        conn.close()

        # Return the results as JSON
        return jsonify(results)

    except Exception as e:
        return jsonify({"error": str(e)}), 500

@app.route('/range_transcriptions', methods=['GET'])
@cross_origin(origin="https://protest.morelos.dev")
def get_transcriptions():
    """
    Example endpoint to get transcriptions filtered by:
      - radio_stream
      - optional start_time (YYYY-MM-DDTHH:MM)
      - optional end_time   (YYYY-MM-DDTHH:MM)
    """
    db_file = os.path.join("backend", "transcriptions.db")

    # Query parameters from the request
    radio_stream = request.args.get('radio_stream')
    start_time   = request.args.get('start_time')  # expects e.g. "2023-09-01T10:00"
    end_time     = request.args.get('end_time')    # expects e.g. "2023-09-02T09:59"

    # Basic validation
    if not radio_stream:
        return jsonify({"error": "Missing radio_stream"}), 400

    try:
        conn = sqlite3.connect(db_file)
        cursor = conn.cursor()

        # Build a base query
        sql = "SELECT * FROM transcriptions WHERE radio_stream = ?"
        params = [radio_stream]

        # If start_time is provided, filter by start_time
        if start_time:
            sql += " AND start_time >= ?"
            params.append(start_time)

        # If end_time is provided, filter by end_time
        if end_time:
            sql += " AND start_time <= ?"
[truncated — 2045 more characters]
```

### frontend/src/index.tsx

```typescript
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root') as HTMLElement
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

```

### frontend/src/App.tsx

```typescript
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import { Map } from './pages/Map';
import HeadRadio from './pages/HeadRadio';
import Radio from './pages/Radio';
import { Login } from './pages/Login';
import { Signup } from './pages/Signup';
import { AuthProvider } from './contexts/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import 'leaflet/dist/leaflet.css';

function App() {
  return (
    <AuthProvider>
      <Router>
        <div className="App">
          <Routes>
            <Route path="/login" element={<Login />} />
            <Route path="/signup" element={<Signup />} />
            <Route path="/" element={<Navigate to="/map" replace />} />
            <Route path="/map" element={
              <ProtectedRoute>
                <Map />
              </ProtectedRoute>
            } />
            <Route path="/head_radio" element={
              <ProtectedRoute>
                <HeadRadio />
              </ProtectedRoute>
            } />
            <Route path="/radio" element={
              <ProtectedRoute>
                <Radio />
              </ProtectedRoute>
            } />
          </Routes>
        </div>
      </Router>
    </AuthProvider>
  );
}

export default App;
```

### start_backend.sh

```shell
#!/bin/bash

# Set up Python virtual environment
python3.10 -m venv venv
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt

# Start Flask app in the background
python backend/app.py &

```

### backend/wsgi.py

```python
# wsgi.py
from app import app

if __name__ == "__main__":
    app.run()

```

### frontend/postcss.config.js

```javascript
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

```

### frontend/tailwind.config.js

```javascript
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./src/**/*.{js,ts,jsx,tsx}"
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

```

### radio_stream/run_listener.sh

```shell
#!/bin/bash

python radio_listener.py --stream_name "Fox" --stream_url "https://live.amperwave.net/direct/foxnewsradio-foxnewsradioaac-imc?source=fnr.web" --model_type "base"
```

[33 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]