diff --git a/.gitignore b/.gitignore
index 1b86f22..3f2acea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.venv
__pycache__
-postgres-data
\ No newline at end of file
+postgres-data
+blogs
\ No newline at end of file
diff --git a/blogHandler.py b/blogHandler.py
new file mode 100644
index 0000000..ded565c
--- /dev/null
+++ b/blogHandler.py
@@ -0,0 +1,51 @@
+import os
+import datetime
+
+import psycopg2
+
+import orgHandler
+import dbHandler
+
+blogDir: str = "./blogs"
+debug: bool = True
+
+def debugPrint(msg: str) -> None:
+ if debug:
+ print("(BLOG HANDLER) PRINT: " + msg)
+
+# Returns new Blog ID
+def createBlogEntry(dbConnection: psycopg2.extensions.connection, userID: int, categoryID: int) -> int:
+ datePosted = datetime.datetime.now()
+ datePostedStr = datePosted.strftime("%G-%m-%d %X")
+ debugPrint("Now creating new user with following attributes : userID = " + str(userID) + ", categoryID = " + str(categoryID) + ", datePosted = " + datePostedStr)
+ newRow = dbHandler.insertRow(dbConnection,
+ 'blogs',
+ ['authorID', 'categoryID', 'datePosted' ],
+ [userID, categoryID, datePostedStr])
+ return newRow[0]
+
+def uploadBlog(dbConnection: psycopg2.extensions.connection, userID: int, orgRawIn: str):
+ try:
+ orgParsedOut = orgHandler.orgToHTML(orgRawIn)
+ if orgParsedOut:
+ blogID = createBlogEntry(dbConnection, userID, 1)
+
+ newBlogDir = blogDir + "/" + str(blogID)
+ debugPrint("Attempting to create new blog directory " + newBlogDir + "...")
+ os.mkdir(newBlogDir)
+
+ debugPrint("Attempting to write new blog file " + newBlogDir + ".org...")
+ orgFileData = open(newBlogDir + "/" + str(blogID) + ".org", 'w')
+ orgFileData.write(orgRawIn)
+ orgFileData.close()
+
+ debugPrint("Attempting to write new blog file " + blogDir + "/" + str(blogID) + "/" + str(blogID) + ".html...")
+ HTMLFileData = open(newBlogDir + "/" + str(blogID) + ".html", 'w')
+ HTMLFileData.write(orgParsedOut)
+ HTMLFileData.close()
+ return blogID
+ else:
+ return False
+ except Exception as error:
+ debugPrint("Create blog failed! " + repr(error))
+ return False
diff --git a/dbHandler.py b/dbHandler.py
index faab479..7c894a4 100644
--- a/dbHandler.py
+++ b/dbHandler.py
@@ -65,13 +65,17 @@ def initTable(dbConnection: psycopg2.extensions.connection, tableName: str, tabl
# These base functions should not be called directly as they perform no string query sanitisation (Therefore vulnerable to SQL injection attacks)
def _commitQuery(dbConnection: psycopg2.extensions.connection, query: sql.Composable) -> list:
- debugPrint("Commit query executing...")
- dbCursor = dbConnection.cursor()
- dbCursor.execute(query)
- dbConnection.commit()
- dbResults = dbCursor.fetchall()
- dbCursor.close()
- return dbResults
+ try:
+ debugPrint("Commit query executing...")
+ dbCursor = dbConnection.cursor()
+ dbCursor.execute(query)
+ dbConnection.commit()
+ dbResults = dbCursor.fetchall()
+ dbCursor.close()
+ return dbResults
+ except Exception as error:
+ errorPrint("Commit query failed! " + repr(error))
+ return None
def _execQuery(dbConnection: psycopg2.extensions.connection, query: sql.Composable) -> list:
try:
debugPrint("Exec query executing...")
@@ -85,7 +89,7 @@ def _execQuery(dbConnection: psycopg2.extensions.connection, query: sql.Composab
return None
# Callable helper functions
-def insertRow(dbConnection: psycopg2.extensions.connection, tableName: str, tableFormat: list[str], tableValues: list):
+def insertRow(dbConnection: psycopg2.extensions.connection, tableName: str, tableFormat: list[str], tableValues: list) -> list:
debugPrint("Attempting to insert new row (" + str(tableFormat) + ") VALUES (" + str(tableValues) + ") into table name " + tableName + "...")
sanitisedQuery = sql.SQL("""
INSERT INTO {table} ({format})
@@ -94,14 +98,14 @@ def insertRow(dbConnection: psycopg2.extensions.connection, tableName: str, tabl
""").format(
table=sql.Identifier(tableName),
format=sql.SQL(", ").join(
- sql.Identifier(value) for value in tableFormat
+ sql.Identifier(value.lower()) for value in tableFormat
),
values=sql.SQL(", ").join(
sql.Literal(value) for value in tableValues
)
)
debugPrint(sanitisedQuery.as_string(dbConnection))
- _commitQuery(dbConnection, sanitisedQuery)
+ return _commitQuery(dbConnection, sanitisedQuery)[0]
def getFieldValueByID(dbConnection: psycopg2.extensions.connection, tableName: str, RowID: int, tableField: str) -> str:
debugPrint("Attempting to get values of field name " + tableField + " in ID row " + str(RowID) + " in table name " + tableName + "...")
diff --git a/main.py b/main.py
index 99b2e37..4d22bf6 100644
--- a/main.py
+++ b/main.py
@@ -1,3 +1,4 @@
+import os
import sys
import atexit
import signal
@@ -14,6 +15,19 @@ import dbHandler
import userHandler
import securityHandler
import tokenHandler
+import blogHandler
+
+blogDir = "./blogs"
+
+debug: bool = True
+
+def debugPrint(msg: str) -> None:
+ if debug:
+ print("(MAIN) PRINT: " + msg)
+
+def debugInitPrint(msg: str) -> None:
+ if debug:
+ print("(MAIN INIT) PRINT: " + msg)
dbConnection = dbHandler.connect("blorgdb",
"172.20.0.10",
@@ -52,12 +66,17 @@ def apiInit():
ID SERIAL PRIMARY KEY,
AuthorID INTEGER,
CategoryID INTEGER,
- DatePosted TIMESTAMP,
+ DatePosted TIMESTAMP
""")
dbHandler.initTable(dbConnection, "Categories", """
ID SERIAL PRIMARY KEY,
Name VARCHAR(255)
""")
+ debugInitPrint("Creating blog data directory at " + blogDir + "...")
+ if not os.path.exists(blogDir):
+ os.mkdir(blogDir)
+ else:
+ debugInitPrint("Blog data directory already exists! Skipping...")
# userHandler.createUser(dbConnection, "testuser", "Test", "User", "A test user", "TestCountry", "TestTheme", "TestColor", "testuser")
def apiCleanup():
@@ -100,12 +119,12 @@ def postapi(body: ApiBody):
print(body.password)
return body
-class loginBody(BaseModel):
+class postuserLoginBody(BaseModel):
username: str
password: str
rememberMe: bool
-@app.post("/api/login")
-def postlogin(body: loginBody, request: Request):
+@app.post("/api/user/login")
+def postuserLogin(body: postuserLoginBody, request: Request):
try:
if userHandler.checkUserExistence(dbConnection, body.username):
userID = userHandler.getIDByUsername(dbConnection, body.username)
@@ -153,12 +172,14 @@ def getuserPublicInfo(userID: int):
"message": "Get public info failed! UserID provided does not exist."
}
except Exception as error:
+ msg = "Get public info failed! Unexpected server error. " + repr(error)
+ print(msg)
return {
"success": False,
"username": None,
"firstName": None,
"lastName": None,
- "message": "Get public info failed! Unexpected server error. " + repr(error)
+ "message": msg
}
@app.get("/api/user/settings/account")
@@ -182,18 +203,59 @@ def getuserSettingsAccount(authToken: Annotated[str | None, Header()] = None):
"message": "Get user settings failed! authToken provided is not valid."
}
except Exception as error:
+ msg = "Get user settings failed! Unexpected server error. " + repr(error)
+ print(msg)
return {
"success": False,
"username": None,
"firstName": None,
"lastName": None,
- "message": "Get user settings failed! Unexpected server error. " + repr(error)
+ "message": msg
+ }
+
+
+class postblogCreateBody(BaseModel):
+ authToken: str
+ orgContents: str
+@app.post("/api/blog/create")
+def postblogCreate(body: postblogCreateBody):
+ try:
+ if tokenHandler.validateTokenExistence(dbConnection, body.authToken):
+ userID = userHandler.getIDByAuthToken(dbConnection, body.authToken)
+ newBlog = blogHandler.uploadBlog(dbConnection, userID, body.orgContents)
+ if newBlog:
+ return {
+ "success": True,
+ "blogID": newBlog,
+ "message": "Create blog succeeded!"
+ }
+ else:
+ return {
+ "success": False,
+ "blogID": newBlog,
+ "message": "Create blog failed! Unknown error parsing org data."
+ }
+ else:
+ return {
+ "success": False,
+ "blogID": None,
+ "message": "Create blog failed! authToken provided is not valid."
+ }
+ except Exception as error:
+ msg = "Create blog failed! Unexpected server error. " + repr(error)
+ print(msg)
+ return {
+ "success": False,
+ "blogID": None,
+ "message": msg
}
# GET
-# /api/user/ByAuthToken
+# /api/user/IDByAuthToken
# - userID
+
+# Could be merged into a single endpoint which you can optionally query what you need from
# /api/user/publicInfo/{userID}
# - username
# - firstname
@@ -201,19 +263,29 @@ def getuserSettingsAccount(authToken: Annotated[str | None, Header()] = None):
# - profile picture
# - location
# - public email (For contact)
+
+# Could be merged into a single endpoint which you can optionally query what you need from
# /api/user/privateInfo/{userID}
# - private email (For authentication/login)
-# /api/blog/title
-# /api/blog/authorID
-# /api/blog/categoryID
-# /api/blog/pictureLocation
-# /api/blog/description
-# /api/blog/datePosted
+# Could be merged into a single endpoint which you can optionally query what you need from
+# /api/blog/title/{blogID}
+# /api/blog/authorID/{blogID}
+# /api/blog/categoryID/{blogID}
+# /api/blog/pictureURL/{blogID}
+# /api/blog/description/{blogID}
+# /api/blog/datePosted/{blogID}
# POST
# /api/user/changeInfo/{infotype}
+# TODO: Need to overhaul this to accept an input file which we can read in chunks to ensure a too big file isn't sent, frontend will convert textbox/form data appropriately
+# https://github.com/steinnes/content-size-limit-asgi
+# https://github.com/tiangolo/fastapi/issues/362
+# /api/blog/create
+
+# /api/blog/edit
+
@app.get("/api")
def getapi():
return {"Hello": "API!"}
diff --git a/orgHandler.py b/orgHandler.py
index 894b69b..051e502 100644
--- a/orgHandler.py
+++ b/orgHandler.py
@@ -1,12 +1,13 @@
-from orgparse import load
+from orgparse import loads
+blogDir: str = "./blogs"
debug: bool = True
def debugPrint(msg: str) -> None:
if debug:
print("(ORG HANDLER) PRINT: " + msg)
-def checkAndRetrieveMetadata(fileData, metadataName: str):
+def checkAndRetrieveMetadata(fileData: str, metadataName: str):
line = fileData.readline()
metadataFullName = "#+" + metadataName + ":"
if metadataFullName in line:
@@ -15,29 +16,43 @@ def checkAndRetrieveMetadata(fileData, metadataName: str):
else:
debugPrint("Could not find " + metadataFullName + " metadata field of document!")
return False
-
-def orgToHTML(filePath: str):
+def getOrgTitle(blogID: int) -> str:
try:
- fileData = open(filePath, 'r')
- orgRoot = load(filePath)
+ fileData = open(blogDir + "/" + str(blogID) + "/" + str(blogID) + ".org", 'r')
+ title = checkAndRetrieveMetadata(fileData, "TITLE")
+ if title:
+ return title
+ except Exception as error:
+ debugPrint("Error getting blog title! " + repr(error))
- Title = checkAndRetrieveMetadata(fileData, "TITLE")
- if not Title:
- raise Exception("A valid #+TITLE: field is required as the first line of the org page.")
+def getOrgDescription(blogID: str) -> str:
+ try:
+ fileData = open(blogDir + "/" + str(blogID) + "/" + str(blogID) + ".org", 'r')
+ orgRoot = loads(fileData.read())
+
+ fileData.readline()
shortDescription = checkAndRetrieveMetadata(fileData, "DESCRIPTION")
if not shortDescription:
- debugPrint("No valid description found, will generate a placeholder from the blog itself...")
+ debugPrint("No valid description found, will generate a placeholder from the text itself...")
firstText = orgRoot[1].body
shortDescription = (firstText[:60] + "...") if len(firstText) > 60 else firstText
+ return shortDescription
+ except Exception as error:
+ debugPrint("Error getting org title! " + repr(error))
+
+
+def orgToHTML(orgData: str) -> str:
+ try:
+ orgRoot = loads(orgData)
+ parsedHTML = ""
for node in orgRoot[1:]:
if node.heading:
headingLevel = str(node.level)
- print("" + node.heading + "")
+ parsedHTML += "" + node.heading + "" + "\n"
if node.body:
- print(node.body)
+ parsedHTML += node.body + "\n"
+ return parsedHTML
except Exception as error:
debugPrint("Error parsing org! " + repr(error))
-
-orgToHTML("./test.org")
diff --git a/tokenHandler.py b/tokenHandler.py
index 1594bf4..c827afa 100644
--- a/tokenHandler.py
+++ b/tokenHandler.py
@@ -15,15 +15,17 @@ def validateTokenExistence(dbConnection: psycopg2.extensions.connection, authTok
return dbHandler.checkFieldValueExistence(dbConnection, "authtokens", "token", authToken)
def createToken(dbConnection: psycopg2.extensions.connection, userID: int, rememberMe: bool, locationIP: str) -> str:
- debugPrint("Now initialising new token with following attributes : userID = " + str(userID) + ", rememberMe = " + str(rememberMe) + ", locationIP = " + locationIP + "...")
randToken = secrets.token_hex(1023)
dateCreated = datetime.datetime.now()
if rememberMe:
dateExpiry = dateCreated + datetime.timedelta(days=30)
else:
dateExpiry = dateCreated + datetime.timedelta(days=1)
+ dateExpiryStr = dateExpiry.strftime("%G-%m-%d %X")
+ dateCreatedStr = dateCreated.strftime("%G-%m-%d %X")
+ debugPrint("Now initialising new token with following attributes : ownerID = " + str(userID) + ", rememberMe = " + str(rememberMe) + ", locationIP = " + locationIP + "...")
dbHandler.insertRow(dbConnection,
'authtokens',
- ['token', 'ownerid', 'datecreated', 'dateexpiry', 'iplocationcreated'],
- [randToken, userID, dateCreated.strftime("%G-%m-%d %X"), dateExpiry.strftime("%G-%m-%d %X"), locationIP])
+ ['token', 'ownerid', 'datecreated', 'dateexpiry', 'iplocationcreated'],
+ [randToken, userID, dateCreatedStr, dateCreatedStr, locationIP ])
return randToken