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))
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()
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)
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)
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
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)