Compare commits

..
10 Commits
Author SHA1 Message Date
krosh d81f669e3a . 2025-08-18 16:19:14 +03:00
krosh 41a1dab913 . 2025-08-18 15:09:07 +03:00
krosh 22e55a90bb . 2025-08-18 09:10:42 +03:00
krosh 981cc3d1f9 bugs 2025-08-18 08:58:03 +03:00
krosh fc60827261 . 2025-08-18 08:53:38 +03:00
krosh 36aeb7f682 add hyperlinks support 2025-08-18 08:49:31 +03:00
kr0sh512 3b29315129 now on sql 2024-12-23 04:05:46 +03:00
kr0sh512 0e7c185517 / 2024-12-18 00:55:53 +03:00
kr0sh512 b898950f11 fun upd 2024-12-14 23:12:31 +03:00
kr0sh512 d7f499e5c6 - add example config
- add command to configure time for reminds
- little refactoring in tg and for_db
2024-11-11 23:37:15 +03:00
8 changed files with 484 additions and 193 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
config.yaml
database/*
__pycache__/*
__pycache__/*
old.py
test.py
+38
View File
@@ -0,0 +1,38 @@
# TG Keeper
TG Keeper bot allows you to store notes, display them at a specified interval, and recognizes date formats: DD.MM, DD month. It works both in group chats by mentioning @username_bot and in private messages.
## Features
- Convenient setup interface
- Dependency installation via `requirements.txt`
- Initial configuration in the `config.yaml` file
## Installation
1. Clone the repository:
```sh
git clone https://github.com/yourusername/tg-keeper.git
```
2. Navigate to the project directory:
```sh
cd tg-keeper
```
3. Install the dependencies:
```sh
pip install -r requirements.txt
```
## Configuration
1. Open the `config.yaml` file and configure the parameters as you wish.
## Usage
Run the bot:
```sh
python bot.py
```
Now you can add and manage your notes through TG Keeper.
+6
View File
@@ -0,0 +1,6 @@
admin_id: "856850518"
api_token: null
test_token: null
bot_username: "@keeper_inbot"
users_path: "database/users.yaml"
user_path: "database/users/{}.yaml"
+200 -164
View File
@@ -1,178 +1,170 @@
import yaml, os
from datetime import datetime, timedelta
from telebot import types
from psycopg2 import pool
from sshtunnel import SSHTunnelForwarder
connection_pool: pool.SimpleConnectionPool = None
config = yaml.safe_load(open("config.yaml"))
users_path = config["users_path"]
user_path = config["user_path"]
server = SSHTunnelForwarder(
(config["host"], 22),
ssh_private_key="~/.ssh/id_rsa",
ssh_username="krosh",
remote_bind_address=("localhost", config["sql_port"]),
)
server.start()
connection_pool = pool.SimpleConnectionPool(
1,
5,
database=config["sql_database"],
user=config["sql_user"],
password=config["sql_password"],
host="localhost",
port=server.local_bind_port,
)
def into_dict(curs) -> dict:
cols = [desc[0] for desc in curs.description]
data = curs.fetchone()
return dict(zip(cols, data)) if data else None
def into_list(curs) -> list[dict]:
cols = [desc[0] for desc in curs.description]
data = curs.fetchall()
return [dict(zip(cols, row)) for row in data] if data else None
def check_user(user_id: int) -> dict:
users = yaml.safe_load(open(users_path, "r", encoding="utf-8"))
con = connection_pool.getconn()
curs = con.cursor()
curs.execute("SELECT * FROM users WHERE id = %s", (user_id,))
if not users:
users = {}
user = into_dict(curs)
connection_pool.putconn(con)
if user_id in users:
return users[user_id]
return None
return user
def new_user(message: types.Message) -> bool:
if check_user(message.chat.id):
return False
user = {
"id": message.chat.id,
"type": message.chat.type,
"username": message.chat.username,
"first_name": message.chat.first_name,
"last_name": message.chat.last_name,
"time_created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"last_message": None,
"remind_delta": 12 * 60 * 60, # 12 часов по умолчанию
}
users = yaml.safe_load(open(users_path, "r"), encoding="utf-8")
if not users:
users = {}
users[user["id"]] = user
yaml_data = yaml.dump(
users,
default_flow_style=False,
encoding="utf-8",
allow_unicode=True,
width=float("inf"),
sort_keys=False,
con = connection_pool.getconn()
curs = con.cursor()
curs.execute(
"""
INSERT INTO users (id, username, firstname, lastname, chat)
VALUES (%s, %s, %s, %s, %s)
""",
(
message.chat.id,
message.chat.username,
message.chat.first_name,
message.chat.last_name,
message.chat.type,
),
)
con.commit()
connection_pool.putconn(con)
with open(users_path, "wb") as file:
file.write(yaml_data)
return True
return
def add_note(
id: int, text: str, time_notif: datetime = None, list: str = "Default"
id: int, text: str, remind_at: datetime = None, list: str = "Default"
) -> bool:
if not check_user(id):
return False
notes = (
yaml.safe_load(open(user_path.format(id), "r", encoding="utf-8"))
if os.path.exists(user_path.format(id))
else {}
con = connection_pool.getconn()
curs = con.cursor()
curs.execute(
"""
SELECT id FROM lists WHERE name = %s AND user_id = %s
""",
(list, id),
)
if list not in notes:
notes[list] = {"name": list, "description": "Стандартный список", "notes": []}
data = curs.fetchone()
notes[list]["notes"].append(
{
"text": text,
"time_notif": str(time_notif) if time_notif else None,
"time_created": str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
}
)
if True: # TODO: edit sorting
import functools
def comp(x, y):
if not x["time_notif"] and not y["time_notif"]:
return 0
elif not x["time_notif"]:
return -1
elif not y["time_notif"]:
return 1
else:
x_time = datetime.strptime(
x["time_notif"],
"%Y-%m-%d %H:%M:%S",
)
y_time = datetime.strptime(
y["time_notif"],
"%Y-%m-%d %H:%M:%S",
)
if x_time > y_time:
return 1
elif x_time < y_time:
return -1
return 0
notes[list]["notes"].sort(
key=lambda x: datetime.strptime(
x["time_created"],
"%Y-%m-%d %H:%M:%S",
),
reverse=True,
if not data:
curs.execute(
"""
INSERT INTO lists (name, user_id)
VALUES (%s, %s)
""",
(list, id),
)
con.commit()
notes[list]["notes"].sort(
key=functools.cmp_to_key(comp),
)
yaml_data = yaml.dump(
notes,
default_flow_style=False,
encoding="utf-8",
allow_unicode=True,
width=float("inf"),
sort_keys=False,
curs.execute(
"""
INSERT INTO notes (user_id, list_id, content, remind_at)
VALUES (%s, (SELECT id FROM lists WHERE name = %s AND user_id = %s), %s, %s)
""",
(id, list, id, text, remind_at),
)
con.commit()
rowcnt = curs.rowcount
with open(user_path.format(id), "wb") as file:
file.write(yaml_data)
connection_pool.putconn(con)
return True
return True if rowcnt else False
def get_notes(id: int, list="Default") -> dict:
def get_notes(id: int, list="Default") -> list[dict]:
if not check_user(id):
return None
if not os.path.exists(user_path.format(id)):
return None
con = connection_pool.getconn()
curs = con.cursor()
notes = yaml.safe_load(open(user_path.format(id), "r", encoding="utf-8"))
curs.execute(
"""
SELECT * FROM notes
WHERE user_id = %s AND list_id = (SELECT id FROM lists WHERE name = %s AND user_id = %s)
ORDER BY remind_at IS NULL DESC, remind_at ASC, created_at DESC
""",
(id, list, id),
)
return notes[list] if list in notes else None
notes = into_list(curs)
connection_pool.putconn(con)
return notes
def delete_note(id, note_ind, list="Default") -> bool:
if not check_user(id):
return False
if not os.path.exists(user_path.format(id)):
return False
notes = yaml.safe_load(open(user_path.format(id), "r", encoding="utf-8"))
if list not in notes:
return False
if note_ind >= len(notes[list]["notes"]):
return False
notes[list]["notes"].pop(note_ind)
yaml_data = yaml.dump(
notes,
default_flow_style=False,
encoding="utf-8",
allow_unicode=True,
width=float("inf"),
sort_keys=False,
con = connection_pool.getconn()
curs = con.cursor()
curs.execute(
"""
DELETE FROM notes
WHERE id = (
SELECT id FROM notes
WHERE user_id = %s AND list_id = (SELECT id FROM lists WHERE name = %s AND user_id = %s)
ORDER BY remind_at IS NULL DESC, remind_at ASC, created_at DESC
LIMIT 1 OFFSET %s
)
""",
(id, list, id, note_ind),
)
with open(user_path.format(id), "wb") as file:
file.write(yaml_data)
con.commit()
rowcnt = curs.rowcount
connection_pool.putconn(con)
return True
return True if rowcnt else False
def new_message(message: types.Message) -> int:
@@ -182,54 +174,98 @@ def new_message(message: types.Message) -> int:
if not check_user(id):
return None
users = yaml.safe_load(open(users_path, "r", encoding="utf-8"))
user = users[id]
if "last_message" not in user:
user["last_message"] = None
last_id = user["last_message"]
user["last_message"] = new_msg_id
users[id] = user
yaml_data = yaml.dump(
users,
default_flow_style=False,
encoding="utf-8",
allow_unicode=True,
width=float("inf"),
sort_keys=False,
con = connection_pool.getconn()
curs = con.cursor()
curs.execute(
"""
SELECT last_message FROM users WHERE id = %s
""",
(id,),
)
last_id = curs.fetchone()[0]
with open(users_path, "wb") as file:
file.write(yaml_data)
curs.execute(
"""
UPDATE users
SET last_message = %s
WHERE id = %s
""",
(new_msg_id, id),
)
con.commit()
connection_pool.putconn(con)
return last_id
def check_old_notes() -> list[int, str, int]: # возвращает первую устаревшую заметку
users = yaml.safe_load(open(users_path, "r", encoding="utf-8"))
con = connection_pool.getconn()
curs = con.cursor()
curs.execute(
"""
SELECT id FROM users
"""
)
users = curs.fetchall()
for user_id in users.keys():
notes = yaml.safe_load(open(user_path.format(user_id), "r", encoding="utf-8"))
for user_id in users:
curs.execute(
"""
SELECT * FROM notes
WHERE id = %s
ORDER BY remind_at IS NULL DESC, remind_at ASC, created_at DESC
""",
(user_id,),
)
notes = [dict(row) for row in curs.fetchall()]
if "remind_delta" not in users[user_id]:
users[user_id]["remind_delta"] = 12 * 60 * 60 # не сохранится!
if not notes:
continue
timedt = timedelta(seconds=users[user_id]["remind_delta"])
timedt = timedelta(seconds=user_settings(user_id)["remind_delta"])
for list in notes.keys():
for ind in range(len(notes[list]["notes"])):
if (
notes[list]["notes"][ind]["time_notif"]
and datetime.strptime(
notes[list]["notes"][ind]["time_notif"], "%Y-%m-%d %H:%M:%S"
)
- timedt
< datetime.now()
):
return user_id, list, ind
for ind in range(len(notes)):
if (
notes[ind]["remind_at"]
and datetime.strptime(notes[ind]["remind_at"], "%Y-%m-%d %H:%M:%S")
- timedt
< datetime.now()
):
curs.execute(
"""
SELECT name FROM lists
WHERE id = %s
""",
(notes[ind]["list_id"],),
)
list_name = curs.fetchone()
connection_pool.putconn(con)
return (user_id, list_name, ind)
connection_pool.putconn(con)
return None, None, None
def update_user_settings(user_id: int, param: str, value: any) -> bool:
con = connection_pool.getconn()
curs = con.cursor()
query = f"UPDATE users SET {param} = %s WHERE id = %s"
curs.execute(query, (value, user_id))
con.commit()
rowcnt = curs.rowcount
connection_pool.putconn(con)
return True if rowcnt else False
def user_settings(user_id: int) -> dict:
con = connection_pool.getconn()
curs = con.cursor()
curs.execute("SELECT * FROM users WHERE id = %s", (user_id,))
user = into_dict(curs)
connection_pool.putconn(con)
return user
+78
View File
@@ -12,3 +12,81 @@
error_message: "Произошла ошибка. \
\nПожалуйста, попробуйте снова или свяжитесь с @Kr0sH_512"
no_reminders: "У вас нет напоминаний."
not_registered: "Пожалуйста, воспользуйтесь сперва командой /start"
"list_reaction":
[
"👍",
"👎",
"❤",
"🔥",
"🥰",
"👏",
"😁",
"🤔",
"🤯",
"😱",
"🤬",
"😢",
"🎉",
"🤩",
"🤮",
"💩",
"🙏",
"👌",
"🕊",
"🤡",
"🥱",
"🥴",
"😍",
"🐳",
"❤‍🔥",
"🌚",
"🌭",
"💯",
"🤣",
"⚡",
"🍌",
"🏆",
"💔",
"🤨",
"😐",
"🍓",
"🍾",
"💋",
"🖕",
"😈",
"😴",
"😭",
"🤓",
"👻",
"👨‍💻",
"👀",
"🎃",
"🙈",
"😇",
"😨",
"🤝",
"✍",
"🤗",
"🫡",
"🎅",
"🎄",
"☃",
"💅",
"🤪",
"🗿",
"🆒",
"💘",
"🙉",
"🦄",
"😘",
"💊",
"🙊",
"😎",
"👾",
"🤷‍♂",
"🤷",
"🤷‍♀",
"😡",
]
+31
View File
@@ -0,0 +1,31 @@
CREATE type chat_type AS ENUM ('private', 'group', 'supergroup', 'channel');
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username TEXT,
firstname TEXT,
lastname TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_message BIGINT NOT NULL DEFAULT 0,
remind_delta BIGINT NOT NULL DEFAULT 21600,
chat chat_type NOT NULL
);
CREATE Table lists (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users (id),
name TEXT NOT NULL,
discription TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE notes (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users (id),
list_id BIGINT NOT NULL REFERENCES lists (id),
content TEXT NOT NULL,
remind_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
+4
View File
@@ -0,0 +1,4 @@
pyyaml
pytelegrambotapi
schedule
python-telegram-bot
+124 -28
View File
@@ -1,16 +1,17 @@
#!/usr/bin/python3.3
import threading, telebot, schedule, time, yaml
from datetime import datetime
import os, sys, inspect
import os, sys, inspect, random
import re, locale
from telegram.constants import ParseMode
from telebot import types
import for_db as db
config = yaml.safe_load(open("config.yaml"))
lang = yaml.safe_load(open("lang.yaml"))["ru"]
lang = yaml.safe_load(open("lang.yaml", encoding="utf-8"))["ru"]
list_reactions = yaml.safe_load(open("lang.yaml", encoding="utf-8"))["list_reaction"]
bot = telebot.TeleBot(
config["api_token"],
config["test_token"],
colorful_logs=True,
disable_web_page_preview=True,
parse_mode=ParseMode.HTML,
@@ -19,7 +20,6 @@ bot = telebot.TeleBot(
admin_id = config["admin_id"]
bot_username = config["bot_username"]
locale.setlocale(locale.LC_ALL, "ru_RU.UTF-8")
@@ -43,7 +43,10 @@ def restart_bot(message: types.Message):
@bot.message_handler(commands=["start"])
def start(message: types.Message):
send_message(message, lang["start_message"])
send_message(message, lang["welcome_message"])
if not db.check_user(message.chat.id):
db.new_user(message)
return
@@ -54,6 +57,9 @@ def help(message: types.Message):
send_message(message, help_msg)
if not db.check_user(message.chat.id):
db.new_user(message)
return
@@ -70,13 +76,13 @@ def delete_notes_callback(call):
note_ind = int(call.data.split("#")[2])
prefix = call.data.split("#")[0]
note = db.get_notes(call.message.chat.id, prefix)["notes"][note_ind]
note = db.get_notes(call.message.chat.id, prefix)[note_ind]
db.delete_note(call.message.chat.id, note_ind, prefix)
list_notes(call.message, prefix, edit=True)
bot.send_message(call.message.chat.id, f"❌ Удалено:\n\n{note['text']}")
bot.send_message(call.message.chat.id, f"❌ Удалено:\n\n{note['content']}")
return
@@ -85,7 +91,7 @@ def delete_notes_callback(call):
def edit_notes_callback(call):
note_ind = int(call.data.split("#")[2])
prefix = call.data.split("#")[0]
notes = db.get_notes(call.message.chat.id, prefix)["notes"]
notes = db.get_notes(call.message.chat.id, prefix)
note = notes[note_ind]
@@ -105,10 +111,10 @@ def edit_notes_callback(call):
text = f"📝 <b>Заметка {note_ind + 1}</b>"
if note["time_notif"]:
text += f" ⌚️ <b>{datetime.strptime(note['time_notif'], '%Y-%m-%d %H:%M:%S').strftime('%d %B %Y')}</b>"
if note["remind_at"]:
text += f" ⌚️ <b>{note['remind_at'].strftime('%d %B %Y')}</b>"
text += f"\n\n{note['text']}"
text += f"\n\n{note['content']}"
bot.edit_message_text(
text,
@@ -126,7 +132,7 @@ def choose_note_callback(call):
prefix = call.data.split("#")[0]
markup = types.InlineKeyboardMarkup()
num_on_page = 6 # Лимит телеграмма - 8
notes = db.get_notes(call.message.chat.id, prefix)["notes"]
notes = db.get_notes(call.message.chat.id, prefix)
max_page = (len(notes) - 1) // num_on_page
lst_button = []
@@ -187,13 +193,70 @@ def choose_note_callback(call):
return
@bot.message_handler(commands=["settings", "setting", "edit"])
def display_settings(message: types.Message):
@bot.callback_query_handler(func=lambda call: "edit_time" in call.data)
def edit_time(call):
list_time = {
(24 - 8) * 3600: "8:00 в день до напоминания",
(24 - 12) * 3600: "12:00 в день до напоминания",
(24 - 18) * 3600: "18:00 в день до напоминания",
0: "0:00 в день до напоминания",
(-8) * 3600: "8:00 в день напоминания",
(-12) * 3600: "12:00 в день напоминания",
(-18) * 3600: "18:00 в день напоминания",
}
if call.data[-1] == "#":
markup = types.InlineKeyboardMarkup()
for delta in list_time.keys():
markup.add(
types.InlineKeyboardButton(
list_time[delta],
callback_data=f"edit_time^{delta}",
)
)
bot.edit_message_text(
"⏰ Выберите время для уведомлений:",
call.message.chat.id,
call.message.message_id,
reply_markup=markup,
)
return
delta = int(call.data.split("^")[1])
db.update_user_settings(call.message.chat.id, "remind_delta", delta)
bot.edit_message_text(
f"Выбранное время: \n\n{list_time[delta]}",
call.message.chat.id,
call.message.message_id,
)
return
@bot.message_handler(commands=["list"])
@bot.message_handler(commands=["settings", "setting", "edit"])
def display_settings(message: types.Message):
if not db.check_user(message.chat.id):
db.new_user(message)
settings_msg = "Изменить время отправки напоминания"
markup = types.InlineKeyboardMarkup()
markup.add(
types.InlineKeyboardButton(
"Изменить время",
callback_data="edit_time#",
)
)
send_message(message, settings_msg, markup)
return
@bot.message_handler(commands=["list", "lists", "l"])
def list_notes(message: types.Message, list: str = "Default", edit: bool = False):
notes = db.get_notes(message.chat.id, list)
@@ -202,15 +265,13 @@ def list_notes(message: types.Message, list: str = "Default", edit: bool = False
return
list_notes = notes["notes"]
notes_msg = "⚡️ <u>Ваши напоминания</u>:\n\n"
for i in range(len(list_notes)):
for i in range(len(notes)):
notes_msg += f"<b>{i + 1})</b> "
if list_notes[i]["time_notif"]:
notes_msg += f" ⌚️ <b>{datetime.strptime(list_notes[i]['time_notif'], '%Y-%m-%d %H:%M:%S').strftime('%d %B %Y')}:</b>\n"
if notes[i]["remind_at"]:
notes_msg += f" ⌚️ <b>{notes[i]['remind_at'].strftime('%d %B %Y')}:</b>\n"
notes_msg += f"{list_notes[i]['text']}\n\n"
notes_msg += f"{notes[i]['content']}\n\n"
markup = types.InlineKeyboardMarkup()
markup.add(
@@ -228,6 +289,11 @@ def list_notes(message: types.Message, list: str = "Default", edit: bool = False
msg = send_message(message, notes_msg, markup)
reaction = types.ReactionTypeEmoji(random.choice(list_reactions))
bot.set_message_reaction(
message.chat.id, message.message_id, [reaction], is_big=True
)
old_msg_id = db.new_message(msg)
if old_msg_id:
@@ -242,14 +308,37 @@ def text_message(message: types.Message):
return # ignore messages from supergroups
if db.check_user(message.chat.id) is None:
if not db.check_user(message.chat.id):
db.new_user(message)
message.text = message.text.replace(f"{bot_username}", "").strip()
if not message.text:
return
msg_text = message.text
msg_entities = message.entities
if not message.text.replace(f"{bot_username}", "").strip():
if not message.reply_to_message:
return
message.text = message.reply_to_message.text
msg_text = message.reply_to_message.text
msg_entities = message.reply_to_message.entities
message = message.reply_to_message
if msg_entities:
text = msg_text
html_parts = []
last_idx = 0
for ent in msg_entities:
if ent.type == "text_link":
html_parts.append(text[last_idx : ent.offset])
link_text = text[ent.offset : ent.offset + ent.length]
html_parts.append(f'<a href="{ent.url}">{link_text}</a>')
last_idx = ent.offset + ent.length
html_parts.append(text[last_idx:])
msg_text = "".join(html_parts)
msg_text = msg_text.replace(f"{bot_username}", "").strip()
date_patterns = [
r"\b\d{1,2}\s(?:января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)\b",
@@ -298,7 +387,7 @@ def text_message(message: types.Message):
date_found_dt = None
for pattern in date_patterns:
match = re.search(pattern, message.text, re.IGNORECASE)
match = re.search(pattern, msg_text, re.IGNORECASE)
if match:
date_found_dt = translate_date_to_datetime(match.group())
@@ -310,10 +399,15 @@ def text_message(message: types.Message):
db.add_note(
message.chat.id,
message.text,
msg_text,
date_found_dt,
)
reaction = types.ReactionTypeEmoji(random.choice(list_reactions))
bot.set_message_reaction(
message.chat.id, message.message_id, [reaction], is_big=True
)
send_message(message, "📝 Напоминание добавлено:")
list_notes(message)
@@ -328,6 +422,8 @@ def send_message(
if not thread_id:
thread_id = message.message_thread_id
msg = None
try:
msg = bot.send_message(
chat_id=id,
@@ -371,13 +467,13 @@ if __name__ == "__main__":
chat_id, list, ind_note = db.check_old_notes()
if chat_id:
note = db.get_notes(chat_id, list)["notes"][ind_note]
note = db.get_notes(chat_id, list)[ind_note]
db.delete_note(chat_id, ind_note, list)
msg = bot.send_message(
chat_id,
f"{note['text']} \n(Уведомление: {datetime.strptime(note['time_notif'], '%Y-%m-%d %H:%M:%S').strftime('%d %B %Y')})",
f"{note['content']} \n(Уведомление: {note['remind_at'].strftime('%d %B %Y')})",
timeout=1,
)