diff --git a/mautrix_telegram/portal.py b/mautrix_telegram/portal.py
index dd2d697b..96ce8369 100644
--- a/mautrix_telegram/portal.py
+++ b/mautrix_telegram/portal.py
@@ -2319,8 +2319,7 @@ class Portal(DBPortal, BasePortal):
existing_reacts = await DBReaction.get_by_sender(msg.mxid, msg.mx_room, user.tgid)
new_tg_reactions: list[TypeReaction] = []
reactions_to_remove: list[DBReaction] = []
- # TODO use config https://corefork.telegram.org/api/config#reactions-user-max-default
- max_reactions = 3 if user.is_premium else 1
+ max_reactions = await user.get_max_reactions()
max_reactions -= 1 # Leave one reaction of space for the new reaction
for db_reaction in existing_reacts:
if db_reaction.reaction == emoji_id:
@@ -2862,12 +2861,12 @@ class Portal(DBPortal, BasePortal):
return False
@staticmethod
- async def _get_reaction_limit(sender: TelegramID) -> int:
+ async def _get_reaction_limit(source: au.AbstractUser, sender: TelegramID) -> int:
puppet = await p.Puppet.get_by_tgid(sender, create=False)
- # TODO use config https://corefork.telegram.org/api/config#reactions-user-max-default
- if puppet and puppet.is_premium:
- return 3
- return 1
+ is_premium = puppet and puppet.is_premium
+ if isinstance(source, u.User) and not source.is_bot:
+ return await source.get_max_reactions(is_premium)
+ return 3 if is_premium else 1
async def _handle_telegram_reactions_locked(
self,
@@ -2903,7 +2902,7 @@ class Portal(DBPortal, BasePortal):
else:
if is_full or (
new_reactions is not None
- and len(new_reactions) == await self._get_reaction_limit(sender_id)
+ and len(new_reactions) == await self._get_reaction_limit(source, sender_id)
):
removed.append(existing_reaction)
# else: assume the reaction is still there, too much effort to fetch it
diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py
index d0f9f689..c03d8cfd 100644
--- a/mautrix_telegram/user.py
+++ b/mautrix_telegram/user.py
@@ -15,7 +15,7 @@
# along with this program. If not, see .
from __future__ import annotations
-from typing import TYPE_CHECKING, AsyncGenerator, AsyncIterable, Awaitable, NamedTuple, cast
+from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterable, Awaitable, NamedTuple, cast
from datetime import datetime, timezone
import asyncio
import time
@@ -24,6 +24,7 @@ from telethon.errors import AuthKeyDuplicatedError, RPCError, UnauthorizedError
from telethon.tl.custom import Dialog
from telethon.tl.functions.account import UpdateStatusRequest
from telethon.tl.functions.contacts import GetContactsRequest, SearchRequest
+from telethon.tl.functions.help import GetAppConfigRequest
from telethon.tl.functions.messages import GetAvailableReactionsRequest
from telethon.tl.functions.updates import GetStateRequest
from telethon.tl.functions.users import GetUsersRequest
@@ -54,7 +55,7 @@ from mautrix.types import PushActionType, PushRuleKind, PushRuleScope, RoomID, R
from mautrix.util.bridge_state import BridgeState, BridgeStateEvent
from mautrix.util.opt_prometheus import Gauge
-from . import portal as po, puppet as pu
+from . import portal as po, puppet as pu, util
from .abstract_user import AbstractUser
from .db import Message as DBMessage, PgSession, User as DBUser
from .types import TelegramID
@@ -91,6 +92,7 @@ class User(DBUser, AbstractUser, BaseUser):
_available_emoji_reactions_hash: int | None
_available_emoji_reactions_fetched: float
_available_emoji_reactions_lock: asyncio.Lock
+ _app_config: dict[str, Any] | None
def __init__(
self,
@@ -121,6 +123,7 @@ class User(DBUser, AbstractUser, BaseUser):
self._available_emoji_reactions_hash = None
self._available_emoji_reactions_fetched = 0
self._available_emoji_reactions_lock = asyncio.Lock()
+ self._app_config = None
(
self.relaybot_whitelisted,
@@ -734,6 +737,25 @@ class User(DBUser, AbstractUser, BaseUser):
)
return self._available_emoji_reactions
+ def tl_to_json(self) -> Any:
+ pass
+
+ async def get_app_config(self) -> dict[str, Any]:
+ if not self._app_config:
+ cfg = await self.client(GetAppConfigRequest())
+ self._app_config = util.parse_tl_json(cfg)
+ return self._app_config
+
+ async def get_max_reactions(self, is_premium: bool | None = None) -> int:
+ if is_premium is None:
+ is_premium = self.is_premium
+ cfg = await self.get_app_config()
+ return (
+ cfg.get("reactions_user_max_premium", 3)
+ if is_premium
+ else cfg.get("reactions_user_max_default", 1)
+ )
+
# endregion
# region Class instance lookup
diff --git a/mautrix_telegram/util/__init__.py b/mautrix_telegram/util/__init__.py
index 08c390d9..eb5a7e21 100644
--- a/mautrix_telegram/util/__init__.py
+++ b/mautrix_telegram/util/__init__.py
@@ -8,3 +8,4 @@ from .file_transfer import (
)
from .parallel_file_transfer import parallel_transfer_to_telegram
from .recursive_dict import recursive_del, recursive_get, recursive_set
+from .tl_json import parse_tl_json
diff --git a/mautrix_telegram/util/tl_json.py b/mautrix_telegram/util/tl_json.py
new file mode 100644
index 00000000..b99b26d8
--- /dev/null
+++ b/mautrix_telegram/util/tl_json.py
@@ -0,0 +1,39 @@
+# mautrix-telegram - A Matrix-Telegram puppeting bridge
+# Copyright (C) 2022 Tulir Asokan
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+from telethon.tl.types import (
+ JsonArray,
+ JsonBool,
+ JsonNull,
+ JsonNumber,
+ JsonObject,
+ JsonObjectValue,
+ JsonString,
+ TypeJSONValue,
+)
+
+from mautrix.types import JSON
+
+
+def parse_tl_json(val: TypeJSONValue) -> JSON:
+ if isinstance(val, JsonObject):
+ return {entry.key: parse_tl_json(entry.value) for entry in val.value}
+ elif isinstance(val, JsonArray):
+ return [parse_tl_json(item) for item in val.value]
+ elif isinstance(val, (JsonBool, JsonNumber, JsonString)):
+ return val.value
+ elif isinstance(val, JsonNull):
+ return None
+ raise ValueError(f"Unsupported type {type(val)} in TL JSON object")