From 846e4884e72f115570a1d12967eb9eb6eff8b972 Mon Sep 17 00:00:00 2001 From: Tulir Asokan <tulir@maunium.net> Date: Sat, 3 Aug 2019 01:43:07 +0300 Subject: [PATCH] Add bidirectional reaction bridging --- ROADMAP.md | 3 +- .../1a1ea46dc3e1_add_reaction_table.py | 37 +++++ mautrix_facebook/db/__init__.py | 3 +- mautrix_facebook/db/message.py | 3 +- mautrix_facebook/db/reaction.py | 63 ++++++++ mautrix_facebook/matrix.py | 24 +++- mautrix_facebook/portal.py | 136 +++++++++++++++--- mautrix_facebook/user.py | 120 +++++++++------- 8 files changed, 316 insertions(+), 73 deletions(-) create mode 100644 alembic/versions/1a1ea46dc3e1_add_reaction_table.py create mode 100644 mautrix_facebook/db/reaction.py diff --git a/ROADMAP.md b/ROADMAP.md index a4bbc41..ace6436 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -14,6 +14,7 @@ * [ ] Replies * [ ] Mentions * [x] Message redactions + * [x] Message reactions * [x] Presence * [x] Typing notifications * [x] Read receipts @@ -40,7 +41,7 @@ * [ ] Mentions * [ ] Polls * [x] Message unsend - * [ ] Message reactions (not yet implemented in Matrix) + * [x] Message reactions * [ ] Message history * [x] Presence * [ ] Typing notifications diff --git a/alembic/versions/1a1ea46dc3e1_add_reaction_table.py b/alembic/versions/1a1ea46dc3e1_add_reaction_table.py new file mode 100644 index 0000000..c647352 --- /dev/null +++ b/alembic/versions/1a1ea46dc3e1_add_reaction_table.py @@ -0,0 +1,37 @@ +"""Add reaction table + +Revision ID: 1a1ea46dc3e1 +Revises: 26341fb32054 +Create Date: 2019-08-03 00:40:21.380240 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1a1ea46dc3e1' +down_revision = '26341fb32054' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('reaction', + sa.Column('mxid', sa.String(length=255), nullable=False), + sa.Column('mx_room', sa.String(length=255), nullable=False), + sa.Column('fb_msgid', sa.String(length=127), nullable=False), + sa.Column('fb_receiver', sa.String(length=127), nullable=False), + sa.Column('fb_sender', sa.String(length=127), nullable=False), + sa.Column('reaction', sa.String(length=1), nullable=False), + sa.PrimaryKeyConstraint('fb_msgid', 'fb_receiver', 'fb_sender'), + sa.UniqueConstraint('mxid', 'mx_room', name='_mx_react_id_room') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('reaction') + # ### end Alembic commands ### diff --git a/mautrix_facebook/db/__init__.py b/mautrix_facebook/db/__init__.py index b7f7ad7..7026c52 100644 --- a/mautrix_facebook/db/__init__.py +++ b/mautrix_facebook/db/__init__.py @@ -1,13 +1,14 @@ from mautrix.bridge.db import RoomState, UserProfile from .message import Message +from .reaction import Reaction from .portal import Portal from .puppet import Puppet from .user import User def init(db_engine) -> None: - for table in Portal, Message, User, Puppet, UserProfile, RoomState: + for table in Portal, Message, Reaction, User, Puppet, UserProfile, RoomState: table.db = db_engine table.t = table.__table__ table.c = table.t.c diff --git a/mautrix_facebook/db/message.py b/mautrix_facebook/db/message.py index 57c04c3..181d828 100644 --- a/mautrix_facebook/db/message.py +++ b/mautrix_facebook/db/message.py @@ -20,7 +20,6 @@ from sqlalchemy.engine.result import RowProxy from sqlalchemy.sql.expression import ClauseElement from mautrix.types import RoomID, EventID - from mautrix.bridge.db.base import Base @@ -45,7 +44,7 @@ class Message(Base): return cls._select_all(cls.c.fbid == fbid, cls.c.fb_receiver == fb_receiver) @classmethod - def get_by_fbid(cls, fbid: str, fb_receiver: str, index: int) -> Optional['Message']: + def get_by_fbid(cls, fbid: str, fb_receiver: str, index: int = 0) -> Optional['Message']: return cls._select_one_or_none(and_(cls.c.fbid == fbid, cls.c.fb_receiver == fb_receiver, cls.c.index == index)) diff --git a/mautrix_facebook/db/reaction.py b/mautrix_facebook/db/reaction.py new file mode 100644 index 0000000..b266450 --- /dev/null +++ b/mautrix_facebook/db/reaction.py @@ -0,0 +1,63 @@ +# mautrix-facebook - A Matrix-Facebook Messenger puppeting bridge +# Copyright (C) 2019 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 <https://www.gnu.org/licenses/>. +from typing import Optional, Iterable + +from sqlalchemy import Column, String, UniqueConstraint, and_ +from sqlalchemy.engine.result import RowProxy +from sqlalchemy.sql.expression import ClauseElement + +from mautrix.types import RoomID, EventID +from mautrix.bridge.db.base import Base + + +class Reaction(Base): + __tablename__ = "reaction" + + mxid: EventID = Column(String(255), nullable=False) + mx_room: RoomID = Column(String(255), nullable=False) + fb_msgid: str = Column(String(127), primary_key=True) + fb_receiver: str = Column(String(127), primary_key=True) + fb_sender: str = Column(String(127), primary_key=True) + reaction: str = Column(String(1), nullable=False) + + __table_args__ = (UniqueConstraint("mxid", "mx_room", name="_mx_react_id_room"),) + + @classmethod + def scan(cls, row: RowProxy) -> 'Reaction': + mxid, mx_room, fb_msgid, fb_receiver, fb_sender, reaction = row + return cls(mxid=mxid, mx_room=mx_room, fb_msgid=fb_msgid, fb_receiver=fb_receiver, + fb_sender=fb_sender, reaction=reaction) + + @classmethod + def get_by_fbid(cls, fb_msgid: str, fb_receiver: str, fb_sender: str) -> Optional['Reaction']: + return cls._select_one_or_none(and_(cls.c.fb_msgid == fb_msgid, + cls.c.fb_receiver == fb_receiver, + cls.c.fb_sender == fb_sender)) + + @classmethod + def get_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> Optional['Reaction']: + return cls._select_one_or_none(and_(cls.c.mxid == mxid, cls.c.mx_room == mx_room)) + + @property + def _edit_identity(self) -> ClauseElement: + return and_(self.c.fb_msgid == self.fb_msgid, self.c.fb_receiver == self.fb_receiver, + self.c.fb_sender == self.fb_sender) + + def insert(self) -> None: + with self.db.begin() as conn: + conn.execute(self.t.insert().values( + mxid=self.mxid, mx_room=self.mx_room, fb_msgid=self.fb_msgid, + fb_receiver=self.fb_receiver, fb_sender=self.fb_sender, reaction=self.reaction)) diff --git a/mautrix_facebook/matrix.py b/mautrix_facebook/matrix.py index b2c94fa..1102976 100644 --- a/mautrix_facebook/matrix.py +++ b/mautrix_facebook/matrix.py @@ -17,7 +17,8 @@ from typing import List, TYPE_CHECKING from fbchat.models import ThreadType from mautrix.types import (EventID, RoomID, UserID, Event, EventType, MessageEvent, StateEvent, - RedactionEvent, PresenceEventContent, ReceiptEvent, PresenceState) + RedactionEvent, PresenceEventContent, ReceiptEvent, PresenceState, + ReactionEvent, ReactionEventContent, RelationType) from mautrix.errors import MatrixError from mautrix.bridge import BaseMatrixHandler @@ -139,6 +140,24 @@ class MatrixHandler(BaseMatrixHandler): await portal.handle_matrix_redaction(user, event_id) + @classmethod + async def handle_reaction(cls, room_id: RoomID, user_id: UserID, event_id: EventID, + content: ReactionEventContent) -> None: + if content.relates_to.rel_type != RelationType.ANNOTATION: + cls.log.debug(f"Ignoring m.reaction event in {room_id} from {user_id} with unexpected " + f"relation type {content.relates_to.rel_type}") + return + user = u.User.get_by_mxid(user_id) + if not user: + return + + portal = po.Portal.get_by_mxid(room_id) + if not portal: + return + + await portal.handle_matrix_reaction(user, event_id, content.relates_to.event_id, + content.relates_to.key) + async def handle_presence(self, user_id: UserID, info: PresenceEventContent) -> None: if not self.config["bridge.presence"]: return @@ -183,6 +202,9 @@ class MatrixHandler(BaseMatrixHandler): if evt.type == EventType.ROOM_REDACTION: evt: RedactionEvent await self.handle_redaction(evt.room_id, evt.sender, evt.redacts) + elif evt.type == EventType.REACTION: + evt: ReactionEvent + await self.handle_reaction(evt.room_id, evt.sender, evt.event_id, evt.content) elif evt.type == EventType.PRESENCE: await self.handle_presence(evt.sender, evt.content) elif evt.type == EventType.TYPING: diff --git a/mautrix_facebook/portal.py b/mautrix_facebook/portal.py index 0f0b441..cf79842 100644 --- a/mautrix_facebook/portal.py +++ b/mautrix_facebook/portal.py @@ -25,7 +25,7 @@ import magic from fbchat.models import (ThreadType, Thread, User as FBUser, Group as FBGroup, Page as FBPage, Message as FBMessage, Sticker as FBSticker, AudioAttachment, VideoAttachment, FileAttachment, ImageAttachment, LocationAttachment, - ShareAttachment, TypingStatus) + ShareAttachment, TypingStatus, MessageReaction) from mautrix.types import (RoomID, EventType, ContentURI, MessageEventContent, EventID, ImageInfo, MessageType, LocationMessageEventContent, LocationInfo, ThumbnailInfo, FileInfo, AudioInfo, VideoInfo, Format, @@ -34,7 +34,7 @@ from mautrix.appservice import AppService, IntentAPI from mautrix.errors import MForbidden, IntentError, MatrixError from .config import Config -from .db import Portal as DBPortal, Message as DBMessage +from .db import Portal as DBPortal, Message as DBMessage, Reaction as DBReaction from . import puppet as p, user as u if TYPE_CHECKING: @@ -76,7 +76,7 @@ class Portal: _main_intent: Optional[IntentAPI] _create_room_lock: asyncio.Lock _last_bridged_mxid: Optional[EventID] - _dedup: Deque[Tuple[str, str]] + _dedup: Deque[str] _avatar_uri: Optional[ContentURI] _send_locks: Dict[str, asyncio.Lock] _noop_lock: FakeLock = FakeLock() @@ -376,14 +376,56 @@ class Portal: async def handle_matrix_redaction(self, sender: 'u.User', event_id: EventID) -> None: if not self.mxid: return + message = DBMessage.get_by_mxid(event_id, self.mxid) - if not message: + if message: + try: + message.delete() + await sender.unsend(message.fbid) + except Exception: + self.log.exception("Unsend failed") return - try: - message.delete() - await sender.unsend(message.fbid) - except Exception: - self.log.exception("Unsend failed") + + reaction = DBReaction.get_by_mxid(event_id, self.mxid) + if reaction: + try: + reaction.delete() + await sender.reactToMessage(reaction.fb_msgid, reaction=None) + except Exception: + self.log.exception("Removing reaction failed") + + _matrix_to_facebook_reaction = { + "â¤": MessageReaction.HEART, + "â¤ï¸": MessageReaction.HEART, + "ðŸ˜": MessageReaction.LOVE, + "😆": MessageReaction.SMILE, + "😮": MessageReaction.WOW, + "😢": MessageReaction.SAD, + "😠": MessageReaction.ANGRY, + "ðŸ‘": MessageReaction.YES, + "👎": MessageReaction.NO + } + + async def handle_matrix_reaction(self, sender: 'u.User', event_id: EventID, + reacting_to: EventID, raw_reaction: str) -> None: + async with self.require_send_lock(sender.uid): + try: + reaction = self._matrix_to_facebook_reaction[raw_reaction] + except KeyError: + return + + message = DBMessage.get_by_mxid(reacting_to, self.mxid) + if not message: + self.log.debug(f"Ignoring reaction to unknown event {reacting_to}") + return + + existing = DBReaction.get_by_fbid(message.fbid, self.fb_receiver, sender.uid) + if existing and existing.reaction == reaction.value: + return + + await sender.reactToMessage(message.fbid, reaction) + await self._upsert_reaction(existing, self.main_intent, event_id, message, sender, + reaction.value) async def handle_matrix_leave(self, user: 'u.User') -> None: if self.is_direct: @@ -404,6 +446,16 @@ class Portal: # endregion # region Facebook event handling + async def _bridge_own_message_pm(self, source: 'u.User', sender: 'p.Puppet', mid: str) -> bool: + if self.is_direct and sender.fbid == source.uid and not sender.is_real_user: + if self.invite_own_puppet_to_pm: + await self.main_intent.invite_user(self.mxid, sender.mxid) + elif self.az.state_store.get_membership(self.mxid, sender.mxid) != Membership.JOIN: + self.log.warn(f"Ignoring own {mid} in private chat because own puppet is not in" + " room.") + return False + return True + async def handle_facebook_message(self, source: 'u.User', sender: 'p.Puppet', message: FBMessage) -> None: async with self.optional_send_lock(sender.fbid): @@ -416,13 +468,8 @@ class Portal: if not mxid: # Failed to create return - if self.is_direct and sender.fbid == source.uid and not sender.is_real_user: - if self.invite_own_puppet_to_pm: - await self.main_intent.invite_user(self.mxid, sender.mxid) - elif self.az.state_store.get_membership(self.mxid, sender.mxid) != Membership.JOIN: - self.log.warn(f"Ignoring own message {message.uid} in private chat because own" - " puppet is not in room.") - return + if not await self._bridge_own_message_pm(source, sender, f"message {message.uid}"): + return intent = sender.intent_for(self) event_ids = [] if message.sticker: @@ -465,7 +512,8 @@ class Portal: # elif isinstance(attachment, VideoAttachment): # TODO elif isinstance(attachment, FileAttachment): - mxc, mime, size = await self._reupload_fb_photo(attachment.url, intent, attachment.name) + mxc, mime, size = await self._reupload_fb_photo(attachment.url, intent, + attachment.name) event_id = await intent.send_file(self.mxid, mxc, info=FileInfo(size=size, mimetype=mime), file_name=attachment.name) @@ -568,6 +616,60 @@ class Portal: fbid=message_id, fb_receiver=self.fb_receiver, index=0).insert() + async def handle_facebook_reaction_add(self, source: 'u.User', sender: 'p.Puppet', + message_id: str, reaction: str) -> None: + dedup_id = f"react_{message_id}_{sender}_{reaction}" + async with self.optional_send_lock(sender.fbid): + if dedup_id in self._dedup: + return + self._dedup.appendleft(dedup_id) + + existing = DBReaction.get_by_fbid(message_id, self.fb_receiver, sender.fbid) + if existing and existing.reaction == reaction: + return + + if not await self._bridge_own_message_pm(source, sender, f"reaction to {message_id}"): + return + + intent = sender.intent_for(self) + + message = DBMessage.get_by_fbid(message_id, self.fb_receiver) + if not message: + self.log.debug(f"Ignoring reaction to unknown message {message}") + return + + mxid = await intent.react(message.mx_room, message.mxid, reaction) + self.log.debug(f"Reacted to {message.mxid}, got {mxid}") + + await self._upsert_reaction(existing, intent, mxid, message, sender, reaction) + + async def _upsert_reaction(self, existing: DBReaction, intent: IntentAPI, mxid: EventID, + message: DBMessage, sender: Union['u.User', 'p.Puppet'], + reaction: str) -> None: + if existing: + self.log.debug(f"_upsert_reaction redacting {existing.mxid} and inserting {mxid}" + f" (message: {message.mxid})") + await intent.redact(existing.mx_room, existing.mxid) + existing.edit(reaction=reaction, mxid=mxid, mx_room=message.mx_room) + else: + self.log.debug(f"_upsert_reaction inserting {mxid} (message: {message.mxid})") + DBReaction(mxid=mxid, mx_room=message.mx_room, fb_msgid=message.fbid, + fb_receiver=self.fb_receiver, fb_sender=sender.fbid, + reaction=reaction).insert() + + async def handle_facebook_reaction_remove(self, source: 'u.User', sender: 'p.Puppet', + message_id: str) -> None: + if not self.mxid: + return + reaction = DBReaction.get_by_fbid(message_id, self.fb_receiver, sender.fbid) + if reaction: + self.log.debug(f"redacting {reaction.mxid}") + try: + await sender.intent_for(self).redact(reaction.mx_room, reaction.mxid) + except MForbidden: + await self.main_intent.redact(reaction.mx_room, reaction.mxid) + reaction.delete() + # endregion # region Getters diff --git a/mautrix_facebook/user.py b/mautrix_facebook/user.py index 5cc20f2..3aee460 100644 --- a/mautrix_facebook/user.py +++ b/mautrix_facebook/user.py @@ -19,7 +19,7 @@ import asyncio import logging from fbchat import Client -from fbchat.models import Message, ThreadType, User as FBUser, ActiveStatus +from fbchat.models import Message, ThreadType, User as FBUser, ActiveStatus, MessageReaction from mautrix.types import UserID, PresenceState from mautrix.appservice import AppService @@ -66,6 +66,10 @@ class User(Client): # region Sessions + @property + def fbid(self) -> str: + return self.uid + @property def db_instance(self) -> DBUser: if not self._db_instance: @@ -176,7 +180,7 @@ class User(Client): self.log.warn("Unexpected on2FACode call") # raise RuntimeError("No ongoing login command") - async def onLoggedIn(self, email=None) -> None: + async def onLoggedIn(self, email: str = None) -> None: """ Called when the client is successfully logged in @@ -235,7 +239,8 @@ class User(Client): await portal.handle_facebook_message(self, puppet, message_object) async def onColorChange(self, mid=None, author_id=None, new_color=None, thread_id=None, - thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): + thread_type=ThreadType.USER, ts=None, metadata=None, msg=None + ) -> None: """ Called when the client is listening, and somebody changes a thread's color @@ -257,7 +262,8 @@ class User(Client): ) async def onEmojiChange(self, mid=None, author_id=None, new_emoji=None, thread_id=None, - thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): + thread_type=ThreadType.USER, ts=None, metadata=None, msg=None + ) -> None: """ Called when the client is listening, and somebody changes a thread's emoji @@ -278,7 +284,8 @@ class User(Client): ) async def onTitleChange(self, mid=None, author_id=None, new_title=None, thread_id=None, - thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): + thread_type=ThreadType.USER, ts=None, metadata=None, msg=None + ) -> None: """ Called when the client is listening, and somebody changes the title of a thread @@ -301,8 +308,9 @@ class User(Client): return await portal.handle_facebook_name(self, sender, new_title, mid) - async def onImageChange(self, mid=None, author_id=None, new_image=None, thread_id=None, - thread_type=ThreadType.GROUP, ts=None, msg=None): + async def onImageChange(self, mid: str = None, author_id: str = None, new_image: str = None, + thread_id: str = None, thread_type: ThreadType = ThreadType.GROUP, + ts: int = None, msg: Any = None) -> None: """ Called when the client is listening, and somebody changes the image of a thread @@ -326,7 +334,7 @@ class User(Client): async def onNicknameChange(self, mid=None, author_id=None, changed_for=None, new_nickname=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, - msg=None): + msg=None) -> None: """ Called when the client is listening, and somebody changes the nickname of a person @@ -348,7 +356,7 @@ class User(Client): ) async def onAdminAdded(self, mid=None, added_id=None, author_id=None, thread_id=None, - thread_type=ThreadType.GROUP, ts=None, msg=None): + thread_type=ThreadType.GROUP, ts=None, msg=None) -> None: """ Called when the client is listening, and somebody adds an admin to a group thread @@ -356,6 +364,7 @@ class User(Client): :param added_id: The ID of the admin who got added :param author_id: The ID of the person who added the admins :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` :param ts: A timestamp of the action :param msg: A full set of the data recieved """ @@ -376,7 +385,8 @@ class User(Client): self.log.info("{} removed admin: {} in {}".format(author_id, removed_id, thread_id)) async def onApprovalModeChange(self, mid=None, approval_mode=None, author_id=None, - thread_id=None, thread_type=ThreadType.GROUP, ts=None, msg=None): + thread_id=None, thread_type=ThreadType.GROUP, ts=None, + msg=None) -> None: """ Called when the client is listening, and somebody changes approval mode in a group thread @@ -413,7 +423,8 @@ class User(Client): await portal.handle_facebook_seen(self, puppet) async def onMessageDelivered(self, msg_ids=None, delivered_for=None, thread_id=None, - thread_type=ThreadType.USER, ts=None, metadata=None, msg=None): + thread_type=ThreadType.USER, ts=None, metadata=None, msg=None + ) -> None: """ Called when the client is listening, and somebody marks messages as delivered @@ -432,7 +443,8 @@ class User(Client): ) ) - async def onMarkedSeen(self, threads=None, seen_ts=None, ts=None, metadata=None, msg=None): + async def onMarkedSeen(self, threads=None, seen_ts=None, ts=None, metadata=None, msg=None + ) -> None: """ Called when the client is listening, and the client has successfully marked threads as seen @@ -469,8 +481,8 @@ class User(Client): puppet = pu.Puppet.get_by_fbid(author_id) await portal.handle_facebook_unsend(self, puppet, mid) - async def onPeopleAdded(self, mid=None, added_ids=None, author_id=None, thread_id=None, ts=None, - msg=None): + async def onPeopleAdded(self, mid=None, added_ids=None, author_id=None, thread_id=None, + ts=None, msg=None) -> None: """ Called when the client is listening, and somebody adds people to a group thread @@ -486,7 +498,7 @@ class User(Client): ) async def onPersonRemoved(self, mid=None, removed_id=None, author_id=None, thread_id=None, - ts=None, msg=None): + ts=None, msg=None) -> None: """ Called when the client is listening, and somebody removes a person from a group thread @@ -499,7 +511,7 @@ class User(Client): """ self.log.info("{} removed: {} in {}".format(author_id, removed_id, thread_id)) - async def onFriendRequest(self, from_id=None, msg=None): + async def onFriendRequest(self, from_id=None, msg=None) -> None: """ Called when the client is listening, and somebody sends a friend request @@ -508,7 +520,7 @@ class User(Client): """ self.log.info("Friend request from {}".format(from_id)) - async def onInbox(self, unseen=None, unread=None, recent_unread=None, msg=None): + async def onInbox(self, unseen=None, unread=None, recent_unread=None, msg=None) -> None: """ .. todo:: Documenting this @@ -521,7 +533,7 @@ class User(Client): self.log.info("Inbox event: {}, {}, {}".format(unseen, unread, recent_unread)) async def onTyping(self, author_id=None, status=None, thread_id=None, thread_type=None, - msg=None): + msg=None) -> None: """ Called when the client is listening, and somebody starts or stops typing into a chat @@ -535,9 +547,9 @@ class User(Client): """ self.log.info(f"User is typing: {author_id} {status} in {thread_id} {thread_type}") - async def onGamePlayed(self, mid=None, author_id=None, game_id=None, game_name=None, score=None, - leaderboard=None, thread_id=None, thread_type=None, ts=None, - metadata=None, msg=None): + async def onGamePlayed(self, mid=None, author_id=None, game_id=None, game_name=None, + score=None, leaderboard=None, thread_id=None, thread_type=None, ts=None, + metadata=None, msg=None) -> None: """ Called when the client is listening, and somebody plays a game @@ -560,14 +572,15 @@ class User(Client): ) ) - async def onReactionAdded(self, mid=None, reaction=None, author_id=None, thread_id=None, - thread_type=None, ts=None, msg=None): + async def onReactionAdded(self, mid: str = None, reaction: MessageReaction = None, + author_id: str = None, thread_id: str = None, + thread_type: ThreadType = None, ts: int = None, msg: Any = None + ) -> None: """ Called when the client is listening, and somebody reacts to a message :param mid: Message ID, that user reacted to :param reaction: Reaction - :param add_reaction: Whether user added or removed reaction :param author_id: The ID of the person who reacted to the message :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` @@ -576,14 +589,16 @@ class User(Client): :type reaction: models.MessageReaction :type thread_type: models.ThreadType """ - self.log.info( - "{} reacted to message {} with {} in {} ({})".format( - author_id, mid, reaction.name, thread_id, thread_type.name - ) - ) + self.log.debug(f"onReactionAdded({mid}, {reaction}, {author_id}, {thread_id}, " + f"{thread_type})") + fb_receiver = self.uid if thread_type == ThreadType.USER else None + portal = po.Portal.get_by_fbid(thread_id, fb_receiver, thread_type) + puppet = pu.Puppet.get_by_fbid(author_id) + await portal.handle_facebook_reaction_add(self, puppet, mid, reaction.value) - async def onReactionRemoved(self, mid=None, author_id=None, thread_id=None, thread_type=None, - ts=None, msg=None): + async def onReactionRemoved(self, mid: str = None, author_id: str = None, + thread_id: str = None, thread_type: ThreadType = None, + ts: int = None, msg: Any = None) -> None: """ Called when the client is listening, and somebody removes reaction from a message @@ -595,13 +610,14 @@ class User(Client): :param msg: A full set of the data recieved :type thread_type: models.ThreadType """ - self.log.info( - "{} removed reaction from {} message in {} ({})".format( - author_id, mid, thread_id, thread_type - ) - ) + self.log.debug(f"onReactionRemoved({mid}, {author_id}, {thread_id}, {thread_type})") + fb_receiver = self.uid if thread_type == ThreadType.USER else None + portal = po.Portal.get_by_fbid(thread_id, fb_receiver, thread_type) + puppet = pu.Puppet.get_by_fbid(author_id) + await portal.handle_facebook_reaction_remove(self, puppet, mid) - async def onBlock(self, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + async def onBlock(self, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None + ) -> None: """ Called when the client is listening, and somebody blocks client @@ -616,7 +632,8 @@ class User(Client): "{} blocked {} ({}) thread".format(author_id, thread_id, thread_type.name) ) - async def onUnblock(self, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + async def onUnblock(self, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None + ) -> None: """ Called when the client is listening, and somebody blocks client @@ -632,7 +649,7 @@ class User(Client): ) async def onLiveLocation(self, mid=None, location=None, author_id=None, thread_id=None, - thread_type=None, ts=None, msg=None, ): + thread_type=None, ts=None, msg=None) -> None: """ Called when the client is listening and somebody sends live location info @@ -653,7 +670,7 @@ class User(Client): ) async def onCallStarted(self, mid=None, caller_id=None, is_video_call=None, thread_id=None, - thread_type=None, ts=None, metadata=None, msg=None): + thread_type=None, ts=None, metadata=None, msg=None) -> None: """ .. todo:: Make this work with private calls @@ -675,7 +692,8 @@ class User(Client): ) async def onCallEnded(self, mid=None, caller_id=None, is_video_call=None, call_duration=None, - thread_id=None, thread_type=None, ts=None, metadata=None, msg=None): + thread_id=None, thread_type=None, ts=None, metadata=None, msg=None + ) -> None: """ .. todo:: Make this work with private calls @@ -698,7 +716,7 @@ class User(Client): ) async def onUserJoinedCall(self, mid=None, joined_id=None, is_video_call=None, thread_id=None, - thread_type=None, ts=None, metadata=None, msg=None): + thread_type=None, ts=None, metadata=None, msg=None) -> None: """ Called when the client is listening, and somebody joins a group call @@ -717,7 +735,7 @@ class User(Client): ) async def onPollCreated(self, mid=None, poll=None, author_id=None, thread_id=None, - thread_type=None, ts=None, metadata=None, msg=None): + thread_type=None, ts=None, metadata=None, msg=None) -> None: """ Called when the client is listening, and somebody creates a group poll @@ -740,7 +758,7 @@ class User(Client): async def onPollVoted(self, mid=None, poll=None, added_options=None, removed_options=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, - msg=None): + msg=None) -> None: """ Called when the client is listening, and somebody votes in a group poll @@ -762,7 +780,7 @@ class User(Client): ) async def onPlanCreated(self, mid=None, plan=None, author_id=None, thread_id=None, - thread_type=None, ts=None, metadata=None, msg=None): + thread_type=None, ts=None, metadata=None, msg=None) -> None: """ Called when the client is listening, and somebody creates a plan @@ -803,7 +821,7 @@ class User(Client): ) async def onPlanEdited(self, mid=None, plan=None, author_id=None, thread_id=None, - thread_type=None, ts=None, metadata=None, msg=None): + thread_type=None, ts=None, metadata=None, msg=None) -> None: """ Called when the client is listening, and somebody edits a plan @@ -825,7 +843,7 @@ class User(Client): ) async def onPlanDeleted(self, mid=None, plan=None, author_id=None, thread_id=None, - thread_type=None, ts=None, metadata=None, msg=None): + thread_type=None, ts=None, metadata=None, msg=None) -> None: """ Called when the client is listening, and somebody deletes a plan @@ -848,7 +866,7 @@ class User(Client): async def onPlanParticipation(self, mid=None, plan=None, take_part=None, author_id=None, thread_id=None, thread_type=None, ts=None, metadata=None, - msg=None): + msg=None) -> None: """ Called when the client is listening, and somebody takes part in a plan or not @@ -878,7 +896,7 @@ class User(Client): ) ) - async def onQprimer(self, ts=None, msg=None): + async def onQprimer(self, ts=None, msg=None) -> None: """ Called when the client just started listening @@ -913,7 +931,7 @@ class User(Client): """ await self.onChatTimestamp(statuses, msg) - async def onUnknownMesssageType(self, msg=None): + async def onUnknownMesssageType(self, msg: Any = None) -> None: """ Called when the client is listening, and some unknown data was recieved @@ -921,7 +939,7 @@ class User(Client): """ self.log.debug("Unknown message received: {}".format(msg)) - async def onMessageError(self, exception=None, msg=None): + async def onMessageError(self, exception: Exception = None, msg: Any = None) -> None: """ Called when an error was encountered while parsing recieved data -- GitLab