Skip to content

Commit 58b76c8

Browse files
committed
- user-set notifications & alerts
1 parent b04b398 commit 58b76c8

File tree

6 files changed

+113
-20
lines changed

6 files changed

+113
-20
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
- **🗺 Geolocation and map lookups via MapTiler API**
1010
- (with weather forecasts around the world in all OpenAI API supported languages)
1111
- **🧭 Navigation instructions via Openrouteservice API**
12+
- **🔔 Timed reminders & notifications system**
13+
- Users can schedule alerts that trigger at specific times. Configurable per-user limits and optional listing of past reminders
1214
- **📊 Daily token usage tracking & rate limiting for API usage / cost management**
1315
- **🔍 Perplexity API models alongside OpenAI models**
1416
- Useful for fact-checking and supplementing OpenAI's cutoff dates
@@ -238,7 +240,11 @@ If you run into any issues, consult the logs or reach out on the repository's [I
238240
---
239241

240242
# Changelog
241-
- v0.76 – **Premium mode auto-switching** + usage DB synergy
243+
- v0.761 - **Timed notifications / user reminders**
244+
- Brand-new feature: users can set timed reminders (alerts) by requesting reminders that the bot stores in an SQLite database. A separate poller picks them up as soon as they are due, and the bot automatically notifies the user on set times.
245+
- The custom function calling view action can also list your recently passed or deleted reminders (configurable in `[Reminders]` -> `ShowPastRemindersCount`).
246+
- The bot ensures a max limit of pending reminders per user (set with `MaxAlertsPerUser`; set to 0 for unlimited alerts).
247+
- v0.76 - **Premium mode auto-switching** + usage DB synergy
242248
- Added daily usage-based auto-switch logic between “premium” vs. “mini” models (see `[ModelAutoSwitch]` in `config.ini`).
243249
- Once you exceed the `PremiumTokenLimit`, the bot seamlessly switches to the fallback model.
244250
- If that fallback also goes past `MiniTokenLimit`, the bot can either deny usage or proceed, according to `FallbackLimitAction`.

config/config.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ MaxAlertsPerUser = 100
194194
# How often (in seconds) the bot checks for due reminders
195195
PollingIntervalSeconds = 5
196196

197+
# How many old/past reminders to list
198+
ShowPastRemindersCount = 5
199+
197200
# ~~~~~~~~~~~~~~~
198201
# Perplexity API
199202
# ~~~~~~~~~~~~~~~

src/db_utils.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,38 @@ def get_due_reminders(db_path, current_utc_time_str):
237237
rows = _execute_sql(db_path, sql, (current_utc_time_str,), fetch_all=True)
238238
return [{'reminder_id': r[0], 'user_id': r[1], 'chat_id': r[2], 'reminder_text': r[3]} for r in rows] if rows else []
239239

240+
def get_past_reminders_for_user(db_path, user_id, limit=5):
241+
"""
242+
Gets the most recent 'past' reminders (status != 'pending') for a user,
243+
ordered by the time they were due or created.
244+
Typically we consider 'sent','failed_*','deleted' as "past."
245+
"""
246+
if not DB_INITIALIZED_SUCCESSFULLY:
247+
logging.error("DB not initialized. Cannot get past reminders.")
248+
return []
249+
250+
sql = f"""
251+
SELECT reminder_id, reminder_text, due_time_utc, status
252+
FROM {REMINDERS_TABLE_NAME}
253+
WHERE user_id = ?
254+
AND status != 'pending'
255+
ORDER BY due_time_utc DESC
256+
LIMIT ?
257+
"""
258+
rows = _execute_sql(db_path, sql, (user_id, limit), fetch_all=True)
259+
if not rows:
260+
return []
261+
# Build a list of dicts
262+
reminders = []
263+
for row in rows:
264+
reminders.append({
265+
'reminder_id': row[0],
266+
'reminder_text': row[1],
267+
'due_time_utc': row[2],
268+
'status': row[3]
269+
})
270+
return reminders
271+
240272
def update_reminder_status(db_path, reminder_id, new_status):
241273
"""Updates the status of a reminder (e.g., to 'sent' or 'failed')."""
242274
if not DB_INITIALIZED_SUCCESSFULLY:

src/main.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD
6060
)
6161

62+
import db_utils
6263
from bot_token import get_bot_token
6364
from api_key import get_api_key
6465
import bot_commands
@@ -168,10 +169,6 @@ def __init__(self):
168169
from config_paths import CHAT_LOG_FILE_PATH
169170
self.chat_log_file = CHAT_LOG_FILE_PATH
170171

171-
# Read the reminder setting using the full parser and the Reminders section
172-
self.reminders_enabled = self._parser.getboolean('Reminders', 'EnableReminders', fallback=False)
173-
self.logger.info(f"Reminders Enabled according to config: {self.reminders_enabled}")
174-
175172
# Attempt to get bot & API tokens
176173
try:
177174
self.telegram_bot_token = get_bot_token()
@@ -528,6 +525,10 @@ def run(self):
528525
# Get the asyncio event loop
529526
loop = asyncio.get_event_loop()
530527

528+
# Force DB init
529+
if not db_utils.DB_INITIALIZED_SUCCESSFULLY:
530+
db_utils._create_tables_if_not_exist(db_utils.REMINDERS_DB_PATH)
531+
531532
# If reminders are enabled in config, launch reminder poller
532533
if self.reminders_enabled:
533534
loop.create_task(reminder_poller(application))

src/reminder_handler.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
from datetime import datetime, timezone
66
from config_paths import CONFIG_PATH, REMINDERS_DB_PATH
77
import db_utils
8+
from db_utils import get_past_reminders_for_user
89

910
# Load config to get MaxAlertsPerUser
1011
config = configparser.ConfigParser()
1112
config.read(CONFIG_PATH)
13+
SHOW_PAST_REMINDERS_COUNT = config.getint('Reminders', 'ShowPastRemindersCount', fallback=0)
1214

1315
try:
1416
MAX_ALERTS_PER_USER = config.getint('Reminders', 'MaxAlertsPerUser', fallback=30)
@@ -81,19 +83,54 @@ async def handle_view_reminders(user_id):
8183
logger.error("Attempt to view reminders but DB not available!")
8284
return "Error: DB not available. Cannot view reminders."
8385

84-
reminders = db_utils.get_pending_reminders_for_user(REMINDERS_DB_PATH, user_id)
85-
if not reminders:
86-
logger.info(f"User {user_id} has no pending reminders.")
87-
return "You currently have no pending reminders."
88-
89-
logger.info(f"User {user_id} is viewing {len(reminders)} reminders.")
90-
lines = ["Here are your current reminders:"]
91-
for r in reminders:
92-
rid = r['reminder_id']
93-
text = r['reminder_text']
94-
due_utc = r['due_time_utc']
95-
lines.append(f"• Reminder #{rid}: due {due_utc}, text: '{text}'")
96-
return "\n".join(lines)
86+
# 1) Get the pending
87+
pending_reminders = db_utils.get_pending_reminders_for_user(REMINDERS_DB_PATH, user_id)
88+
if pending_reminders:
89+
lines = ["<b>Your current (pending) reminders:</b>"]
90+
for idx, r in enumerate(pending_reminders, start=1):
91+
rid = r['reminder_id']
92+
text = r['reminder_text']
93+
due_utc = r['due_time_utc']
94+
lines.append(f"• Reminder #{idx} (ID {rid}) due <i>{due_utc}</i>\n{text}”")
95+
pending_section = "\n".join(lines)
96+
else:
97+
pending_section = "You have no <b>pending</b> reminders."
98+
99+
# 2) Optionally get the past ones
100+
if SHOW_PAST_REMINDERS_COUNT > 0:
101+
past = get_past_reminders_for_user(REMINDERS_DB_PATH, user_id, SHOW_PAST_REMINDERS_COUNT)
102+
if past:
103+
lines = [f"<b>Up to {SHOW_PAST_REMINDERS_COUNT} most recent past reminders:</b>"]
104+
for idx, r in enumerate(past, start=1):
105+
rid = r['reminder_id']
106+
text = r['reminder_text']
107+
due_utc = r['due_time_utc']
108+
status = r['status']
109+
lines.append(f"• (ID {rid}) was <i>{status}</i> at {due_utc}, text: “{text}”")
110+
past_section = "\n".join(lines)
111+
else:
112+
past_section = "(No past reminders found.)"
113+
else:
114+
past_section = "" # or omit entirely
115+
116+
# 3) Combine them for your final message
117+
full_msg = f"{pending_section}\n\n{past_section}".strip()
118+
return full_msg
119+
120+
# // old logic; no past reminders
121+
# reminders = db_utils.get_pending_reminders_for_user(REMINDERS_DB_PATH, user_id)
122+
# if not reminders:
123+
# logger.info(f"User {user_id} has no pending reminders.")
124+
# return "You currently have no pending reminders."
125+
126+
# logger.info(f"User {user_id} is viewing {len(reminders)} reminders.")
127+
# lines = ["Here are your current reminders:"]
128+
# for r in reminders:
129+
# rid = r['reminder_id']
130+
# text = r['reminder_text']
131+
# due_utc = r['due_time_utc']
132+
# lines.append(f"• Reminder #{rid}: due {due_utc}, text: '{text}'")
133+
# return "\n".join(lines)
97134

98135

99136
async def handle_delete_reminder(user_id, reminder_id):

src/text_message_handler.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,8 +1154,18 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
11541154
chat_history.append({"role": "system", "content": result_msg})
11551155

11561156
elif action == 'view':
1157-
result_msg = await handle_view_reminders(user_id)
1158-
chat_history.append({"role": "system", "content": result_msg})
1157+
raw_result = await handle_view_reminders(user_id)
1158+
prefix = (
1159+
"Here are the user's alerts. Use the user's language when replying and Telegram-compliant "
1160+
"HTML tags instead of Markdown! List the reminders without their database id #'s to the user, "
1161+
"since the numbers are only for database reference [i.e. to delete/edit, etc]). "
1162+
"If the user wasn't asking about past reminders, don't list them. KÄYTÄ VASTAUKSESSA HTML:ÄÄ. ÄLÄ KÄYTÄ MARKDOWNIA.\n\n"
1163+
)
1164+
final_result = prefix + raw_result
1165+
1166+
# Now store that prefixed message in the chat history
1167+
chat_history.append({"role": "system", "content": final_result})
1168+
context.chat_data['chat_history'] = chat_history
11591169

11601170
elif action == 'delete':
11611171
if not reminder_id:
@@ -1186,6 +1196,10 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
11861196
final_reply_content = response_json['choices'][0]['message'].get('content', '')
11871197
final_reply = final_reply_content.strip() if final_reply_content else ""
11881198

1199+
if not final_reply:
1200+
# Provide a fallback message to avoid sending an empty string
1201+
final_reply = "🤔"
1202+
11891203
# log & send
11901204
bot.log_message(
11911205
message_type='Bot',

0 commit comments

Comments
 (0)