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: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.13-slim-bookworm
FROM python:3.13.5-slim-bookworm
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

ADD . /app
Expand Down
41 changes: 41 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,46 @@

import logging

import time
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware import Middleware

#For RateLimiting

class RateLimitMiddleware(BaseHTTPMiddleware):
def __init__(self,app,max_requests: int=100, window_seconds: int =60 ):
super().__init__(app)
self.max_requests = max_requests
self.window_seconds = window_seconds
self._requests = {}

async def dispatch(self,request: Request, call_next):
if not request.url.path.startswith("/events"):
return await call_next(request)

client_ip = request.client.host if request.client else "unknown"
now = time.time()

if client_ip in self._requests:
self._requests[client_ip] = [
ts for ts in self._requests[client_ip]
if now - ts < self.window_seconds
]
else:
self._requests[client_ip]=[]

#Rate Limit checking

if len(self._requests[client_ip]) >= self.max_requests:
return JSONResponse(
{"error": "Rate limit exceeded. Try again later."},
status_code=429
)
self._requests[client_ip].append(now)
return await call_next(request)




engine = None

Expand Down Expand Up @@ -89,4 +129,5 @@ async def endpoint(req: Request):
Route("/health",endpoint=health,methods=["GET"])
],
lifespan=lifespan,
middleware=[Middleware(RateLimitMiddleware,max_requests=100,window_seconds = 60)]
)
20 changes: 20 additions & 0 deletions isabelle/events/buttons/approve_event.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Any
from typing import Callable
import logging

from slack_sdk.web.async_client import AsyncWebClient

Expand Down Expand Up @@ -52,5 +53,24 @@ async def handle_approve_event_btn(ack: Callable, body: dict[str, Any], client:
channel=event["LeaderSlackID"],
text=f"Your event {event["Title"]} has been approved by <@{user_id}>! Please reach out to them if you have any questions or need help.",
)
if env.mailer:
try:
user_info = await client.users_info(user=event["LeaderSlackID"])
host_email = user_info["user"]["profile"].get("email")
if host_email:
#An example
env.mailer.send_email(
host_email,
f"Your event `{event['Title']}' has been approved!",
f"Hi {event['Leader']}!\n\n"
f"Great news - your event `{event['Title']}` has been approved by <@{user_id}>.\n"
f"Start Time: {event['StartTime']}\n"
f"Event Link: {event.get('EventLink','N/A')}\n\n"
f"Reach out to <@{user_id}> if you have any question\n\n"

)

except Exception:
logging.exception("Failed to send approval email")

await client.views_publish(user_id=user_id, view=await get_home(user_id, client))
2 changes: 1 addition & 1 deletion isabelle/events/reaction_added.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,5 @@ async def handle_reaction_added(body, client: AsyncWebClient):
text='Successfully RSVPed to the event. You will receive reminders about the event.'
)
except Exception:
pass
logging.exception("Error sending RSVP confirmation")

5 changes: 2 additions & 3 deletions isabelle/events/views/create_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@
from slack_gfm import rich_text_to_gfm

from isabelle.views.app_home import get_home
from datetime import datetime


async def handle_create_event_view(ack: Callable, body: dict[str, Any], client: AsyncWebClient):
await ack()
view = body["view"]
values = view["state"]["values"]
title = (values["title"]["title"]["value"],)
title = (values["title"]["title"]["value"])
description = values["description"]["description"]["rich_text_value"]["elements"]
md = rich_text_to_md(description)
start_time = datetime.fromtimestamp(values["start_time"]["start_time"]["selected_date_time"])
Expand Down Expand Up @@ -60,7 +59,7 @@ async def handle_create_event_view(ack: Callable, body: dict[str, Any], client:
await client.chat_postEphemeral(
user=body["user"]["id"],
channel=body["user"]["id"],
text=f'An error occurred whilst creating the event "{title[0]}".',
text=(f'Could not create "{title[0]}\n' f'An event with the same titla and start time may already exist\n' f'Please check exiting events or edit them instead')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"titla" seems to be a typo. Right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

)
return

Expand Down
14 changes: 14 additions & 0 deletions isabelle/events/views/reject_event.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from typing import Any
from typing import Callable
import logging

from slack_sdk.web.async_client import AsyncWebClient

Expand Down Expand Up @@ -71,3 +72,16 @@ async def handle_reject_event_view(ack: Callable, body: dict[str, Any], client:
message,
],
)
if env.mailer:
try:
info = await client.users_info(user=event["LeaderSlackID"])
email_addr = info["user"]["profile"]["email"]
#rejection example(modificate!!!):
env.mailer.send_email(
email_addr,
f"Your event {event['Title']} has been rejected",
f"Hey {event['Leader']} has been rejected by <@{body['user']['id']}>,\n\n"
f"Please reach out to them if you have any questions or need help"
)
except Exception:
logging.exception("Failed to send rejection email")
8 changes: 5 additions & 3 deletions isabelle/tables.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from piccolo.table import Table
from piccolo.columns import Varchar,Boolean,Timestamp, SmallInt, Text, Array, UUID

from piccolo.columns import Varchar,Boolean,Timestamp, SmallInt, Text, Array, UUID,Timestamptz
from datetime import datetime

# Schema copied form airtable using PascalCase
class Event(Table):
Expand Down Expand Up @@ -42,4 +42,6 @@ class Event(Table):
InterestedUsers = Array(base_column=Text(),default=[])
InterestCount = SmallInt() # I know this could easily be calculated but I will try to keep this as close to the airtable as possible
rsvpMsg = Text(null=True)
Tags = Array(base_column=Text(), default=[])
Tags = Array(base_column=Text(), default=[])
created_at = Timestamptz(auto_update=datetime.now,default=datetime.now)
updated_at = Timestamptz(auto_update=datetime.now,default=datetime.now)
17 changes: 16 additions & 1 deletion isabelle/utils/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ async def create_event(
tags: Optional[List[str]] = None,
) -> Optional[Event]:

##Checking for duplicate events

existing = await Event.select().where(
Event.Title == title,
Event.StartTime == start_time,
Event.Cancelled == False
).first()

if existing:
logging.warning(
f"Duplicate event blocked: `{title} at {start_time}"
f"(existing ID: {existing.get('id')})"
)
return None

raw_description_json = json.dumps({
"type": "rich_text",
"elements": raw_description,
Expand Down Expand Up @@ -84,7 +99,7 @@ async def get_all_events(self, include_unapproved: bool = False) -> List[Event]:
return await query.order_by(Event.StartTime)

async def get_upcoming_events(self, include_unapproved: bool = False) -> List[Event]:
now = datetime.now()
now = datetime.now(timezone.utc).replace(tzinfo=None)
query = Event.select().where(
Event.StartTime > now,
Event.Cancelled == False
Expand Down
4 changes: 4 additions & 0 deletions isabelle/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def __init__(self):

unset = [key for key, value in self.__dict__.items() if value == "unset"]

if self.environemnt == "development":
unset = [key for key in unset if key not in ("airtable_api_key", "airtable_base_id")]


if unset:
raise ValueError(f"Missing environment variables: {', '.join(unset)}")

Expand Down
4 changes: 2 additions & 2 deletions isabelle/utils/rsvp_checker.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
import logging
import time
from datetime import datetime
from datetime import datetime,timezone
from typing import Any

from slack_sdk.web.async_client import AsyncWebClient
Expand Down Expand Up @@ -60,7 +60,7 @@ async def check_rsvps():
await env.database.update_event(str(event["id"]), **{"Sent1HourReminder": True})

elif start_time - time.time() <= 0 and not event.get(
"SentStartingReminder", True
"SentStartingReminder", False
):
pass
for user in rsvps:
Expand Down
7 changes: 6 additions & 1 deletion isabelle/utils/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from isabelle.events.reaction_added import handle_reaction_added
from isabelle.events.reaction_removed import handle_reaction_removed
from isabelle.utils.env import env
from isabelle.events.commands.create_event import handle_create_event_cmd
from isabelle.views.app_home import get_home
from slack_bolt.async_app import AsyncApp

Expand Down Expand Up @@ -111,4 +112,8 @@ async def edit_ama_fields_btn(ack: Callable, body: dict[str, Any], client: Async

@app.view("edit_ama_fields")
async def edit_ama_fields_view(ack: Callable, body: dict[str, Any], client: AsyncWebClient):
await handle_edit_ama_fields_view(ack, body, client)
await handle_edit_ama_fields_view(ack, body, client)

@app.command("/create-event")
async def create_event_cmd(ack: Callable, body: dict[str,Any],client: AsyncWebClient):
await handle_create_event_cmd(ack,body,client)
7 changes: 4 additions & 3 deletions isabelle/views/app_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
async def get_home(user_id: str, client: AsyncWebClient):
sad_member = await user_in_safehouse(user_id)
user_info = await client.users_info(user=user_id)
now = datetime.now(timezone.utc).replace(tzinfo=None)
ws_admin = (
True
if user_info["user"]["is_admin"]
Expand All @@ -28,14 +29,14 @@ async def get_home(user_id: str, client: AsyncWebClient):
event
for event in events
if event["StartTime"]
> datetime.now()
> now
]
current_events = [
event
for event in events
if datetime.now()
if now
< event.get("EndTime")
and datetime.now()
and now
> event["StartTime"]
]

Expand Down
2 changes: 1 addition & 1 deletion isabelle/views/edit_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ async def get_edit_event_modal(event_id: str):
"callback_id": "edit_event",
"notify_on_close": True,
"private_metadata": event_id,
"title": {"type": "plain_text", "text": "Add Event", "emoji": True},
"title": {"type": "plain_text", "text": "Edit Event", "emoji": True},
"submit": {"type": "plain_text", "text": "Submit", "emoji": True},
"close": {"type": "plain_text", "text": "More options", "emoji": True},
"blocks": [
Expand Down