Massive update, schemas and models
This commit is contained in:
parent
877a70d6e1
commit
df203943ac
1
.env
Normal file
1
.env
Normal file
|
|
@ -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
|
||||||
57
app.py
57
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)
|
||||||
|
)
|
||||||
55
dtos.py
Normal file
55
dtos.py
Normal file
|
|
@ -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
|
||||||
BIN
flowsms.db
Normal file
BIN
flowsms.db
Normal file
Binary file not shown.
13
loggerino.py
Normal file
13
loggerino.py
Normal file
|
|
@ -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
|
||||||
10
models.py
10
models.py
|
|
@ -9,10 +9,9 @@ from uuid import uuid4, UUID
|
||||||
class SMSContact(Base):
|
class SMSContact(Base):
|
||||||
__tablename__ = 'contact'
|
__tablename__ = 'contact'
|
||||||
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
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()
|
messages: Mapped[List["SMSMessage"]] = relationship()
|
||||||
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now())
|
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):
|
class SMSMessage(Base):
|
||||||
|
|
@ -25,9 +24,8 @@ class SMSMessage(Base):
|
||||||
message = Column(String, nullable=False)
|
message = Column(String, nullable=False)
|
||||||
timestamp = Column(DateTime, nullable=False)
|
timestamp = Column(DateTime, nullable=False)
|
||||||
cost: Mapped["SMSCost"] = relationship(back_populates="message")
|
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())
|
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):
|
class SMSCost(Base):
|
||||||
|
|
@ -38,7 +36,6 @@ class SMSCost(Base):
|
||||||
message_id: Mapped[UUID] = mapped_column(ForeignKey("message.id"))
|
message_id: Mapped[UUID] = mapped_column(ForeignKey("message.id"))
|
||||||
message: Mapped["SMSMessage"] = relationship(back_populates="cost")
|
message: Mapped["SMSMessage"] = relationship(back_populates="cost")
|
||||||
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now())
|
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):
|
class SMSMetadata(Base):
|
||||||
|
|
@ -49,6 +46,5 @@ class SMSMetadata(Base):
|
||||||
message_type = Column(String, nullable=False)
|
message_type = Column(String, nullable=False)
|
||||||
status = Column(String, nullable=False)
|
status = Column(String, nullable=False)
|
||||||
message_id: Mapped[UUID] = mapped_column(ForeignKey("message.id"))
|
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())
|
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now())
|
||||||
updated_at = Column(TIMESTAMP(timezone=True), default=None, onupdate=func.now())
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
fastapi
|
fastapi
|
||||||
python-dotenv
|
python-dotenv
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
|
uvicorn
|
||||||
|
|
|
||||||
30
service.py
30
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
|
||||||
Loading…
Reference in a new issue