<commit_before>import asyncio
import logging
import re
import typing

import discord
from discord.ext import commands
from discord.ext.commands import Context

from cogbot import checks
from cogbot.cog_bot import CogBot, ServerId
from cogbot.extensions.helpchat.help_chat_server_state import HelpChatServerState

log = logging.getLogger(__name__)


class HelpChat:
    def __init__(self, bot: CogBot, ext: str):
        self.bot: CogBot = bot
        self.server_state: typing.Dict[ServerId, HelpChatServerState] = {}
        self.options = self.bot.state.get_extension_state(ext)
        self.server_options: typing.Dict[ServerId, typing.Any] = {}
        self.ext = ext
        self.readied = False

    def get_state(self, server: discord.Server) -> HelpChatServerState:
        return self.server_state.get(server.id)

    def set_state(self, server: discord.Server, state: HelpChatServerState):
        self.server_state[server.id] = state

    def remove_state(self, server: discord.Server):
        del self.server_state[server.id]

    async def create_state(
        self, server: discord.Server, server_options: dict
    ) -> HelpChatServerState:
        state = HelpChatServerState(self.ext, self.bot, server, **server_options)
        self.set_state(server, state)
        await state.on_ready()
        return state

    async def setup_state(self, server: discord.Server) -> HelpChatServerState:
        server_options = self.server_options[server.id]
        # if options is just a string, use remote/external location
        if isinstance(server_options, str):
            state_address = server_options
            try:
                log.info(
                    f"Loading state data for server {server} from: {state_address}"
                )
                actual_server_options = await self.bot.load_json(state_address)
                log.info(f"Successfully loaded state data for server {server}.")
            except:
                log.exception(
                    f"Failed to load state data for server {server}. Skipping..."
                )
                return
            state = await self.create_state(server, actual_server_options)
        # otherwise, use embedded/local config
        else:
            state = await self.create_state(server, server_options)
        return state

    async def reload_state(self, server: discord.Server, ctx: Context = None):
        # remember and remove the old state object
        old_state = self.get_state(server)
        self.remove_state(server)
        # let the user know things are happening
        if ctx:
            old_state.log.info(f"{ctx.message.author} initiated a state reload.")
            await old_state.log_to_channel(
                emoji="🔄",
                description=f"initiated a state reload. Stand by...",
                message=ctx.message,
                actor=ctx.message.author,
                color=discord.Color.blue(),
            )
            await self.bot.add_reaction(ctx.message, "🤖")
        else:
            old_state.log.info(f"Initiated an automatic state reload.")
            await old_state.log_to_channel(
                emoji="🔄",
                description="Initiated an automatic state reload. Stand by...",
                color=discord.Color.blue(),
            )
        # print channel health check, before reload
        await old_state.log_to_channel(
            emoji="🔄",
            description="Channel health check, before reload:\n"
            + await self.get_channels_check_message(old_state),
            color=discord.Color.blue(),
        )
        # create and set the new state object
        old_state.log.info(f"Creating new state object...")
        await old_state.log_to_channel(
            emoji="🔄",
            description="Setting up new state...",
            color=discord.Color.blue(),
        )
        new_state = None
        try:
            new_state = await self.setup_state(server)
            new_state.log.info(f"Successfully created new state object: {new_state}")
            if ctx:
                await self.bot.add_reaction(ctx.message, "👍")
        except:
            old_state.log.exception(f"Failed to create new state object.")
            await old_state.log_to_channel(
                emoji="🔥",
                description="State reload FAILED! Have a nice day!",
                color=discord.Color.red(),
            )
            if ctx:
                await self.bot.add_reaction(ctx.message, "🔥")
            return
        # destroy the old state object
        new_state.log.info(f"Destroying old state object: {old_state}")
        if await old_state.destroy():
            new_state.log.info(f"Successfully destroyed old state object.")
            if ctx:
                await self.bot.add_reaction(ctx.message, "😄")
        else:
            new_state.log.error(f"Failed to destroy old state object.")
            if ctx:
                await self.bot.add_reaction(ctx.message, "😬")
        # print another channel health check, after reload
        await new_state.log_to_channel(
            emoji="🔄",
            description="Channel health check, after reload:\n"
            + await self.get_channels_check_message(new_state),
            color=discord.Color.blue(),
        )
        # let the user know we're done
        await new_state.log_to_channel(
            emoji="🔄",
            description="State reload complete! Have a nice day!",
            color=discord.Color.blue(),
        )
        if ctx:
            await self.bot.add_reaction(ctx.message, "👌")

    async def on_ready(self):
        # because on_ready can be called more than once
        # and because disconnection might invalidate the client cache
        # (including all existing in-memory objects)
        if self.readied:
            log.warning(
                "Hook on_ready() was called again; previous state objects will be recreated..."
            )
        else:
            log.info(f"Identifying servers and their channels for the first time...")
            self.readied = True
        # construct server state objects for easier context management
        for server_key, server_options in self.options.get("servers", {}).items():
            server = self.bot.get_server_from_key(server_key)
            if not server:
                log.error(f"Skipping unknown server {server_key}.")
                continue
            self.server_options[server.id] = server_options
            # if previous state exists, recreate it and log some important things
            if self.get_state(server):
                await self.reload_state(server)
            # otherwise, just setup a new state
            await self.setup_state(server)

    async def get_channels_check_message(self, state: HelpChatServerState) -> str:
        lines = []
        for cached_ch in state.channels:
            actual_ch: discord.Channel = self.bot.get_channel(cached_ch.id)
            # pointing to the same object
            if cached_ch is actual_ch:
                lines.append(f"👌 {cached_ch.name}")<commit_msg>[helpchat] Fix on_ready sometimes setting up a new state twice<commit_after>import asyncio
import logging
import re
import typing

import discord
from discord.ext import commands
from discord.ext.commands import Context

from cogbot import checks
from cogbot.cog_bot import CogBot, ServerId
from cogbot.extensions.helpchat.help_chat_server_state import HelpChatServerState

log = logging.getLogger(__name__)


class HelpChat:
    def __init__(self, bot: CogBot, ext: str):
        self.bot: CogBot = bot
        self.server_state: typing.Dict[ServerId, HelpChatServerState] = {}
        self.options = self.bot.state.get_extension_state(ext)
        self.server_options: typing.Dict[ServerId, typing.Any] = {}
        self.ext = ext
        self.readied = False

    def get_state(self, server: discord.Server) -> HelpChatServerState:
        return self.server_state.get(server.id)

    def set_state(self, server: discord.Server, state: HelpChatServerState):
        self.server_state[server.id] = state

    def remove_state(self, server: discord.Server):
        del self.server_state[server.id]

    async def create_state(
        self, server: discord.Server, server_options: dict
    ) -> HelpChatServerState:
        state = HelpChatServerState(self.ext, self.bot, server, **server_options)
        self.set_state(server, state)
        await state.on_ready()
        return state

    async def setup_state(self, server: discord.Server) -> HelpChatServerState:
        server_options = self.server_options[server.id]
        # if options is just a string, use remote/external location
        if isinstance(server_options, str):
            state_address = server_options
            try:
                log.info(
                    f"Loading state data for server {server} from: {state_address}"
                )
                actual_server_options = await self.bot.load_json(state_address)
                log.info(f"Successfully loaded state data for server {server}.")
            except:
                log.exception(
                    f"Failed to load state data for server {server}. Skipping..."
                )
                return
            state = await self.create_state(server, actual_server_options)
        # otherwise, use embedded/local config
        else:
            state = await self.create_state(server, server_options)
        return state

    async def reload_state(self, server: discord.Server, ctx: Context = None):
        # remember and remove the old state object
        old_state = self.get_state(server)
        self.remove_state(server)
        # let the user know things are happening
        if ctx:
            old_state.log.info(f"{ctx.message.author} initiated a state reload.")
            await old_state.log_to_channel(
                emoji="🔄",
                description=f"initiated a state reload. Stand by...",
                message=ctx.message,
                actor=ctx.message.author,
                color=discord.Color.blue(),
            )
            await self.bot.add_reaction(ctx.message, "🤖")
        else:
            old_state.log.info(f"Initiated an automatic state reload.")
            await old_state.log_to_channel(
                emoji="🔄",
                description="Initiated an automatic state reload. Stand by...",
                color=discord.Color.blue(),
            )
        # print channel health check, before reload
        await old_state.log_to_channel(
            emoji="🔄",
            description="Channel health check, before reload:\n"
            + await self.get_channels_check_message(old_state),
            color=discord.Color.blue(),
        )
        # create and set the new state object
        old_state.log.info(f"Creating new state object...")
        await old_state.log_to_channel(
            emoji="🔄",
            description="Setting up new state...",
            color=discord.Color.blue(),
        )
        new_state = None
        try:
            new_state = await self.setup_state(server)
            new_state.log.info(f"Successfully created new state object: {new_state}")
            if ctx:
                await self.bot.add_reaction(ctx.message, "👍")
        except:
            old_state.log.exception(f"Failed to create new state object.")
            await old_state.log_to_channel(
                emoji="🔥",
                description="State reload FAILED! Have a nice day!",
                color=discord.Color.red(),
            )
            if ctx:
                await self.bot.add_reaction(ctx.message, "🔥")
            return
        # destroy the old state object
        new_state.log.info(f"Destroying old state object: {old_state}")
        if await old_state.destroy():
            new_state.log.info(f"Successfully destroyed old state object.")
            if ctx:
                await self.bot.add_reaction(ctx.message, "😄")
        else:
            new_state.log.error(f"Failed to destroy old state object.")
            if ctx:
                await self.bot.add_reaction(ctx.message, "😬")
        # print another channel health check, after reload
        await new_state.log_to_channel(
            emoji="🔄",
            description="Channel health check, after reload:\n"
            + await self.get_channels_check_message(new_state),
            color=discord.Color.blue(),
        )
        # let the user know we're done
        await new_state.log_to_channel(
            emoji="🔄",
            description="State reload complete! Have a nice day!",
            color=discord.Color.blue(),
        )
        if ctx:
            await self.bot.add_reaction(ctx.message, "👌")

    async def on_ready(self):
        # because on_ready can be called more than once
        # and because disconnection might invalidate the client cache
        # (including all existing in-memory objects)
        if self.readied:
            log.warning(
                "Hook on_ready() was called again; previous state objects will be recreated..."
            )
        else:
            log.info(f"Identifying servers and their channels for the first time...")
            self.readied = True
        # construct server state objects for easier context management
        for server_key, server_options in self.options.get("servers", {}).items():
            server = self.bot.get_server_from_key(server_key)
            if not server:
                log.error(f"Skipping unknown server {server_key}.")
                continue
            self.server_options[server.id] = server_options
            # if previous state exists, recreate it and log some important things
            if self.get_state(server):
                await self.reload_state(server)
            # otherwise, just setup a new state
            else:
                await self.setup_state(server)

    async def get_channels_check_message(self, state: HelpChatServerState) -> str:
        lines = []
        for cached_ch in state.channels:
            actual_ch: discord.Channel = self.bot.get_channel(cached_ch.id)
            # pointing to the same object
            if cached_ch is actual_ch:
                lines.append(f"👌 {cached_ch.name}")
{'fraction_non_alphanumeric': 0.08816332237992029, 'fraction_numerical': 0.0, 'mean_word_length': 2.029866553696251, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4138294', 'n_tokens_mistral': 3906, 'n_tokens_neox': 3823, 'n_words': 1021}