Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ DB_PORT="3306"
DB_USER="root"
DB_PASSWORD=""
DB_NAME="meetcostwatcher"

CREDENTIALS={}
5 changes: 4 additions & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
__pycache__
.venv
env
.env
.env

credentials.json
token.json
204 changes: 204 additions & 0 deletions backend/app/meet/google_calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import datetime
import os
import json
from dotenv import load_dotenv
from app.models import Meeting, User, meeting_users
from app import db

from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build

# credentials_path = 'credentials.json'
load_dotenv()
credentials_str = os.getenv("CREDENTIALS")
credentials_json = json.loads(credentials_str)
token_path = "token.json"

SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"]


def authorize():
creds = None

# Local token for authorization
try:
creds = Credentials.from_authorized_user_file(token_path, SCOPES)
except Exception as e:
creds = None

print(creds)

# Loggin by browser if token not found
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
# flow = InstalledAppFlow.from_client_secrets_file(credentails_path, SCOPES)
flow = InstalledAppFlow.from_client_config(credentials_json, SCOPES)
creds = flow.run_local_server(port=8080)
print(creds)

with open(token_path, "w") as token:
token.write(creds.to_json())

print("Authorized!")

return creds


def calculate_meeting_cost(participiants, duration) -> float:
total_cost = 0.0

for participant in participiants:
hours = duration / 60
participant_cost = hours * participant.hourly_cost
total_cost += participant_cost

return round(total_cost, 2)


def process_events(events):
new_meeting_count = 0
for event in events:
if event.get("hangoutLink"):
if save_single_event(event):
new_meeting_count += 1
return new_meeting_count


def save_single_event(event):
hangout_link = event.get("hangoutLink")
if not hangout_link:
return False

# Standaryzuj format linku do spotkania
meet_token = hangout_link.replace("https://meet.google.com/", "").strip()

name = event.get("summary", "").strip()
start_str = event["start"].get("dateTime", event["start"].get("date"))
end_str = event["end"].get("dateTime", event["end"].get("date"))
participants = [
att["email"].lower().strip()
for att in event.get("attendees", [])
if "email" in att
]
room = event.get("location", "").strip()
description = event.get("description", "").strip()

try:
start = datetime.datetime.fromisoformat(start_str.replace("Z", "+00:00"))
end = datetime.datetime.fromisoformat(end_str.replace("Z", "+00:00"))
created_at = (
datetime.datetime.fromisoformat(event.get("created").replace("Z", "+00:00"))
if event.get("created")
else datetime.datetime.utcnow()
)
except Exception as e:
print(f"Parse error, event '{name}': {e}")
return False

organizer_email = event.get("organizer", {}).get("email", "").strip()
if not organizer_email:
print(f"Missing organizer email for event: {name}")
return False

owner = User.query.filter_by(username=organizer_email).first()
if not owner:
print(f"Organizer not found in database: {organizer_email}")
return False

existing_meeting = Meeting.query.filter(
(Meeting.token == meet_token) & (Meeting.start_datetime == start)
).first()

if existing_meeting:
print(f"Meeting already exists: {name} ({meet_token}) at {start}")
return False

meeting_users = get_or_create_users(participants)
meeting_users.append(owner)

duration = (end - start).seconds // 60

meeting = Meeting(
name=name,
start_datetime=start,
duration=duration,
room_name=room,
cost=calculate_meeting_cost(meeting_users, duration),
token=meet_token,
created_at=created_at,
description=description,
owner_id=owner.id,
)

for user in meeting_users:
meeting.users.append(user)

try:
db.session.add(meeting)
db.session.commit()
print(f'+ Added meeting: "{name}" ({meet_token}) at {start}')
return True
except Exception as e:
db.session.rollback()
print(f"Error saving meeting {name}: {str(e)}")
return False


def get_or_create_users(participants_emails):
users = []
for email in participants_emails:
user = User.query.filter_by(username=email).first()
if not user:
user = User(
username=email,
role_name="unknown",
hourly_cost=0.0,
app_role="EMPLOYEE",
)
# user.hash_password("123")
db.session.add(user)
db.session.commit()
print(f'* Added user: "{email}"')
users.append(user)
print(users)
return users


def save_meetings_from_calendar():
creds = authorize()
service = build("calendar", "v3", credentials=creds)

now = datetime.datetime.utcnow().isoformat() + "Z"
print("Getting events with Google Meet link...\n")
events_result = (
service.events()
.list(
calendarId="primary",
singleEvents=True,
)
.execute()
)
events = events_result.get("items", [])

if not events:
print("No events with Google Meet.")
return

print(f"Get {len(events)} events from Google Calendar. Uploading to database...\n")

try:
db.session.execute(meeting_users.delete())
db.session.query(Meeting).delete()
db.session.commit()
except Exception as e:
db.session.rollback()

db.session.commit()
new_num = process_events(events)
print(
f"Added {new_num} meetings. {len(events) - new_num} already exist in the database.\n"
)
2 changes: 1 addition & 1 deletion backend/app/models/Meeting.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
class Meeting(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), nullable=False)
token = db.Column(db.String(128), nullable=False, unique=True)
token = db.Column(db.String(128), nullable=True, unique=False)
start_datetime = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
duration = db.Column(db.Integer, nullable=False) # Duration in minutes
room_name = db.Column(db.String(64))
Expand Down
20 changes: 20 additions & 0 deletions backend/app/routes/meetings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from app.types import MeetingsFilters as MeetingsFilterParser
from flask import abort
from app.auth import auth
from app.meet.google_calendar import save_meetings_from_calendar
from .common import build_return_type

api = Namespace("meetings", description="meeting operations")
Expand Down Expand Up @@ -182,3 +183,22 @@ def post(self, token):
return meetings_single_resolver(token=token)
except Exception:
abort(404)


@api.route("/sync")
class MeetingSync(Resource):
@api.response(200, "Success")
@api.response(400, "Invalid request")
@api.response(401, "Unauthorized")
def post(self):
"""Sync meetings with Google Calendar"""
user = auth.current_user()
data = request.get_json()

try:
filters = MeetingsFilterParser(**data) if data else None
sync_result = save_meetings_from_calendar()
return meetings_all_resolver(filters=filters, user=user)
except Exception as e:
print(f"Sync error: {str(e)}")
abort(400, description=str(e))
5 changes: 5 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,10 @@ passlib==1.7.4
Flask-HTTPAuth==4.8.0
black==25.1.0
flask-restx==1.3.0
flask-cors==6.0.0

google-api-python-client==2.169.0
google-auth==2.39.0
google-auth-oauthlib==1.2.2
pytest==8.4.0
flask-cors==6.0.1
3 changes: 3 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?

firebase.json
firebase.ts
Loading