Skip to content

Commit

Permalink
Improved user search + logging + some error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
etrian-dev committed May 5, 2022
1 parent 83bf172 commit 3adecc5
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 56 deletions.
5 changes: 3 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
- User detail page
- Encryption/decryption of message data with RSA
- logout function and proper authentication (maybe sessions)
- Fix msgstore to write into ./instance
- Improve searching (onchange, but clearing the previous matches from the select)
- Fix timestamp for message viewing order in messages.html
+ Needs revision on Chat.py:display_chat
- In msg.py all function that modify a chat (adding a message, deleting one) should update the
last_mod_tm field of the Chat object in the db
- Edit db schema to have a timestamp for the last two attributes
- Develop & testing of query maker (in db.py)
# Container
- Add volume to make source changes shared between host and container
4 changes: 3 additions & 1 deletion chatroom/Admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ def show_users():
for user in cursor.fetchall():
all_users[user['user_id']] = {
"username": user['username'],
"password": user['password']}
"password": user['password'],
"pub_key": [user['pk_e'].hex()[:20] + "...", user['pk_n'].hex()[:20] + "..."],
"priv_key": user['pk_d'].hex()[:20] + "..."}
return render_template('admin_console.html', all_users=all_users)
except DatabaseError:
return "DB error"
70 changes: 45 additions & 25 deletions chatroom/Chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from datetime import datetime
from sqlite3 import Connection, DatabaseError
from json import load
import logging

from flask import (Blueprint, request, render_template)
from . import db
from . import Msg
Expand Down Expand Up @@ -83,27 +85,29 @@ def create_chat(creator):
# <creator>
error = None
matching_users = []
if request.method == 'POST':
if request.method == 'POST' and 'matching-users' in request.form:
# get the username of the user the creator wants to chat with
username = request.form['matching-users']
# get matching users
# this value exists in the database because checks are made by the search service
# and it's unique because it's returned trough a form select
user_id = request.form['matching-users']
db_conn = db.get_db()
try:
cursor = db_conn.execute('''
SELECT user_id, username, password
FROM Users
WHERE user_id=?;''', [username])

# TODO: handle multiple users with the same username
WHERE user_id=?;''', [user_id])
# user_id is primary key, so at most one tuple is fetched from the db
match = cursor.fetchone()
if match is not None:
# FIXME: probably should modify the db schema for the last two attributes -> timestamp
cursor.execute('''
INSERT INTO Chats(participant1, participant2, creation_tm, last_mod_tm)
VALUES (?,?,datetime(),datetime());
''', [creator, match['user_id']])
db_conn.commit()
cursor.close()

logging.info(f"Chat between {creator} and {match['user_id']} created at {datetime.now()}")
return redirect(url_for('chat.display_chat',
user=creator, other=match['user_id']))
except DatabaseError:
Expand All @@ -117,33 +121,46 @@ def display_chat(user, other):
chat_info['this_user_id'] = user
chat_info['other_user_id'] = other
db_conn = db.get_db()

# fetch user IDs and usernames
# of the current user and the other participant in the chat
cur = db_conn.execute('''
SELECT username FROM Users WHERE user_id = ?;
''', [user])
chat_info['this_user'] = cur.fetchone()['username']
cur.execute('''
SELECT user_id,username FROM Users WHERE user_id = ?;
''', [other])
chat_info['other_user'] = cur.fetchone()['username']
# fetch the chat
try:
cur = db_conn.execute('''
SELECT username FROM Users WHERE user_id = ?;
''', [user])
chat_info['this_user'] = cur.fetchone()['username']
cur.execute('''
SELECT user_id,username FROM Users WHERE user_id = ?;
''', [other])
chat_info['other_user'] = cur.fetchone()['username']
except:
logging.error(f"Either user {user} or {other} not found in the database")
return render_template('404_not_found.html')

# build breadcrumb
breadcrumb = {}
breadcrumb['home'] = url_for('chat.home_user', user_id=user)
breadcrumb[chat_info['other_user']] = url_for(
'chat.display_chat', user=user, other=other)
chat_info['breadcrumb'] = breadcrumb

# fetch the chat
# (the or is needed because we don't know who created the chat)
cur.execute('''
SELECT * FROM Chats
WHERE (participant1 = ? AND ? = participant2) OR (participant1 = ? AND participant2 = ?);
''', [user, other, other, user])
chat = cur.fetchone()
# check if the chat exists
if chat is None:
logging.error(f"Chat between {user} and {other} not found")
return render_template('404_not_found.html')
# fetch all messages sent by the other user
cur.execute('''
SELECT * FROM Messages WHERE chatref = ? AND sender == ? ;
''', [chat['chat_id'], other])
msgs_encoded = cur.fetchall()
# build breadcrumb
breadcrumb = {}
breadcrumb['home'] = url_for('chat.home_user', user_id=user)
breadcrumb[chat_info['other_user']] = url_for(
'chat.display_chat', user=user, other=other)
chat_info['breadcrumb'] = breadcrumb

# decode messages
messages = []
for msg in msgs_encoded:
Expand All @@ -159,8 +176,9 @@ def display_chat(user, other):
{'msg_id': msg['msg_id'],
'sender': sender,
'receiver': receiver,
'data': Msg.decrypt_message(user, msg['msg_data'])})
# fetch all messages sent by this user
'data': Msg.decrypt_message(user, msg['msg_data']),
'tstamp': msg['tstamp']})
# fetch all messages sent by this user from the msgstore
try:
with open(Msg.get_msgstore(user, other), 'r') as msgstore:
sent_messages = load(msgstore)
Expand All @@ -169,12 +187,14 @@ def display_chat(user, other):
{'msg_id': msg['msg_id'],
'sender': chat_info['this_user'],
'receiver': chat_info['other_user'],
'data': msg['data']})
print(sent_messages)
'data': msg['data'],
'tstamp': msg['tstamp']})
except FileNotFoundError:
pass # no messages sent by this user yet
messages.sort(key=lambda x: x['tstamp'])
chat_info['messages'] = messages

logging.info(f"User {user} requested the chat with {other}: {len(messages)} messages served")
return render_template('messages.html', **chat_info)


Expand Down
57 changes: 41 additions & 16 deletions chatroom/Msg.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ def __init__(self, data: str, stamp: int):
from . import db
from sqlite3 import Connection, Cursor, DatabaseError
from json import (load, loads, dump, dumps)
from os import fspath, path, SEEK_END
from os import fspath, path, SEEK_END, SEEK_SET
from time import time
import logging

from flask import (
Blueprint, flash, g, current_app, redirect, render_template, request, session, url_for, make_response
Expand Down Expand Up @@ -87,34 +89,57 @@ def send_message(sender, recipient):

# encrypt
encrypted_msg = encrypt_msg(recipient, msg_data)

# get the timestamp
tstamp = int(time())

# insert the message
cur.execute('''
INSERT INTO Messages(chatref,sender,recipient,msg_data)
VALUES (?, ?, ?, ?);
''', [chat_id, sender, recipient, encrypted_msg.to_bytes(length=encrypted_msg.bit_length() // 8 + 1, byteorder='big')])
INSERT INTO Messages(chatref,sender,recipient,msg_data,tstamp)
VALUES (?, ?, ?, ?, ?);
''',
[chat_id,
sender,
recipient,
encrypted_msg.to_bytes(length=encrypted_msg.bit_length() // 8 + 1, byteorder='big'),
tstamp])
# store the last row id == msg_id in this case
msg_id = cur.lastrowid
db_conn.commit()
# fetch the message id
logging.info(f"{sender} sent message {msg_id} to {recipient} at {tstamp}")

# Save the message to the msgstore
msg_obj = {"msg_id": msg_id, "sender": sender, "recipient": recipient, "data": msg_data}
msg_obj = {
"msg_id": msg_id,
"sender": sender,
"recipient": recipient,
"data": msg_data,
"tstamp": tstamp}
file_exists = path.exists(get_msgstore(sender,recipient))
if file_exists:
with open(get_msgstore(sender, recipient), 'r+b') as msgstore:
# cancel array end character
b = msgstore.seek(-1, SEEK_END)
print('seek = ', b)
msgstore.write((',\n' + dumps(msg_obj) + ' ]').encode(encoding='utf-8'))
# if there are no sent messages from sender to recipient, write a new array
# with one element
r = msgstore.read(2).decode(encoding='utf-8')
if r == '[]':
msgstore.seek(0, SEEK_SET)
msgstore.write(('[ ' + dumps(msg_obj) + ' ]').encode(encoding='utf-8'))
else:
# otherwise a line is added, by extending the array with a comma and a newline
# and then rewriting a ']' at the end
b = msgstore.seek(-1, SEEK_END)
msgstore.write((',\n' + dumps(msg_obj) + ' ]').encode(encoding='utf-8'))
else:
# creates and writes the array containing this message
with open(get_msgstore(sender, recipient), 'a') as msgstore:
msgstore.write('[ ' + dumps(msg_obj) + ' ]')
print('last pos = ', msgstore.tell())

print(f"{sender} said to {recipient}: {msg_data}")
# return an empty response, with a 201 Created code
return redirect(url_for('chat.display_chat', user=sender, other=recipient))
except DatabaseError:
return f"Error"
flash(f"Failed to send the message")
except:
flash(f"Some error occurred")
# Redirects the user to the chat page after creating the resource
return redirect(url_for('chat.display_chat', user=sender, other=recipient))



@blueprint.route('/<int:msg_id>', methods=['GET', 'PUT'])
Expand Down
21 changes: 17 additions & 4 deletions chatroom/Search.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import datetime
from sqlite3 import Connection, Cursor, DatabaseError
from json import dumps
import logging

from flask import (
Blueprint, flash, g, redirect, render_template, request, session, url_for
Expand All @@ -16,11 +17,23 @@

@blueprint.route('/', methods=['GET'])
def search_user():
'''This method allows searching for users in the database.
It receives a GET request with an URL like
/search/?user=<str>, where the string may be empty (no matches in that case).
The function returns a json array of objects
{ "user_id": <uid>, "username": <user> } whose username has a prefix
equal to the argument of the query. If the match is in the middle of the string
that user is not added to the array.
'''
logging.debug(f"search query: {request.args}")
matching_users = []
if request.method == 'GET':
# Does not match any user if the search query is empty
if request.method == 'GET' and 'user' in request.args and len(request.args['user']) > 0 :
query = request.args['user']
print(query)

db_conn = db.get_db()

try:
cursor = db_conn.execute('''
SELECT user_id, username
Expand All @@ -34,8 +47,8 @@ def search_user():
"user_id": row['user_id'],
"username": row['username']
})
print(len(matching_users))
except DatabaseError as err:
print('SQLite error: %s' % (' '.join(err.args)))
print("Exception class is: ", err.__class__)
return jsonify(matching_users)
logging.debug(f"{len(matching_users)} mathches found: {matching_users}")
return jsonify(matching_users)
23 changes: 19 additions & 4 deletions chatroom/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import os
import logging
from datetime import datetime

from flask import Flask, render_template

Expand All @@ -7,21 +9,34 @@


def create_app(testing=None):

app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY='dev',
DATABASE=os.path.join(app.instance_path, 'chatroom.sqlite'),
#EXPLAIN_TEMPLATE_LOADING=True,
)

# ensure the instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass
if not os.path.exists(app.instance_path):
try:
os.makedirs(app.instance_path)
except OSError:
print(f"Failed creating instance directory at {app.instance_path}")

# if testing is None:
# app.config.from_pyfile('config.py', silent=True)
# else:
# app.config.from_mapping(testing)

# setup logging
logging.basicConfig(
filename=os.path.join(app.instance_path, 'chatroom.log'),
filemode='w',
level=logging.DEBUG,
encoding='utf-8')
logging.info(f"Application launched at {datetime.now()}")

app.register_blueprint(Auth.blueprint)
app.register_blueprint(Chat.blueprint)
app.register_blueprint(Msg.blueprint)
Expand Down
2 changes: 2 additions & 0 deletions chatroom/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# https://flask.palletsprojects.com/en/2.0.x/tutorial/database/

import sqlite3
import logging

import click
from flask import current_app, g
Expand All @@ -28,6 +29,7 @@ def init_db():
with current_app.open_resource('db_schema.sql', 'r') as f:
contents = f.read()
gdb.executescript(contents)
logging.info(f"Initialized database at {current_app.config['DATABASE']} from script {f.name}")
# TODO: untested
def querydb(query: str, args: list):
'''Generator that queries the database and returns the resulting rows.
Expand Down
4 changes: 3 additions & 1 deletion chatroom/db_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,7 @@ CREATE TABLE Messages (
sender INTEGER REFERENCES Users(user_id),
recipient INTEGER REFERENCES Users(user_id),
msg_data BLOB,
CHECK (sender != recipient)
tstamp INTEGER NOT NULL,
CHECK (sender != recipient),
CHECK (tstamp > 0)
);
11 changes: 11 additions & 0 deletions chatroom/templates/404_not_found.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<DOCTYPE html>
<html>
<head>
<script src="{{ url_for('static', filename='jquery-3.6.0.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='spectre.css') }}"></link>
<title>Login</title>
</head>
<body>
404
</body>
</html>
11 changes: 9 additions & 2 deletions chatroom/templates/admin_console.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@
</head>
<body>
{% for usr in all_users.keys() %}
<div class='card-body'>
{{ all_users[usr]['username'] }} : {{ all_users[usr]['password'] }}
<div class='card'>
<div class='card-header'>{{ all_users[usr]['username'] }}</div>
<div class='card-body'>
<ul>
<li>password: {{ all_users[usr]['password']}}</li>
<li>public key: {{ all_users[usr]['pub_key']}}</li>
<li>private key: {{ all_users[usr]['priv_key']}}</li>
</ul>
</div>
</div>
{% endfor %}
<body>
Expand Down
Loading

0 comments on commit 3adecc5

Please sign in to comment.