From df203943ac87ebee157c12ff061cb32e079e49f7 Mon Sep 17 00:00:00 2001 From: Carlos Rivas Date: Thu, 22 Aug 2024 00:49:09 -0700 Subject: [PATCH] Massive update, schemas and models --- .env | 1 + app.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++ dtos.py | 55 +++++++++++++++++++++++++++++++++++++++++++++ flowsms.db | Bin 0 -> 40960 bytes loggerino.py | 13 +++++++++++ models.py | 10 +++------ requirements.txt | 1 + service.py | 30 +++++++++++++++++++++++++ 8 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 .env create mode 100644 dtos.py create mode 100644 flowsms.db create mode 100644 loggerino.py diff --git a/.env b/.env new file mode 100644 index 0000000..c7f72ad --- /dev/null +++ b/.env @@ -0,0 +1 @@ +WHITELISTED_IPS = 52.88.246.140,52.10.220.50,54.190.46.191,52.43.82.110,127.0.0.1 \ No newline at end of file diff --git a/app.py b/app.py index e69de29..50b6f8c 100644 --- a/app.py +++ b/app.py @@ -0,0 +1,57 @@ +import logging +import uvicorn + +from database import engine, Base +from dotenv import load_dotenv +from fastapi import FastAPI, Request, status +from fastapi.responses import JSONResponse +from loggerino import timestamp_log_config +from os import getenv +from schemas import SMSMessage +from service import inbound_sms_handler, retrieve_sms_messages_by_phone_number +from sqlalchemy.orm import Session, joinedload +from uvicorn.config import LOGGING_CONFIG + + +# Create instance of FastAPI +app = FastAPI() + +# Load dotenv file (.env) +load_dotenv() + +# Load custom logger format +logger = logging.getLogger("uvicorn.error") + +# Create tables (if they don't exist) +Base.metadata.create_all(bind=engine) + +# Load whitelisted IPs +WHITELISTED_IPS = getenv("WHITELISTED_IPS").split(',') + + +@app.middleware("http") +async def validate_ip(request: Request, call_next): + ip = str(request.headers.get("x-forwarded-for", str(request.client.host))) + + if ip not in WHITELISTED_IPS: + data = {"message": f"IP {ip} is not allowed to access this resource."} + return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content=data) + + return await call_next(request) + + +@app.post("/sms-message", status_code=status.HTTP_200_OK) +async def receive_sms_message(message: SMSMessage): + inbound_sms_handler(message) + + +@app.get('/sms-message/{number}') +def get_sms_messages_by_phone_number(number: str, limit: int = 10, page: int = 1): + contact = retrieve_sms_messages_by_phone_number(number, limit, page) + return {'status': 'success', 'results': len(contact.messages), 'response': contact} + + +if __name__ == "__main__": + uvicorn.run( + app, host="0.0.0.0", port=8000, log_config=timestamp_log_config(LOGGING_CONFIG) + ) diff --git a/dtos.py b/dtos.py new file mode 100644 index 0000000..523f32b --- /dev/null +++ b/dtos.py @@ -0,0 +1,55 @@ +from models import SMSMessage as SMSDBMessage, SMSContact, SMSCost, SMSMetadata +from schemas import SMSMessage + + +def to_db(message: SMSMessage) -> SMSContact: + payload = message.data.attributes + + sms_metadata = SMSMetadata() + sms_metadata.message_callback_url = payload.message_callback_url + sms_metadata.message_encoding = payload.message_encoding + sms_metadata.message_type = payload.message_type + sms_metadata.status = payload.status + + sms_cost = SMSCost() + sms_cost.amount_display = payload.amount_display + sms_cost.amount_nanodollars = payload.amount_nanodollars + + sms_message = SMSDBMessage() + sms_message.from_number = payload.from_number + sms_message.direction = payload.direction + sms_message.is_mms = payload.is_mms + sms_message.message = payload.message + sms_message.timestamp = payload.timestamp + sms_message.cost = sms_cost + sms_message.sms_metadata = sms_metadata + + sms_contact = SMSContact() + sms_contact.phone_number = payload.to_number + sms_contact.messages = [sms_message] + return sms_contact + + +def to_db_existing_contact(message: SMSMessage) -> SMSDBMessage: + payload = message.data.attributes + + sms_metadata = SMSMetadata() + sms_metadata.message_callback_url = payload.message_callback_url + sms_metadata.message_encoding = payload.message_encoding + sms_metadata.message_type = payload.message_type + sms_metadata.status = payload.status + + sms_cost = SMSCost() + sms_cost.amount_display = payload.amount_display + sms_cost.amount_nanodollars = payload.amount_nanodollars + + sms_message = SMSDBMessage() + sms_message.from_number = payload.from_number + sms_message.direction = payload.direction + sms_message.is_mms = payload.is_mms + sms_message.message = payload.message + sms_message.timestamp = payload.timestamp + sms_message.cost = sms_cost + sms_message.sms_metadata = sms_metadata + + return sms_message \ No newline at end of file diff --git a/flowsms.db b/flowsms.db new file mode 100644 index 0000000000000000000000000000000000000000..43c5dbc4d21d08bef55a784f4a77bf9cacf6c07a GIT binary patch literal 40960 zcmeI5&u<&Y6~{?Y5+zFF76lAJ5R7i20F}{5nwg#5nRO2>tu8yF8_7Ca@}rF_o)>bP z4}ZJl{UWR);Wt;D35S3H5C8%|00;m9AOHluWde_W zHCJ4|c{BgX+dUzhre%6Ui=G%g&u{NnD+g8opmJxY>W^Oa*Guya?Qh?!?5_vpmcP4q z;O`#p?7Zoh=3A!Q6-TD7M6)S{`bGWpr0M^xvY!+lThO#sM>pC>R~G7hbZo{J==MbK zv^)0D$dk)FRVPODjIO)?fDi81st*n-wRilts&^}gI|u&y_Tm11b@!ltcIaX=-`T%k ztL(q$|Frs^zuwTdlBVC?+ppe#dpCJ){oMF1f4_RS>gw9AK5zt6I_W8;TU+z9%iA~e zxkg)?54*o?y8p@R;*NyIRQ+zZQ-LObov^%=fY>JcrSHeFi&MyDx#$|*PQgxg>&OQ2gy12Z$n*Zc* zI1bW&8g|#r1-@r}c=;f(C!JQkecF=dWGvraXLNMz7}AXsqk4@_d+f=LZoSp&`givB zcB+-#abs%iX}yMX+>PvG|1CG7lgs+Tso&~oIBV~P!Js1;ub>pXWYhz}1xw3j$ zR}G;jVo01yBIKmZ5;0U!VbfB+Bx0zd!=0D%`J@J6AQ&k7z)trcqXSo zxbQUR>GEI8Z!Z0Q@!yL-ce}6v0U!VbfB+Bx0zd!=00AKIKSrRxQk<`?)mG=WDuJdP zBNHo$142y@$RI#0mK+;|1I`d82*)H!j%MkGjka{t0_c0D*(8+GWTk|V&M12rvm^sK zq_+v)WE{C=bmTlYne>KW{%$)m1Dc4jm zV>zRNvQnGKD1t(Y0vllVjBi;|>I-voP*Hzn`rJ200!}05a=8t48Qzi!=TC?zi$bY_ z;QD)^gFi;3lIPTKmZ5;0U!Vb zfB+Bx0zlyPBXCrlt*x%sTxviV5hDa)$WS7QR4P&^jzZ-mdaU^yXcHmo{?aAurgBWi z#L?`)P#7T_M>vdF7*OM8aYLbUR#lQVVM7#LG?aB?`c*^tXl7uDA?ajttfUk0sd8> z#^o4<%tTx}m^`uq*^7p*)&VXhf_&QR9P{uo2a z_y33b|C-bPAA6s9Eztj8zqdJPF%SR(KmZ5;0U!VbfB+Bx0zlx^CXj0F8G)Tt|IZ4_ zr22nWKqb}xvw|V1{?9T383X-4BM33j|1$yzss6t>pYuB9zn5!EPnUkU_^I241qc8E zAOHk_01yBIKmZ5;fmfM8zg(QF?Rhu9=cH~*HO7oEA_K~-mKY&Mt-&Nvf``Eez20&6 z_STkdcDk+Z=1Jq>rs!_D&5hwtvZH3FeWW_tXwz&wG$%$+pojO%vm=KrF)`6f3uW9a z0+w?uoYaX>9Aa(*HHkwL=;rYE?L4&tw2ISvBWZP zGZwfh0|F`u({_?Bn?SGcm#0P!38ZCU+?VgEIM6N%;$>{y%ne0Ni_(-{_!iKmZ5; z0U!VbfB+Bx0zd!=00AKI`Vkm>*Z%5gSE~PKMW9ms-)2OGQvE+GQj_ZcS<#bJ|IdnO L41WJFBg*h4v|G*Q literal 0 HcmV?d00001 diff --git a/loggerino.py b/loggerino.py new file mode 100644 index 0000000..14bfae5 --- /dev/null +++ b/loggerino.py @@ -0,0 +1,13 @@ +from typing import Any, Dict + + +def timestamp_log_config(uvicorn_log_config: Dict[str, Any]) -> Dict[str, Any]: + datefmt = "%d-%m-%Y %H:%M:%S" + formatters = uvicorn_log_config["formatters"] + formatters["default"]["fmt"] = "%(levelprefix)s [%(asctime)s] %(message)s" + formatters["access"]["fmt"] = ( + '%(levelprefix)s [%(asctime)s] %(client_addr)s - "%(request_line)s" %(status_code)s' + ) + formatters["access"]["datefmt"] = datefmt + formatters["default"]["datefmt"] = datefmt + return uvicorn_log_config diff --git a/models.py b/models.py index 9e8c96a..834728c 100644 --- a/models.py +++ b/models.py @@ -9,10 +9,9 @@ from uuid import uuid4, UUID class SMSContact(Base): __tablename__ = 'contact' id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4) - phone_number = Column(String, nullable=False) + phone_number = Column(String, nullable=False, unique=True) messages: Mapped[List["SMSMessage"]] = relationship() created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now()) - updated_at = Column(TIMESTAMP(timezone=True), default=None, onupdate=func.now()) class SMSMessage(Base): @@ -25,9 +24,8 @@ class SMSMessage(Base): message = Column(String, nullable=False) timestamp = Column(DateTime, nullable=False) cost: Mapped["SMSCost"] = relationship(back_populates="message") - metadata: Mapped["SMSMetadata"] = relationship(back_populates="message") + sms_metadata: Mapped["SMSMetadata"] = relationship(back_populates="message") created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now()) - updated_at = Column(TIMESTAMP(timezone=True), default=None, onupdate=func.now()) class SMSCost(Base): @@ -38,7 +36,6 @@ class SMSCost(Base): message_id: Mapped[UUID] = mapped_column(ForeignKey("message.id")) message: Mapped["SMSMessage"] = relationship(back_populates="cost") created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now()) - updated_at = Column(TIMESTAMP(timezone=True), default=None, onupdate=func.now()) class SMSMetadata(Base): @@ -49,6 +46,5 @@ class SMSMetadata(Base): message_type = Column(String, nullable=False) status = Column(String, nullable=False) message_id: Mapped[UUID] = mapped_column(ForeignKey("message.id")) - message: Mapped["SMSMessage"] = relationship(back_populates="metadata") + message: Mapped["SMSMessage"] = relationship(back_populates="sms_metadata") created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now()) - updated_at = Column(TIMESTAMP(timezone=True), default=None, onupdate=func.now()) diff --git a/requirements.txt b/requirements.txt index 878ea94..5950719 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ fastapi python-dotenv sqlalchemy +uvicorn diff --git a/service.py b/service.py index e69de29..9219e44 100644 --- a/service.py +++ b/service.py @@ -0,0 +1,30 @@ +from database import get_db +from dtos import to_db, to_db_existing_contact +from models import SMSContact, SMSMessage as SMSDBMessage +from schemas import SMSMessage +from sqlalchemy.orm import joinedload + + +def inbound_sms_handler(message: SMSMessage) -> None: + db = next(get_db()) + payload = None + + sms_contact_exists = db.query(SMSContact).filter(SMSContact.phone_number == message.data.attributes.to_number).first() + + if sms_contact_exists: + payload = to_db_existing_contact(message) + payload.to_number = sms_contact_exists.id + else: + payload = to_db(message) + + db.add(payload) + db.commit() + db.refresh(payload) + + + +def retrieve_sms_messages_by_phone_number(number: str, limit: int , page: int) -> SMSContact: + db = next(get_db()) + skip = (page - 1) * limit + messages = db.query(SMSContact).filter(SMSContact.phone_number == number).options(joinedload(SMSContact.messages)).limit(limit).offset(skip).first() + return messages