Skip to content

Services

loglife.app.services.reminder.worker

Background worker for checking and sending scheduled reminders.

Runs a minutely job to check if any user reminders are due for their timezone.

build_journal_reminder_message(user_id)

Construct the journaling reminder message, listing untracked goals if any.

Source code in src/loglife/app/services/reminder/worker.py
70
71
72
73
74
75
76
77
78
79
80
def build_journal_reminder_message(user_id: int) -> str:
    """Construct the journaling reminder message, listing untracked goals if any."""
    goals_not_tracked = get_goals_not_tracked_today(user_id)

    if not goals_not_tracked:
        return JOURNAL_REMINDER_MESSAGE.replace("\n\n<goals_not_tracked_today>", "")

    goals_list = "\n".join([f"- {goal.goal_description}" for goal in goals_not_tracked])
    replacement = f"{messages.REMINDER_UNTRACKED_HEADER}{goals_list}"

    return JOURNAL_REMINDER_MESSAGE.replace("<goals_not_tracked_today>", replacement)

build_standard_reminder_message(goal)

Construct a standard reminder message for a specific goal.

Source code in src/loglife/app/services/reminder/worker.py
83
84
85
86
87
88
def build_standard_reminder_message(goal: Goal) -> str:
    """Construct a standard reminder message for a specific goal."""
    return REMINDER_MESSAGE.replace("<goal_emoji>", goal.goal_emoji).replace(
        "<goal_description>",
        goal.goal_description,
    )

check_reminders()

Check all reminders and send notifications when scheduled time matches.

Source code in src/loglife/app/services/reminder/worker.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def check_reminders() -> None:
    """Check all reminders and send notifications when scheduled time matches."""
    goals_with_reminders: list[Goal] = db.goals.get_all_with_reminders()
    if not goals_with_reminders:
        return

    # Batch fetch all users to avoid N+1 queries
    users_map: dict[int, User] = {user.id: user for user in db.users.get_all()}
    now_utc: datetime = datetime.now(UTC)

    for goal in goals_with_reminders:
        user = users_map.get(goal.user_id)
        if not user:
            continue

        if is_reminder_due(goal, user, now_utc):
            process_due_reminder(user, goal)

get_timezone_safe(timezone_str)

Get ZoneInfo, falling back to UTC if timezone is invalid or unknown.

Parameters:

Name Type Description Default
timezone_str str

Timezone string in IANA format (e.g., "Asia/Karachi", "America/New_York")

required

Returns:

Type Description
ZoneInfo

A ZoneInfo object for the given timezone string, or UTC if the timezone

ZoneInfo

is invalid or unknown.

Source code in src/loglife/app/services/reminder/worker.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def get_timezone_safe(timezone_str: str) -> ZoneInfo:
    """Get ZoneInfo, falling back to UTC if timezone is invalid or unknown.

    Arguments:
        timezone_str: Timezone string in IANA format (e.g., "Asia/Karachi",
            "America/New_York")

    Returns:
        A ZoneInfo object for the given timezone string, or UTC if the timezone
        is invalid or unknown.

    """
    timezone_str = timezone_str.strip()
    try:
        return ZoneInfo(timezone_str)
    except (ZoneInfoNotFoundError, ValueError):
        return ZoneInfo("UTC")

is_reminder_due(goal, user, now_utc)

Check if a reminder is due at the current time for the user's timezone.

Source code in src/loglife/app/services/reminder/worker.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def is_reminder_due(goal: Goal, user: User, now_utc: datetime) -> bool:
    """Check if a reminder is due at the current time for the user's timezone."""
    if not goal.reminder_time:
        return False

    tz = get_timezone_safe(user.timezone)
    local_now = now_utc.astimezone(tz)

    try:
        # reminder_time is likely a string "HH:MM:SS" or datetime
        time_parts = str(goal.reminder_time).split(":")
        rem_hour = int(time_parts[0])
        rem_minute = int(time_parts[1])
    except (ValueError, IndexError):
        logger.warning(
            "Invalid reminder time format for goal %s: %s",
            goal.id,
            goal.reminder_time,
        )
        return False

    return local_now.hour == rem_hour and local_now.minute == rem_minute

process_due_reminder(user, goal)

Process a due reminder by fetching the goal and sending the notification.

Source code in src/loglife/app/services/reminder/worker.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def process_due_reminder(user: User, goal: Goal) -> None:
    """Process a due reminder by fetching the goal and sending the notification."""
    is_journaling = goal.goal_emoji == "📓" and goal.goal_description == "journaling"

    if is_journaling:
        message = build_journal_reminder_message(user.id)
    else:
        message = build_standard_reminder_message(goal)

    queue_async_message(user.phone_number, message, client_type=user.client_type)
    logger.info(
        "Sent reminder '%s' to %s",
        goal.goal_description,
        user.phone_number,
    )

start_reminder_service()

Start the reminder service in a daemon thread.

Source code in src/loglife/app/services/reminder/worker.py
140
141
142
143
144
145
146
147
148
def start_reminder_service() -> threading.Thread:
    """Start the reminder service in a daemon thread."""
    t: threading.Thread = threading.Thread(
        target=_reminder_worker,
        daemon=True,
    )
    t.start()
    logger.info("Reminder service thread %s started (daemon=%s)", t.name, t.daemon)
    return t