Skip to content
Merged
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
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# TOKEN do seu bot gerado pelo BotFather
TELEGRAM_BOT_TOKEN=

# Sistema de eventos a ser utilizado (open_event ou meetup)
EVENT_EXTRACTOR=open_event

# URL para extrair os eventos (Open Event ou Meetup)
URL=

# Fuso horário do bot (padrão: America/Sao_Paulo)
# Qualquer timezone válida do IANA, ex: Europe/Lisbon, America/New_York
TIMEZONE=America/Sao_Paulo

# ID do grupo no Telegram onde o bot mandará as mensagens agendadas
GROUP_CHAT_ID=
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ TELEGRAM_BOT_TOKEN=
EVENT_EXTRACTOR=<meetup ou open_event>
# URL para extrair os eventos (Meetup ou Open Event)
URL=
# Fuso horário do bot (padrão: America/Sao_Paulo)
TIMEZONE=America/Sao_Paulo
# ID do grupo no Telegram onde o bot mandará as mensagens agendadas
GROUP_CHAT_ID=
# ID do tópico do grupo no Telegram onde o bot mandará as mensagens agendadas (opcional)
Expand Down
16 changes: 11 additions & 5 deletions grupy_sanca_agenda_bot/database.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import UTC, datetime
from pathlib import Path
from typing import List

Expand Down Expand Up @@ -42,8 +42,14 @@ def save_cache(events: List[Event]):
new_events = [event for event in events if event.identifier not in existing_ids]
if not new_events:
return

session.add_all([EventModel(**event.model_dump(exclude={"id"})) for event in new_events])
session.add_all(
[
EventModel(
**{**event.model_dump(exclude={"id"}), "date_time": event.date_time.replace(tzinfo=None)}
)
for event in new_events
]
)
session.commit()


Expand Down Expand Up @@ -74,7 +80,7 @@ def load_cache() -> List[Event]:
with SessionLocal() as session:
events = session.scalars(
select(EventModel)
.where(EventModel.date_time >= datetime.now())
.where(EventModel.date_time >= datetime.now(UTC).replace(tzinfo=None))
.order_by(EventModel.date_time.asc())
).all()

Expand All @@ -83,7 +89,7 @@ def load_cache() -> List[Event]:
id=event.id,
identifier=event.identifier,
title=event.title,
date_time=event.date_time,
date_time=event.date_time.replace(tzinfo=UTC),
location=event.location,
description=event.description,
link=event.link,
Expand Down
5 changes: 2 additions & 3 deletions grupy_sanca_agenda_bot/events.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import html
import re
from asyncio import gather
from datetime import datetime
from zoneinfo import ZoneInfo
from datetime import UTC, datetime

from bs4 import BeautifulSoup
from httpx import AsyncClient, Timeout
Expand Down Expand Up @@ -128,7 +127,7 @@ async def get_json_content(self, url):
return resp.json() if resp else None

def extract_datetime(self, timestamp):
return datetime.fromisoformat(timestamp).astimezone(ZoneInfo("America/Sao_Paulo"))
return datetime.fromisoformat(timestamp).astimezone(UTC)

def extract_location(self, raw_location):
return raw_location if raw_location else "Evento Online"
Expand Down
6 changes: 4 additions & 2 deletions grupy_sanca_agenda_bot/scheduler.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import pytz
from zoneinfo import ZoneInfo

from apscheduler.schedulers.asyncio import AsyncIOScheduler

from grupy_sanca_agenda_bot import event_extractor
from grupy_sanca_agenda_bot.settings import settings
from grupy_sanca_agenda_bot.utils import (
PeriodEnum,
filter_events,
Expand All @@ -11,7 +13,7 @@


def setup_scheduler(application, loop):
scheduler = AsyncIOScheduler(timezone=pytz.timezone("America/Sao_Paulo"), event_loop=loop)
scheduler = AsyncIOScheduler(timezone=ZoneInfo(settings.TIMEZONE), event_loop=loop)

scheduler.add_job(
send_monthly_events,
Expand Down
1 change: 1 addition & 0 deletions grupy_sanca_agenda_bot/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Settings(BaseSettings):
TELEGRAM_BOT_TOKEN: str
URL: str
EVENT_EXTRACTOR: EventExtractorEnum = EventExtractorEnum.open_event
TIMEZONE: str = "America/Sao_Paulo"
GROUP_CHAT_ID: str
GROUP_CHAT_TOPIC_ID: str | None = None
ADMINS: list[int] | None = None
Expand Down
23 changes: 11 additions & 12 deletions grupy_sanca_agenda_bot/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

import pytz
from telegram import Update
from telegram.ext import Application

Expand All @@ -27,23 +27,21 @@ async def reply_message(message: str, update: Update) -> None:


def filter_events(events: Event, period=PeriodEnum.agenda):
tz = pytz.timezone("America/Sao_Paulo")

today = datetime.now(tz)
now = datetime.now(ZoneInfo(settings.TIMEZONE))
if period == PeriodEnum.mensal:
start = today.replace(day=1, hour=0, minute=0, second=0)
start = now.replace(day=1, hour=0, minute=0, second=0)
end = (start + timedelta(days=31)).replace(day=1, hour=0, minute=0, second=0) - timedelta(seconds=1)
elif period == PeriodEnum.semanal:
start = today - timedelta(days=today.weekday())
start = now - timedelta(days=now.weekday())
end = start + timedelta(days=6)
elif period == PeriodEnum.hoje:
start = today.replace(hour=0, minute=0, second=0)
end = today.replace(hour=23, minute=59, second=59)
start = now.replace(hour=0, minute=0, second=0)
end = now.replace(hour=23, minute=59, second=59)
elif period == PeriodEnum.agenda:
start = today
end = today + timedelta(days=365)
start = now
end = now + timedelta(days=365)

return [event for event in events if start <= event.date_time.astimezone(tz) <= end]
return [event for event in events if start <= event.date_time <= end]


def slice_events(events, quantity):
Expand All @@ -53,8 +51,9 @@ def slice_events(events, quantity):
def format_event_message(events: Event, header="", description=True):
message = f"*📅 {header}:*\n\n"
for event in events:
date_time = event.date_time.astimezone(ZoneInfo(settings.TIMEZONE)).strftime("%d/%m/%Y às %Hh%M")
message += f"*{event.title}*\n\n"
message += f"*🕒 Data e Hora:* {event.date_time.strftime('%d/%m/%Y às %Hh%M')}\n"
message += f"*🕒 Data e Hora:* {date_time}\n"
message += f"*📍 Local:* {event.location}\n\n"
if description:
message += f"*📝 Descrição:*\n{event.description}\n\n"
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ dependencies = [
"pydantic-settings>=2.6.1",
"python-dotenv>=1.0.1",
"python-telegram-bot>=21.9",
"pytz>=2024.2",
"sqlalchemy>=2.0.44",
]

Expand Down
9 changes: 5 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pytest import fixture

from grupy_sanca_agenda_bot.schemas import Event
from grupy_sanca_agenda_bot.settings import settings


@fixture
Expand Down Expand Up @@ -99,7 +100,7 @@ def events():
identifier="1",
title="Event 1",
date_time=datetime.fromisoformat("2024-07-10T20:00:00-03:00").replace(
tzinfo=ZoneInfo("America/Sao_Paulo")
tzinfo=ZoneInfo(settings.TIMEZONE)
),
location="Location 1",
description="Description 1",
Expand All @@ -110,7 +111,7 @@ def events():
identifier="2",
title="Event 2",
date_time=datetime.fromisoformat("2024-07-15T15:00:00-03:00").replace(
tzinfo=ZoneInfo("America/Sao_Paulo")
tzinfo=ZoneInfo(settings.TIMEZONE)
),
location="Location 2",
description="Description 2",
Expand All @@ -121,7 +122,7 @@ def events():
identifier="3",
title="Event 3",
date_time=datetime.fromisoformat("2024-07-20T18:00:00-03:00").replace(
tzinfo=ZoneInfo("America/Sao_Paulo")
tzinfo=ZoneInfo(settings.TIMEZONE)
),
location="Location 3",
description="Description 3",
Expand All @@ -132,7 +133,7 @@ def events():
identifier="4",
title="Event 4",
date_time=datetime.fromisoformat("2024-08-05T19:00:00-03:00").replace(
tzinfo=ZoneInfo("America/Sao_Paulo")
tzinfo=ZoneInfo(settings.TIMEZONE)
),
location="Location 4",
description="Description 4",
Expand Down
5 changes: 4 additions & 1 deletion tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,13 @@ async def test_next_with_events(mock_load_events, mock_reply_message, events):
mock_load_events.return_value = events
mock_update = mock.MagicMock()
mock_context = mock.Mock()

formatted_date_time = "10/07/2024 às 20h00"

message = (
"*📅 Próximo Evento:*\n\n"
f"*{events[0].title}*\n\n"
f"*🕒 Data e Hora:* {events[0].date_time.strftime('%d/%m/%Y às %Hh%M')}\n"
f"*🕒 Data e Hora:* {formatted_date_time}\n"
f"*📍 Local:* {events[0].location}\n\n"
f"*📝 Descrição:*\n{events[0].description}\n\n"
f"🔗 [Clique aqui para se inscrever no evento]({events[0].link})\n\n"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class TestOpenEventExtractor:

def test_extract_datetime(self):
assert self.extractor.extract_datetime("2025-09-19T10:00:00+00:00") == datetime(
2025, 9, 19, 7, 0, tzinfo=ZoneInfo("America/Sao_Paulo")
2025, 9, 19, 7, 0, tzinfo=ZoneInfo(settings.TIMEZONE)
)

def test_extract_location(self):
Expand Down
59 changes: 47 additions & 12 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ async def test_reply_message():
mock_update.message.reply_text.assert_awaited_once_with("test reply", parse_mode="Markdown")


@freeze_time("2025-10-10 08:00:00", tz_offset=-3)
@freeze_time("2025-10-10 08:00:00")
def test_filter_events_mensal():
events = [
Event(
id=None,
identifier="1",
title="Evento 1",
date_time=datetime.fromisoformat("2025-10-10T10:00:00-03:00"),
description="Descrição1",
description="Descrição 1",
location="Local 1",
link="http://example.com/event1",
),
Expand All @@ -86,7 +86,7 @@ def test_filter_events_mensal():
identifier="2",
title="Evento 2",
date_time=datetime.fromisoformat("2025-10-20T10:00:00-03:00"),
description="Descrição2",
description="Descrição 2",
location="Local 2",
link="http://example.com/event2",
),
Expand All @@ -95,7 +95,7 @@ def test_filter_events_mensal():
identifier="3",
title="Evento 3",
date_time=datetime.fromisoformat("2025-07-05T10:00:00-03:00"),
description="Descrição3",
description="Descrição 3",
location="Local 3",
link="http://example.com/event3",
),
Expand All @@ -105,15 +105,15 @@ def test_filter_events_mensal():
assert all(event.title in ["Evento 1", "Evento 2"] for event in filtered)


@freeze_time("2025-10-10 08:00:00", tz_offset=-3)
@freeze_time("2025-10-10 08:00:00")
def test_filter_events_semanal():
events = [
Event(
id=None,
identifier="1",
title="Evento 1",
date_time=datetime.fromisoformat("2025-10-10T10:00:00-03:00"),
description="Descrição1",
description="Descrição 1",
location="Local 1",
link="http://example.com/event1",
),
Expand All @@ -122,7 +122,7 @@ def test_filter_events_semanal():
identifier="2",
title="Evento 2",
date_time=datetime.fromisoformat("2025-10-11T10:00:00-03:00"),
description="Descrição2",
description="Descrição 2",
location="Local 2",
link="http://example.com/event2",
),
Expand All @@ -131,7 +131,7 @@ def test_filter_events_semanal():
identifier="3",
title="Evento 3",
date_time=datetime.fromisoformat("2025-10-20T10:00:00-03:00"),
description="Descrição3",
description="Descrição 3",
location="Local 3",
link="http://example.com/event3",
),
Expand All @@ -141,15 +141,15 @@ def test_filter_events_semanal():
assert all(event.title in ["Evento 1", "Evento 2"] for event in filtered)


@freeze_time("2025-10-10 08:00:00", tz_offset=-3)
@freeze_time("2025-10-10 08:00:00")
def test_filter_events_hoje():
events = [
Event(
id=None,
identifier="1",
title="Evento 1",
date_time=datetime.fromisoformat("2025-10-10T10:00:00-03:00"),
description="Descrição1",
description="Descrição 1",
location="Local 1",
link="http://example.com/event1",
),
Expand All @@ -158,7 +158,7 @@ def test_filter_events_hoje():
identifier="2",
title="Evento 2",
date_time=datetime.fromisoformat("2025-10-11T10:00:00-03:00"),
description="Descrição2",
description="Descrição 2",
location="Local 2",
link="http://example.com/event2",
),
Expand All @@ -167,7 +167,7 @@ def test_filter_events_hoje():
identifier="3",
title="Evento 3",
date_time=datetime.fromisoformat("2025-10-12T10:00:00-03:00"),
description="Descrição3",
description="Descrição 3",
location="Local 3",
link="http://example.com/event3",
),
Expand All @@ -177,6 +177,41 @@ def test_filter_events_hoje():
assert filtered[0].title == "Evento 1"


@freeze_time("2025-10-10 15:45:00")
def test_filter_events_agenda():
events = [
Event(
id=None,
identifier="1",
title="Evento 1",
date_time=datetime.fromisoformat("2025-10-10T10:00:00-03:00"),
description="Descrição 1",
location="Local 1",
link="http://example.com/event1",
),
Event(
id=None,
identifier="2",
title="Evento 2",
date_time=datetime.fromisoformat("2025-10-10T16:00:00-03:00"),
description="Descrição 2",
location="Local 2",
link="http://example.com/event2",
),
Event(
id=None,
identifier="3",
title="Evento 3",
date_time=datetime.fromisoformat("2025-10-12T10:00:00-03:00"),
description="Descrição 3",
location="Local 3",
link="http://example.com/event3",
),
]
filtered = filter_events(events, period=PeriodEnum.agenda)
assert len(filtered) == 2


@pytest.mark.parametrize(
"events,slice_size",
[
Expand Down
Loading
Loading