Example #1
0
class Wechaty(AsyncIOEventEmitter):
    """
    docstring
    """

    _global_instance: Optional['Wechaty'] = None

    # define the event

    # save login user contact_id
    contact_id: str

    def __init__(self, options: Optional[WechatyOptions] = None):
        """
        docstring
        """
        super().__init__()

        if options is None:
            options = WechatyOptions(puppet='wechaty-puppet-hostie')
        if options.puppet_options is None:
            options.puppet_options = PuppetOptions()

        self.Tag = Tag
        self.Contact = Contact
        self.Friendship = Friendship
        self.Message = Message
        self.Room = Room
        self.Image = Image
        self.RoomInvitation = RoomInvitation
        self.Favorite = Favorite
        self.MiniProgram = MiniProgram
        # TODO -> url-link, miniprogram

        self.started: bool = False

        self.puppet: Puppet = self._load_puppet(options)

        self._name: Optional[str] = None

        self.state = StateSwitch()
        self._ready_state = StateSwitch()

        self._watchdog = Watchdog(DEFAULT_TIMEOUT)

    @staticmethod
    def _load_puppet(options: WechatyOptions) -> Puppet:
        """
        dynamic load puppet
        :param options:
        :return:
        """
        if options.puppet is None:
            raise Exception('puppet not exist')

        if isinstance(options.puppet, Puppet):
            return options.puppet

        if isinstance(options.puppet, PuppetModuleName):
            if options.puppet != 'wechaty-puppet-hostie':
                raise TypeError('Python Wechaty only supports wechaty-puppet-hostie right now.'
                                'This puppet is not supported: ' + options.puppet)

            #
            # wechaty-puppet-hostie
            #
            hostie_module = __import__('wechaty_puppet_hostie')
            if not hasattr(hostie_module, 'HostiePuppet'):
                raise Exception('HostiePuppet not exist in '
                                'wechaty-puppet-hostie')

            hostie_puppet_class = getattr(hostie_module, 'HostiePuppet')
            if not issubclass(hostie_puppet_class, Puppet):
                raise TypeError(f'Type {hostie_puppet_class} '
                                f'is not correct')

            return hostie_puppet_class(options.puppet_options)

        raise TypeError('puppet expected type is [Puppet, '
                        'PuppetModuleName(str)]')

    def __str__(self):
        """str format of the Room object"""
        return 'Wechaty<{0}, {1}>'.format(self.name, self.contact_id)

    @classmethod
    def instance(cls: Type[Wechaty], options: Optional[WechatyOptions] = None
                 ) -> Wechaty:
        """
        get or create global wechaty instance
        :return:
        """
        log.info('instance()')

        if cls._global_instance is None:
            cls._global_instance = cls(options)

        # Huan(202003): how to remove cast?
        return cls._global_instance
        # return cast(Wechaty, cls._global_instance)
        # return cls._global_instance

    @property
    def name(self) -> str:
        """name"""
        if self._name is None:
            return 'default_puppet'
        return self._name

    def on(self, event, f=None) -> Wechaty:
        """
        listen wechaty event
        :param event:
        :param f:
        :return:
        """
        log.info('on() listen event <%s> with <%s>', event, f)
        super().on(event, f)
        return self

    def emit(self, event, *args, **kwargs):
        """
        emit wechaty event
        :param event:
        :param args:
        :param kwargs:
        :return:
        """
        log.debug('emit() event <%s> <%s>',
                  [str(item) for item in args],
                  kwargs)
        super().emit(event, *args, **kwargs)

    async def on_error(self, payload: EventErrorPayload):
        """
        listen error event for puppet

        this is friendly for code typing
        """

    async def on_heartbeat(self, payload: EventHeartbeatPayload):
        """
        listen heartbeat event for puppet

        this is friendly for code typing
        """

    async def on_friendship(self, friendship: Friendship):
        """
        listen friendship event for puppet

        this is friendly for code typing
        """

    async def on_login(self, contact: Contact):
        """
        listen login event for puppet

        this is friendly for code typing
        """

    async def on_logout(self, contact: Contact):
        """
        listen logout event for puppet

        this is friendly for code typing
        """

    async def on_message(self, msg: Message):
        """
        listen message event for puppet

        this is friendly for code typing
        """

    async def on_ready(self, payload: EventReadyPayload):
        """
        listen ready event for puppet

        this is friendly for code typing
        """

    async def on_room_invite(self, room_invitation: RoomInvitation):
        """
        listen room_invitation event for puppet

        this is friendly for code typing
        """

    async def on_room_join(self, room: Room, invitees: List[Contact],
                           inviter: Contact, date: datetime):
        """
        listen room_join event for puppet

        this is friendly for code typing
        """

    async def on_room_leave(self, room: Room, leavers: List[Contact],
                            remover: Contact, date: datetime):
        """
        listen room_leave event for puppet

        room, leavers, remover, date

        this is friendly for code typing
        """

    async def on_room_topic(self, room: Room, new_topic: str, old_topic: str,
                            changer: Contact, date: datetime):
        """
        listen room_topic event for puppet

        this is friendly for code typing
        """

    async def on_scan(self, status: ScanStatus, qr_code: Optional[str] = None,
                      data: Optional[str] = None):
        """
        listen scan event for puppet

        this is friendly for code typing
        """

    async def start(self):
        """
        start wechaty bot
        :return:
        """
        await self.init_puppet()
        await self.init_puppet_event_bridge(self.puppet)

        async def start_watchdog():

            food = WatchdogFood(timeout=3)

            async def ask_for_food(last_food, last_feed):
                log.debug('dog ask for food <%s> <%s> ...',
                          last_food, last_feed)
                await self.puppet.ding()

            self._watchdog.on('sleep', ask_for_food)
            self._watchdog.feed(food)
            while True:
                log.debug('bot tick <%s>', datetime.now())
                await self._watchdog.sleep()
                is_death = self._watchdog.starved_to_death()
                if is_death:
                    await self.restart()
                    break

        loop = asyncio.get_event_loop()
        asyncio.run_coroutine_threadsafe(start_watchdog(), loop)
        log.info('starting ...')
        await self.puppet.start()

    async def restart(self):
        """restart the wechaty bot"""
        log.info('restarting the bot ...')
        # await self.puppet.stop()
        # await self.puppet.start()
        await self.start()

    # pylint: disable=R0912,R0915,R0914
    async def init_puppet_event_bridge(self, puppet: Puppet):
        """
        init puppet event stream
        """
        log.info('init_puppet_event_bridge() <%s>', puppet)
        event_names = PUPPET_EVENT_DICT.keys()
        for event_name in event_names:
            if event_name == 'dong':
                def dong_listener(payload: EventDongPayload):
                    log.debug('receive <dong> event <%s>', payload)
                    self.emit('dong', payload.data)
                    # feed food to the dog
                    food = WatchdogFood(timeout=30)
                    self._watchdog.feed(food)

                puppet.on('dong', dong_listener)
            elif event_name == 'error':
                async def error_listener(payload: EventErrorPayload):
                    log.info('receive <error> event <%s>', payload)
                    self.emit('error', payload)
                    await self.on_error(payload)

                puppet.on('error', error_listener)

            elif event_name == 'heart-beat':
                async def heartbeat_listener(payload: EventHeartbeatPayload):
                    log.info('receive <heart-beat> event <%s>', payload)
                    self.emit('heartbeat', payload.data)
                    await self.on_heartbeat(payload)

                puppet.on('heart-beat', heartbeat_listener)

            elif event_name == 'friendship':
                async def friendship_listener(payload: EventFriendshipPayload):
                    log.info('receive <friendship> event <%s>', payload)
                    friendship = self.Friendship.load(payload.friendship_id)
                    await friendship.ready()
                    self.emit('friendship', friendship)
                    friendship.contact().emit('friendship', friendship)
                    await self.on_friendship(friendship)

                puppet.on('friendship', friendship_listener)

            elif event_name == 'login':
                async def login_listener(payload: EventLoginPayload):
                    # set login contact_id
                    self.contact_id = payload.contact_id
                    log.info('receive <login> event <%s>', payload)
                    contact = self.Contact.load(payload.contact_id)
                    await contact.ready()
                    self.emit('login', Contact)
                    await self.on_login(contact)

                puppet.on('login', login_listener)

            elif event_name == 'logout':
                async def logout_listener(payload: EventLogoutPayload):
                    # TODO -> should to ContactSelf
                    log.info('receive <logout> event <%s>', payload)
                    contact = self.Contact.load(payload.contact_id)
                    await contact.ready()
                    self.emit('logout', Contact)
                    await self.on_logout(contact)

                puppet.on('logout', logout_listener)

            elif event_name == 'message':
                async def message_listener(payload: EventMessagePayload):
                    log.debug('receive <message> event <%s>', payload)
                    msg = self.Message.load(payload.message_id)
                    await msg.ready()
                    log.info('receive message <%s>', msg)
                    self.emit('message', msg)
                    await self.on_message(msg)

                    room = msg.room()
                    if room is not None:
                        room.emit('message', room)

                puppet.on('message', message_listener)

            elif event_name == 'ready':
                async def ready_listener(payload: EventReadyPayload):
                    log.info('receive <ready> event <%s>')
                    self.emit('ready', payload)
                    self._ready_state.on(True)
                    await self.on_ready(payload)

                puppet.on('ready', ready_listener)

            elif event_name == 'room-invite':
                async def room_invite_listener(payload: EventRoomInvitePayload):
                    log.info('receive <room-invite> event <%s>')
                    invitation = self.RoomInvitation.load(
                        payload.room_invitation_id)
                    self.emit('room-invite', invitation)
                    await self.on_room_invite(invitation)

                puppet.on('room-invite', room_invite_listener)

            elif event_name == 'room-join':
                async def room_join_listener(payload: EventRoomJoinPayload):
                    log.info('receive <room-join> event <%s>')
                    room = self.Room.load(payload.room_id)
                    await room.ready()

                    invitees = [self.Contact.load(invitee_id)
                                for invitee_id in payload.invited_ids]
                    for invitee in invitees:
                        await invitee.ready()

                    inviter = self.Contact.load(payload.inviter_id)
                    await inviter.ready()

                    date = datetime.fromtimestamp(payload.time_stamp)
                    self.emit('room-join', room, invitees, inviter, date)
                    await self.on_room_join(room, invitees, inviter, date)

                    room.emit('join', invitees, inviter, date)

                puppet.on('room-join', room_join_listener)

            elif event_name == 'room-leave':
                async def room_leave_listener(payload: EventRoomLeavePayload):
                    log.info('receive <room-leave> event <%s>')
                    room = self.Room.load(payload.room_id)
                    # room info is dirty now
                    await room.ready(force_sync=True)

                    leavers = [self.Contact.load(inviter_id) for inviter_id
                               in payload.removed_ids]

                    for leaver in leavers:
                        await leaver.ready()

                    remover = self.Contact.load(payload.remover_id)
                    await remover.ready()

                    date = datetime.fromtimestamp(payload.time_stamp)
                    self.emit('room-leave', room, leavers, remover, date)
                    await self.on_room_leave(room, leavers, remover, date)

                    room.emit('leave', leavers, remover, date)

                    if self.puppet.self_id() in payload.removed_ids:
                        pass
                        # await self.puppet.room_payload(payload.room_id)
                        # await self.puppet.room_member_payload_dirty(
                        #     payload.room_id)

                puppet.on('room-leave', room_leave_listener)

            elif event_name == 'room-topic':
                async def room_topic_listener(payload: EventRoomTopicPayload):
                    log.info('receive <room-topic> event <%s>')
                    room = self.Room.load(payload.room_id)
                    await room.ready()

                    changer = self.Contact.load(payload.changer_id)
                    await changer.ready()

                    date = datetime.fromtimestamp(payload.time_stamp)

                    self.emit('room-topic', room, payload.new_topic,
                              payload.old_topic, changer, date)

                    await self.on_room_topic(room, payload.new_topic,
                                             payload.old_topic, changer, date)

                    room.emit('topic', payload.new_topic, payload.old_topic,
                              changer, date)

                puppet.on('room-topic', room_topic_listener)

            elif event_name == 'scan':
                async def scan_listener(payload: EventScanPayload):
                    log.info('receive <scan> event <%s>')
                    qr_code = '' if payload.qrcode is None \
                        else payload.qrcode
                    if payload.status == ScanStatus.Waiting:
                        qr_terminal(qr_code)
                    self.emit('scan', payload.status, qr_code,  payload.data)
                    await self.on_scan(payload.status, qr_code,  payload.data)

                puppet.on('scan', scan_listener)

            elif event_name == 'reset':
                log.info('receive <reset> event <%s>')
            else:
                raise ValueError(f'event_name <{event_name}> unsupported!')

            log.info('initPuppetEventBridge() puppet.on(%s) (listenerCount:%s) '
                     'registering...',
                     event_name, puppet.listener_count(event_name))

    def add_listener_function(self, event: str, listener):
        """add listener function to event emitter"""
        self.on(event, listener)

    async def init_puppet(self):
        """
        init puppet grpc connection
        """

        self.Message.set_puppet(self.puppet)
        self.Room.set_puppet(self.puppet)
        self.RoomInvitation.set_puppet(self.puppet)
        self.Contact.set_puppet(self.puppet)
        self.Friendship.set_puppet(self.puppet)
        self.Image.set_puppet(self.puppet)
        self.Tag.set_puppet(self.puppet)

        self.Message.set_wechaty(self)
        self.Room.set_wechaty(self)
        self.RoomInvitation.set_wechaty(self)
        self.Contact.set_wechaty(self)
        self.Friendship.set_wechaty(self)
        self.Image.set_wechaty(self)
        self.Tag.set_wechaty(self)

    async def stop(self):
        """
        stop the wechaty
        """
        log.info('wechaty is stoping ...')
        await self.puppet.stop()
Example #2
0
class Wechaty(AsyncIOEventEmitter):
    """
    docstring
    """

    _global_instance: Optional['Wechaty'] = None

    # define the event

    # save login user contact_id
    contact_id: str

    def __init__(self, options: Optional[WechatyOptions] = None):
        """
        docstring
        """
        super().__init__()

        if options is None:
            options = WechatyOptions(puppet='wechaty-puppet-hostie')
        if options.puppet_options is None:
            options.puppet_options = PuppetOptions()

        self._options = options

        # pylint: disable=C0103
        self.Tag: Type[Tag] = Tag
        # pylint: disable=C0103
        self.Contact: Type[Contact] = Contact
        # pylint: disable=C0103
        self.ContactSelf: Type[ContactSelf] = ContactSelf
        # pylint: disable=C0103
        self.Friendship: Type[Friendship] = Friendship
        # pylint: disable=C0103
        self.Message: Type[Message] = Message
        # pylint: disable=C0103
        self.Room: Type[Room] = Room
        # pylint: disable=C0103
        self.Image: Type[Image] = Image
        # pylint: disable=C0103
        self.RoomInvitation: Type[RoomInvitation] = RoomInvitation
        self.Favorite: Type[Favorite] = Favorite
        self.MiniProgram: Type[MiniProgram] = MiniProgram
        # TODO -> url-link, miniprogram

        self.started: bool = False

        self._name: Optional[str] = None

        self.state = StateSwitch()
        self._ready_state = StateSwitch()

        self._puppet: Optional[Puppet] = None

        # Create watchdog on start, and allow to shutdown the watchdog by
        # setting it to None
        self._watchdog: Optional[Watchdog] = None
        self._watchdog_task: Optional[asyncio.Task] = None

        self._plugin_manager: WechatyPluginManager = WechatyPluginManager(self)

    @property
    def puppet(self) -> Puppet:
        """
        Always expected to return a non-null puppet instance, or raise an error.
        :return:
        """
        if not self._puppet:
            raise WechatyStatusError('Wechaty puppet not loaded!')
        return self._puppet

    @staticmethod
    def _load_puppet(options: WechatyOptions) -> Puppet:
        """
        dynamic load puppet
        :param options:
        :return:
        """
        if options.puppet is None:
            raise WechatyConfigurationError('puppet not exist')

        if isinstance(options.puppet, Puppet):
            return options.puppet

        if isinstance(options.puppet, PuppetModuleName):
            if options.puppet != 'wechaty-puppet-hostie':
                raise TypeError(
                    'Python Wechaty only supports wechaty-puppet-hostie right now.'
                    'This puppet is not supported: ' + options.puppet)

            #
            # wechaty-puppet-hostie
            #
            hostie_module = __import__('wechaty_puppet_hostie')
            if not hasattr(hostie_module, 'HostiePuppet'):
                raise WechatyConfigurationError('HostiePuppet not exist in '
                                                'wechaty-puppet-hostie')

            hostie_puppet_class = getattr(hostie_module, 'HostiePuppet')
            if not issubclass(hostie_puppet_class, Puppet):
                raise WechatyConfigurationError(f'Type {hostie_puppet_class} '
                                                f'is not correct')

            return hostie_puppet_class(options.puppet_options)

        raise WechatyConfigurationError('puppet expected type is [Puppet, '
                                        'PuppetModuleName(str)]')

    def __str__(self):
        """str format of the Room object"""
        return 'Wechaty<{0}, {1}>'.format(self.name, self.contact_id)

    @classmethod
    def instance(cls: Type[Wechaty],
                 options: Optional[WechatyOptions] = None) -> Wechaty:
        """
        get or create global wechaty instance
        :return:
        """
        log.info('instance()')

        if cls._global_instance is None:
            cls._global_instance = cls(options)

        # Huan(202003): how to remove cast?
        return cls._global_instance
        # return cast(Wechaty, cls._global_instance)
        # return cls._global_instance

    def use(self, plugin: Union[WechatyPlugin,
                                List[WechatyPlugin]]) -> Wechaty:
        """register the plugin"""
        if isinstance(plugin, WechatyPlugin):
            plugins = [plugin]
        else:
            plugins = plugin
        for item in plugins:
            self._plugin_manager.add_plugin(item)
        return self

    @property
    def name(self) -> str:
        """name"""
        if self._name is None:
            return 'default_puppet'
        return self._name

    def on(self, event, f=None) -> Wechaty:
        """
        listen wechaty event
        :param event:
        :param f:
        :return:
        """
        log.info('on() listen event <%s> with <%s>', event, f)
        super().on(event, f)
        return self

    def emit(self, event, *args, **kwargs):
        """
        emit wechaty event
        :param event:
        :param args:
        :param kwargs:
        :return:
        """
        log.debug('emit() event <%s> <%s>', [str(item) for item in args],
                  kwargs)
        super().emit(event, *args, **kwargs)

    async def on_error(self, payload: EventErrorPayload):
        """
        listen error event for puppet

        this is friendly for code typing
        """

    async def on_heartbeat(self, payload: EventHeartbeatPayload):
        """
        listen heartbeat event for puppet

        this is friendly for code typing
        """

    async def on_friendship(self, friendship: Friendship):
        """
        listen friendship event for puppet

        this is friendly for code typing
        """

    async def on_login(self, contact: Contact):
        """
        listen login event for puppet

        this is friendly for code typing
        """

    async def on_logout(self, contact: Contact):
        """
        listen logout event for puppet

        this is friendly for code typing
        """

    async def on_message(self, msg: Message):
        """
        listen message event for puppet

        this is friendly for code typing
        """

    async def on_ready(self, payload: EventReadyPayload):
        """
        listen ready event for puppet

        this is friendly for code typing
        """

    async def on_room_invite(self, room_invitation: RoomInvitation):
        """
        listen room_invitation event for puppet

        this is friendly for code typing
        """

    async def on_room_join(self, room: Room, invitees: List[Contact],
                           inviter: Contact, date: datetime):
        """
        listen room_join event for puppet

        this is friendly for code typing
        """

    async def on_room_leave(self, room: Room, leavers: List[Contact],
                            remover: Contact, date: datetime):
        """
        listen room_leave event for puppet

        room, leavers, remover, date

        this is friendly for code typing
        """

    async def on_room_topic(self, room: Room, new_topic: str, old_topic: str,
                            changer: Contact, date: datetime):
        """
        listen room_topic event for puppet

        this is friendly for code typing
        """

    async def on_scan(self,
                      status: ScanStatus,
                      qr_code: Optional[str] = None,
                      data: Optional[str] = None):
        """
        listen scan event for puppet

        this is friendly for code typing
        """

    async def start(self):
        """
        start wechaty bot
        :return:
        """
        async def start_watchdog():
            try:
                self._watchdog = Watchdog(DEFAULT_TIMEOUT)
                watch_dog_id = id(self._watchdog)
                food = WatchdogFood(timeout=3)

                async def ask_for_food(last_food, last_feed):
                    log.debug('dog ask for food <%s> <%s> ...', last_food,
                              last_feed)
                    # Cancel when stopped, or the puppet.ding() call will got an error
                    if self.puppet and self._watchdog and id(
                            self._watchdog) == watch_dog_id:
                        await self.puppet.ding()

                self._watchdog.on('sleep', ask_for_food)
                self._watchdog.feed(food)
                # If we set _watchdog to None or a new one, this loop should break.
                while self._watchdog and id(self._watchdog) == watch_dog_id:
                    log.debug('bot tick <%s>', datetime.now())
                    await self._watchdog.sleep()
                    is_death = self._watchdog.starved_to_death()
                    if is_death:
                        await self.restart()
                        break
            except asyncio.CancelledError:
                # When we stop the bot, we call cancel on the self._watchdog_task.
                # In this case, a CancelledError will raise then, just exit this function
                # to terminate the watchdog
                pass

        # If the network is shut-down, we should catch the connection
        # error and restart after a minute.
        try:

            await self.init_puppet()
            await self.init_puppet_event_bridge(self.puppet)

            log.info('starting puppet ...')
            await self.puppet.start()

            self.started = True

            self._watchdog_task = asyncio.create_task(start_watchdog())

        except (requests.exceptions.ConnectionError, StreamTerminatedError,
                OSError):

            log.error(
                'The network is not good, the bot will try to restart after 60 seconds.'
            )
            await asyncio.sleep(60)
            await self.restart()

        except WechatyPuppetError:
            traceback.print_exc()
            loop = asyncio.get_event_loop()
            loop.stop()

        # except Exception:
        #     traceback.print_exc()
        #     loop = asyncio.get_event_loop()
        #     loop.stop()

    async def restart(self):
        """restart the wechaty bot"""
        log.info('restarting the bot ...')
        await self.stop()
        await self.start()

    # pylint: disable=R0912,R0915,R0914
    async def init_puppet_event_bridge(self, puppet: Puppet):
        """
        init puppet event stream
        """
        log.info('init_puppet_event_bridge() <%s>', puppet)
        event_names = PUPPET_EVENT_DICT.keys()
        for event_name in event_names:
            if event_name == 'dong':

                def dong_listener(payload: EventDongPayload):
                    log.debug('receive <dong> event <%s>', payload)
                    self.emit('dong', payload.data)
                    # feed food to the dog
                    food = WatchdogFood(timeout=30)
                    if self._watchdog:
                        self._watchdog.feed(food)

                puppet.on('dong', dong_listener)
            elif event_name == 'error':

                async def error_listener(payload: EventErrorPayload):
                    if isinstance(payload, EventErrorPayload):
                        log.info('receive <error> event <%s>', payload)
                        self.emit('error', payload)
                        await self.on_error(payload)
                    else:
                        log.error('internal error <%s>', payload)

                puppet.on('error', error_listener)

            elif event_name == 'heart-beat':

                async def heartbeat_listener(payload: EventHeartbeatPayload):
                    log.info('receive <heart-beat> event <%s>', payload)
                    self.emit('heartbeat', payload.data)
                    await self.on_heartbeat(payload)

                puppet.on('heart-beat', heartbeat_listener)

            elif event_name == 'friendship':

                async def friendship_listener(payload: EventFriendshipPayload):
                    log.info('receive <friendship> event <%s>', payload)
                    friendship = self.Friendship.load(payload.friendship_id)
                    await friendship.ready()
                    self.emit('friendship', friendship)

                    # this method will cause _events error, refer to
                    # :https://github.com/wechaty/python-wechaty/issues/122
                    # and this feature is considering to be removed, refer to
                    # https://github.com/wechaty/python-wechaty/issues/127

                    # friendship.contact().emit('friendship', friendship)
                    await self.on_friendship(friendship)

                    await self._plugin_manager.emit_events(
                        'friendship', friendship)

                puppet.on('friendship', friendship_listener)

            elif event_name == 'login':

                async def login_listener(payload: EventLoginPayload):

                    # init the plugins
                    await self._plugin_manager.init_plugins()

                    # set login contact_id
                    self.contact_id = payload.contact_id
                    log.info('receive <login> event <%s>', payload)
                    contact = self.ContactSelf.load(payload.contact_id)
                    await contact.ready()
                    self.emit('login', contact)
                    await self.on_login(contact)

                    # emit the login event to plugins
                    await self._plugin_manager.emit_events('login', contact)

                puppet.on('login', login_listener)

            elif event_name == 'logout':

                async def logout_listener(payload: EventLogoutPayload):
                    # TODO -> should to ContactSelf
                    log.info('receive <logout> event <%s>', payload)
                    contact = self.ContactSelf.load(payload.contact_id)
                    await contact.ready()
                    self.emit('logout', contact)
                    await self.on_logout(contact)

                    # emit the logout event to plugins
                    await self._plugin_manager.emit_events('logout', contact)

                puppet.on('logout', logout_listener)

            elif event_name == 'message':

                async def message_listener(payload: EventMessagePayload):
                    log.debug('receive <message> event <%s>', payload)
                    msg = self.Message.load(payload.message_id)
                    await msg.ready()
                    log.info('receive message <%s>', msg)
                    self.emit('message', msg)
                    await self.on_message(msg)

                    room = msg.room()
                    if room is not None:
                        room.emit('message', room)

                    # emit the message event to plugins
                    await self._plugin_manager.emit_events('message', msg)

                puppet.on('message', message_listener)

            elif event_name == 'ready':

                async def ready_listener(payload: EventReadyPayload):
                    log.info('receive <ready> event <%s>')
                    self.emit('ready', payload)
                    self._ready_state.on(True)
                    await self.on_ready(payload)

                puppet.on('ready', ready_listener)

            elif event_name == 'room-invite':

                async def room_invite_listener(
                        payload: EventRoomInvitePayload):
                    log.info('receive <room-invite> event <%s>')
                    invitation = self.RoomInvitation.load(
                        payload.room_invitation_id)
                    self.emit('room-invite', invitation)
                    await self.on_room_invite(invitation)

                    # emit the room-invite event to plugins
                    await self._plugin_manager.emit_events(
                        'room-invite', invitation)

                puppet.on('room-invite', room_invite_listener)

            elif event_name == 'room-join':

                async def room_join_listener(payload: EventRoomJoinPayload):
                    log.info('receive <room-join> event <%s>')
                    room = self.Room.load(payload.room_id)
                    await room.ready()

                    invitees = [
                        self.Contact.load(invitee_id)
                        for invitee_id in payload.invited_ids
                    ]
                    for invitee in invitees:
                        await invitee.ready()

                    inviter = self.Contact.load(payload.inviter_id)
                    await inviter.ready()

                    # timestamp is from hostie-server, but the value range is
                    # 10^10 ~ 10^13
                    # refer to
                    # :https://github.com/wechaty/python-wechaty/issues/1290
                    date = timestamp_to_date(payload.timestamp)

                    self.emit('room-join', room, invitees, inviter, date)
                    await self.on_room_join(room, invitees, inviter, date)

                    room.emit('join', invitees, inviter, date)

                    # emit the room-join event to plugins
                    await self._plugin_manager.emit_events(
                        'room-join', room, invitees, inviter, date)

                puppet.on('room-join', room_join_listener)

            elif event_name == 'room-leave':

                async def room_leave_listener(payload: EventRoomLeavePayload):
                    log.info('receive <room-leave> event <%s>')
                    room = self.Room.load(payload.room_id)
                    # room info is dirty now
                    await room.ready(force_sync=True)

                    leavers = [
                        self.Contact.load(inviter_id)
                        for inviter_id in payload.removed_ids
                    ]

                    for leaver in leavers:
                        await leaver.ready()

                    remover = self.Contact.load(payload.remover_id)
                    await remover.ready()

                    date = timestamp_to_date(payload.timestamp)

                    self.emit('room-leave', room, leavers, remover, date)
                    await self.on_room_leave(room, leavers, remover, date)

                    room.emit('leave', leavers, remover, date)

                    if self.puppet.self_id() in payload.removed_ids:
                        pass
                        # await self.puppet.room_payload(payload.room_id)
                        # await self.puppet.room_member_payload_dirty(
                        #     payload.room_id)

                    # emit the room-leave event to plugins
                    await self._plugin_manager.emit_events(
                        'room-leave', leavers, remover, date)

                puppet.on('room-leave', room_leave_listener)

            elif event_name == 'room-topic':

                async def room_topic_listener(payload: EventRoomTopicPayload):
                    log.info('receive <room-topic> event <%s>')
                    room = self.Room.load(payload.room_id)
                    await room.ready()

                    changer = self.Contact.load(payload.changer_id)
                    await changer.ready()

                    date = timestamp_to_date(payload.timestamp)

                    self.emit('room-topic', room, payload.new_topic,
                              payload.old_topic, changer, date)

                    await self.on_room_topic(room, payload.new_topic,
                                             payload.old_topic, changer, date)

                    room.emit('topic', payload.new_topic, payload.old_topic,
                              changer, date)

                    # emit the room-topic to plugins
                    await self._plugin_manager.emit_events(
                        'room-topic', room, payload.new_topic,
                        payload.old_topic, changer, date)

                puppet.on('room-topic', room_topic_listener)

            elif event_name == 'scan':

                async def scan_listener(payload: EventScanPayload):
                    log.info('receive <scan> event <%s>')
                    qr_code = '' if payload.qrcode is None \
                        else payload.qrcode
                    if payload.status == ScanStatus.Waiting:
                        qr_terminal(qr_code)
                    self.emit('scan', payload.status, qr_code, payload.data)
                    await self.on_scan(payload.status, qr_code, payload.data)

                    # emit the scan event to plugins
                    await self._plugin_manager.emit_events(
                        'scan', payload.status, qr_code, payload.data)

                puppet.on('scan', scan_listener)

            elif event_name == 'reset':
                log.info('receive <reset> event <%s>')
            else:
                raise WechatyOperationError(
                    f'event_name <{event_name}> unsupported!')

            log.info(
                'initPuppetEventBridge() puppet.on(%s) (listenerCount:%s) '
                'registering...', event_name,
                puppet.listener_count(event_name))

    def add_listener_function(self, event: str, listener):
        """add listener function to event emitter"""
        self.on(event, listener)

    async def init_puppet(self):
        """
        init puppet grpc connection
        """
        # Recreate puppet instance
        self._puppet = self._load_puppet(self._options)

        # Using metaclass to create a dynamic subclass to server multi bot instances.
        meta_info = dict(_puppet=self.puppet, _wechaty=self, abstract=False)
        self.Contact = type('Contact', (Contact, ), meta_info)
        self.ContactSelf = type('ContactSelf', (ContactSelf, ), meta_info)
        self.Favorite = type('Favorite', (Favorite, ), meta_info)
        self.Friendship = type('Friendship', (Friendship, ), meta_info)
        self.Image = type('Image', (Image, ), meta_info)
        self.Message = type('Message', (Message, ), meta_info)
        self.MiniProgram = type('MiniProgram', (MiniProgram, ), meta_info)
        self.Room = type('Room', (Room, ), meta_info)
        self.RoomInvitation = type('RoomInvitation', (RoomInvitation, ),
                                   meta_info)
        self.Tag = type('Tag', (Tag, ), meta_info)

    async def stop(self):
        """
        stop the wechaty
        """
        log.info('wechaty is stoping ...')
        self.started = False

        # Shutdown the watchdog
        if self._watchdog:
            log.info('stopping - unsubscribe watchdog events')
            self._watchdog.remove_all_listeners()
            log.info('stopping - unset watchdog')
            self._watchdog = None
        if self._watchdog_task:
            log.info('stopping - stop watchdog task')
            self._watchdog_task.cancel()
            log.info('stopping - unset watchdog task')
            self._watchdog_task = None

        # deinit puppet
        log.info('stopping - stop puppet')
        try:
            await self.puppet.stop()
        except OSError as e:
            # OSError: [Errno 101] Network is unreachable
            if e.errno == 101:
                pass
            else:
                raise e

        log.info('stopping - unset puppet')
        self._puppet = None
        log.info('wechaty has been stopped gracefully!')

    def user_self(self) -> ContactSelf:
        """
        get user self
        :return:
        """
        user_id = self.puppet.self_id()
        user = self.ContactSelf.load(user_id)
        return user

    def self(self) -> Contact:
        """
        get user self
        :return: user_self
        """
        return self.user_self()