Beispiel #1
0
class NabMastodond(nabservice.NabService, asyncio.Protocol, StreamListener):
    DAEMON_PIDFILE = "/run/nabmastodond.pid"

    RETRY_DELAY = 15 * 60  # Retry to reconnect every 15 minutes.
    NABPAIRING_MESSAGE_RE = (
        r"NabPairing (?P<cmd>Proposal|Acceptation|Rejection|Divorce|Ears "
        r'(?P<left>[0-9]+) (?P<right>[0-9]+)) - (?:<a href=")?'
        r"https://github.com/nabaztag2018/pynab")
    PROTOCOL_MESSAGES = {
        "proposal":
        "Would you accept to be my spouse? "
        "(NabPairing Proposal - https://github.com/nabaztag2018/pynab)",
        "acceptation":
        "Oh yes, I do accept to be your spouse "
        "(NabPairing Acceptation - https://github.com/nabaztag2018/pynab)",
        "rejection":
        "Sorry, I cannot be your spouse right now "
        "(NabPairing Rejection - https://github.com/nabaztag2018/pynab)",
        "divorce":
        "I think we should split. Can we skip the lawyers? "
        "(NabPairing Divorce - https://github.com/nabaztag2018/pynab)",
        "ears":
        "Let's dance (NabPairing Ears {left} {right} - "
        "https://github.com/nabaztag2018/pynab)",
    }

    def __init__(self):
        super().__init__()
        self.mastodon_client = None
        self.mastodon_stream_handle = None
        self.current_access_token = None
        self.listening_to_ears = False

    async def __config(self):
        from . import models

        return await sync_to_async(models.Config.load)()

    async def reload_config(self):
        await self.setup_streaming(True)
        await self.setup_initial_state()

    def close_streaming(self):
        if (self.mastodon_stream_handle
                and self.mastodon_stream_handle.connection):
            self.mastodon_stream_handle.close()
        self.current_access_token = None
        self.mastodon_stream_handle = None
        self.mastodon_client = None

    def on_update(self, status):
        asyncio.run_coroutine_threadsafe(
            self.loop_update(self.mastodon_client, status), self.loop)

    def on_notification(self, notification):
        if ("type" in notification and notification["type"] == "mention"
                and "status" in notification):
            asyncio.run_coroutine_threadsafe(
                self.loop_update(self.mastodon_client, notification["status"]),
                self.loop,
            )

    async def loop_update(self, mastodon_client, status):
        config = await self.__config()
        (status_id,
         status_date) = await self.process_status(config, mastodon_client,
                                                  status)
        if status_id is not None and (
                config.last_processed_status_id is None
                or status_id > config.last_processed_status_id):
            config.last_processed_status_id = status_id
        if (status_date is not None
                and status_date > config.last_processed_status_date):
            config.last_processed_status_date = status_date
        await sync_to_async(config.save)()

    async def process_conversations(self, mastodon_client, conversations):
        config = await self.__config()
        max_date = config.last_processed_status_date
        max_id = config.last_processed_status_id
        conversations_last_statuses = map(attrgetter("last_status"),
                                          conversations)
        for status in sorted(conversations_last_statuses,
                             key=attrgetter("id")):
            (status_id,
             status_date) = await self.process_status(config, mastodon_client,
                                                      status)
            if status_id is not None and (max_id is None
                                          or status_id > max_id):
                max_id = status_id
            if status_date is not None and (status_date is None
                                            or status_date > max_date):
                max_date = status_date
        config.last_processed_status_date = max_date
        config.last_processed_status_id = max_id
        await sync_to_async(config.save)()

    async def process_status(self, config, mastodon_client, status):
        try:
            status_id = status["id"]
            status_date = status["created_at"]
            skip = False
            if config.last_processed_status_id is not None:
                skip = status_id <= config.last_processed_status_id
            skip = skip or config.last_processed_status_date > status_date
            if not skip:
                await self.do_process_status(config, mastodon_client, status)
            return (status_id, status_date)
        except KeyError as e:
            print(
                f"Unexpected status from mastodon, missing slot {e}\n{status}")
            return (None, None)

    async def do_process_status(self, config, mastodon_client, status):
        if status["visibility"] == "direct":
            sender_account = status["account"]
            sender_url = sender_account["url"]
            if (sender_url !=
                    "https://" + config.instance + "/@" + config.username):
                sender = sender_account["acct"]
                if "@" not in sender:
                    sender = sender + "@" + config.instance
                if "display_name" in sender_account:
                    sender_name = sender_account["display_name"]
                else:
                    sender_name = sender_account["username"]
                type, params = self.decode_dm(status)
                if type is not None:
                    await self.transition_state(
                        config,
                        mastodon_client,
                        sender,
                        sender_name,
                        type,
                        params,
                        status["created_at"],
                    )

    async def transition_state(
        self,
        config,
        mastodon_client,
        sender,
        sender_name,
        type,
        params,
        message_date,
    ):
        current_state = config.spouse_pairing_state
        matching = (config.spouse_handle is not None
                    and config.spouse_handle == sender)
        if current_state is None:
            if type == "proposal":
                config.spouse_handle = sender
                config.spouse_pairing_state = "waiting_approval"
                config.spouse_pairing_date = message_date
                await self.play_message("proposal_received", sender_name)
            elif type == "acceptation" or type == "ears":
                NabMastodond.send_dm(mastodon_client, sender, "divorce")
            # else ignore message
        elif current_state == "proposed":
            if matching and type == "rejection":
                config.spouse_handle = None
                config.spouse_pairing_state = None
                config.spouse_pairing_date = message_date
                await self.play_message("proposal_refused", sender_name)
            elif matching and type == "divorce":
                config.spouse_handle = None
                config.spouse_pairing_state = None
                config.spouse_pairing_date = message_date
                await self.play_message("proposal_refused", sender_name)
            elif matching and type == "acceptation":
                config.spouse_handle = sender
                config.spouse_pairing_state = "married"
                config.spouse_pairing_date = message_date
                await self.send_start_listening_to_ears()
                await self.play_message("proposal_accepted", sender_name)
            elif matching and type == "proposal":
                NabMastodond.send_dm(mastodon_client, sender, "acceptation")
                config.spouse_handle = sender
                config.spouse_pairing_state = "married"
                config.spouse_pairing_date = message_date
                await self.send_start_listening_to_ears()
                await self.play_message("proposal_accepted", sender_name)
            elif not matching and (type == "acceptation" or type == "ears"):
                NabMastodond.send_dm(mastodon_client, sender, "divorce")
            elif not matching and type == "proposal":
                NabMastodond.send_dm(mastodon_client, sender, "rejection")
            # else ignore
        elif current_state == "waiting_approval":
            if matching and type == "rejection":
                config.spouse_handle = None
                config.spouse_pairing_state = None
                config.spouse_pairing_date = message_date
            elif matching and type == "divorce":
                config.spouse_handle = None
                config.spouse_pairing_state = None
                config.spouse_pairing_date = message_date
                await self.play_message("pairing_cancelled", sender_name)
            elif matching and type == "acceptation":
                NabMastodond.send_dm(mastodon_client, sender, "divorce")
                config.spouse_handle = None
                config.spouse_pairing_state = None
                config.spouse_pairing_date = message_date
            elif type == "proposal":
                if not matching:
                    NabMastodond.send_dm(mastodon_client, config.spouse_handle,
                                         "rejection")
                    config.spouse_handle = sender
                config.spouse_pairing_date = message_date
                await self.play_message("proposal_received", sender_name)
            elif matching and type == "acceptation":
                NabMastodond.send_dm(mastodon_client, sender, "divorce")
            elif not matching and (type == "acceptation" or type == "ears"):
                NabMastodond.send_dm(mastodon_client, sender, "divorce")
            # else ignore
        elif current_state == "married":
            if matching and type == "rejection":
                config.spouse_handle = None
                config.spouse_pairing_state = None
                config.spouse_pairing_date = message_date
                await self.send_stop_listening_to_ears()
                await self.play_message("pairing_cancelled", sender_name)
            elif matching and type == "divorce":
                config.spouse_handle = None
                config.spouse_pairing_state = None
                config.spouse_pairing_date = message_date
                await self.send_stop_listening_to_ears()
                await self.play_message("pairing_cancelled", sender_name)
            elif matching and type == "acceptation":
                config.spouse_pairing_date = message_date
            elif matching and type == "proposal":
                NabMastodond.send_dm(mastodon_client, sender, "acceptation")
                config.spouse_pairing_date = message_date
            elif not matching and (type == "acceptation" or type == "ears"):
                NabMastodond.send_dm(mastodon_client, sender, "divorce")
            elif not matching and type == "proposal":
                NabMastodond.send_dm(mastodon_client, sender, "rejection")
            elif matching and type == "ears":
                await self.play_message("ears", sender_name)
                config.spouse_left_ear_position = params["left"]
                config.spouse_right_ear_position = params["right"]
                config.spouse_pairing_date = message_date
                await self.send_ears(params["left"], params["right"])
            # else ignore

    async def play_message(self, message, sender_name):
        """
        Play pairing protocol message
        """
        if message == "ears":
            packet = (
                '{"type":"command",'
                '"sequence":[{"audio":["nabmastodond/communion.wav"]}]}\r\n')
        elif message == "proposal_received":
            packet = (
                '{"type":"message",'
                '"signature":{"audio":["nabmastodond/respirations/*.mp3"]},'
                '"body":[{"audio":["nabmastodond/proposal_received.mp3"]}]}'
                "\r\n")
        elif message == "proposal_refused":
            packet = (
                '{"type":"message",'
                '"signature":{"audio":["nabmastodond/respirations/*.mp3"]},'
                '"body":[{"audio":["nabmastodond/proposal_refused.mp3"]}]}'
                "\r\n")
        elif message == "proposal_accepted":
            packet = (
                '{"type":"message",'
                '"signature":{"audio":["nabmastodond/respirations/*.mp3"]},'
                '"body":[{"audio":["nabmastodond/proposal_accepted.mp3"]}]}'
                "\r\n")
        elif message == "pairing_cancelled":
            packet = (
                '{"type":"message",'
                '"signature":{"audio":["nabmastodond/respirations/*.mp3"]},'
                '"body":[{"audio":["nabmastodond/pairing_cancelled.mp3"]}]}'
                "\r\n")
        elif message == "setup":
            packet = (
                '{"type":"message",'
                '"signature":{"audio":["nabmastodond/respirations/*.mp3"]},'
                '"body":[{"audio":["nabmastodond/setup.mp3"]}]}'
                "\r\n")
        self.writer.write(packet.encode("utf8"))
        await self.writer.drain()

    async def send_start_listening_to_ears(self):
        if self.listening_to_ears is False:
            packet = '{"type":"mode","mode":"idle","events":["ears"]}\r\n'
            self.writer.write(packet.encode("utf8"))
            await self.writer.drain()
            self.listening_to_ears = True

    async def send_stop_listening_to_ears(self):
        if self.listening_to_ears:
            packet = '{"type":"mode","mode":"idle","events":[]}\r\n'
            self.writer.write(packet.encode("utf8"))
            await self.writer.drain()
            self.listening_to_ears = False

    async def send_ears(self, left_ear, right_ear):
        packet = f'{{"type":"ears","left":{left_ear},"right":{right_ear}}}\r\n'
        self.writer.write(packet.encode("utf8"))
        await self.writer.drain()

    @staticmethod
    def send_dm(mastodon_client, target, message, params={}):
        """
        Send a DM following pairing protocol
        """
        message_str = NabMastodond.PROTOCOL_MESSAGES[message].format(**params)
        status = "@" + target + " " + message_str
        return mastodon_client.status_post(status, visibility="direct")

    def decode_dm(self, status):
        m = re.search(NabMastodond.NABPAIRING_MESSAGE_RE, status["content"])
        if m:
            if "Ears" in m.group("cmd"):
                return (
                    "ears",
                    {
                        "left": int(m.group("left")),
                        "right": int(m.group("right")),
                    },
                )
            return m.group("cmd").lower(), None
        return None, None

    async def setup_streaming(self, reloading=False):
        config = await self.__config()
        setup = (reloading and self.mastodon_client is None
                 and config.spouse_handle is None)
        if config.access_token is None:
            self.close_streaming()
        else:
            if config.access_token != self.current_access_token:
                self.close_streaming()
            if self.mastodon_client is None:
                try:
                    self.mastodon_client = Mastodon(
                        client_id=config.client_id,
                        client_secret=config.client_secret,
                        access_token=config.access_token,
                        api_base_url="https://" + config.instance,
                    )
                    self.current_access_token = config.access_token
                    if setup:
                        await self.play_message("setup", config.spouse_handle)
                except MastodonUnauthorizedError:
                    self.current_access_token = None
                    config.access_token = None
                    await sync_to_async(config.save)()
                except MastodonError as e:
                    print(f"Unexpected mastodon error: {e}")
                    await asyncio.sleep(NabMastodond.RETRY_DELAY)
                    await self.setup_streaming()
            if (self.mastodon_client is not None
                    and self.mastodon_stream_handle is None):
                self.mastodon_stream_handle = self.mastodon_client.stream_user(
                    self, run_async=True, reconnect_async=True)
            if self.mastodon_client is not None:
                conversations = self.mastodon_client.conversations(
                    since_id=config.last_processed_status_id)
                await self.process_conversations(self.mastodon_client,
                                                 conversations)

    async def process_nabd_packet(self, packet):
        if packet["type"] == "ears_event":
            config = await self.__config()
            if config.spouse_pairing_state == "married":
                if self.mastodon_client:
                    await self.play_message("ears", config.spouse_handle)
                    config.spouse_left_ear_position = packet["left"]
                    config.spouse_right_ear_position = packet["right"]
                    await sync_to_async(config.save)()
                    NabMastodond.send_dm(
                        self.mastodon_client,
                        config.spouse_handle,
                        "ears",
                        {
                            "left": packet["left"],
                            "right": packet["right"]
                        },
                    )

    async def setup_initial_state(self):
        config = await self.__config()
        if config.spouse_pairing_state == "married":
            await self.send_start_listening_to_ears()
            if config.spouse_left_ear_position is not None:
                await self.send_ears(
                    config.spouse_left_ear_position,
                    config.spouse_right_ear_position,
                )
        else:
            await self.send_stop_listening_to_ears()

    def run(self):
        super().connect()
        self.loop = asyncio.get_event_loop()
        self.loop.run_until_complete(self.setup_streaming())
        self.loop.run_until_complete(self.setup_initial_state())
        try:
            self.loop.run_forever()
        except KeyboardInterrupt:
            pass
        finally:
            self.running = False  # signal to exit
            self.writer.close()
            self.close_streaming()
            tasks = asyncio.all_tasks(self.loop)
            for t in [t for t in tasks if not (t.done() or t.cancelled())]:
                self.loop.run_until_complete(
                    t)  # give canceled tasks the last chance to run
            self.loop.close()
Beispiel #2
0
    api_base_url = 'https://' + DOMAIN
)

lid = 102992003218277202
recs = {}

h2t = html2text.HTML2Text()
h2t.ignore_links = True

#pp = pprint.PrettyPrinter(indent=4)

with open("namelist",'r') as x:
    names = x.readlines()

while True:
    r = th.conversations(min_id=lid);
    
    for conv in r[::-1]:
        #print(conv.last_status.id,conv.unread, conv.last_status.account.acct)
        if conv.unread:
            
            #pp.pprint(conv);
            name = conv.last_status.account.acct
            if '@' in name:
                PM('表白墙只允许在本站使用 @'+name)
            else:
                rec = recs.get(name,[])
                cur_time = time.time()
                print('rec',rec)
                rec = [mes for mes in rec if cur_time - mes[0] < REC_TIME]
                if(len(rec) == MAX_SEND):