Exemple #1
0
class MinecraftBot():
	def __init__(self, username, server, port, commands):
		self.username = username
		self.server = server
		self.port = port
		self.commands = commands
		self.bot = Connection(server, port, username=username, allowed_versions=[47])
		self.bot.register_packet_listener(self.handle_join_game, clientbound.play.JoinGamePacket)

		log("INFO", "Trying to connect {0} to {1}:{2}.".format(username, server, port))
		self.bot.connect()
		threading.Thread(target=self.execute_go, args=["/go"], daemon=True).start()

	def execute_go(self, command):
		time.sleep(15)
		self.execute_command(command)

	def handle_join_game(self, join_game_packet):
		log("INFO", "{0} is connected to {1}:{2}.".format(self.username, self.server, self.port))
		time.sleep(3)
		self.execute_command(self.commands[0])

	def execute_command(self, command):
		log("INFO", "{0} is doing command {1}".format(self.username, command))
		packet = serverbound.play.ChatPacket()
		packet.message = command
		self.bot.write_packet(packet)

	def disconnect(self):
		log("INFO", "Disconnecting {0} from {1}:{2}.".format(self.username, self.server, self.port))
		self.bot.disconnect()
		log("INFO", "{0} is disconnected of {1}:{2}.".format(self.username, self.server, self.port))
Exemple #2
0
class Player:
    """
    A class built to handle all required actions to maintain:
     - Gaining auth tokens, and connecting to online minecraft servers.
     - Clientbound chat
     - Serverbound chat

    Warnings
    --------
    This class explicitly expects a username & password, then expects to
    be able to connect to a server in online mode.
    If you wish to add different functionality please view the example
    headless client, `start.py`, for how to implement it.
    """

    def __init__(self, username, password, *, admins=None):
        """
        Init handles the following:
         - Client Authentication
         - Setting the current connection state
         - Setting the recognized 'admins' for this instance

        Parameters
        ----------
        username : String
            Used for authentication
        password : String
            Used for authentication
        admins : list, optional
            The minecraft accounts to auto accept tpa's requests from

        Raises
        ------
        YggdrasilError
            Username or Password was incorrect

        """
        self.kickout = False
        self.admins = [] if admins is None else admins

        self.auth_token = authentication.AuthenticationToken()
        self.auth_token.authenticate(username, password)

    def Parser(self, data):
        """
        Converts the chat packet received from the server
        into human readable strings

        Parameters
        ----------
        data : JSON
            The chat data json receive from the server

        Returns
        -------
        message : String
            The text received from the server in human readable form

        """
        message = DefaultParser(data)  # This is where you would call other parsers

        if not message:
            return False

        if "teleport" in message.lower():
            self.HandleTpa(message)

        return message

    def HandleTpa(self, message):
        """
        Using the given message, figure out whether or not to accept the tpa

        Parameters
        ----------
        message : String
            The current chat, where 'tpa' was found in message.lower()

        """
        try:
            found = re.search(
                "(.+?) has requested that you teleport to them.", message
            ).group(1)
            if found in self.admins:
                self.SendChat("/tpyes")
                return
        except AttributeError:
            pass

        try:
            found = re.search("(.+?) has requested to teleport to you.", message).group(
                1
            )
            if found in self.admins:
                self.SendChat("/tpyes")
                return
        except AttributeError:
            pass

    def SendChat(self, msg):
        """
        Send a given message to the server

        Parameters
        ----------
        msg : String
            The message to send to the server

        """
        msg = str(msg)
        if len(msg) > 0:
            packet = serverbound.play.ChatPacket()
            packet.message = msg
            self.connection.write_packet(packet)

    def ReceiveChat(self, chat_packet):
        """
        The listener for ClientboundChatPackets

        Parameters
        ----------
        chat_packet : ClientboundChatPacket
            The incoming chat packet
        chat_packet.json : JSON
            The chat packet to pass of to our Parser for handling

        """
        message = self.Parser(chat_packet.json_data)
        if not message:
            # This means our Parser failed lol
            print("Parser failed")
            return

        print(message)

    def SetServer(self, ip, port=25565, handler=None):
        """
        Sets the server, ready for connection

        Parameters
        ----------
        ip : str
            The server to connect to
        port : int, optional
            The port to connect on
        handler : Function pointer, optional
            Points to the function used to handle Clientbound chat packets

        """
        handler = handler or self.ReceiveChat

        self.ip = ip
        self.port = port
        self.connection = Connection(
            ip, port, auth_token=self.auth_token, handle_exception=print
        )

        self.connection.register_packet_listener(
            handler, clientbound.play.ChatMessagePacket
        )

        self.connection.exception_handler(print)

    def Connect(self):
        """
        Actually connect to the server for this player and maintain said connection

        Notes
        -----
        This is a blocking function and will not return until `Disconnect()` is called on said instance.

        """
        self.connection.connect()

        print(f"Connected to server with: {self.auth_token.username}")

        while True:
            time.sleep(1)
            if self.kickout:
                break

    def Disconnect(self):
        """
        In order to disconnect the client, and break the blocking loop
        this method must be called

        """
        self.kickout = True
        self.connection.disconnect()
Exemple #3
0
class MinecraftDiscordBridge():
    def __init__(self):
        self.return_code = os.EX_OK
        self.session_token = ""
        self.uuid_cache = bidict()
        self.webhooks = []
        self.bot_username = ""
        self.next_message_time = datetime.now(timezone.utc)
        self.previous_message = ""
        self.player_list = bidict()
        self.previous_player_list = bidict()
        self.accept_join_events = False
        self.tab_header = ""
        self.tab_footer = ""
        # Initialize the discord part
        self.discord_bot = discord.Client()
        self.config = Configuration("config.json")
        self.connection_retries = 0
        self.auth_token = None
        self.connection = None
        self.setup_logging(self.config.logging_level)
        self.database_session = DatabaseSession()
        self.logger = logging.getLogger("bridge")
        self.database_session.initialize(self.config)
        self.bot_perms = discord.Permissions()
        self.bot_perms.update(manage_messages=True, manage_webhooks=True)
        # Async http request pool
        self.req_future_session = FuturesSession(max_workers=100)
        self.reactor_thread = Thread(target=self.run_auth_server,
                                     args=(self.config.auth_port, ))
        self.aioloop = asyncio.get_event_loop()
        # We need to import twisted after setting up the logger because twisted hijacks our logging
        from . import auth_server
        auth_server.DATABASE_SESSION = self.database_session
        if self.config.es_enabled:
            if self.config.es_auth:
                self.es_logger = ElasticsearchLogger(self.req_future_session,
                                                     self.config.es_url,
                                                     self.config.es_username,
                                                     self.config.es_password)
            else:
                self.es_logger = ElasticsearchLogger(self.req_future_session,
                                                     self.config.es_url)

        @self.discord_bot.event
        async def on_ready():  # pylint: disable=W0612
            self.logger.info("Discord bot logged in as %s (%s)",
                             self.discord_bot.user.name,
                             self.discord_bot.user.id)
            self.logger.info(
                "Discord bot invite link: %s",
                discord.utils.oauth_url(client_id=self.discord_bot.user.id,
                                        permissions=self.bot_perms))
            await self.discord_bot.change_presence(
                activity=discord.Game("mc!help for help"))
            self.webhooks = []
            session = self.database_session.get_session()
            channels = session.query(DiscordChannel).all()
            session.close()
            for channel in channels:
                channel_id = channel.channel_id
                discord_channel = self.discord_bot.get_channel(channel_id)
                if discord_channel is None:
                    session = self.database_session.get_session()
                    session.query(DiscordChannel).filter_by(
                        channel_id=channel_id).delete()
                    session.close()
                    continue
                channel_webhooks = await discord_channel.webhooks()
                found = False
                for webhook in channel_webhooks:
                    if webhook.name == "_minecraft" and webhook.user == self.discord_bot.user:
                        self.webhooks.append(webhook.url)
                        found = True
                    self.logger.debug("Found webhook %s in channel %s",
                                      webhook.name, discord_channel.name)
                if not found:
                    # Create the hook
                    await discord_channel.create_webhook(name="_minecraft")

        @self.discord_bot.event
        async def on_message(message):  # pylint: disable=W0612
            # We do not want the bot to reply to itself
            if message.author == self.discord_bot.user:
                return
            this_channel = message.channel.id

            # PM Commands
            if message.content.startswith("mc!help"):
                try:
                    send_channel = message.channel
                    if isinstance(message.channel, discord.abc.GuildChannel):
                        await message.delete()
                        dm_channel = message.author.dm_channel
                        if not dm_channel:
                            await message.author.create_dm()
                        send_channel = message.author.dm_channel
                    msg = self.get_discord_help_string()
                    await send_channel.send(msg)
                    return
                except discord.errors.Forbidden:
                    if isinstance(message.author, discord.abc.User):
                        msg = "{}, please allow private messages from this bot.".format(
                            message.author.mention)
                        error_msg = await message.channel.send(msg)
                        await asyncio.sleep(3)
                        await error_msg.delete()
                    return

            elif message.content.startswith("mc!register"):
                try:
                    send_channel = message.channel
                    if isinstance(message.channel, discord.abc.GuildChannel):
                        await message.delete()
                        dm_channel = message.author.dm_channel
                        if not dm_channel:
                            await message.author.create_dm()
                        send_channel = message.author.dm_channel
                    session = self.database_session.get_session()
                    discord_account = session.query(DiscordAccount).filter_by(
                        discord_id=message.author.id).first()
                    if not discord_account:
                        new_discord_account = DiscordAccount(message.author.id)
                        session.add(new_discord_account)
                        session.commit()
                        discord_account = session.query(
                            DiscordAccount).filter_by(
                                discord_id=message.author.id).first()

                    new_token = self.generate_random_auth_token(16)
                    account_link_token = AccountLinkToken(
                        message.author.id, new_token)
                    discord_account.link_token = account_link_token
                    session.add(account_link_token)
                    session.commit()
                    msg = "Please connect your minecraft account to `{}.{}:{}` in order to link it to this bridge!"\
                        .format(new_token, self.config.auth_dns, self.config.auth_port)
                    session.close()
                    del session
                    await send_channel.send(msg)
                    return
                except discord.errors.Forbidden:
                    if isinstance(message.author, discord.abc.User):
                        msg = "{}, please allow private messages from this bot.".format(
                            message.author.mention)
                        error_msg = await message.channel.send(msg)
                        await asyncio.sleep(3)
                        await error_msg.delete()
                    return

            # Global Commands
            elif message.content.startswith("mc!chathere"):
                if isinstance(message.channel, discord.abc.PrivateChannel):
                    msg = "Sorry, this command is only available in public channels."
                    await message.channel.send(msg)
                    return
                if message.author.id not in self.config.admin_users:
                    await message.delete()
                    try:
                        dm_channel = message.author.dm_channel
                        if not dm_channel:
                            await message.author.create_dm()
                        dm_channel = message.author.dm_channel
                        msg = "Sorry, you do not have permission to execute that command!"
                        await dm_channel.send(msg)
                        return
                    except discord.errors.Forbidden:
                        if isinstance(message.author, discord.abc.User):
                            msg = "{}, please allow private messages from this bot.".format(
                                message.author.mention)
                            error_msg = await message.channel.send(msg)
                            await asyncio.sleep(3)
                            await error_msg.delete()
                        return
                session = self.database_session.get_session()
                channels = session.query(DiscordChannel).filter_by(
                    channel_id=this_channel).all()
                if not channels:
                    new_channel = DiscordChannel(this_channel)
                    session.add(new_channel)
                    session.commit()
                    session.close()
                    del session
                    webhook = await message.channel.create_webhook(
                        name="_minecraft")
                    self.webhooks.append(webhook.url)
                    msg = "The bot will now start chatting here! To stop this, run `mc!stopchathere`."
                    await message.channel.send(msg)
                else:
                    msg = "The bot is already chatting in this channel! To stop this, run `mc!stopchathere`."
                    await message.channel.send(msg)
                    return

            elif message.content.startswith("mc!stopchathere"):
                if isinstance(message.channel, discord.abc.PrivateChannel):
                    msg = "Sorry, this command is only available in public channels."
                    await message.channel.send(msg)
                    return
                if message.author.id not in self.config.admin_users:
                    await message.delete()
                    try:
                        dm_channel = message.author.dm_channel
                        if not dm_channel:
                            await message.author.create_dm()
                        dm_channel = message.author.dm_channel
                        msg = "Sorry, you do not have permission to execute that command!"
                        await dm_channel.send(msg)
                        return
                    except discord.errors.Forbidden:
                        if isinstance(message.author, discord.abc.User):
                            msg = "{}, please allow private messages from this bot.".format(
                                message.author.mention)
                            error_msg = await message.channel.send(msg)
                            await asyncio.sleep(3)
                            await error_msg.delete()
                        return
                session = self.database_session.get_session()
                deleted = session.query(DiscordChannel).filter_by(
                    channel_id=this_channel).delete()
                session.commit()
                session.close()
                for webhook in await message.channel.webhooks():
                    if webhook.name == "_minecraft" and webhook.user == self.discord_bot.user:
                        # Copy the list to avoid some problems since
                        # we're deleting indicies form it as we loop
                        # through it
                        if webhook.url in self.webhooks[:]:
                            self.webhooks.remove(webhook.url)
                        await webhook.delete()
                if deleted < 1:
                    msg = "The bot was not chatting here!"
                    await message.channel.send(msg)
                    return
                else:
                    msg = "The bot will no longer chat here!"
                    await message.channel.send(msg)
                    return

            elif message.content.startswith("mc!tab"):
                send_channel = message.channel
                try:
                    if isinstance(message.channel, discord.abc.GuildChannel):
                        await message.delete()
                        dm_channel = message.author.dm_channel
                        if not dm_channel:
                            await message.author.create_dm()
                        send_channel = message.author.dm_channel
                    player_list = ", ".join(
                        list(map(lambda x: x[1], self.player_list.items())))
                    msg = "{}\n" \
                        "Players online: {}\n" \
                        "{}".format(self.escape_markdown(
                            self.strip_colour(self.tab_header)), self.escape_markdown(
                                self.strip_colour(player_list)), self.escape_markdown(
                                    self.strip_colour(self.tab_footer)))
                    await send_channel.send(msg)
                    return
                except discord.errors.Forbidden:
                    if isinstance(message.author, discord.abc.User):
                        msg = "{}, please allow private messages from this bot.".format(
                            message.author.mention)
                        error_msg = await message.channel.send(msg)
                        await asyncio.sleep(3)
                        await error_msg.delete()
                    return

            elif message.content.startswith("mc!botlink"):
                send_channel = message.channel
                try:
                    if isinstance(message.channel, discord.abc.GuildChannel):
                        await message.delete()
                        dm_channel = message.author.dm_channel
                        if not dm_channel:
                            await message.author.create_dm()
                        send_channel = message.author.dm_channel
                    msg = "Use the following link to invite this bot to a guild:\n{}".format(
                        discord.utils.oauth_url(
                            client_id=self.discord_bot.user.id,
                            permissions=self.bot_perms))
                    await send_channel.send(msg)
                    return
                except discord.errors.Forbidden:
                    if isinstance(message.author, discord.abc.User):
                        msg = "{}, please allow private messages from this bot.".format(
                            message.author.mention)
                        error_msg = await message.channel.send(msg)
                        await asyncio.sleep(3)
                        await error_msg.delete()
                    return

            elif message.content.startswith("mc!about"):
                send_channel = message.channel
                try:
                    if isinstance(message.channel, discord.abc.GuildChannel):
                        await message.delete()
                        dm_channel = message.author.dm_channel
                        if not dm_channel:
                            await message.author.create_dm()
                        send_channel = message.author.dm_channel
                    msg = "This bot is running minecraft-discord-bridge version {}.\n" \
                          "The source code is available at https://github.com/starcraft66/minecraft-discord-bridge" \
                        .format(minecraft_discord_bridge.__version__)
                    await send_channel.send(msg)
                    return
                except discord.errors.Forbidden:
                    if isinstance(message.author, discord.abc.User):
                        msg = "{}, please allow private messages from this bot.".format(
                            message.author.mention)
                        error_msg = await message.channel.send(msg)
                        await asyncio.sleep(3)
                        await error_msg.delete()
                    return

            elif message.content.startswith("mc!"):
                # Catch-all
                send_channel = message.channel
                try:
                    if isinstance(message.channel, discord.abc.GuildChannel):
                        await message.delete()
                        dm_channel = message.author.dm_channel
                        if not dm_channel:
                            await message.author.create_dm()
                        send_channel = message.author.dm_channel
                    msg = "Unknown command, type `mc!help` for a list of commands."
                    await send_channel.send(msg)
                    return
                except discord.errors.Forbidden:
                    if isinstance(message.author, discord.abc.User):
                        msg = "{}, please allow private messages from this bot.".format(
                            message.author.mention)
                        error_msg = await message.channel.send(msg)
                        await asyncio.sleep(3)
                        await error_msg.delete()
                    return

            elif "https://discord.gg" in message.content.lower():
                await message.delete()  # Deletes the message
                # Add something more if you want to

                msg = f"{message.author.mention} invites aren't allowed!"  # Your message

                await send_channel.send(msg)

            elif not message.author.bot:
                session = self.database_session.get_session()
                channel_should_chat = session.query(DiscordChannel).filter_by(
                    channel_id=this_channel).first()
                if channel_should_chat:
                    await message.delete()
                    discord_user = session.query(DiscordAccount).filter_by(
                        discord_id=message.author.id).first()
                    if discord_user:
                        if discord_user.minecraft_account:
                            minecraft_uuid = discord_user.minecraft_account.minecraft_uuid
                            session.close()
                            del session
                            minecraft_username = self.mc_uuid_to_username(
                                minecraft_uuid)

                            # Max chat message length: 256, bot username does not count towards this
                            # Does not count|Counts
                            # <BOT_USERNAME> minecraft_username: message
                            padding = 2 + len(minecraft_username)

                            message_to_send = self.remove_emoji(
                                message.clean_content.encode('utf-8').decode(
                                    'ascii', 'replace')).strip()
                            message_to_discord = self.escape_markdown(
                                message.clean_content)

                            total_len = padding + len(message_to_send)
                            if total_len > 256:
                                message_to_send = message_to_send[:(256 -
                                                                    padding)]
                                message_to_discord = message_to_discord[:(
                                    256 - padding)]
                            elif not message_to_send:
                                return

                            session = self.database_session.get_session()
                            channels = session.query(DiscordChannel).all()
                            session.close()
                            del session
                            if message_to_send == self.previous_message or \
                                    datetime.now(timezone.utc) < self.next_message_time:
                                send_channel = message.channel
                                try:
                                    if isinstance(message.channel,
                                                  discord.abc.GuildChannel):
                                        dm_channel = message.author.dm_channel
                                        if not dm_channel:
                                            await message.author.create_dm()
                                        send_channel = message.author.dm_channel
                                    msg = "Your message \"{}\" has been rate-limited.".format(
                                        message.clean_content)
                                    await send_channel.send(msg)
                                    return
                                except discord.errors.Forbidden:
                                    if isinstance(message.author,
                                                  discord.abc.User):
                                        msg = "{}, please allow private messages from this bot.".format(
                                            message.author.mention)
                                        error_msg = await message.channel.send(
                                            msg)
                                        await asyncio.sleep(3)
                                        await error_msg.delete()
                                    return

                            self.previous_message = message_to_send
                            self.next_message_time = datetime.now(
                                timezone.utc) + timedelta(
                                    seconds=self.config.message_delay)

                            self.logger.info(
                                "Outgoing message from discord: Username: %s Message: %s",
                                minecraft_username, message_to_send)

                            for channel in channels:
                                discord_channel = self.discord_bot.get_channel(
                                    channel.channel_id)
                                if not discord_channel:
                                    session = self.database_session.get_session(
                                    )
                                    session.query(DiscordChannel).filter_by(
                                        channel_id=channel.channel_id).delete(
                                        )
                                    session.close()
                                    continue
                                webhooks = await discord_channel.webhooks()
                                for webhook in webhooks:
                                    if webhook.name == "_minecraft":
                                        await webhook.send(
                                            username=minecraft_username,
                                            avatar_url=
                                            "https://visage.surgeplay.com/face/160/{}"
                                            .format(minecraft_uuid),
                                            content=message_to_discord)

                            packet = serverbound.play.ChatPacket()
                            packet.message = "{}: {}".format(
                                minecraft_username, message_to_send)
                            self.connection.write_packet(packet)
                    else:
                        send_channel = message.channel
                        try:
                            if isinstance(message.channel,
                                          discord.abc.GuildChannel):
                                dm_channel = message.author.dm_channel
                                if not dm_channel:
                                    await message.author.create_dm()
                                send_channel = message.author.dm_channel
                            msg = "Unable to send chat message: there is no Minecraft account linked to this discord " \
                                "account, please run `mc!register`."
                            await send_channel.send(msg)
                            return
                        except discord.errors.Forbidden:
                            if isinstance(message.author, discord.abc.User):
                                msg = "{}, please allow private messages from this bot.".format(
                                    message.author.mention)
                                error_msg = await message.channel.send(msg)
                                await asyncio.sleep(3)
                                await error_msg.delete()
                            return
                        finally:
                            session.close()
                            del session
                else:
                    session.close()
                    del session

    def run(self):
        self.logger.debug(
            "Checking if the server {} is online before connecting.")

        if not self.config.mc_online:
            self.logger.info("Connecting in offline mode...")
            while not self.is_server_online():
                self.logger.info(
                    'Not connecting to server because it appears to be offline.'
                )
                time.sleep(15)
            self.bot_username = self.config.mc_username
            self.connection = Connection(
                self.config.mc_server,
                self.config.mc_port,
                username=self.config.mc_username,
                handle_exception=self.minecraft_handle_exception)
        else:
            self.auth_token = authentication.AuthenticationToken()
            try:
                self.auth_token.authenticate(self.config.mc_username,
                                             self.config.mc_password)
            except YggdrasilError as ex:
                self.logger.info(ex)
                sys.exit(os.EX_TEMPFAIL)
            self.bot_username = self.auth_token.profile.name
            self.logger.info("Logged in as %s...",
                             self.auth_token.profile.name)
            while not self.is_server_online():
                self.logger.info(
                    'Not connecting to server because it appears to be offline.'
                )
                time.sleep(15)
            self.connection = Connection(
                self.config.mc_server,
                self.config.mc_port,
                auth_token=self.auth_token,
                handle_exception=self.minecraft_handle_exception)

        self.register_handlers(self.connection)
        self.connection_retries += 1
        self.reactor_thread.start()
        self.connection.connect()
        try:
            self.aioloop.run_until_complete(
                self.discord_bot.start(self.config.discord_token))
        except (KeyboardInterrupt, SystemExit):
            # log out of discord
            self.aioloop.run_until_complete(self.discord_bot.logout())
            # log out of minecraft
            self.connection.disconnect()
            # shut down auth server
            from twisted.internet import reactor
            reactor.callFromThread(reactor.stop)
            # clean up auth server thread
            self.reactor_thread.join()
        finally:
            # close the asyncio event loop discord uses
            self.aioloop.close()
        return self.return_code

    def mc_uuid_to_username(self, mc_uuid: str):
        if mc_uuid not in self.uuid_cache:
            try:
                short_uuid = mc_uuid.replace("-", "")
                mojang_response = self.req_future_session.get(
                    "https://api.mojang.com/user/profiles/{}/names".format(
                        short_uuid)).result().json()
                if len(mojang_response) > 1:
                    # Multiple name changes
                    player_username = mojang_response[-1]["name"]
                else:
                    # Only one name
                    player_username = mojang_response[0]["name"]
                self.uuid_cache[mc_uuid] = player_username
                return player_username
            except RequestException as ex:
                self.logger.error(ex, exc_info=True)
                self.logger.error(
                    "Failed to lookup %s's username using the Mojang API.",
                    mc_uuid)
        else:
            return self.uuid_cache[mc_uuid]

    def mc_username_to_uuid(self, username: str):
        if username not in self.uuid_cache.inv:
            try:
                player_uuid = self.req_future_session.get(
                    "https://api.mojang.com/users/profiles/minecraft/{}".
                    format(username)).result().json()["id"]
                long_uuid = uuid.UUID(player_uuid)
                self.uuid_cache.inv[username] = str(long_uuid)
                return player_uuid
            except RequestException:
                self.logger.error(
                    "Failed to lookup %s's username using the Mojang API.",
                    username)
        else:
            return self.uuid_cache.inv[username]

    def get_discord_help_string(self):
        help_str = (
            "Admin commands:\n"
            "`mc!chathere`: Starts outputting server messages in this channel\n"
            "`mc!stopchathere`: Stops outputting server messages in this channel\n"
            "User commands:\n"
            "`mc!tab`: Sends you the content of the server's player/tab list\n"
            "`mc!register`: Starts the minecraft account registration process\n"
            "`mc!botlink`: Sends you the link to invite this bot to a guild\n"
            "`mc!about`: Sends you information about the running bridge\n"
            "To start chatting on the minecraft server, please register your account using `mc!register`."
        )
        return help_str

    # https://stackoverflow.com/questions/33404752/removing-emojis-from-a-string-in-python
    def remove_emoji(self, dirty_string):
        emoji_pattern = re.compile(
            "["
            u"\U0001F600-\U0001F64F"  # emoticons
            u"\U0001F300-\U0001F5FF"  # symbols & pictographs
            u"\U0001F680-\U0001F6FF"  # transport & map symbols
            u"\U0001F1E0-\U0001F1FF"  # flags (iOS)
            u"\U0001F900-\U0001FAFF"  # CJK Compatibility Ideographs
            # u"\U00002702-\U000027B0"
            # u"\U000024C2-\U0001F251"
            "]+",
            flags=re.UNICODE)
        return emoji_pattern.sub(r'', dirty_string)

    def escape_markdown(self, md_string):
        # Don't mess with urls
        url_regex = re.compile(
            r'^(?:http|ftp)s?://'  # http:// or https://
            r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # domain...
            r'localhost|'  # localhost...
            r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|'  # ...or ipv4
            r'\[?[A-F0-9]*:[A-F0-9:]+\]?)'  # ...or ipv6
            r'(?::\d+)?'  # optional port
            r'(?:/?|[/?]\S+)$',
            re.IGNORECASE)
        escaped_string = ""
        # Split the message into pieces, each "word" speparated into a string is a piece
        # Discord ignores formatting characters in urls so we can't just replace the whole
        # string... We need to go through words one by one to find out what is a url (don't)
        # escape) and what isn't (escape).
        for piece in md_string.split(" "):
            if url_regex.match(piece):
                escaped_string += "{} ".format(piece)
                continue
            # Absolutely needs to go first or it will replace our escaping slashes!
            piece = piece.replace("\\", "\\\\")
            piece = piece.replace("_", "\\_")
            piece = piece.replace("*", "\\*")
            escaped_string += "{} ".format(piece)
        if escaped_string.startswith(">"):
            escaped_string = "\\" + escaped_string
        escaped_string.strip()
        return escaped_string

    def strip_colour(self, dirty_string):
        colour_pattern = re.compile(
            u"\U000000A7"  # selection symbol
            ".",
            flags=re.UNICODE)
        return colour_pattern.sub(r'', dirty_string)

    def setup_logging(self, level):
        if level.lower() == "debug":
            log_level = logging.DEBUG
        else:
            log_level = logging.INFO
        log_format = "%(asctime)s:%(name)s:%(levelname)s:%(message)s"
        logging.basicConfig(filename="bridge_log.log",
                            format=log_format,
                            level=log_level)
        stdout_logger = logging.StreamHandler(sys.stdout)
        stdout_logger.setFormatter(logging.Formatter(log_format))
        logging.getLogger().addHandler(stdout_logger)

    def run_auth_server(self, port):
        # We need to import twisted after setting up the logger because twisted hijacks our logging
        from twisted.internet import reactor
        from .auth_server import AuthFactory

        # Create factory
        factory = AuthFactory()

        # Listen
        self.logger.info("Starting authentication server on port %d", port)

        factory.listen("", port)
        reactor.run(installSignalHandlers=False)

    def generate_random_auth_token(self, length):
        letters = string.ascii_lowercase + string.digits + string.ascii_uppercase
        return ''.join(random.choice(letters) for i in range(length))

    def handle_disconnect(self, json_data=""):
        self.logger.info('Disconnected.')
        if json_data:
            self.logger.info("Disconnect json data: %s", json_data)
        if self.connection_retries >= self.config.failsafe_retries:
            self.logger.info(
                "Failed to join the server %s times in a row. Exiting.",
                self.connection_retries)
            self.logger.info(
                "Use a process supervisor if you wish to automatically restart the bridge."
            )
            # This is possibly a huge hack... Since we cannot reliably raise exceptions on this thread
            # for them to be caught on the main thread, we call interrupt_main to raise a KeyboardInterrupt
            # on main and tell it to shut the bridge down.
            self.return_code = os.EX_TEMPFAIL
            _thread.interrupt_main()
            return
        self.previous_player_list = self.player_list.copy()
        self.accept_join_events = False
        self.player_list = bidict()
        if self.connection.connected:
            self.logger.info(
                "Forced a disconnection because the connection is still connected."
            )
            self.connection.disconnect(immediate=True)
        time.sleep(15)
        while not self.is_server_online():
            self.logger.info(
                'Not reconnecting to server because it appears to be offline.')
            time.sleep(15)
        self.logger.info('Reconnecting.')
        self.connection_retries += 1
        self.connection.connect()

    def handle_disconnect_packet(self, disconnect_packet):
        self.handle_disconnect(disconnect_packet.json_data)

    def minecraft_handle_exception(self, exception, exc_info):
        self.logger.error("A minecraft exception occured! %s:",
                          exception,
                          exc_info=exc_info)
        self.handle_disconnect()

    def is_server_online(self):
        server = MinecraftServer.lookup("{}:{}".format(self.config.mc_server,
                                                       self.config.mc_port))
        try:
            status = server.status()
            del status
            return True
        except ConnectionRefusedError:
            return False
        # AttributeError: 'TCPSocketConnection' object has no attribute 'socket'
        # This might not be required as it happens upstream
        except AttributeError:
            return False

    def register_handlers(self, connection):
        connection.register_packet_listener(self.handle_join_game,
                                            clientbound.play.JoinGamePacket)

        connection.register_packet_listener(self.handle_chat,
                                            clientbound.play.ChatMessagePacket)

        connection.register_packet_listener(
            self.handle_health_update, clientbound.play.UpdateHealthPacket)

        connection.register_packet_listener(self.handle_disconnect_packet,
                                            clientbound.play.DisconnectPacket)

        connection.register_packet_listener(
            self.handle_tab_list, clientbound.play.PlayerListItemPacket)

        connection.register_packet_listener(
            self.handle_player_list_header_and_footer_update,
            clientbound.play.PlayerListHeaderAndFooterPacket)

    def handle_player_list_header_and_footer_update(self,
                                                    header_footer_packet):
        self.logger.debug("Got Tablist H/F Update: header=%s",
                          header_footer_packet.header)
        self.logger.debug("Got Tablist H/F Update: footer=%s",
                          header_footer_packet.footer)
        self.tab_header = json.loads(header_footer_packet.header)["text"]
        self.tab_footer = json.loads(header_footer_packet.footer)["text"]

    def handle_tab_list(self, tab_list_packet):
        self.logger.debug("Processing tab list packet")
        for action in tab_list_packet.actions:
            if isinstance(
                    action,
                    clientbound.play.PlayerListItemPacket.AddPlayerAction):
                self.logger.debug(
                    "Processing AddPlayerAction tab list packet, name: %s, uuid: %s",
                    action.name, action.uuid)
                username = action.name
                player_uuid = action.uuid
                if action.name not in self.player_list.inv:
                    self.player_list.inv[action.name] = action.uuid
                else:
                    # Sometimes we get a duplicate add packet on join idk why
                    return
                if action.name not in self.uuid_cache.inv:
                    self.uuid_cache.inv[action.name] = action.uuid
                # Initial tablist backfill
                if self.accept_join_events:
                    webhook_payload = {
                        'username':
                        username,
                        'avatar_url':
                        "https://visage.surgeplay.com/face/160/{}".format(
                            player_uuid),
                        'content':
                        '',
                        'embeds': [{
                            'color': 65280,
                            'title': '**Joined the game**'
                        }]
                    }
                    for webhook in self.webhooks:
                        self.req_future_session.post(webhook,
                                                     json=webhook_payload)
                    if self.config.es_enabled:
                        self.es_logger.log_connection(
                            uuid=action.uuid,
                            reason=ConnectionReason.CONNECTED,
                            count=len(self.player_list))
                    return
                else:
                    # The bot's name is sent last after the initial back-fill
                    if action.name == self.bot_username:
                        self.accept_join_events = True
                        if self.config.es_enabled:
                            diff = set(self.previous_player_list.keys()) - set(
                                self.player_list.keys())
                            for idx, player_uuid in enumerate(diff):
                                self.es_logger.log_connection(
                                    uuid=player_uuid,
                                    reason=ConnectionReason.DISCONNECTED,
                                    count=len(self.previous_player_list) -
                                    (idx + 1))
                        # Don't bother announcing the bot's own join message (who cares) but log it for analytics still
                        if self.config.es_enabled:
                            self.es_logger.log_connection(
                                uuid=action.uuid,
                                reason=ConnectionReason.CONNECTED,
                                count=len(self.player_list))

                if self.config.es_enabled:
                    self.es_logger.log_connection(uuid=action.uuid,
                                                  reason=ConnectionReason.SEEN)
            if isinstance(
                    action,
                    clientbound.play.PlayerListItemPacket.RemovePlayerAction):
                self.logger.debug(
                    "Processing RemovePlayerAction tab list packet, uuid: %s",
                    action.uuid)
                username = self.mc_uuid_to_username(action.uuid)
                player_uuid = action.uuid
                webhook_payload = {
                    'username':
                    username,
                    'avatar_url':
                    "https://visage.surgeplay.com/face/160/{}".format(
                        player_uuid),
                    'content':
                    '',
                    'embeds': [{
                        'color': 16711680,
                        'title': '**Left the game**'
                    }]
                }
                for webhook in self.webhooks:
                    self.req_future_session.post(webhook, json=webhook_payload)
                del self.uuid_cache[action.uuid]
                if action.uuid in self.player_list:
                    del self.player_list[action.uuid]
                if self.config.es_enabled:
                    self.es_logger.log_connection(
                        uuid=action.uuid,
                        reason=ConnectionReason.DISCONNECTED,
                        count=len(self.player_list))

    def handle_join_game(self, join_game_packet):
        self.logger.info('Connected and joined game as entity id %d',
                         join_game_packet.entity_id)
        self.player_list = bidict()
        self.connection_retries = 0

    def handle_chat(self, chat_packet):
        json_data = json.loads(chat_packet.json_data)
        if "extra" not in json_data:
            return
        chat_string = ""
        for chat_component in json_data["extra"]:
            chat_string += chat_component["text"]

        # Handle chat message
        regexp_match = re.match("<(.*?)> (.*)", chat_string, re.M | re.I)
        if regexp_match:
            username = regexp_match.group(1)
            original_message = regexp_match.group(2)
            player_uuid = self.mc_username_to_uuid(username)
            if username.lower() == self.bot_username.lower():
                # Don't relay our own messages
                if self.config.es_enabled:
                    bot_message_match = re.match(
                        "<{}> (.*?): (.*)".format(self.bot_username.lower()),
                        chat_string, re.M | re.I)
                    if bot_message_match:
                        self.es_logger.log_chat_message(
                            uuid=self.mc_username_to_uuid(
                                bot_message_match.group(1)),
                            display_name=bot_message_match.group(1),
                            message=bot_message_match.group(2),
                            message_unformatted=chat_string)
                        self.es_logger.log_raw_message(
                            msg_type=chat_packet.Position.name_from_value(
                                chat_packet.position),
                            message=chat_packet.json_data)
                return
            self.logger.info(
                "Incoming message from minecraft: Username: %s Message: %s",
                username, original_message)
            self.logger.debug("msg: %s", repr(original_message))
            message = self.escape_markdown(
                self.remove_emoji(original_message.strip().replace(
                    "@", "@\N{zero width space}")))
            webhook_payload = {
                'username':
                username,
                'avatar_url':
                "https://visage.surgeplay.com/face/160/{}".format(player_uuid),
                'content':
                '{}'.format(message)
            }
            for webhook in self.webhooks:
                self.req_future_session.post(webhook, json=webhook_payload)
            if self.config.es_enabled:
                self.es_logger.log_chat_message(
                    uuid=player_uuid,
                    display_name=username,
                    message=original_message,
                    message_unformatted=chat_string)
        if self.config.es_enabled:
            self.es_logger.log_raw_message(
                msg_type=chat_packet.Position.name_from_value(
                    chat_packet.position),
                message=chat_packet.json_data)

    def handle_health_update(self, health_update_packet):
        if health_update_packet.health <= 0:
            self.logger.debug("Respawned the player because it died")
            packet = serverbound.play.ClientStatusPacket()
            packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN
            self.connection.write_packet(packet)
Exemple #4
0
class bot:
    def __init__(self,
                 username,
                 password,
                 bot_ign,
                 reply_rate=20,
                 whitelist=False):
        self.username = username
        self.password = password
        self.bot_ign = bot_ign
        self.debug = False
        self.whitelist = whitelist

        self.reply_rate = int(reply_rate)

        self.auth_token = authentication.AuthenticationToken()
        try:
            self.auth_token.authenticate(self.username, self.password)
        except YggdrasilError as error:
            print(error)
            exit()
        print("Logged in as %s." % self.auth_token.username)
        self.connection = Connection("mc.hypixel.net",
                                     25565,
                                     auth_token=self.auth_token)

        self.command_delay = 0
        self.msgQueue = []
        self.partyQueue = []
        self.commandQueue = []
        self.msgCurrentChannel = ""
        self.party = {"inP": False, "from": "", "timestamp": 0}
        self.partyConfig = {}
        self.playercooldown = {}
        self.cooldownTimer = time.time()
        self.heartbeat = time.time() + 120
        self.heartbeatCooldown = time.time() + 120
        self.msgformat = msgformat.formats(self.bot_ign, 24)
        self.bots = {x: 0 for x in msgformat.bots if x != self.bot_ign}
        self.current_load = 0
        self.inQueue = False
        self.inQueueTime = 0
        self.muted = False
        self.muteDuration = 3600
        self.unmutetime = 0
        self.muteheartbeat = 0
        self.leaderBuffer = []
        self.mods = []
        self.whitelisted = []
        try:
            with open("whitelisted.txt", "r") as file:
                self.whitelisted = [x for x in file.read().split("\n")]
        except Exception:
            self.whitelisted = []
        print("whitelisted loaded", len(self.whitelisted))

    def initialize(self):
        self.connection.register_packet_listener(
            self.handle_join_game, clientbound.play.JoinGamePacket)
        self.connection.register_packet_listener(
            self.handle_chat, clientbound.play.ChatMessagePacket)
        self.connection.connect()

    def disconnect(self):
        self.msgQueue = []
        self.partyQueue = []
        self.friendQueue = []
        self.connection.disconnect(True)
        exit()

    def send_chat(self, text, delay=0.6, bypass=False):
        if not self.inQueue or bypass:
            text = text[:255]  # limit to 255 characters
            packet = serverbound.play.ChatPacket()
            packet.message = text
            self.connection.write_packet(packet)
            if self.debug:
                debugtext = "".join(x for x in text if x not in "-⛬⛫⛭⛮⛶_")
                print(debugtext)
        self.command_delay = time.time()
        time.sleep(delay * 1.05)

    def handle_join_game(self, packet):
        print(packet)
        self.heartbeat = time.time() - 50
        self.heartbeatCooldown = time.time() - 50
        time.sleep(0.5)
        self.send_chat("/p leave")
        print('Connected.')

    def handle_chat(self, chat_packet):
        try:
            chat_raw = str(chat_packet.json_data)
            chat_json = json.loads(chat_raw)
            msg = util.raw_to_msg(chat_json)
            if not (self.muted):
                if ("red" in chat_raw and len(msg) < 75
                        and "+]" not in msg) or self.debug:
                    debugtext = "".join(x for x in msg if x not in "-⛬⛫⛭⛮⛶_")
                    print(debugtext)

                if ("red" in chat_raw and len(msg) < 75 and "+]" not in msg):
                    mutedetect = "".join(x for x in msg if x not in "-⛬⛫⛭⛮⛶_")
                    if "Your mute will expire in" in mutedetect:
                        muted = True
                        duration = mutedetect[
                            mutedetect.index("Your mute will expire in") + 25:]
                        duration = duration.split(' Find')
                        del duration[1]
                        duration = duration.pop(0)
                        print(f'You are muted for {duration}.')

                if "extra" in chat_json:
                    # On party request
                    if chat_raw.count("/party accept") >= 2:
                        for data in chat_json["extra"]:
                            if "/party accept" in str(data):
                                user = data["clickEvent"]["value"].split()[-1]
                                if (user not in self.whitelisted
                                    ) and self.whitelist:
                                    return  # whitelist
                                if self.cooldowncheck(user, 5):
                                    return  # cooldown
                                self.partyQueue.append({
                                    "mode": "queue",
                                    "user": user
                                })
                                return
                        return

                    # On heartbeat
                    elif "HeartBeat-KeepAlive" in chat_raw and "from" not in chat_raw.lower(
                    ) and self.bot_ign in chat_raw:
                        if time.time() - self.heartbeat > 70 and self.debug:
                            self.debug = False
                        self.heartbeat = time.time()
                        onlinehb = ['bwstatsv2']
                        onlinehb.append([
                            int(time.time()), self.bot_ign,
                            min(
                                int(
                                    max(self.current_load, 1) /
                                    max(self.reply_rate, 0.1) * 100), 100)
                        ])
                        onlinehb = [
                            x for x in onlinehb if time.time() - x[0] < 130
                        ]
                        msgformat.bots = list(
                            set([
                                x[1] for x in onlinehb
                                if time.time() - x[0] < 130
                            ]))
                        self.bots = {}
                        for bot in onlinehb:
                            if bot[1] in self.bots:
                                self.bots[bot[1]] = max(
                                    bot[2], self.bots[bot[1]])
                            else:
                                self.bots[bot[1]] = bot[2]
                        if self.debug:
                            for bot in self.bots:
                                print(bot, self.bots[bot], "%")
                            # print(msgformat.bots)
                        return

                    elif "Party Leader" in chat_raw and "●" in chat_raw:
                        leader = [
                            leader
                            for leader in msg[msg.index(":") + 1:].split("●")
                            if len(leader) > 1
                        ]
                        leader = [leader.split()[-1] for leader in leader]
                        leader = leader.pop(0)
                        self.leaderBuffer.append(leader)

                    elif "Party Moderators" in chat_raw and "●" in chat_raw:
                        mods = [
                            mods
                            for mods in msg[msg.index(":") + 1:].split("●")
                            if len(mods) > 1
                        ]
                        mods = [mods.split()[-1] for mods in mods]
                        self.mods.append(self.leaderBuffer[0])

                    # On party list return
                    elif "Party Members" in chat_raw and "●" in chat_raw:
                        # Party members (2): [VIP] MinuteBrain ● [MVP+] Its_Me_Me ●
                        users = [
                            user
                            for user in msg[msg.index(":") + 1:].split("●")
                            if len(user) > 1
                        ]
                        users = [user.split()[-1]
                                 for user in users]  # remove ranks
                        users.append(self.leaderBuffer[0])
                        users.extend(self.mods)
                        self.leaderBuffer = []
                        self.mods = []
                        users.remove(self.bot_ign)  # remove bot from the list
                        self.partyQueue = [{
                            "mode": "list",
                            "user": users
                        }] + self.partyQueue  # put on top of the queue
                        return

                    # On msg request
                    elif ("From "
                          in chat_raw) and ("light_purple"
                                            in chat_raw) and (self.bot_ign
                                                              not in chat_raw):
                        self.chat_msg(msg)
                        return

                    # On open PM channel
                    elif {
                            "text": " for the next 5 minutes. Use ",
                            "color": "green"
                    } in chat_json["extra"]:
                        user = msg[msg.index("with") +
                                   4:msg.index("for")].split()[-1]
                        #print(user)
                        self.msgCurrentChannel = user
                        return

                    # On friend request
                    elif ("Click to" in chat_raw) and ("/f accept "
                                                       in chat_raw):
                        for data in chat_json["extra"]:
                            if "/f accept " in str(data).lower():
                                user = data["clickEvent"]["value"].split()[-1]
                                if self.cooldowncheck(user, 2):
                                    return  # cooldown
                                self.commandQueue.append({
                                    "command": "friend_request",
                                    "user": user
                                })
                                return
                        return

                    # On queue
                    elif ("The game starts in" in chat_raw) or (
                            "has joined" in msg and "/" in msg
                    ) or ("has quit" in msg
                          and "/" in msg) or ("The party leader, " in chat_raw
                                              and "yellow" in chat_raw):
                        if not (self.inQueue
                                ) and time.time() - self.inQueueTime > 5:
                            self.inQueue = True
                            self.inQueueTime = time.time()
                            print("Blocked - " + self.party["from"])
                            self.cooldowncheck(self.party["from"], 60)
                            self.party["inP"] = True
                            self.party["timestamp"] = time.time() + 99999
                            time.sleep(1)
                            self.party["inP"] = True
                            self.send_chat("/pchat :( hey! don't warp me!",
                                           0.07, True)
                            self.send_chat("/p leave", 1, True)
                            self.send_chat("/hub bw", 0.7, True)
                            for _ in range(15):
                                self.send_chat("/golimbo", 0.07, True)
                            self.party["inP"] = True
                            self.party["timestamp"] = time.time() + 5
                            self.inQueue = False
                            self.inQueueTime = time.time()
                        return

                    # On whereami respond
                    elif "You are currently connected to server" in msg and "aqua" in chat_raw.lower(
                    ):
                        if "lobby" in msg:
                            self.commandQueue.append({"command": "in_lobby"})
                        else:
                            self.commandQueue.append({"command": "in_game"})
                        return
            else:  # while muted
                print('idk ur muted or smth')

        except Exception as error_code:
            print("chat handle error!!", error_code)

    def chat_msg(self, msg):
        # >>> msg = 'From [MVP+] FatDubs: FatDubs gamerboy80 5'
        if "+send" not in msg.lower():
            msg = "".join([
                char for char in msg if char.lower() in
                "[]:abcdefghijklmnopqrstuvwxyz0123456789_ +/"
            ])
        msg = " ".join(msg.split())  # remove double space
        msg = msg.replace("+ ", "+").replace("++", "+").replace("+]", "]")
        user = msg[:msg.index(":")].split()[-1]
        if (user not in self.whitelisted) and self.whitelist:
            return  # whitelist
        agus = msg[msg.index(":") + 1:].split()
        # user = '******'
        # agus = ['FatDubs', 'gamerboy80', '5']

        mode = 0  # stats mode
        if len(agus) > 1 and "+send" not in "".join(agus).lower():
            mode = agus[-1]
            if mode in [str(x) for x in range(6)]:
                mode = int(mode)
                agus.pop(-1)
            else:
                mode = 0

        # user = '******'
        # agus = ['FatDubs', 'gamerboy80']
        # mode = 5

        if self.cooldowncheck(user, 1) and user.lower() not in ["fatdubs"]:
            return  # player cooldown

        # commands
        if "+" in msg:
            command = agus[0]

            if command.lower() == "+send" and user.lower() in [
                    "fatdubs"
            ] and len(agus) >= 2:
                self.commandQueue.append({
                    "command": "send_command",
                    "send": " ".join(agus[1:])
                })

            elif command.lower() == "+limbo" and user.lower() in ["fatdubs"]:
                print("Warp to Limbo")
                for _ in range(15):
                    self.send_chat("/golimbo", 0.07, True)
                self.send_chat("/whereami")

            elif command.lower() == "+whitelist" and user.lower() in [
                    "fatdubs"
            ] and len(agus) >= 2:
                self.whitelistChange = ("".join(agus[1:]))
                print('whitelist queue - ' + self.whitelistChange)

            elif command.lower() == "+stop" and user.lower() in ["fatdubs"]:
                print('disconnecting..')
                self.disconnect()

            elif command.lower() in ["+pmode", "+setpartymode"]:
                self.partyConfig[user] = mode
                self.msgQueue = [{
                    "msgMode": "party_mode",
                    "user": user,
                    "mode": mode
                }] + self.msgQueue

            elif command.lower() == "+resetcooldown" and user.lower() in [
                    "fatdubs"
            ]:
                self.playercooldown = {}
                print('reset cooldown')

            elif command.lower() in ["+reload", "+reloadall"] and user.lower(
            ) in ["fatdubs"] + [x.lower() for x in list(self.bots)]:
                print("Reloading...")
                try:
                    reload(msgformat)
                    self.msgformat = msgformat.formats(self.bot_ign, 24)
                    self.bots = {
                        x: 0
                        for x in msgformat.bots if x != self.bot_ign
                    }
                except Exception:
                    print("Fail to reload msgformat")
                try:
                    reload(hypixelapi)
                except Exception:
                    print("Fail to reload hypixelapi")

            elif command.lower() == "+debug" and user.lower() in ["fatdubs"]:
                self.debug = not (self.debug)
                print(f"Debug : {self.debug}")
            else:
                self.msgQueue = [{
                    "msgMode": "wrong_syntax",
                    "user": user
                }] + self.msgQueue

            return

        # stats request
        if self.current_load <= self.reply_rate:
            if len(agus) > 0:
                if len(agus[0]) <= 16:
                    if len(agus) == 1:
                        self.msgQueue = [{
                            "msgMode": "stats",
                            "replyto": user,
                            "username": agus[0],
                            "mode": mode
                        }] + self.msgQueue
                    elif len(agus) > 1 and len(agus) <= 4:
                        self.msgQueue = [{
                            "msgMode": "stats_multiple",
                            "replyto": user,
                            "username": agus,
                            "mode": mode
                        }] + self.msgQueue
                    else:
                        self.msgQueue = [{
                            "msgMode": "wrong_syntax",
                            "user": user
                        }] + self.msgQueue
                else:
                    self.msgQueue = [{
                        "msgMode": "wrong_syntax",
                        "user": user
                    }] + self.msgQueue
            else:
                self.msgQueue = [{
                    "msgMode": "wrong_syntax",
                    "user": user
                }] + self.msgQueue

    def cooldowncheck(self, user, n=1):
        if user not in self.playercooldown:
            self.playercooldown[user] = n
        else:
            if self.playercooldown[user] > 6:
                self.playercooldown[user] += 3
            else:
                self.playercooldown[user] += n

        if self.playercooldown[user] > 100 and user.lower() not in ["fatdubs"]:
            self.commandQueue.append({"command": "ignore", "user": user})
            print("Ignored", user, self.playercooldown[user])
            return True
        elif self.playercooldown[user] > 6 and user.lower() not in ["fatdubs"]:
            print("Reject spam from", user, self.playercooldown[user])
            return True
        else:
            self.current_load += 1
            return False

    def cooldown_tick(self):
        if time.time() - self.cooldownTimer >= 6:
            self.cooldownTimer = time.time()
            for user in list(self.playercooldown):
                self.playercooldown[user] -= 1
            self.playercooldown = {
                x: self.playercooldown[x]
                for x in list(self.playercooldown)
                if self.playercooldown[x] > 0
            }

    def msg_tick(self):
        if len(self.msgQueue) > 0:
            currentQueue = self.msgQueue.pop(0)
            if currentQueue["msgMode"] == "stats":
                replyTo = currentQueue["replyto"]
                username = currentQueue["username"]
                if currentQueue["username"].lower() == "me":
                    username = currentQueue["replyto"]
                mode = currentQueue["mode"]
                if self.msgCurrentChannel != replyTo:
                    while time.time() - self.command_delay < 0.5:
                        time.sleep(0.05)
                    self.send_chat("/r", 0)
                data = hypixelapi.getPlayer(username, hypixelapi.nextKey())
                raw = hypixelapi.convert(data, mode, "msg")
                msg = self.msgformat.msg(raw,
                                         replyTo.lower() == username.lower())
                while time.time() - self.command_delay < 0.7:
                    time.sleep(0.05)
                if replyTo.lower() == self.msgCurrentChannel.lower():
                    print(f"(R) {replyTo} --> {username}")
                    self.send_chat(msg, 0.4)
                else:
                    if hypixelapi.getPlayer(
                            replyTo, hypixelapi.nextKey())["msgsetting"]:
                        print(f"(MSG) {replyTo} --> {username}")
                        self.send_chat(f"/msg {replyTo} " + msg, 0.4)
                    else:
                        print(f"(MSG) Couldn't reply to {replyTo}")
                self.msgCurrentChannel = ""

            if currentQueue["msgMode"] == "stats_multiple":
                replyTo = currentQueue["replyto"]
                usernames = currentQueue["username"]
                mode = currentQueue["mode"]
                #util.dict_increment(self.quotaChange,replyTo,len(usernames))
                if self.msgCurrentChannel != replyTo:
                    while time.time() - self.command_delay < 0.6:
                        time.sleep(0.05)
                    self.send_chat("/r", 0)
                handle = util.multithreading(usernames, mode)
                handle.start()
                raws = [handle.output[x] for x in list(handle.output)]
                msg = list(self.msgformat.party(raws, mode))[0]
                msg = msgformat.insertInvis(msg, 20)
                while time.time() - self.command_delay < 0.7:
                    time.sleep(0.05)
                if replyTo.lower() == self.msgCurrentChannel.lower():
                    print(f"(R) {replyTo} --> {usernames}")
                    self.send_chat(msg, 0.4)
                else:
                    if hypixelapi.getPlayer(
                            replyTo, hypixelapi.nextKey())["msgsetting"]:
                        print(f"(MSG) {replyTo} --> {username}")
                        self.send_chat(f"/msg {replyTo} " + msg, 0.4)
                    else:
                        print(f"(MSG) Can't send msg {replyTo}")
                self.msgCurrentChannel = ""

            elif currentQueue["msgMode"] == "wrong_syntax":
                print(f"Wrong_syntax: {currentQueue['user']}")
                while time.time() - self.command_delay < 0.5:
                    time.sleep(0.05)
                self.send_chat("/r " + self.msgformat.wrong_syntax(), 0.5)

            elif currentQueue["msgMode"] == "party_mode":
                print(
                    f"Party Mode: {currentQueue['user']} --> {currentQueue['mode']}"
                )
                while time.time() - self.command_delay < 0.5:
                    time.sleep(0.05)
                self.send_chat(
                    "/r " + self.msgformat.party_mode(currentQueue["mode"]),
                    0.5)

    def party_chat_transit(self, msg, delay=0.5):
        while len(self.msgQueue) > 0:
            self.msg_tick()
            time.sleep(0.05)
        while time.time() - self.command_delay < delay:
            time.sleep(0.05)
        self.send_chat(msg, delay)
        self.party["timestamp"] = time.time()

    def party_tick(self):
        if len(self.partyQueue) > 0 and len(self.msgQueue) == 0:
            currentQueue = self.partyQueue.pop(0)
            if currentQueue["mode"] == "queue" and self.party[
                    "inP"]:  # requeue if in party
                #print("Party Requeued !!")
                self.partyQueue.append(currentQueue)
            else:
                if currentQueue["mode"] == "queue":
                    self.party = {"inP": True, "from": currentQueue["user"]}
                    #util.dict_increment(self.quotaChange,self.party["from"],1)
                    while time.time() - self.command_delay < 0.5:
                        time.sleep(0.05)  # prevent sending command too fast
                    self.party_chat_transit(f"/p accept {self.party['from']}",
                                            0.4)
                    self.party_chat_transit(f"/pl", 0.3)
                elif currentQueue["mode"] == "list":
                    users = currentQueue['user']
                    print("Party list -", " ".join(users))
                    for user in users:
                        if user.lower() != self.bot_ign and user in list(
                                self.bots):
                            print("multiple bots in party!!!!!")
                            self.cooldowncheck(self.party["from"], 20)
                            while time.time() - self.command_delay < 2:
                                self.msg_tick()
                                time.sleep(0.05)
                            self.send_chat("/p leave")
                            self.party["inP"] = False
                            return
                    if len(users) <= self.msgformat.party_max:
                        if self.party["from"] in self.partyConfig:
                            mode = self.partyConfig[self.party["from"]]
                        else:
                            mode = 0
                        handle = util.multithreading(users, mode)
                        handle.start()
                        raws = [handle.output[x] for x in list(handle.output)]
                        msgs = self.msgformat.party(raws, mode)
                        while time.time() - self.command_delay < 0.3:
                            time.sleep(0.05)
                        for msg in msgs:
                            self.party_chat_transit(f"/pchat {msg}", 0.3)
                    else:
                        while time.time() - self.command_delay < 0.3:
                            time.sleep(0.05)
                        self.party_chat_transit(
                            "/pchat " + self.msgformat.party_too_large(), 0.3)
                    while time.time() - self.command_delay < 1:
                        self.msg_tick()
                        time.sleep(0.05)
                    self.send_chat("/p leave")
                    self.party["inP"] = False
        if self.party["inP"] and time.time() - self.party["timestamp"] > 2:
            print("Party timeout", self.party["from"])
            while time.time() - self.command_delay < 0.8:
                time.sleep(0.05)
            self.send_chat("/p leave", 0.3)
            self.party["inP"] = False

    def command_tick(self):
        if len(self.commandQueue) > 0:
            currentQueue = self.commandQueue.pop(0)

            if currentQueue["command"] == "friend_request":
                print(f"Friend accepted - {currentQueue['user']}")
                while time.time() - self.command_delay < 0.7:
                    time.sleep(0.05)
                self.send_chat(f"/f accept {currentQueue['user']}", 0.3)

            elif currentQueue["command"] == "send_command":
                print(f"Command sent - {currentQueue['send']}")
                while time.time() - self.command_delay < 0.7:
                    time.sleep(0.05)
                self.send_chat(currentQueue["send"], 0.3, True)

            elif currentQueue["command"] == "in_game":
                print("Warp to Lobby")
                while time.time() - self.command_delay < 0.5:
                    time.sleep(0.05)
                self.send_chat("/hub bw", 0.3, True)

            elif currentQueue["command"] == "in_lobby":
                print("Warp to Limbo")
                for _ in range(15):
                    self.send_chat("/golimbo", 0.07, True)
                self.send_chat("/whereami")

            elif currentQueue["command"] == "ignore":
                print(f"Ignored - {currentQueue['user']}")
                while time.time() - self.command_delay < 0.7:
                    time.sleep(0.05)
                self.send_chat(f"/ignore add {currentQueue['user']}")

    def heartbeat_tick(self):
        if time.time() - self.heartbeat > 610:
            self.connection.disconnect(True)
            raise Exception("No heartbeat detect! Reconnecting")
            return

        if time.time() - self.heartbeat > 120 and not (self.debug):
            #self.debug = True
            self.connection.connect()
            #print("Debug : True")
            print("Reconnecting")

        if time.time() - self.heartbeat > 60 and time.time(
        ) - self.heartbeatCooldown > 30:
            heartbeat_length = time.time() - self.heartbeat
            random_msg = "".join(
                [chr(random.randint(64, 125)) for _ in range(30)])
            while time.time() - self.command_delay < 0.5:
                time.sleep(0.7)
            self.send_chat(
                f"/msg {self.bot_ign} HeartBeat-KeepAlive {random_msg}",
                0.3)  #comment this to test heartbeat restart system.
            self.heartbeatCooldown = time.time()
            self.send_chat("/whereami", 0.2)

            if self.current_load > self.reply_rate:
                print("Overloaded!! <-----")
            self.current_load = 0

            if time.time() - self.heartbeat > 300:
                self.connection.connect()
                print("Reconnecting")

            print(f"Heartbeat ({int(heartbeat_length)}sec)")
            return

    def tick(self):
        self.heartbeat_tick()
        try:
            self.party_tick()
            self.msg_tick()
            self.command_tick()
            self.cooldown_tick()
        except Exception as error_code:
            print("Tick error! (skiped) -", error_code)
Exemple #5
0
class Minecraft():
    def __init__(self,
                 username,
                 password=None,
                 server=None,
                 versions=("1.12.2", "1.12.2"),
                 auto_connect=False):
        self.username = username
        self.password = password
        self.server = server
        self.versions = versions

        self.event = lambda x: print(x)

        self.auth_token = authentication.AuthenticationToken(
            username=self.username, access_token=self.password)
        print("authenticated: %s" % self.auth_token.authenticate(
            self.username, self.password, invalidate_previous=False))
        self.connection = Connection(self.server,
                                     auth_token=self.auth_token,
                                     allowed_versions=self.versions)

        self.connection.register_packet_listener(lambda x: self.print_chat(x),
                                                 ChatMessagePacket)

        print(self.auth_token)
        print(self.connection)

        if auto_connect:
            self._loop_reconect()

    def _loop_reconect(self):
        Timer(30.0, self._loop_reconect).start()
        try:
            self.connect()
        except:
            pass

    def add_chat_event(self, event):
        self.event = event

    def connect(self):
        self.connection.connect()

    def disconnect(self, immediate=False):
        self.connection.disconnect(immediate=immediate)

    def send_message(self, message):
        if message.strip() == "" or message == None:
            return False
        packet = ChatPacket()
        packet.message = message
        self.connection.write_packet(packet)

    def print_chat(self, chat_packet):
        try:
            a = json.loads(chat_packet.json_data)
        except Exception as e:
            pass
        else:
            self.event(self.parse_chat(a))

    def parse_chat(self, chat_data):
        try:
            chat_data["extra"][0]["extra"]
            try:
                return (True, self.parse_chat(chat_data["extra"][0])[1])
            except Exception as e:
                print(e)
        except:
            try:
                return (False, "".join([x["text"]
                                        for x in chat_data["extra"]]))
            except:
                pass
            return
Exemple #6
0
class Player:
    __retries = 0

    def __init__(self, account: str, password: str, server_address: str,
                 port: int, version: int, auto_reconnect: bool,
                 auto_respawn: bool, lang: Lang):
        self.__email = account
        self.__password = base64.b64encode(password.encode())
        self.__lang = lang

        self.__logger = logging.getLogger("Auth")
        logging.basicConfig(level=logging.INFO)

        tokens = self.__get_tokens()
        self.__auth = authentication.AuthenticationToken(
            username=self.__email,
            access_token=tokens["access"],
            client_token=tokens["client"])
        self.auth()

        self.__auto_reconnect = auto_reconnect
        self.__auto_respawn = auto_respawn
        self.__connection = Connection(address=server_address,
                                       port=port,
                                       initial_version=version,
                                       auth_token=self.__auth)
        if not self.__auth.authenticated:
            return
        self.username = self.__auth.profile.name

        self.__logger = logging.getLogger(self.username)

        self.__connection.register_packet_listener(
            self.handle_join_game, clientbound.play.JoinGamePacket)
        self.__connection.register_packet_listener(
            self.print_chat, clientbound.play.ChatMessagePacket)
        self.__connection.register_packet_listener(
            self.handle_disconnect, clientbound.play.DisconnectPacket)
        self.__connection.register_packet_listener(
            self.handle_health_change, clientbound.play.UpdateHealthPacket)
        self.__connection.register_exception_handler(self.handle_exception)
        try:
            self.__connection.connect()
        except Exception as e:
            self.__logger.error(str(e))
            self.__retry()

    # def connect(self, ip, port):
    #     self.__init(self.username Connection)

    def __get_tokens(self) -> dict:
        try:
            with open('./data.json', 'r') as fs:
                auth = json.load(fs)
        except FileNotFoundError:
            return {"access": None, "client": None}
        else:
            if self.__email in auth:
                return auth[self.__email]
            else:
                return {"access": None, "client": None}

    def __refresh_tokens(self, access: str, client: str):
        auth = {}
        try:
            with open('./data.json', 'r') as fs:
                auth = json.load(fs)
        except FileNotFoundError:
            pass
        finally:
            auth[self.__email] = {"access": access, "client": client}
            with open('./data.json', 'w') as fs:
                json.dump(auth, fs, indent=2)

    def auth(self):
        try:
            self.__auth.refresh()
        except YggdrasilError:
            self.__login()
        except ValueError:
            self.__login()
        else:
            self.__logger.info(
                self.__lang.lang("main.auth.still_valid").format(
                    email=self.__email))
            self.__refresh_tokens(access=self.__auth.access_token,
                                  client=self.__auth.client_token)

    def __login(self):
        self.__logger.info(
            self.__lang.lang("main.auth.login").format(email=self.__email))
        try:
            self.__auth.authenticate(username=self.__email,
                                     password=base64.b64decode(
                                         self.__password).decode())
        except YggdrasilError as e:
            self.__logger.error(
                self.__lang.lang("main.auth.error").format(email=self.__email,
                                                           message=str(e)))
        else:
            self.__refresh_tokens(access=self.__auth.access_token,
                                  client=self.__auth.client_token)

    def reconnect(self):
        try:
            self.__connection.connect()
        except Exception as e:
            self.__logger.error(str(e))
            self.__retry()

    # noinspection PyUnusedLocal
    def handle_join_game(self, join_game_packet):
        self.__logger.info(
            self.__lang.lang("player.connected").format(
                server=self.__connection.options.address,
                port=self.__connection.options.port))
        self.__retries = 0
        packet = serverbound.play.ClientSettingsPacket()
        packet.locale = self.__lang.lang_name
        packet.view_distance = 10
        packet.chat_mode = packet.ChatMode.FULL
        packet.chat_colors = False
        packet.displayed_skin_parts = packet.SkinParts.ALL
        packet.main_hand = AbsoluteHand.RIGHT
        self.__connection.write_packet(packet)

    def print_chat(self, chat_packet):
        self.__logger.info("[{position}] {message}".format(
            position=chat_packet.field_string('position'),
            message=self.__lang.parse_json(json.loads(chat_packet.json_data))))

    def handle_disconnect(self, disconnect_packet):
        self.__logger.warning(
            self.__lang.lang("player.connection.lost").format(
                reason=self.__lang.parse_json(
                    json.loads(disconnect_packet.json_data))))
        if self.__auto_reconnect:
            self.__retry()

    def handle_health_change(self, health_packet):
        self.__logger.warning(
            self.__lang.lang("player.health.changed").format(
                health=str(health_packet.health),
                food=str(health_packet.food),
                saturation=str(health_packet.food_saturation)))

        if self.__auto_respawn and health_packet.health == 0:
            self.__logger.info(self.__lang.lang("player.respawn.hint"))
            timer = threading.Timer(1.0, self.respawn)
            timer.start()

    def handle_exception(self, e, info):
        if type(info[1]) == LoginDisconnect:
            message = str(e).replace(
                'The server rejected our login attempt with: "',
                '').replace('".', '')
            try:
                self.__logger.error(
                    self.__lang.lang("player.connection.rejected").format(
                        reason=self.__lang.parse_json(json.loads(message))))
            except json.decoder.JSONDecodeError:
                self.__logger.error(
                    self.__lang.lang("player.connection.rejected").format(
                        reason=message))
        elif type(info[1]) == YggdrasilError:
            self.__logger.error(self.__lang.lang("player.session.expired"))
            self.auth()
            timer = threading.Timer(1.0, self.reconnect)
            timer.start()
            return
        else:
            self.__logger.error("{type}: {message}".format(type=type(info[1]),
                                                           message=str(e)))
        if self.__auto_reconnect:
            if not self.__connection.connected:
                self.__retry()

    def __retry(self):
        self.__retries += 1
        if self.__retries >= 6:
            self.__retries = 0
            return
        self.__logger.info(
            self.__lang.lang("player.connection.retry").format(
                times=str(self.__retries)))
        timer = threading.Timer(5.0, self.reconnect)
        timer.start()

    def respawn(self):
        packet = serverbound.play.ClientStatusPacket()
        packet.action_id = serverbound.play.ClientStatusPacket.RESPAWN
        self.__connection.write_packet(packet)
        self.__logger.info(self.__lang.lang("player.respawned"))

    def disconnect(self):
        self.__connection.disconnect()
        self.__logger.info(self.__lang.lang("player.disconnected"))

    def toggle_auto_respawn(self):
        self.__auto_respawn = not self.__auto_respawn
        self.__logger.info(
            self.__lang.lang("player.auto_respawn.toggle").format(
                value=self.__auto_respawn))

    def toggle_auto_reconnect(self):
        self.__auto_reconnect = not self.__auto_reconnect
        self.__logger.info(
            self.__lang.lang("player.auto_reconnect.toggle").format(
                value=self.__auto_reconnect))

    def chat(self, text: str):
        if text == "":
            return
        packet = serverbound.play.ChatPacket()
        packet.message = text
        self.__connection.write_packet(packet)