first commit

This commit is contained in:
Jadowyne Ulve 2025-05-24 09:23:37 -05:00
commit 2440d4ef22
17 changed files with 742 additions and 0 deletions

Binary file not shown.

Binary file not shown.

21
config.py Normal file
View File

@ -0,0 +1,21 @@
#!/usr/bin/python
from configparser import ConfigParser
import json
def config(filename='database.ini', section='postgresql'):
# create a parser
parser = ConfigParser()
# read config file
parser.read(filename)
# get section, default to postgresql
db = {}
if parser.has_section(section):
params = parser.items(section)
for param in params:
db[param[0]] = param[1]
else:
raise Exception('Section {0} not found in the {1} file'.format(section, filename))
return db

6
database.ini Normal file
View File

@ -0,0 +1,6 @@
[postgresql]
host = 192.168.1.67
database = postgres
user = test
password = test
port = 5432

106
database.py Normal file
View File

@ -0,0 +1,106 @@
import config, psycopg2
def tupleDictionaryFactory(columns, row):
columns = [desc[0] for desc in columns]
return dict(zip(columns, row))
def create_messages():
database_config = config.config()
with psycopg2.connect(**database_config) as conn:
try:
with conn.cursor() as cur:
with open('sql/CREATE/messages.sql') as file:
sql = file.read()
cur.execute(sql)
except (Exception, psycopg2.DatabaseError) as error:
print(error)
conn.rollback()
return None
def create_channels():
database_config = config.config()
with psycopg2.connect(**database_config) as conn:
try:
with conn.cursor() as cur:
with open('sql/CREATE/channels.sql') as file:
sql = file.read()
cur.execute(sql)
except (Exception, psycopg2.DatabaseError) as error:
print(error)
conn.rollback()
return None
def get_channel(id):
database_config = config.config()
with psycopg2.connect(**database_config) as conn:
try:
with conn.cursor() as cur:
sql = f"SELECT * FROM channels WHERE id=%s;"
cur.execute(sql, (id,))
rows = cur.fetchone()
if rows:
rows = tupleDictionaryFactory(cur.description, rows)
return rows
return {}
except (Exception, psycopg2.DatabaseError) as error:
print(error)
conn.rollback()
return {}
def insert_message(payload):
database_config = config.config()
with psycopg2.connect(**database_config) as conn:
try:
with conn.cursor() as cur:
with open('sql/INSERT/messages.sql') as file:
sql = file.read()
cur.execute(sql, payload)
rows = cur.fetchone()
if rows:
rows = tupleDictionaryFactory(cur.description, rows)
return rows
return {}
except (Exception, psycopg2.DatabaseError) as error:
print(error)
conn.rollback()
return {}
def select_message(id):
database_config = config.config()
with psycopg2.connect(**database_config) as conn:
try:
with conn.cursor() as cur:
with open('sql/SELECT/messages.sql') as file:
sql = file.read()
cur.execute(sql, (id, ))
rows = cur.fetchone()
if rows:
rows = tupleDictionaryFactory(cur.description, rows)
return rows
return {}
except (Exception, psycopg2.DatabaseError) as error:
print(error)
conn.rollback()
return {}
def select_messages(payload):
database_config = config.config()
with psycopg2.connect(**database_config) as conn:
try:
with conn.cursor() as cur:
with open('sql/SELECT/messagesByChannel.sql') as file:
sql = file.read()
cur.execute(sql, payload)
rows = cur.fetchall()
if rows:
rows = [tupleDictionaryFactory(cur.description, row) for row in rows]
return rows
return []
except (Exception, psycopg2.DatabaseError) as error:
print(error)
conn.rollback()
return []

5
sql/CREATE/channels.sql Normal file
View File

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS channels (
id SERIAL PRIMARY KEY,
channel_name char(64) NOT NULL,
channel_description TEXT
);

11
sql/CREATE/messages.sql Normal file
View File

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
timestamp TIMESTAMP,
channel_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
message_content TEXT NOT NULL,
CONSTRAINT fk_channel_id
FOREIGN KEY(channel_id)
REFERENCES channels(id)
ON DELETE CASCADE
);

4
sql/INSERT/messages.sql Normal file
View File

@ -0,0 +1,4 @@
INSERT INTO messages
(timestamp, channel_id, user_id, message_content)
VALUES (%s, %s, %s, %s)
RETURNING *;

7
sql/SELECT/messages.sql Normal file
View File

@ -0,0 +1,7 @@
SELECT messages.*,
row_to_json(users.*) as user,
row_to_json(channels.*) As channel
FROM messages
LEFT JOIN users ON messages.user_id = users.id
LEFT JOIN channels ON messages.channel_id = channels.id
WHERE messages.id=%s;

View File

@ -0,0 +1,14 @@
SELECT * FROM (
SELECT messages.*,
row_to_json(users.*) as user,
row_to_json(channels.*) As channel
FROM messages
LEFT JOIN users ON messages.user_id = users.id
LEFT JOIN channels ON messages.channel_id = channels.id
WHERE messages.channel_id = %s
ORDER BY messages.timestamp DESC
LIMIT %s
) AS subquery
ORDER BY subquery.timestamp ASC;

1
static/css/uikit.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

1
static/js/uikit-icons.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
static/js/uikit.min.js vendored Normal file

File diff suppressed because one or more lines are too long

365
templates/index.html Normal file
View File

@ -0,0 +1,365 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8" />
<title id="title"></title>
<!-- Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<!-- Material Symbols - Outlined Set -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<!-- Material Symbols - Rounded Set -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded" rel="stylesheet" />
<!-- Material Symbols - Sharp Set -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp" rel="stylesheet" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/uikit.min.css') }}"/>
<script src="{{ url_for('static', filename='js/uikit.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/uikit-icons.min.js') }}"></script>
</head>
<style>
:root {
--body-background: #121212;
--background: #1c1c1c;
--primary-color: #f7f7f7;
--accent-color: #ffb3b3;
--secondary-text: #666666;
--highlight: #ffd700;
--font: 'Arial', sans-serif;;
}
body {
background-color: var(--body-background);
color: var(--primary-color);
font-family: var(--font);
}
.subtitle{
font-size: 8px;
}
.my-nav-bar {
width: 100%;
height: 40px;
padding: 10px;
background-color: var(--body-background);
box-shadow: 0 2px 5px var(--background);
position: fixed;
top: 0;
left: 0;
z-index: 1000;
}
.my-nav-bar inline {
display: flex;
gap: 10px;
}
.nav-button{
height: 40px;
margin: 0;
box-sizing: border-box;
background-color: transparent;
color: var(--primary-color);
border: none;
padding: 10px;
border-radius: 4px;
transition: background-color 0.3s, box-shadow 0.3s;
cursor: pointer;
text-overflow: ellipsis;
font-size: 18px;
}
.nav-button span {
font-size: 18px;
width: 18px;
height: 18px;
vertical-align: middle;
margin-right: 5px;
}
.nav-title{
margin: 0px;
font-size: x-large;
margin-left: 20px;
}
.chat-container {
background-color: var(--body-background);
min-height: calc(100vh - 120px);
margin-bottom: 60px;
margin-top: 60px;
display: flex;
flex-direction: column;
}
.chat-container > * {
flex-grow: 1;
}
.floating-square {
width: 100%;
height: 50px;
padding: 10px;
background-color: var(--body-background);
position: fixed;
bottom: 0;
left: 0;
display: flexbox;
align-items: center;
}
.full-width-input {
padding: 10px;
width: calc(100% - 70px);
height: auto; /* Allow dynamic height */
min-height: 48px; /* Starting height */
resize: vertical; /* Allow vertical resizing */
overflow: hidden;
vertical-align: bottom;
box-sizing: border-box;
border: none;
border-bottom: 2px solid var(--secondary-text);
border-radius: 0px;
transition: border-color 0.3s;
outline: none;
background-color: var(--background);
color: var(--primary-color);
}
.full-width-input:focus {
border-bottom-color: var(--primary-color); /* Highlight upon focus */
}
.full-width-button {
width: 50px;
margin: 0;
vertical-align: bottom;
padding: 10px;
min-width: 50px;
border-radius: 4px; /* Match textarea */
border: none; /* Match textarea */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
background-color: var(--background);
color: var(--primary-color);
transition: background-color 0.3s, box-shadow 0.3s;
cursor: pointer;
text-overflow: ellipsis;
}
.full-width-button:hover {
background-color: var(--background); /* Darker tone on hover */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); /* Enhanced hover elevation */
}
.full-width-button:active {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.message-card {
padding: 10px;
margin: 0px;
background-color: var(--body-background);
color: var(--primary-color);
}
.user-info {
display: flex;
align-items: center;
}
.avatar {
border-radius: 50%;
width: 40px;
margin-right: 10px;
}
.username {
font-weight: bold;
margin-right: 5px;
font-family: var(--font);
}
.timestamp {
font-size: 0.9em;
color: #b0b0b0;
font-family: var(--font);
}
.message-content {
margin-top: 5px;
margin-left:50px;
margin-right: 5px;
font-family: var(--font);
}
</style>
<body>
<div class="my-nav-bar">
<inline>
<a href="/logout" class="nav-button"><span class="material-icons">menu</span>Menu</a>
<p id="room-title" class="nav-title">Room Title</p>
</inline>
</div>
<div class="chat-container" >
<div id="chat" style="height: auto;">
</div>
</div>
<div id="test" class="floating-square">
<textarea id="messageInput" class="full-width-input" placeholder="Type your thoughts..."></textarea>
<button id="sendButton" onclick="sendMessage()" class="full-width-button"><span class="material-symbols-outlined">
send
</span></button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js" integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==" crossorigin="anonymous"></script>
<script type="text/javascript" charset="utf-8">
var socket = io.connect('http://192.168.1.45:5812/');
var current_room = 1;
var current_username = 'unknown';
var current_channel;
var user;
fetch('/get_session')
.then(response => response.json())
.then(data => {
user = data['user'] // Revel in the data!
current_username = user[1]
})
.catch(error => console.error('Error fetching session:', error));
socket.on('messageReceive', function(data) {
console.log('Received:', data);
let message = data.message;
// instead of adding the message do I just reload the messages? what kind of infranstructure would that require.
addMessage([data])
});
socket.on('joined', function(data) {
current_channel = data
document.getElementById('room-title').innerHTML = data['channel_name']
});
const textarea = document.getElementById('messageInput');
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = this.scrollHeight + 'px';
document.getElementById('test').style.height = this.scrollHeight + 'px'
});
textarea.addEventListener('blur', function() {
this.style.height = 'auto';
this.style.height = '40px';
document.getElementById('test').style.height = '50px'
this.blur()
});
textarea.addEventListener('keydown', function(event) {
if (event.key === 'Enter' && !event.shiftKey) { // Checks if Enter is pressed without shift
event.preventDefault();
sendMessage(); // Replace with your function
}
});
joinRoom(current_room)
function addMessage(data){
let message_card = document.createElement('div')
message_card.setAttribute('class', 'message-card')
let user_info = document.createElement('div')
user_info.setAttribute('class', 'user-info')
let image = document.createElement('img')
image.setAttribute('class', 'avatar')
image.setAttribute('src', "static/images/placeholder.webp")
let username = document.createElement('span')
username.setAttribute('class', 'username')
username.innerHTML = data[0]['user']['username']
let timestamp = document.createElement('span')
timestamp.setAttribute('class', 'timestamp')
timestamp.innerHTML = data[0]['timestamp']
user_info.append(image)
user_info.append(username)
user_info.append(timestamp)
message_card.append(user_info)
let message = document.createElement('div')
message.setAttribute('class', 'message-content')
test = `${data[0]['message_content']}`
for(let i = 1; i < data.length; i++){
test = test + `<br>${data[i]['message_content']}`
}
message.innerHTML = `${test}`
message_card.append(message)
document.getElementById('chat').append(message_card)
window.scrollTo(0, document.body.scrollHeight);
}
function sendMessage(){
let message = document.getElementById('messageInput')
let currentTime = new Date();
let timeString = currentTime.toLocaleTimeString();
let dateString = currentTime.toLocaleDateString();
let dateTimeString = `${dateString} ${timeString}`;
socket.emit('messageSend', {'message_content': message.value, 'channel_id': current_room, 'user_id': user[0], 'timestamp': currentTime})
message.value = '';
}
async function joinRoom(channel_id) {
socket.emit('join', {'channel_id': channel_id});
current_room = channel_id
const url = new URL('/get_channel_messsages', window.location.origin);
url.searchParams.append('channel_id', channel_id);
const response = await fetch(url);
data = await response.json()
messages = data.messages;
console.log(messages.length)
console.log(messages)
let combinedMessages = []
let previous_message;
let num_messages = 0
let grouped = [], tempGroup = [messages[0]];
let startTime = new Date(messages[0].timestamp);
for (let i = 1; i < messages.length; i++) {
if ((new Date(messages[i].timestamp) - startTime <= 300000) &&
(messages[i].user.id === messages[i - 1].user.id)) {
tempGroup.push(messages[i]);
} else {
grouped.push(tempGroup);
tempGroup = [messages[i]];
startTime = new Date(messages[i].timestamp);
}
}
grouped.push(tempGroup);
console.log(grouped)
for (let i = 0; i< grouped.length; i++){
addMessage(grouped[i])
}
}
// To leave a room
function leaveRoom(username, room) {
socket.emit('leave', { username, room });
}
</script>
</body>
</html>

64
templates/login.html Normal file
View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8" />
<title id="title"></title>
<!-- Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<!-- Material Symbols - Outlined Set -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<!-- Material Symbols - Rounded Set -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded" rel="stylesheet" />
<!-- Material Symbols - Sharp Set -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp" rel="stylesheet" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/uikit.min.css') }}"/>
<script src="{{ url_for('static', filename='js/uikit.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/uikit-icons.min.js') }}"></script>
</head>
<style>
:root {
--body-background: #121212;
--background: #1c1c1c;
--primary-color: #f7f7f7;
--accent-color: #ffb3b3;
--secondary-text: #666666;
--highlight: #ffd700;
--font: 'Arial', sans-serif;;
}
body, html {
height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--body-background);
color: var(--primary-color);
font-family: var(--font);
}
.login-container {
text-align: center;
}
</style>
<body>
<div class="login-container">
<form action="/login" method="post">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username">
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password">
</div>
<button type="submit">Login</button>
</form>
</div>
</body>
<script></script>
</html>

136
webserver.py Normal file
View File

@ -0,0 +1,136 @@
from flask import Flask, render_template, session, request, redirect, url_for, jsonify
from flask_socketio import SocketIO, join_room, leave_room
import psycopg2
import config, database
from functools import wraps
from dateutil import parser
import datetime
import pytz, json
app = Flask(__name__)
app.secret_key = '11gs22h2h1a4h6ah8e413a45'
socketio = SocketIO(app)
@app.context_processor
def inject_user():
if 'user_id' in session.keys() and session['user_id'] is not None:
database_config = config.config()
with psycopg2.connect(**database_config) as conn:
try:
with conn.cursor() as cur:
sql = f"SELECT * FROM users WHERE id=%s;"
cur.execute(sql, (session['user_id'],))
user = cur.fetchone()
session['user'] = user
except (Exception, psycopg2.DatabaseError) as error:
print(error)
conn.rollback()
return dict(username="")
return dict(
user = user
)
return dict(user=[])
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
if 'user' not in session or session['user'] == None:
return redirect('/login')
return func(*args, **kwargs)
return wrapper
@login_required
@app.route('/')
def index():
return render_template('index.html')
@app.route('/get_session')
def get_session():
return {'user': session.get('user')}
@app.route('/get_channel_messsages', methods=["GET"])
def get_channel_messages():
channel_id = request.args['channel_id']
limit = 100
page = 1
offset = (page - 1) * limit
messages = database.select_messages((channel_id, limit))
return jsonify(messages=messages)
@app.route('/logout', methods=['GET'])
def logout():
if 'user' in session.keys():
session['user'] = None
return redirect('/login')
@app.route('/login', methods=["POST", "GET"])
def login():
session.clear()
if request.method == "POST":
username = request.form.get('username')
password = request.form.get('password')
database_config = config.config()
with psycopg2.connect(**database_config) as conn:
try:
with conn.cursor() as cur:
sql = f"SELECT * FROM users WHERE username=%s;"
cur.execute(sql, (username,))
user = cur.fetchone()
print(user)
except (Exception, psycopg2.DatabaseError) as error:
print(error)
conn.rollback()
print(password, user[2])
if user and user[2] == password:
print(password, user[2])
session['user'] = user
return redirect('/')
if 'user' not in session.keys():
session['user'] = None
return render_template('login.html')
@socketio.on('join')
def on_join(data):
print(data)
channel = database.get_channel(data['channel_id'])
room = channel['channel_name']
join_room(room)
socketio.emit('joined', channel, to=request.sid)
@socketio.on('leave')
def on_leave(data):
username = data['username']
room = data['room']
print(f"{username} joined the {room}!")
leave_room(room)
@socketio.on('messageSend')
def handle_event(data):
print('Received:', data)
channel = database.get_channel(data['channel_id'])
payload = (
datetime.datetime.now(),
data['channel_id'],
data['user_id'],
data['message_content']
)
row = database.insert_message(payload)
message = database.select_message(row['id'])
print(message)
message['timestamp'] = str(message['timestamp'])
socketio.emit('messageReceive', message, to=channel['channel_name'])
if __name__ == '__main__':
socketio.run(app, host="0.0.0.0", port=5812, debug=True)