TeLLMgramBot 3.10.2__tar.gz → 3.10.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/PKG-INFO +2 -1
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/README.md +1 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/database.py +103 -34
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/initialize.py +1 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot.egg-info/PKG-INFO +2 -1
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/setup.py +1 -1
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/LICENSE +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/TeLLMgramBot.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/__init__.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/conversation.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/message_handlers.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/models.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/providers/__init__.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/providers/base.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/providers/factory.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/providers/openai_provider.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/utils.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot/web_utils.py +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot.egg-info/requires.txt +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/TeLLMgramBot.egg-info/top_level.txt +0 -0
- {tellmgrambot-3.10.2 → tellmgrambot-3.10.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TeLLMgramBot
|
|
3
|
-
Version: 3.10.
|
|
3
|
+
Version: 3.10.3
|
|
4
4
|
Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
|
|
5
5
|
Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
|
|
6
6
|
Author: Digital Heresy
|
|
@@ -44,6 +44,7 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
|
|
|
44
44
|
* Ask questions about message history across all your chats using natural language; the bot will search, attribute messages to speakers, and include messages from other bots.
|
|
45
45
|
* Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "Show me the last few messages."
|
|
46
46
|
* All search filters (speaker, chat, date) are optional. Results are ordered most-recent-first. Configure `search_limit` to control how many results to return (default: 30).
|
|
47
|
+
* Search automatically finds users and chats by their current or past names, so you can reference them however you remember them.
|
|
47
48
|
* Token limits measure conversation length and determine when to prune oldest messages to stay within model limits.
|
|
48
49
|
* The bot loads the user's full history across all chats up to 50% of the token budget. In private chats, shared group context fills the remaining budget, enabling the bot to reference group conversations from a private context.
|
|
49
50
|
* This eliminates amnesia when switching between private and group chats.
|
|
@@ -12,6 +12,7 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
|
|
|
12
12
|
* Ask questions about message history across all your chats using natural language; the bot will search, attribute messages to speakers, and include messages from other bots.
|
|
13
13
|
* Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "Show me the last few messages."
|
|
14
14
|
* All search filters (speaker, chat, date) are optional. Results are ordered most-recent-first. Configure `search_limit` to control how many results to return (default: 30).
|
|
15
|
+
* Search automatically finds users and chats by their current or past names, so you can reference them however you remember them.
|
|
15
16
|
* Token limits measure conversation length and determine when to prune oldest messages to stay within model limits.
|
|
16
17
|
* The bot loads the user's full history across all chats up to 50% of the token budget. In private chats, shared group context fills the remaining budget, enabling the bot to reference group conversations from a private context.
|
|
17
18
|
* This eliminates amnesia when switching between private and group chats.
|
|
@@ -6,6 +6,7 @@ searching message history, and retrieving conversation context. The schema inclu
|
|
|
6
6
|
table (indexed by chat_id and user_id, with is_private flag), a users table (profile data
|
|
7
7
|
and private_mode flag), and a chats table for speaker/chat resolution in search results.
|
|
8
8
|
"""
|
|
9
|
+
import json
|
|
9
10
|
import os
|
|
10
11
|
from typing import Optional
|
|
11
12
|
|
|
@@ -42,19 +43,21 @@ CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id, created_at)
|
|
|
42
43
|
CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id, created_at);
|
|
43
44
|
|
|
44
45
|
CREATE TABLE IF NOT EXISTS users (
|
|
45
|
-
user_id
|
|
46
|
-
username
|
|
47
|
-
first_name
|
|
48
|
-
last_name
|
|
49
|
-
private_mode
|
|
50
|
-
|
|
46
|
+
user_id INTEGER PRIMARY KEY,
|
|
47
|
+
username TEXT,
|
|
48
|
+
first_name TEXT,
|
|
49
|
+
last_name TEXT,
|
|
50
|
+
private_mode INTEGER NOT NULL DEFAULT 0,
|
|
51
|
+
previous_names TEXT,
|
|
52
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
51
53
|
);
|
|
52
54
|
|
|
53
55
|
CREATE TABLE IF NOT EXISTS chats (
|
|
54
|
-
chat_id
|
|
55
|
-
chat_type
|
|
56
|
-
chat_title
|
|
57
|
-
|
|
56
|
+
chat_id INTEGER PRIMARY KEY,
|
|
57
|
+
chat_type TEXT,
|
|
58
|
+
chat_title TEXT,
|
|
59
|
+
previous_titles TEXT,
|
|
60
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
58
61
|
);
|
|
59
62
|
"""
|
|
60
63
|
|
|
@@ -71,7 +74,7 @@ def set_db_filename(filename: str) -> None:
|
|
|
71
74
|
Args:
|
|
72
75
|
filename: DB filename, e.g. 'MyBot_conversations.db'. A '.db' suffix is
|
|
73
76
|
appended automatically if not already present. Must be a plain
|
|
74
|
-
basename
|
|
77
|
+
basename - path separators and absolute paths are rejected.
|
|
75
78
|
|
|
76
79
|
Raises:
|
|
77
80
|
ValueError: If filename contains path components or is absolute.
|
|
@@ -133,14 +136,19 @@ async def init_db() -> None:
|
|
|
133
136
|
# Migration: drop username column now stored in the normalized users table.
|
|
134
137
|
if 'username' in existing:
|
|
135
138
|
await db.execute("ALTER TABLE messages DROP COLUMN username")
|
|
136
|
-
# Migration: add chat_type to chats table if absent.
|
|
139
|
+
# Migration: add chat_type and previous_titles to chats table if absent.
|
|
137
140
|
chats_cols = {row[1] async for row in await db.execute("PRAGMA table_info(chats)")}
|
|
138
141
|
if 'chat_type' not in chats_cols:
|
|
139
142
|
await db.execute("ALTER TABLE chats ADD COLUMN chat_type TEXT")
|
|
143
|
+
if 'previous_titles' not in chats_cols:
|
|
144
|
+
await db.execute("ALTER TABLE chats ADD COLUMN previous_titles TEXT")
|
|
140
145
|
# Migration: merge private_mode table into users.private_mode and drop the old table.
|
|
146
|
+
# Also add previous_names column for name history tracking.
|
|
141
147
|
users_cols = {row[1] async for row in await db.execute("PRAGMA table_info(users)")}
|
|
142
148
|
if 'private_mode' not in users_cols:
|
|
143
149
|
await db.execute("ALTER TABLE users ADD COLUMN private_mode INTEGER NOT NULL DEFAULT 0")
|
|
150
|
+
if 'previous_names' not in users_cols:
|
|
151
|
+
await db.execute("ALTER TABLE users ADD COLUMN previous_names TEXT")
|
|
144
152
|
tables = {row[0] async for row in await db.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
|
145
153
|
if 'private_mode' in tables:
|
|
146
154
|
await db.execute(
|
|
@@ -595,7 +603,10 @@ async def upsert_user(
|
|
|
595
603
|
Insert or update a user record in the users table.
|
|
596
604
|
|
|
597
605
|
Always reflects the latest Telegram profile data. Called on every interaction
|
|
598
|
-
so that name changes are picked up automatically.
|
|
606
|
+
so that name changes are picked up automatically. When first/last name changes,
|
|
607
|
+
the old display name (first_name + space + last_name) is appended to the
|
|
608
|
+
previous_names column (JSON array, deduplicated) to enable search by
|
|
609
|
+
historical identities.
|
|
599
610
|
|
|
600
611
|
Args:
|
|
601
612
|
user_id: Telegram user ID.
|
|
@@ -604,13 +615,40 @@ async def upsert_user(
|
|
|
604
615
|
last_name: Telegram last name, may be None.
|
|
605
616
|
"""
|
|
606
617
|
async with aiosqlite.connect(get_db_path()) as db:
|
|
618
|
+
async with db.execute(
|
|
619
|
+
"SELECT first_name, last_name, previous_names FROM users WHERE user_id = ?",
|
|
620
|
+
(user_id,),
|
|
621
|
+
) as cur:
|
|
622
|
+
existing = await cur.fetchone()
|
|
623
|
+
|
|
624
|
+
updated_prev = None
|
|
625
|
+
if existing is not None:
|
|
626
|
+
old_first, old_last, prev = existing
|
|
627
|
+
old_display = f"{old_first or ''} {old_last or ''}".strip()
|
|
628
|
+
new_display = f"{first_name or ''} {last_name or ''}".strip()
|
|
629
|
+
if old_display and old_display != new_display:
|
|
630
|
+
if prev:
|
|
631
|
+
try:
|
|
632
|
+
seen = json.loads(prev)
|
|
633
|
+
if not isinstance(seen, list):
|
|
634
|
+
seen = [seen] if seen else []
|
|
635
|
+
except json.JSONDecodeError:
|
|
636
|
+
seen = [n.strip() for n in prev.split(",") if n.strip()]
|
|
637
|
+
else:
|
|
638
|
+
seen = []
|
|
639
|
+
if old_display not in seen:
|
|
640
|
+
seen.append(old_display)
|
|
641
|
+
updated_prev = json.dumps(seen)
|
|
642
|
+
else:
|
|
643
|
+
updated_prev = prev
|
|
644
|
+
|
|
607
645
|
await db.execute(
|
|
608
|
-
"INSERT INTO users (user_id, username, first_name, last_name) VALUES (?, ?, ?, ?) "
|
|
646
|
+
"INSERT INTO users (user_id, username, first_name, last_name, previous_names) VALUES (?, ?, ?, ?, ?) "
|
|
609
647
|
"ON CONFLICT(user_id) DO UPDATE SET "
|
|
610
648
|
"username = excluded.username, first_name = excluded.first_name, "
|
|
611
|
-
"last_name = excluded.last_name, "
|
|
649
|
+
"last_name = excluded.last_name, previous_names = excluded.previous_names, "
|
|
612
650
|
"updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')",
|
|
613
|
-
(user_id, username, first_name, last_name),
|
|
651
|
+
(user_id, username, first_name, last_name, updated_prev),
|
|
614
652
|
)
|
|
615
653
|
await db.commit()
|
|
616
654
|
|
|
@@ -624,7 +662,9 @@ async def upsert_chat(
|
|
|
624
662
|
Insert or update a chat record in the chats table.
|
|
625
663
|
|
|
626
664
|
Always reflects the latest Telegram chat metadata. Called on every interaction
|
|
627
|
-
so that chat type and title changes are picked up automatically.
|
|
665
|
+
so that chat type and title changes are picked up automatically. When the
|
|
666
|
+
chat_title changes, the old title is appended to the previous_titles column
|
|
667
|
+
(JSON array, deduplicated) to enable search by historical identities.
|
|
628
668
|
|
|
629
669
|
Args:
|
|
630
670
|
chat_id: Telegram chat ID.
|
|
@@ -632,12 +672,38 @@ async def upsert_chat(
|
|
|
632
672
|
chat_title: Display name of the group/channel, or None for private chats.
|
|
633
673
|
"""
|
|
634
674
|
async with aiosqlite.connect(get_db_path()) as db:
|
|
675
|
+
async with db.execute(
|
|
676
|
+
"SELECT chat_title, previous_titles FROM chats WHERE chat_id = ?",
|
|
677
|
+
(chat_id,),
|
|
678
|
+
) as cur:
|
|
679
|
+
existing = await cur.fetchone()
|
|
680
|
+
|
|
681
|
+
updated_prev = None
|
|
682
|
+
if existing is not None:
|
|
683
|
+
old_title, prev = existing
|
|
684
|
+
if old_title and old_title != chat_title:
|
|
685
|
+
if prev:
|
|
686
|
+
try:
|
|
687
|
+
seen = json.loads(prev)
|
|
688
|
+
if not isinstance(seen, list):
|
|
689
|
+
seen = [seen] if seen else []
|
|
690
|
+
except json.JSONDecodeError:
|
|
691
|
+
seen = [t.strip() for t in prev.split(",") if t.strip()]
|
|
692
|
+
else:
|
|
693
|
+
seen = []
|
|
694
|
+
if old_title not in seen:
|
|
695
|
+
seen.append(old_title)
|
|
696
|
+
updated_prev = json.dumps(seen)
|
|
697
|
+
else:
|
|
698
|
+
updated_prev = prev
|
|
699
|
+
|
|
635
700
|
await db.execute(
|
|
636
|
-
"INSERT INTO chats (chat_id, chat_type, chat_title) VALUES (?, ?, ?) "
|
|
701
|
+
"INSERT INTO chats (chat_id, chat_type, chat_title, previous_titles) VALUES (?, ?, ?, ?) "
|
|
637
702
|
"ON CONFLICT(chat_id) DO UPDATE SET "
|
|
638
703
|
"chat_type = excluded.chat_type, chat_title = excluded.chat_title, "
|
|
704
|
+
"previous_titles = excluded.previous_titles, "
|
|
639
705
|
"updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')",
|
|
640
|
-
(chat_id, chat_type, chat_title),
|
|
706
|
+
(chat_id, chat_type, chat_title, updated_prev),
|
|
641
707
|
)
|
|
642
708
|
await db.commit()
|
|
643
709
|
|
|
@@ -679,9 +745,12 @@ async def search_messages(
|
|
|
679
745
|
|
|
680
746
|
Resolves speaker_query and chat_query using exact-match-first logic:
|
|
681
747
|
1. Case-insensitive exact match - if exactly one entity matches, proceed.
|
|
682
|
-
2. Partial LIKE match - if exactly one accessible
|
|
683
|
-
|
|
684
|
-
|
|
748
|
+
2. Partial LIKE match (including historical names/titles) - if exactly one accessible
|
|
749
|
+
entity matches, proceed. Searches current username/first_name/last_name for speakers
|
|
750
|
+
and current chat_title for chats; falls back to previous_names and previous_titles
|
|
751
|
+
columns to find entities by their past identities. Zero matches: colloquial fallback
|
|
752
|
+
(chat: all accessible; speaker: return empty). Two or more matches: return an
|
|
753
|
+
ambiguity sentinel dict instead of results.
|
|
685
754
|
|
|
686
755
|
Leading `@` is stripped from speaker_query automatically.
|
|
687
756
|
Access control: results are always scoped to accessible_chat_ids (the caller's
|
|
@@ -716,7 +785,7 @@ async def search_messages(
|
|
|
716
785
|
# Resolve speaker_query -> user_ids (exact match first, then LIKE)
|
|
717
786
|
# Scoped to users who have at least one message in accessible_chat_ids (s3kp).
|
|
718
787
|
# Batched over accessible_chat_ids to stay within SQLite's 999-parameter limit;
|
|
719
|
-
# each batch leaves
|
|
788
|
+
# each batch leaves 5 slots for name-matching params (exact: 3, partial: 5).
|
|
720
789
|
speaker_ids: Optional[list[int]] = None
|
|
721
790
|
speaker_amb: Optional[dict] = None
|
|
722
791
|
if speaker_query:
|
|
@@ -746,8 +815,8 @@ async def search_messages(
|
|
|
746
815
|
else:
|
|
747
816
|
like = f"%{q}%"
|
|
748
817
|
partial_seen: dict[int, tuple] = {}
|
|
749
|
-
for i in range(0, len(accessible_chat_ids), _SQLITE_MAX_PARAMS -
|
|
750
|
-
chunk = accessible_chat_ids[i:i + _SQLITE_MAX_PARAMS -
|
|
818
|
+
for i in range(0, len(accessible_chat_ids), _SQLITE_MAX_PARAMS - 5):
|
|
819
|
+
chunk = accessible_chat_ids[i:i + _SQLITE_MAX_PARAMS - 5]
|
|
751
820
|
chunk_ph = ','.join('?' * len(chunk))
|
|
752
821
|
async with db.execute(
|
|
753
822
|
f"SELECT DISTINCT u.user_id, "
|
|
@@ -756,8 +825,8 @@ async def search_messages(
|
|
|
756
825
|
f"FROM users u JOIN messages m ON m.user_id = u.user_id "
|
|
757
826
|
f"WHERE m.chat_id IN ({chunk_ph}) "
|
|
758
827
|
f"AND (u.username LIKE ? OR u.first_name LIKE ? OR u.last_name LIKE ? "
|
|
759
|
-
f"OR (u.first_name || ' ' || u.last_name) LIKE ?)",
|
|
760
|
-
(*chunk, like, like, like, like),
|
|
828
|
+
f"OR (u.first_name || ' ' || u.last_name) LIKE ? OR u.previous_names LIKE ?)",
|
|
829
|
+
(*chunk, like, like, like, like, like),
|
|
761
830
|
) as cur:
|
|
762
831
|
for row in await cur.fetchall():
|
|
763
832
|
partial_seen.setdefault(row[0], row)
|
|
@@ -792,23 +861,23 @@ async def search_messages(
|
|
|
792
861
|
# Check for any global match first to distinguish colloquial fallback
|
|
793
862
|
# (zero global matches) from access-control rejection (global match, not accessible).
|
|
794
863
|
async with db.execute(
|
|
795
|
-
"SELECT 1 FROM chats WHERE chat_title LIKE ? LIMIT 1",
|
|
796
|
-
(like,),
|
|
864
|
+
"SELECT 1 FROM chats WHERE chat_title LIKE ? OR previous_titles LIKE ? LIMIT 1",
|
|
865
|
+
(like, like),
|
|
797
866
|
) as cur:
|
|
798
867
|
has_global = await cur.fetchone() is not None
|
|
799
868
|
if not has_global:
|
|
800
869
|
pass # Zero global matches: colloquial fallback (resolved_chat_ids stays None)
|
|
801
870
|
else:
|
|
802
871
|
# Batch accessible_chat_ids to stay within SQLite's 999-parameter limit;
|
|
803
|
-
# reserve
|
|
872
|
+
# reserve 2 slots for the LIKE parameters (chat_title + previous_titles).
|
|
804
873
|
partial_accessible_seen: dict[int, tuple] = {}
|
|
805
|
-
for i in range(0, len(accessible_chat_ids), _SQLITE_MAX_PARAMS -
|
|
806
|
-
chunk = accessible_chat_ids[i:i + _SQLITE_MAX_PARAMS -
|
|
874
|
+
for i in range(0, len(accessible_chat_ids), _SQLITE_MAX_PARAMS - 2):
|
|
875
|
+
chunk = accessible_chat_ids[i:i + _SQLITE_MAX_PARAMS - 2]
|
|
807
876
|
chunk_ph = ','.join('?' * len(chunk))
|
|
808
877
|
async with db.execute(
|
|
809
878
|
f"SELECT chat_id, chat_title FROM chats "
|
|
810
|
-
f"WHERE chat_title LIKE ? AND chat_id IN ({chunk_ph})",
|
|
811
|
-
(like, *chunk),
|
|
879
|
+
f"WHERE (chat_title LIKE ? OR previous_titles LIKE ?) AND chat_id IN ({chunk_ph})",
|
|
880
|
+
(like, like, *chunk),
|
|
812
881
|
) as cur:
|
|
813
882
|
for row in await cur.fetchall():
|
|
814
883
|
partial_accessible_seen.setdefault(row[0], row)
|
|
@@ -122,6 +122,7 @@ _SYSTEM_APPENDIX = (
|
|
|
122
122
|
"Always invoke the search tool before concluding a message does not exist.\n"
|
|
123
123
|
"Messages marked private are never shared between chats.\n"
|
|
124
124
|
"Never reveal or repeat Telegram user IDs, chat IDs, or any other internal numeric identifiers in your responses.\n"
|
|
125
|
+
"When your context includes a '[Replying to Name, ...]' prefix indicating you were triggered via a reply, acknowledge the original author by name in your response.\n"
|
|
125
126
|
"Current date and time: (not yet known)\n"
|
|
126
127
|
)
|
|
127
128
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TeLLMgramBot
|
|
3
|
-
Version: 3.10.
|
|
3
|
+
Version: 3.10.3
|
|
4
4
|
Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
|
|
5
5
|
Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
|
|
6
6
|
Author: Digital Heresy
|
|
@@ -44,6 +44,7 @@ The basic goal of this project is to create a bridge between a Telegram Bot and
|
|
|
44
44
|
* Ask questions about message history across all your chats using natural language; the bot will search, attribute messages to speakers, and include messages from other bots.
|
|
45
45
|
* Example: "Who said thanks for the breakdown?" or "What did George say about the project?" or "Show me the last few messages."
|
|
46
46
|
* All search filters (speaker, chat, date) are optional. Results are ordered most-recent-first. Configure `search_limit` to control how many results to return (default: 30).
|
|
47
|
+
* Search automatically finds users and chats by their current or past names, so you can reference them however you remember them.
|
|
47
48
|
* Token limits measure conversation length and determine when to prune oldest messages to stay within model limits.
|
|
48
49
|
* The bot loads the user's full history across all chats up to 50% of the token budget. In private chats, shared group context fills the remaining budget, enabling the bot to reference group conversations from a private context.
|
|
49
50
|
* This eliminates amnesia when switching between private and group chats.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|