diff --git a/.forgejo/workflows/build-python-app.yml b/.forgejo/workflows/build-python-app.yml
new file mode 100644
index 0000000..5c19ede
--- /dev/null
+++ b/.forgejo/workflows/build-python-app.yml
@@ -0,0 +1,53 @@
+name: Build Python App
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ build:
+ runs-on: ubuntu-22.04
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install requirements
+ run: |
+ python3 -m pip install --upgrade pip
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+
+ - name: Lint with Ruff
+ run: |
+ pip install ruff
+ ruff format . --target-version=py311
+ continue-on-error: true
+
+ - name: Test with pytest
+ run: |
+ pip install coverage
+ coverage run -m pytest -v -s
+
+ - name: Generate Coverage Report
+ run: |
+ coverage report -m
+
+ - name: Login to Digital Hippo Labs Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DIPPOLABS_USERNAME }}
+ password: ${{ secrets.DIPPOLABS_TOKEN }}
+ registry: hub.digitalhippo.tech
+
+ - name: Build Docker Image
+ uses: docker/build-push-action@v6
+ with:
+ push: true
+ tags: hub.digitalhippo.tech/flowsms:latest
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..db04aa3
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,27 @@
+FROM python:3.11-alpine
+
+WORKDIR /deployment
+
+# Setup virtual environment
+ENV VIRTUAL_ENV=/opt/venv
+RUN python3 -m venv $VIRTUAL_ENV
+ENV PATH="$VIRTUAL_ENV/bin:$PATH"
+
+# Install dependencies:
+COPY requirements.txt .
+RUN ["pip", "install", "-r", "requirements.txt"]
+
+# Copy the application:
+ADD app.py .
+ADD database.py .
+ADD dtos.py .
+ADD loggerino.py .
+ADD models.py .
+ADD schemas.py .
+ADD service.py .
+
+# Expose the port
+EXPOSE 8000
+
+# Run the application:
+CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--reload"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9de1680
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# FlowSMS - Flowroute SMS Listener
\ No newline at end of file
diff --git a/app.py b/app.py
index d8315f5..23dc948 100644
--- a/app.py
+++ b/app.py
@@ -1,10 +1,14 @@
import logging
import uvicorn
+from arel import HotReload, Path
from database import engine, Base
from dotenv import load_dotenv
-from fastapi import FastAPI, Request, status
-from fastapi.responses import JSONResponse
+from fastapi import FastAPI, Request, status, Header
+from fastapi.encoders import jsonable_encoder
+from fastapi.responses import JSONResponse, HTMLResponse
+from fastapi.staticfiles import StaticFiles
+from fastapi.templating import Jinja2Templates
from loggerino import timestamp_log_config
from os import getenv
from schemas import SMSMessage
@@ -13,26 +17,77 @@ from service import (
retrieve_sms_messages_by_phone_number_handler,
delete_sms_message_by_uuid_handler
)
+from typing import Optional
from uuid import UUID
from uvicorn.config import LOGGING_CONFIG
-# Create instance of FastAPI
-app = FastAPI()
+# --------------------------------------------------------------------------------
+# Bootstrap
+# --------------------------------------------------------------------------------
-# Load dotenv file (.env)
load_dotenv()
-# Load custom logger format
+
+# --------------------------------------------------------------------------------
+# App Creation
+# --------------------------------------------------------------------------------
+
+app = FastAPI()
+
+
+# --------------------------------------------------------------------------------
+# Logger
+# --------------------------------------------------------------------------------
+
logger = logging.getLogger("uvicorn.error")
-# Create tables (if they don't exist)
+
+# --------------------------------------------------------------------------------
+# Database
+# --------------------------------------------------------------------------------
+
Base.metadata.create_all(bind=engine)
-# Load whitelisted IPs
+
+# --------------------------------------------------------------------------------
+# Application Configurations
+# --------------------------------------------------------------------------------
+
WHITELISTED_IPS = getenv("WHITELISTED_IPS").split(',')
+# --------------------------------------------------------------------------------
+# Static Files
+# --------------------------------------------------------------------------------
+
+app.mount("/static", StaticFiles(directory="static"), name="static")
+
+
+# --------------------------------------------------------------------------------
+# Templates
+# --------------------------------------------------------------------------------
+
+templates = Jinja2Templates(directory="templates")
+
+
+# --------------------------------------------------------------------------------
+# Hot Reload
+# --------------------------------------------------------------------------------
+
+if _debug := getenv("DEBUG"):
+ hot_reload = HotReload(paths=[Path(".")])
+ app.add_websocket_route("/hot-reload", route=hot_reload, name="hot-reload")
+ app.add_event_handler("startup", hot_reload.startup)
+ app.add_event_handler("shutdown", hot_reload.shutdown)
+ templates.env.globals["DEBUG"] = _debug
+ templates.env.globals["hot_reload"] = hot_reload
+
+
+# --------------------------------------------------------------------------------
+# Middleware
+# --------------------------------------------------------------------------------
+
@app.middleware("http")
async def validate_ip(request: Request, call_next):
ip = str(request.headers.get("x-forwarded-for", str(request.client.host)))
@@ -44,17 +99,30 @@ async def validate_ip(request: Request, call_next):
return await call_next(request)
+# --------------------------------------------------------------------------------
+# Routes
+# --------------------------------------------------------------------------------
+
+@app.get("/", summary="Main page that redirects to application", response_class=HTMLResponse)
+async def index(request: Request):
+ return templates.TemplateResponse("index.html", {"request": request})
+
+
+@app.get('/sms-message/{number}', response_class=HTMLResponse)
+def get_sms_messages_by_phone_number(request: Request, number: str, hx_request: Optional[str] = Header(None), cost: bool = False, metadata: bool = False):
+ contact = retrieve_sms_messages_by_phone_number_handler(number, cost, metadata)
+ if hx_request:
+ return templates.TemplateResponse(
+ request=request, name="messages.html", context={"messages": contact}
+ )
+ return JSONResponse(content=jsonable_encoder({'status': 'success', 'results': len(contact), 'response': contact}))
+
+
@app.post("/sms-message", status_code=status.HTTP_200_OK)
async def post_sms_message(message: SMSMessage):
receive_sms_messages_handler(message)
-@app.get('/sms-message/{number}', status_code=status.HTTP_200_OK)
-def get_sms_messages_by_phone_number(number: str, cost: bool = False, metadata: bool = False):
- contact = retrieve_sms_messages_by_phone_number_handler(number, cost, metadata)
- return {'status': 'success', 'results': len(contact), 'response': contact}
-
-
@app.delete('/sms-message/{uuid}', status_code=status.HTTP_202_ACCEPTED)
def delete_sms_message_by_uuid(uuid: UUID):
delete_sms_message_by_uuid_handler(uuid)
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..a6eb758
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,14 @@
+services:
+
+ flosms:
+ container_name: 'flosms'
+ build:
+ context: ./
+ dockerfile: ./Dockerfile
+ tags:
+ - "hub.digitalhippo.tech/flosms"
+
+networks:
+ default:
+ external:
+ name: nginx-network
diff --git a/flowsms.db b/flowsms.db
index 08fbe5b..67312b9 100644
Binary files a/flowsms.db and b/flowsms.db differ
diff --git a/requirements.txt b/requirements.txt
index 5950719..46dd9cb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,6 @@
+arel
fastapi
+jinja2
python-dotenv
sqlalchemy
uvicorn
diff --git a/static/css/style.css b/static/css/style.css
new file mode 100644
index 0000000..fcf8642
--- /dev/null
+++ b/static/css/style.css
@@ -0,0 +1,92 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+body {
+ min-height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: url('../images/background.jpg') center / cover;
+}
+
+main.table {
+ width: 82vw;
+ height: 90vh;
+ background-color: #fff5;
+ backdrop-filter: blur(7px);
+ box-shadow: 0 .4rem .8rem #0005;
+ border-radius: .8rem;
+ overflow: hidden;
+}
+
+.table__header {
+ width: 100%;
+ height: 10%;
+ background-color: #fff4;
+ padding: .8rem 1rem;
+}
+
+.table__content {
+ width: 95%;
+ max-height: calc(89% - .8rem);
+ background-color: #fffb;
+ margin: .8rem auto;
+ border-radius: .6rem;
+ overflow: auto;
+}
+
+.table__content::-webkit-scrollbar-thumb {
+ border-radius: .5rem;
+ background-color: #0004;
+ visibility: hidden;
+}
+
+.table__content:hover::-webkit-scrollbar-thumb {
+ visibility: visible;
+}
+
+table, th, td {
+ padding: 1rem;
+ border-collapse: collapse;
+ text-align: left;
+}
+
+thead th {
+ position: sticky;
+ top: 0;
+ left: 0;
+ background-color: #d5d1defe;
+}
+
+tbody tr:nth-child(even) {
+ background-color: #0000000b;
+}
+
+td img {
+ width: 36px;
+ height: 36px;
+ margin-right: .5rem;
+ border-radius: 50%;
+ vertical-align: middle;
+}
+
+button {
+ outline: none;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ padding: 10px;
+ color: white;
+ background-color: red;
+ font-size: medium;
+}
+
+@media (max-width: 1920px) {
+ td:not(:first-of-type) {
+ min-width: 22rem;
+ }
+}
\ No newline at end of file
diff --git a/static/images/background.jpg b/static/images/background.jpg
new file mode 100644
index 0000000..0fa2d35
Binary files /dev/null and b/static/images/background.jpg differ
diff --git a/static/images/favicon.png b/static/images/favicon.png
new file mode 100644
index 0000000..79a97b7
Binary files /dev/null and b/static/images/favicon.png differ
diff --git a/static/images/user_3177440.png b/static/images/user_3177440.png
new file mode 100644
index 0000000..2b8b658
Binary files /dev/null and b/static/images/user_3177440.png differ
diff --git a/static/js/script.js b/static/js/script.js
new file mode 100644
index 0000000..e69de29
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..37e4e41
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+ Table Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ From Number
+ Message
+ Timestamp
+ Actions
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/messages.html b/templates/messages.html
new file mode 100644
index 0000000..be032cc
--- /dev/null
+++ b/templates/messages.html
@@ -0,0 +1,12 @@
+{% for message in messages %}
+
+ {{message.from_number}}
+ {{message.message}}
+ {{message.timestamp}}
+
+
+
+
+
+
+{% endfor %}
\ No newline at end of file