This commit is contained in:
Carlos Rivas 2024-08-23 21:08:27 -07:00
parent 3aea98c315
commit 20f069dd67
14 changed files with 319 additions and 14 deletions

View file

@ -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

27
Dockerfile Normal file
View file

@ -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"]

1
README.md Normal file
View file

@ -0,0 +1 @@
# FlowSMS - Flowroute SMS Listener

96
app.py
View file

@ -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)

14
docker-compose.yml Normal file
View file

@ -0,0 +1,14 @@
services:
flosms:
container_name: 'flosms'
build:
context: ./
dockerfile: ./Dockerfile
tags:
- "hub.digitalhippo.tech/flosms"
networks:
default:
external:
name: nginx-network

Binary file not shown.

View file

@ -1,4 +1,6 @@
arel
fastapi
jinja2
python-dotenv
sqlalchemy
uvicorn

92
static/css/style.css Normal file
View file

@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 MiB

BIN
static/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

0
static/js/script.js Normal file
View file

36
templates/index.html Normal file
View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Table Demo</title>
<link rel="stylesheet" href="/static/css/style.css">
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
<!-- Favicons -->
<link href="/static/images/favicon.png" rel="icon">
</head>
<body>
<main class="table">
<section class="table__header">
<h1>Digital Hippo Labs - FlowSMS</h1>
</section>
<section class="table__content">
<table>
<thead>
<tr>
<th>From Number</th>
<th>Message</th>
<th>Timestamp</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="messages" hx-get="/sms-message/17072064097" hx-swap="innerHTML" hx-trigger="load, every 2s"></tbody>
</table>
</section>
</main>
</body>
<script src="https://kit.fontawesome.com/b031afcd74.js" crossorigin="anonymous"></script>
</html>

12
templates/messages.html Normal file
View file

@ -0,0 +1,12 @@
{% for message in messages %}
<tr>
<td>{{message.from_number}}</td>
<td>{{message.message}}</td>
<td>{{message.timestamp}}</td>
<td>
<button hx-delete="/sms-message/{{message.id}}">
<i class="fa-solid fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}