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

Digital Hippo Labs - FlowSMS

+
+
+ + + + + + + + + + +
From NumberMessageTimestampActions
+
+
+ + + \ 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