diff --git a/docs/intro.rst b/docs/intro.rst index 39a17fd..2749607 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -134,17 +134,18 @@ Listening & Events Now, we are finally at the point we have all been waiting for: Creating an automatic Facebook bot! -To get started, you create your methods that will handle your events:: +To get started, you create a listener object:: - def on_message(event): + listener = fbchat.Listener(session=session, chat_on=False, foreground=False) + +The you use that to register methods that will handle your events:: + + @listener.register + def on_message(event: fbchat.MessageEvent): print(f"Message from {event.author.id}: {event.message.text}") -And then you create a listener object, and start handling the incoming events:: +And then you start handling the incoming events:: - listener = fbchat.Listener.connect(session, False, False) - - for event in listener.listen(): - if isinstance(event, fbchat.MessageEvent): - on_message(event) + listener.run() View the :ref:`examples` to see some more examples illustrating the event system. diff --git a/examples/echobot.py b/examples/echobot.py index d196621..212e291 100644 --- a/examples/echobot.py +++ b/examples/echobot.py @@ -1,17 +1,15 @@ import fbchat session = fbchat.Session.login("", "") - -listener = fbchat.Listener.connect(session, chat_on=False, foreground=False) +listener = fbchat.Listener(session=session, chat_on=False, foreground=False) -def on_message(event): +@listener.register +def on_message(event: fbchat.MessageEvent): print(f"{event.message.text} from {event.author.id} in {event.thread.id}") # If you're not the author, echo if event.author.id != session.user.id: event.thread.send_text(event.message.text) -for event in listener.listen(): - if isinstance(event, fbchat.MessageEvent): - on_message(event) +listener.run() diff --git a/examples/keepbot.py b/examples/keepbot.py index 1529f90..4ef661e 100644 --- a/examples/keepbot.py +++ b/examples/keepbot.py @@ -15,10 +15,10 @@ old_nicknames = { } session = fbchat.Session.login("", "") - -listener = fbchat.Listener.connect(session, chat_on=False, foreground=False) +listener = fbchat.Listener(session=session, chat_on=False, foreground=False) +@listener.register def on_color_set(event: fbchat.ColorSet): if old_thread_id != event.thread.id: return @@ -27,6 +27,7 @@ def on_color_set(event: fbchat.ColorSet): event.thread.set_color(old_color) +@listener.register def on_emoji_set(event: fbchat.EmojiSet): if old_thread_id != event.thread.id: return @@ -35,6 +36,7 @@ def on_emoji_set(event: fbchat.EmojiSet): event.thread.set_emoji(old_emoji) +@listener.register def on_title_set(event: fbchat.TitleSet): if old_thread_id != event.thread.id: return @@ -43,6 +45,7 @@ def on_title_set(event: fbchat.TitleSet): event.thread.set_title(old_title) +@listener.register def on_nickname_set(event: fbchat.NicknameSet): if old_thread_id != event.thread.id: return @@ -55,6 +58,7 @@ def on_nickname_set(event: fbchat.NicknameSet): event.thread.set_nickname(event.subject.id, old_nickname) +@listener.register def on_people_added(event: fbchat.PeopleAdded): if old_thread_id != event.thread.id: return @@ -64,6 +68,7 @@ def on_people_added(event: fbchat.PeopleAdded): event.thread.remove_participant(added.id) +@listener.register def on_person_removed(event: fbchat.PersonRemoved): if old_thread_id != event.thread.id: return @@ -75,16 +80,4 @@ def on_person_removed(event: fbchat.PersonRemoved): event.thread.add_participants([removed.id]) -for event in listener.listen(): - if isinstance(event, fbchat.ColorSet): - on_color_set(event) - elif isinstance(event, fbchat.EmojiSet): - on_emoji_set(event) - elif isinstance(event, fbchat.TitleSet): - on_title_set(event) - elif isinstance(event, fbchat.NicknameSet): - on_nickname_set(event) - elif isinstance(event, fbchat.PeopleAdded): - on_people_added(event) - elif isinstance(event, fbchat.PersonRemoved): - on_person_removed(event) +listener.run() diff --git a/examples/removebot.py b/examples/removebot.py index 14d449a..0c83f27 100644 --- a/examples/removebot.py +++ b/examples/removebot.py @@ -1,10 +1,10 @@ import fbchat session = fbchat.Session.login("", "") - -listener = fbchat.Listener.connect(session, chat_on=False, foreground=False) +listener = fbchat.Listener(session=session, chat_on=False, foreground=False) +@listener.register def on_message(event): # We can only kick people from group chats, so no need to try if it's a user chat if not isinstance(event.thread, fbchat.Group): @@ -14,6 +14,4 @@ def on_message(event): event.thread.remove_participant(event.author.id) -for event in listener.listen(): - if isinstance(event, fbchat.MessageEvent): - on_message(event) +listener.run() diff --git a/fbchat/_listen.py b/fbchat/_listen.py index 4ff4bed..a7ee27a 100644 --- a/fbchat/_listen.py +++ b/fbchat/_listen.py @@ -1,11 +1,12 @@ import attr +import inspect import random import paho.mqtt.client import requests from ._common import log, kw_only from . import _util, _exception, _session, _graphql, _events -from typing import Iterable, Optional +from typing import Iterable, Optional, Mapping, Callable HOST = "edge-chat.facebook.com" @@ -97,6 +98,9 @@ def fetch_sequence_id(session: _session.Session) -> int: return int(sequence_id) +HandlerT = Callable[[_events.Event], None] + + @attr.s(slots=True, kw_only=kw_only, eq=False) class Listener: """Helper, to listen for incoming Facebook events. @@ -119,6 +123,7 @@ class Listener: _sync_token = attr.ib(None, type=Optional[str]) _sequence_id = attr.ib(None, type=Optional[int]) _tmp_events = attr.ib(factory=list, type=Iterable[_events.Event]) + _handlers = attr.ib(factory=dict, type=Mapping[HandlerT, _events.Event]) def __attrs_post_init__(self): # Configure callbacks @@ -302,7 +307,8 @@ class Listener: Yields events when they arrive. - This will automatically reconnect on errors. + This will automatically reconnect on errors, except if the errors are one of + `PleaseRefresh` or `NotLoggedIn`. Example: Print events continually. @@ -398,3 +404,57 @@ class Listener: # # def browser_close(self): # info = self._mqtt.publish("/browser_close", payload=b"{}", qos=1) + + def register(func: HandlerT) -> HandlerT: + """Register a function that will be called when .run is called. + + The input function must take a single annotated argument. + + Should be used as a function decorator. + + Example: + >>> @listener.register + >>> def my_handler(event: fbchat.Event): + ... print(f"New event: {event}") + """ + try: + parameter = next(iter(inspect.signature(func).parameters.values())) + except Exception as e: # TODO: More precise exceptions + raise ValueError("Invalid function. Must have at least an argument") from e + + if parameter.annotation is parameter.empty: + raise ValueError("Invalid function. Must be annotated") + + if not issubclass(parameter.annotation, _events.Event): + raise ValueError("Invalid function. Annotation must be an event class") + + # TODO: More error checks, e.g. kw_only parameters + + self._handlers[func] = parameter.annotation + + def unregister(func: HandlerT): + """Unregister a previously registered function.""" + try: + self._handlers.pop(func) + except KeyError: + raise ValueError("Tried to unregister a function that was not registered") + + def run(self) -> None: + """Run the listening loop, and dispatch incoming events to registered handlers. + + This uses `.listen`, which reconnect on errors, except if the errors are one of + `PleaseRefresh` or `NotLoggedIn`. + + Example: + Print incoming messages. + + >>> @listener.register + >>> def print_msg(event: fbchat.MessageEvent): + ... print(event.message.text) + ... + >>> listener.run() + """ + for event in self.listen(): + for handler, event_cls in self._handlers.items(): + if isinstance(event, event_cls): + handler(event)