code:
import asyncio
from datetime import date, time, datetime, timedelta, timezone
import json
import re
import sys
import traceback
from typing import Union, Optional
from io import BytesIO

import aioredis
import discord
import motor.motor_asyncio
import sentry_sdk

from prometheus_client import Gauge
from prometheus_async.aio.web import start_http_server

from . import utils
from . import notifications
from . import reporting
from . import site
from . import commands
from .tcnati_interface import handle_tcnati_message
from .commands import handle_cmd_string
from .config import config
from .interactions import Interactions

sentry_sdk.init(config.sentry_dsn)

WEBSOCKET_LATENCY = Gauge(
    "websocket_latency_seconds", "Discord Gateway websocket latency"
)

class NotifierClient(discord.Client):
    perms_integer = 35840
    cmd_regex = r"\"([^\"]+)\"|\'([^\']+)\'|\`\`\`([^\`]+)\`\`\`|\`([^\`]+)\`|(\S+)"

    def __init__(self, *args, **kwargs):
        self.summon_prefixes = ["b!"]
        super().__init__(*args, **kwargs)

        self.redis: aioredis.Redis = None
        self.redis_sub: aioredis.Redis = None
        self.db: motor.motor_asyncio.AsyncIOMotorDatabase = None

    async def health_test_loop(self):
        while not self.is_ready():
            print("Health check: waiting for ready status...")
            await asyncio.sleep(5)

        while True:
            if self.is_ready():
                try:
                    if self.is_closed():
                        return

                    WEBSOCKET_LATENCY.set(self.latency)
                except Exception as e:
                    print(
                        "Health check threw exception {}, restarting...".format(str(e))
                    )
                    return

            await asyncio.sleep(5)

    def resolve_user(self, user: Union[int, str]) -> Optional[discord.Member]:
        dev_server: discord.Guild = self.get_guild(config.primary_server_id)

        try:
            return dev_server.get_member(int(user))
        except ValueError:
            pass

        if user[-1] == ">":
            if user[:3] == "<@!":
                return dev_server.get_member(int(user[3:-1]))
            elif user[:2] == "<@":
                return dev_server.get_member(int(user[2:-1]))
        return dev_server.get_member_named(user)

    def iter_channels(self, channel_id_list):
        for ch_id in channel_id_list:
            channel = self.get_channel(ch_id)
            if channel is not None:
                yield channel

    async def log_notify(self, msg, **kwargs):
        timestamp = datetime.now(timezone.utc).isoformat()
        for channel in self.iter_channels(config.logging_channels):
            await channel.send("`[{}]` {}".format(timestamp, msg), **kwargs)

    async def error_notify(
        self,
        msg,
        ext_data: Optional[str] = None,
        ext_data_name: Optional[str] = None,
        **kwargs,
    ):
        with sentry_sdk.configure_scope() as scope:
            scope.level = "error"
            sentry_sdk.capture_message(msg)

        timestamp = datetime.now(timezone.utc).isoformat()
        formatted = "`[{}]` {}".format(timestamp, msg)[:1900]
        if ext_data is not None:
            ext_data = str(ext_data).encode("utf-8")
            if ext_data_name is None:
                ext_data_name = "data.txt"

            for channel in self.iter_channels(config.notifications_channels):
                with BytesIO(ext_data) as bio:
                    await channel.send(
                        formatted,
                        file=discord.File(bio, filename=ext_data_name),
                        **kwargs,
                    )
        else:
            for channel in self.iter_channels(config.notifications_channels):
                await channel.send(formatted, **kwargs)

    async def reply(self, original_msg, reply_text, **kwargs):
        ch = original_msg.channel
        return await ch.send(
            "<@!{}> | {:s}".format(original_msg.author.id, reply_text), **kwargs
        )

    async def reply_report(self, original_msg, header, main_text, **kwargs):
        ch = original_msg.channel
        return await ch.send(
            "<@!{}> | **{:s}**\n```{:s}```".format(
                original_msg.author.id, header, main_text
            ),
            **kwargs,
        )

    async def log_exception(self, where):
        exc_type, exc_val, exc_tb = sys.exc_info()
        tb = "".join(traceback.format_exception(exc_type, exc_val, exc_tb))
        await self.error_notify(
            "Exception caught " + where, ext_data=tb, ext_data_name="exception.log"
        )
        traceback.print_exception(exc_type, exc_val, exc_tb)

    async def on_error(self, ev, *args, **kwargs):
        ext_error_info = ""

        with sentry_sdk.configure_scope() as scope:
            scope.set_tag("discord-event", ev)
            sentry_sdk.capture_exception()

        if str(ev) == "on_message":
            msg = args[0]
            is_cmd = False

            content = msg.content.strip()

            for prefix in config.summon_prefixes:
                if content.startswith(prefix):
                    is_cmd = True
                    break
            else:
                is_cmd = isinstance(msg.channel, discord.DMChannel)

            if is_cmd:
                ext_error_info = (
                    " when handing command from **{}#{}** ({}): `{}`".format(
                        msg.author.name,
                        msg.author.discriminator,
                        msg.author.id,
                        msg.content.strip(),
                    )
                )
            else:
                ext_error_info = " when handing message from **{}#{}** ({})".format(
                    msg.author.name, msg.author.discriminator, msg.author.id
                )

        where = "in `{}` handler{}".format(str(ev), ext_error_info)
        await self.log_exception(where)

    async def on_ready(self):
        print("Logged in as")
        print(self.user.name)
        print(self.user.id)
        print("------")
        print("Invite URL:")
        print(
            "https://discordapp.com/api/oauth2/authorize?client_id={}&scope=bot%20applications.commands&permissions={}".format(
                self.user.id, self.perms_integer
            )
        )

        config.summon_prefixes.append("<@{}>".format(self.user.id))
        config.summon_prefixes.append("<@!{}>".format(self.user.id))

        loop = asyncio.get_event_loop()
        self.db_client = motor.motor_asyncio.AsyncIOMotorClient(
            "localhost", 27017, io_loop=loop
        )

        self.db = self.db_client.spnati_usage_stats
        self.redis = await aioredis.create_redis(config.primary_redis_url)
        self.redis_pub = await aioredis.create_redis(config.redis_pubsub_url)
        self.redis_sub = await aioredis.create_redis(config.redis_pubsub_url)
        self.interactions = Interactions(self)

        v = await utils.get_version()
        await self.log_notify("SPNATI Utilities Bot version {} starting up!".format(v))

        await notifications.initialize(self)
        await reporting.initialize(self)
        await commands.initialize(self)

        await self.interactions.start()
        self.presence_loop = asyncio.create_task(self.update_presence_loop())
        self.health_test = asyncio.create_task(self.health_test_loop())
        self.propagation_loop = asyncio.create_task(
            site.observe_commit_propagation_loop(self)
        )

    async def on_message(self, msg):
        if msg.author.id == self.user.id:
            return

        if msg.type != discord.MessageType.default or msg.author.bot:
            return

        content = msg.content.strip()

        with sentry_sdk.configure_scope() as scope:
            scope.user = {
                "id": msg.author.id,
                "username": "{}#{}".format(msg.author.name, msg.author.discriminator),
            }

            scope.set_extra("message_id", msg.id)
            scope.set_extra("channel", msg.channel.id)
            if msg.guild:
                scope.set_extra("guild", msg.guild.id)

            scope.set_extra("content", content)

            for prefix in config.summon_prefixes:
                if content.startswith(prefix):
                    content = content[len(prefix) :].strip()
                    return await handle_cmd_string(self, msg, content)

            if isinstance(msg.channel, discord.DMChannel):
                return await handle_cmd_string(self, msg, content)
            else:
                await handle_tcnati_message(self, msg)

                if msg.reference is not None:
                    ref_id = msg.reference.message_id
                    if ref_id is not None:
                        refs_mask_message = bool(
                            await self.redis.exists(
                                "masquerade:messages:" + str(ref_id)
                            )
                        )
                        if refs_mask_message:
                            await commands.masquerade.handle_mask_speak_reply(
                                self, msg, ref_id
                            )

    async def on_raw_reaction_add(self, payload):
        if payload.guild_id is None:
            return

        if payload.user_id == self.user.id:
            return

        raw_user = self.get_user(payload.user_id)
        if raw_user is None or raw_user.bot:
            return

        with sentry_sdk.configure_scope() as scope:
            scope.user = {
                "id": raw_user.id,
                "username": "{}#{}".format(raw_user.name, raw_user.discriminator),
            }

            scope.set_extra("message_id", payload.message_id)
            scope.set_extra("guild", payload.guild_id)

        return await reporting.handle_bug_report_reaction(
            self, payload.user_id, payload.message_id, payload.emoji.name
        )

    async def update_presence_loop(self):
        while True:
            try:
                counts = await site.get_recent_report_counts(self)
                await self.change_presence(
                    status=discord.Status.online,
                    activity=discord.Game(
                        "w/ {:d} reports and {:d} users!".format(*counts)
                    ),
                )
            except:
                traceback.print_exc()

            await asyncio.sleep(5)

async def serve_forever():
    v = await utils.get_version()
    intents: discord.Intents = discord.Intents.default()
    intents.members = True

    client = NotifierClient(
        activity=discord.Game("Version {}".format(v)), intents=intents
    )

    print(f"Starting version {v}...")

    metrics_server = await start_http_server(addr="127.0.0.1", port=9160)

    try:
        await client.start(config.token)
    except KeyboardInterrupt:
        await client.logout()

    await metrics_server.close()

def main():
    loop = asyncio.get_event_loop()
    loop.run_until_complete(serve_forever())


code:
from typing import Optional

import discord

from .config import config

class Mask(object):
    def __init__(self, client, mask_id):
        self.client = client
        self.id = mask_id

        self.redis_key = "masquerade:mask:" + str(mask_id)

    async def get_character(self):
        return await self.client.redis.hget(
            self.redis_key, "character", encoding="utf-8"
        )

    async def get_name(self):
        return await self.client.redis.hget(self.redis_key, "name", encoding="utf-8")

    async def get_channel_id(self):
        channel_id = await self.client.redis.hget(
            self.redis_key, "channel", encoding="utf-8"
        )
        if channel_id is None:
            return None

        return int(channel_id)

    async def user_in_mask(self, user) -> bool:
        uid = user
        if not isinstance(uid, int):
            uid = user.id

        return await self.client.redis.sismember(self.redis_key + ":members", uid)

    async def get_users(self):
        members = await self.client.redis.smembers(
            self.redis_key + ":members", encoding="utf-8"
        )
        return list(map(int, members))

    async def set_users(self, users):
        await self.client.redis.delete(self.redis_key + ":members")
        await self.client.redis.sadd(self.redis_key + ":members", *users)

    async def set_character(self, character_id):
        prev_char = await self.get_character()
        if prev_char is not None:
            await self.client.redis.delete("masquerade:character:" + prev_char)

        await self.client.redis.hset(self.redis_key, "character", character_id)
        await self.client.redis.set("masquerade:character:" + character_id, self.id)

    async def set_name(self, name):
        await self.client.redis.hset(self.redis_key, "name", name)

    async def set_channel(self, channel):
        if isinstance(channel, discord.TextChannel):
            channel_id = channel.id
        else:
            channel_id = int(channel)

        return await self.client.redis.hset(self.redis_key, "channel", channel_id)

async def get_all_mask_ids(client):
    members = await client.redis.smembers("masquerade:masks", encoding="utf-8")
    return list(map(int, members))

async def is_mask_enabled(client, mask_id):
    return bool(await client.redis.sismember("masquerade:masks", str(mask_id)))

async def enable_mask_id(client, mask_id):
    await client.redis.sadd("masquerade:masks", str(mask_id))

async def disable_mask_id(client, mask_id):
    await client.redis.srem("masquerade:masks", str(mask_id))

async def get_character_mask(client, character_id) -> Optional[Mask]:
    mask_id = await client.redis.get(
        "masquerade:character:" + character_id, encoding="utf-8"
    )
    if mask_id is None:
        return None

    return Mask(client, int(mask_id))

def private_channel_check(channel):
    if channel.id in config.masquerade.get("channel_blacklist", []):
        return False

    if isinstance(
        channel, discord.DMChannel
    ) or channel.guild.id in config.masquerade.get("safe_servers", []):
        return True

    if (
        channel.guild.id != config.primary_server_id
        or channel.category_id != config.masquerade.get("primary_channel_category", 0)
    ):
        return False

    return True

repo_name: spnati/serve

license: mit


code:
import asyncio
import aioredis
import discord
import traceback
import base64
import io

from .update_alerts import process_staleness_warnings
from .config import config
from .reporting.routing import find_character_channel

UPDATE_BRANCH_PREFIX = "bot-update-"

async def process_notification(client, channel, payload):
    if payload["type"] == "kisekae":
        if "filename" in payload:
            f = discord.File(str(payload["filename"]))
        elif "data" in payload:
            img_data = base64.b64decode(payload["data"].encode("utf-8"))
            f = discord.File(io.BytesIO(img_data), filename="kisekae.png")

        await client.log_notify(
            "Processed Kisekae code from {:s}:".format(str(payload["source"])),
            file=f,
        )
    elif payload["type"] == "staleness":
        asyncio.create_task(process_staleness_warnings(client))
    elif payload["type"] == "notify":
        if payload["level"] == "log":
            if "file" in payload:
                await client.log_notify(
                    payload["message"], file=discord.File(str(payload["file"]))
                )
            else:
                await client.log_notify(payload["message"])
        elif payload["level"] == "error":
            if "file" in payload:
                await client.error_notify(
                    payload["message"], file=discord.File(str(payload["file"]))
                )
            else:
                await client.error_notify(payload["message"])

async def notifications_handler(client, channel):
    while True:
        try:
            while await channel.wait_message():
                payload = await channel.get_json()
                await process_notification(client, channel, payload)
        except:
            traceback.print_exc()

async def handle_merge_request_event(client, channel, event_data):
    mr_data = event_data["object_attributes"]
    if "action" not in mr_data:
        return

    if (
        (mr_data["action"] not in ["merge", "update"])
        or (event_data["user"]["id"] == config.bot_gitlab_id)
        or (mr_data["source_project_id"] != config.staging_project_id)
        or (mr_data["target_project_id"] != config.upstream_project_id)
        or (not mr_data["source_branch"].startswith(UPDATE_BRANCH_PREFIX))
    ):
        return

    update_character = mr_data["source_branch"][len(UPDATE_BRANCH_PREFIX) :]
    character_channel = find_character_channel(client, update_character)
    if character_channel is None:
        return

    if mr_data["action"] == "merge":
        await character_channel.send(
            "✅  Merge request **!{}** has been merged. Thank you for your contribution to SPNatI!".format(mr_data["iid"])
        )
    elif mr_data["action"] == "update":
        prev_labels = set(label["title"] for label in mr_data["labels"]["previous"])
        new_labels = set(label["title"] for label in mr_data["labels"]["current"])

        if ("Awaiting Corrections" not in prev_labels) and (
            "Awaiting Corrections" in new_labels
        ):
            await character_channel.send(
                "⚠  Merge request **!{}** has been marked as **Awaiting Corrections.**".format(
                    mr_data["iid"]
                )
            )
        elif ("Awaiting Review" not in prev_labels) and (
            "Awaiting Review" in new_labels
        ):
            await character_channel.send(
                "ℹ  Merge request **!{}** has been marked as **Awaiting Review.**".format(
                    mr_data["iid"]
                )
            )

async def process_gitlab_event(client, channel, event_data):
    if event_data["object_kind"] == "merge_request":
        return await handle_merge_request_event(client, channel, event_data)

async def gitlab_event_handler(client, channel):
    while True:
        try:
            while await channel.wait_message():
                payload = await channel.get_json()
                await process_gitlab_event(client, channel, payload)
        except:
            traceback.print_exc()

async def initialize(client):
    res = await client.redis_sub.subscribe("utilities:notifications")
    tsk = asyncio.create_task(notifications_handler(client, res[0]))

    res2 = await client.redis_sub.subscribe("utilities:gitlab_events")
    tsk2 = asyncio.create_task(gitlab_event_handler(client, res2[0]))

    return tsk, tsk2


code:
from __future__ import annotations

from aioredis import Redis, RedisError
import discord
from typing import Set, Iterable, Union, MutableSet, Optional, Iterator

from .config import config

REDIS_CHARACTER_DATA_PREFIX = "permissions:character:"
REDIS_USER_DATA_PREFIX = "permissions:user:"

PossiblySnowflake = Union[discord.abc.Snowflake, int, str]

def _ensure_snowflake(x: PossiblySnowflake) -> int:
    """Try to interpret the given value as (something containing) a Snowflake."""
    try:
        return x.id
    except AttributeError:
        return int(x)

class SnowflakeSet(MutableSet[PossiblySnowflake]):
    def __init__(self, it: Optional[Iterable[PossiblySnowflake]] = None):
        self._data: Set[int] = set()
        if it is not None:
            self._data = set(map(_ensure_snowflake, it))

    def __contains__(self, v: PossiblySnowflake) -> bool:
        return _ensure_snowflake(v) in self._data

    def __iter__(self) -> Iterator[int]:
        return self._data.__iter__()

    def __len__(self) -> int:
        return len(self._data)

    def add(self, elem: PossiblySnowflake):
        return self._data.add(_ensure_snowflake(elem))

    def discard(self, elem: PossiblySnowflake):
        return self._data.discard(_ensure_snowflake(elem))

async def bind(redis: Redis, user: PossiblySnowflake, character: str):
    uid = _ensure_snowflake(user)
    tr = redis.multi_exec()
    tr.sadd(REDIS_CHARACTER_DATA_PREFIX + character, str(uid))
    tr.sadd(REDIS_USER_DATA_PREFIX + str(uid), character)
    await tr.execute()

async def unbind(redis: Redis, user: PossiblySnowflake, character: str):
    uid = _ensure_snowflake(user)
    tr = redis.multi_exec()
    tr.srem(REDIS_CHARACTER_DATA_PREFIX + character, str(uid))
    tr.srem(REDIS_USER_DATA_PREFIX + str(uid), character)
    await tr.execute()

async def db_check(redis: Redis, user: PossiblySnowflake, character: str) -> bool:
    """Check permissions exactly as they are stored in the database."""
    uid = _ensure_snowflake(user)
    v = await redis.sismember(REDIS_CHARACTER_DATA_PREFIX + character, str(uid))
    return bool(v)

async def check(redis: Redis, user: PossiblySnowflake, character: str) -> bool:
    uid = _ensure_snowflake(user)

    if uid in config.authorized_users:
        return True

    v = await redis.sismember(REDIS_CHARACTER_DATA_PREFIX + character, str(uid))
    return bool(v)

async def get_developers(redis: Redis, character: str) -> SnowflakeSet:
    uid_list = await redis.smembers(
        REDIS_CHARACTER_DATA_PREFIX + character, encoding="utf-8"
    )
    return SnowflakeSet(uid_list)

async def get_characters(redis: Redis, user: PossiblySnowflake) -> Set[str]:
    char_list = await redis.smembers(
        REDIS_USER_DATA_PREFIX + str(_ensure_snowflake(user)), encoding="utf-8"
    )
    return set(char_list)


code:
from .handler import initialize
from .reactions import handle_bug_report_reaction


code:
import re
from itertools import islice
from typing import Iterable, TypeVar

import dateutil.parser
from importlib import resources
from bs4 import BeautifulSoup
from bs4.element import Tag

from ..git.info import get_commit_data
from ..git import git_ops
from . import templates as templates
from ..utils import format_name
from . import spam_filter

_T = TypeVar("T")

BUG_TYPES = {
    "freeze": "Game Freeze",
    "display": "Graphical Issue",
    "other": "Other Game Issue",
    "character": "Character Defect",
    "feedback": "Character Feedback",
    "auto": "Automatically Sent Report",
}

def get_template(name):
    return resources.read_text(templates, name + ".md").strip()

URL_REGEX = r"(https?://)(\S{2,})"
MENTION_REGEX = r"<(?:@[!&]?|#|a?:[^:]+:)\d+>"

def sanitize_report_message(msg):
    msg = re.sub(MENTION_REGEX, "", msg)

    def anti_embed(match):
        return match[1] + " " + match[2]

    msg = re.sub(URL_REGEX, anti_embed, msg)

    msg = re.sub("", "**", msg)
    msg = re.sub("", "**", msg)
    msg = re.sub("", "*", msg)
    msg = re.sub("", "*", msg)
    msg = re.sub("", "__", msg)
    msg = re.sub("", "__", msg)
    msg = re.sub("<s>", "~~", msg)
    msg = re.sub("</s>", "~~", msg)

    return msg

async def format_report_notification(client, report_id, report):
    if report_id is None:
        report_id = "<Unknown>"

    if report["epilogue"] is not None and report["type"] != "auto":
        return await format_epilogue_report(client, report_id, report)
    elif report["type"] == "character":
        return await format_character_report(client, report_id, report)
    elif report["type"] == "feedback":
        return await format_feedback_report(client, report_id, report)
    elif report["type"] == "auto":
        return await format_auto_report(client, report_id, report)
    else:
        return await format_general_report(client, report_id, report)

async def format_commit(client, report):
    if "commit" in report and report["commit"] is not None:
        commit = await get_commit_data(report["commit"])

        if commit is None:
            return report["commit"][:8]

        dt = dateutil.parser.parse(commit["created_at"])
        return "{} - {} ({})".format(
            commit["short_id"], commit["title"], dt.strftime("%c")
        )
    else:
        return "<Unknown Commit>"

def format_game_phase(report):
    visible_screen = report["circumstances"]["visibleScreens"][0]
    cur_phase = report["circumstances"]["gamePhase"].lower().strip()
    cur_round = report["circumstances"]["currentRound"]

    last_stripped = None
    if (
        "recentLoser" in report["circumstances"]
        and report["circumstances"]["recentLoser"] is not None
        and report["circumstances"]["recentLoser"] >= 0
    ):
        last_stripped = report["circumstances"]["recentLoser"]

    last_stripped_label = "(Unknown Player)"
    if last_stripped is not None:
        if last_stripped == 0:
            last_stripped_label = "Human"
        elif last_stripped > 0:
            try:
                last_stripped_label = next("(" + format_name(pl["id"]) + ")"
                                           for pl in report["table"]
                                           if pl is not None and int(pl["slot"]) == last_stripped)
            except StopIteration:
                pass

    if visible_screen == "main-select-screen":
        formatted_phase = "Character Selection"
    elif cur_phase.startswith("ending"):
        formatted_phase = "Game Over"
    elif (
        cur_phase == ""
        or cur_phase is None
        or report["circumstances"].get("gameOver", False)
    ):
        formatted_phase = "Waiting on Forfeits"
    elif cur_phase == "deal" and cur_round < 0:
        formatted_phase = "Game Start"
    elif cur_phase == "deal" and cur_round >= 0:
        formatted_phase = "After Stripping {}".format(last_stripped_label)
    elif cur_phase == "reveal" or cur_phase == "exchange":
        formatted_phase = "Cards Dealt"
    elif cur_phase == "strip":
        formatted_phase = "Stripping {}".format(last_stripped_label)
    elif cur_phase == "continue":
        formatted_phase = "Must Strip {}".format(last_stripped_label)
    elif cur_phase == "masturbate":
        formatted_phase = "Must Forfeit {}".format(last_stripped_label)
    elif cur_phase.startswith("continue"):
        formatted_phase = "Finishing"
    else:
        formatted_phase = "Unknown"

    if report["circumstances"].get("rollback", False):
        formatted_phase += " (Rolled-Back)"

    return formatted_phase

def format_spam_score(report):
    if report["spam_class"] == spam_filter.UNKNOWN:
        spam_class = "Unknown"
    elif report["spam_class"] == spam_filter.UNCERTAIN:
        spam_class = "Uncertain"
    elif report["spam_class"] == spam_filter.SPAM:
        spam_class = "Likely"
    elif report["spam_class"] == spam_filter.NOT_SPAM:
        spam_class = "Unlikely"

    return "**Spam Classification:** {} (score {:.3f})".format(
        spam_class, report["spam_score"]
    )

async def format_character_report(client, report_id, report):
    dialogue = None
    image = None

    slot_data = ["human", "<empty>", "<empty>", "<empty>", "<empty>"]

    for pl in report["table"]:
        if pl is None:
            continue

        slot_data[int(pl["slot"])] = format_name(pl["id"])
        slot_data[int(pl["slot"])] += " ({})".format(pl["stage"])

        if pl["id"] == report["character"]:
            try:
                dialogue = '"' + pl["currentLine"] + '"'
                dialogue = re.sub("`", "'", dialogue)
            except KeyError:
                dialogue = "<no dialogue set>"

            image = pl.get("currentImage", "<no image set>")

    charName = format_name(report["character"])
    situation = format_game_phase(report)

    formatted = "**{char} Bug Report {id}:**".format(char=charName, id=report_id)

    if report.get("commit", None) is not None:
        if report["commit"] not in client.current_deployed_commit:
            formatted += "\n**Outdated Commit:** {commit}".format(
                commit=await format_commit(client, report)
            )

    formatted += """
**Table:** {table_0} | {table_1} | {table_2} | {table_3} | {table_4} | **Situation**: {situation}

**Current {char} Image:** {image}
**Current Dialogue:** {dialogue}
{spam_class}

**Description:** {desc}
For more detailed information, DM <@!{self_id}> with `bug {id}`.""".format(
        self_id=client.user.id,
        id=report_id,
        desc=sanitize_report_message(report["description"]),
        char=charName,
        situation=situation,
        image=image,
        dialogue=dialogue,
        spam_class=format_spam_score(report),
        table_0=slot_data[0],
        table_1=slot_data[1],
        table_2=slot_data[2],
        table_3=slot_data[3],
        table_4=slot_data[4],
    )

    return formatted

def iter_take_at(iterable: Iterable[_T], index: int) -> _T:
    if not isinstance(index, int):
        raise TypeError("expected int, got {}".format(index))

    try:
        return next(islice(iterable, index, index + 1))
    except StopIteration:
        raise IndexError("iterable object has no {}-ith element".format(index))

async def extract_epilogue_context(report):
    reported_data = report["epilogue"]
    have_reported_context = False

    try:
        sceneName = reported_data["sceneName"]
        have_reported_context = True
    except KeyError:
        sceneName = None

    try:
        lastText = '"' + reported_data["lastText"] + '"'
        have_reported_context = True
    except (KeyError, TypeError):
        lastText = "<none>"

    if have_reported_context:
        return (sceneName, lastText)

    if "commit" in report and report["commit"] is not None:
        ref = report["commit"]
    else:
        ref = "HEAD"
    path = "opponents/{}/behaviour.xml".format(report["epilogue"]["player"])

    try:
        xml_data = (await git_ops.get_file(ref, path)).decode("utf-8")
    except git_ops.CommandError:
        return None

    soup = BeautifulSoup(xml_data, features="html.parser")
    for epilogue in soup.find_all("epilogue", recursive=True):
        title = str(epilogue.title.string)
        gender = epilogue.attrs.get("gender", "any")
        if title != reported_data["epilogue"] or gender != reported_data["gender"]:
            continue

        try:
            scene: Tag = iter_take_at(
                epilogue.find_all("scene"), reported_data["scene"]
            )
        except IndexError:
            continue

        directives = list(scene.find_all("directive"))
        if len(directives) <= reported_data["directive"]:
            continue

        cur_idx = reported_data["directive"]
        while cur_idx >= 0:
            directive: Tag = directives[cur_idx]

            try:
                if directive["type"] == "text":
                    break
            except KeyError:
                pass

            cur_idx -= 1

        try:
            scene_name = scene["name"]
        except KeyError:
            scene_name = None

        if cur_idx < 0:
            text = "<none>"
        else:
            text = '"' + str(directive.string) + '"'

        return (scene_name, text)

async def format_epilogue_situation(
    client, report, include_location=False, include_player=False
):
    reported_data = report["epilogue"]

    epilogue_player = format_name(reported_data["player"])
    gender_desc = reported_data["gender"].strip().capitalize()

    if len(gender_desc) > 0:
        gender_desc = " (" + gender_desc + ")"

    formatted_context = "\n**Epilogue:** "
    if include_player:
        formatted_context += epilogue_player + " / "

    formatted_context += reported_data["epilogue"] + gender_desc

    if include_location:
        try:
            ctx = await extract_epilogue_context(report)
        except Exception:
            await client.log_exception("in extract_epilogue_context")
            ctx = None

        scene_name = None
        current_text = "<could not identify context>"
        if ctx is not None:
            scene_name = ctx[0]
            current_text = ctx[1]

        if scene_name is not None:
            formatted_context += ' - Scene {scene} ("{name}")'.format(
                scene=reported_data["scene"] + 1, name=scene_name
            )
        else:
            formatted_context += " - Scene " + str(reported_data["scene"] + 1)

        formatted_context += ", Directive " + str(reported_data["directive"] + 1)
        formatted_context += "\n**Current Text:** " + current_text

    return formatted_context

async def format_epilogue_report(client, report_id, report):
    report_has_character = report["character"] is not None
    if report_has_character:
        charName = format_name(report["character"])
    else:
        charName = "General"

    is_bug_report = report["type"] != "feedback"
    if is_bug_report:
        template = "**{charName} Bug Report {id}:**"
    else:
        template = "**{charName} Feedback Report {id}:**"

    formatted = template.format(charName=charName, id=report_id)
    if report.get("commit", None) is not None:
        if report["commit"] not in client.current_deployed_commit:
            formatted += "\n**Outdated Commit:** {commit}".format(
                commit=await format_commit(client, report)
            )

    if is_bug_report:
        if not report_has_character:
            formatted += "\n**Type**: " + BUG_TYPES[report["type"]]

            formatted += "\n**User-Agent**: `{}`\n**Origin:**: `{}`".format(
                report["circumstances"]["userAgent"], report["circumstances"]["origin"],
            )
    else:
        formatted += "\n**Reporter**: " + report["ip"]

    formatted += await format_epilogue_situation(
        client, report, is_bug_report, not report_has_character
    )

    formatted += "\n" + format_spam_score(report)

    formatted += """\n
**Description:** {desc}
For more detailed information, DM <@!{self_id}> with `bug {id}`.""".format(
        self_id=client.user.id,
        id=report_id,
        desc=sanitize_report_message(report["description"]),
    )

    return formatted

async def format_feedback_report(client, report_id, report):
    slot_data = ["<empty>", "<empty>", "<empty>", "<empty>"]

    for pl in report["table"]:
        if pl is None or int(pl["slot"]) == 0:
            continue

        slot_data[int(pl["slot"]) - 1] = format_name(pl["id"])

        if pl["id"] == report["character"]:
            try:
                dialogue = '"' + pl["currentLine"] + '"'
                dialogue = re.sub("`", "'", dialogue)
            except KeyError:
                dialogue = "<no dialogue set>"

            image = pl.get("currentImage", "<no image set>")

    if report["character"] is not None:
        charName = format_name(report["character"])
    else:
        charName = "General"

    formatted = "**{char} Feedback Report {id}:**".format(char=charName, id=report_id)

    if report.get("commit", None) is not None:
        if report["commit"] not in client.current_deployed_commit:
            formatted += "\n**Outdated Commit:** {commit}".format(
                commit=await format_commit(client, report)
            )

    formatted += """
**Reporter:** {ip}
**Table:** {table_1} | {table_2} | {table_3} | {table_4}""".format(
        ip=report["ip"],
        table_1=slot_data[0],
        table_2=slot_data[1],
        table_3=slot_data[2],
        table_4=slot_data[3],
    )

    if (not report["circumstances"]["gamePhase"].lower().strip().startswith("ending")) and (
        report["character"] is not None
    ):
        formatted += "\n\n**Current {char} Image:** {image}\n**Current Dialogue:** {dialogue}".format(
            char=charName, image=image, dialogue=dialogue
        )

    formatted += """
{spam_class}

**Description:** {desc}
For more detailed information, DM <@!{self_id}> with `bug {id}`.""".format(
        self_id=client.user.id,
        id=report_id,
        desc=sanitize_report_message(report["description"]),
        spam_class=format_spam_score(report),
    )

    return formatted

async def format_auto_report(client, report_id, report):
    stack = report["jsErrors"][-1].get("stack", "<none sent>")
    message = report["jsErrors"][-1].get("message", "<none sent>")
    errType = report["jsErrors"][-1].get("type", "<none sent>")

    if "file:" in stack:
        stack = "<stacktrace omitted for privacy>"

    if "file:" in message:
        stack = "<message omitted for privacy>"

    if "file:" in errType:
        errType = "<error type omitted for privacy>"

    template = get_template("auto_report_template")

    report_msg = template.format(
        id=report_id,
        session=report["session"],
        ua=report["circumstances"]["userAgent"],
        origin=report["circumstances"]["origin"],
        ip=report["ip"],
        commit=await format_commit(client, report),
        n_js_err=len(report["jsErrors"]),
        type=errType,
        message=message,
        stack=stack,
    )

    if (
        ("monika" in report["description"].lower())
        or ("monika" in stack)
        or ("glitch" in stack.lower())
    ):
        return "(NOTE: Exception caught in Monika code)\n" + report_msg
    else:
        return report_msg

async def format_general_report(client, report_id, report):
    if len(report["description"].strip()) <= 0:
        return

    template = get_template("default_notification_template")

    return template.format(
        self_id=client.user.id,
        id=report_id,
        session=report["session"],
        type=BUG_TYPES[report["type"]],
        ua=report["circumstances"]["userAgent"],
        origin=report["circumstances"]["origin"],
        ip=report["ip"],
        commit=await format_commit(client, report),
        n_js_err=len(report["jsErrors"]),
        spam_class=format_spam_score(report),
        desc=sanitize_report_message(report["description"]),
    )

async def format_detailed_bug_report(client, doc):
    if doc is None:
        return None

    report = get_template("detailed_report_header").format(
        id=doc.get("_id", "<Unknown>"),
        session=doc["session"],
        type=BUG_TYPES[doc["type"]],
        ua=doc["circumstances"]["userAgent"],
        origin=doc["circumstances"]["origin"],
        ip=doc["ip"],
        commit=await format_commit(client, doc),
        situation=format_game_phase(doc),
        desc=doc["description"],
        gamePhase=doc["circumstances"]["gamePhase"],
        recentLoser=doc["circumstances"].get("recentLoser", "Unknown"),
        previousLoser=doc["circumstances"].get("previousLoser", "Unknown"),
        gameOver=doc["circumstances"].get("gameOver", "Unknown"),
        currentRound=doc["circumstances"]["currentRound"],
        currentTurn=doc["circumstances"]["currentTurn"],
        visibleScreens=doc["circumstances"]["visibleScreens"],
        playerGender=doc["player"]["gender"],
        playerSize=doc["player"]["size"],
    )

    report += "\n"

    if doc["table"] is not None:
        for pl in doc["table"]:
            if pl is None:
                continue

            try:
                pl["currentLine"] = '"' + pl["currentLine"] + '"'
            except KeyError:
                pl["currentLine"] = "<no dialogue set>"

            pl["currentImage"] = pl.get("currentImage", "<no image set>")

            report += get_template("detailed_report_table_info").format(**pl) + "\n"

    for n, jsErr in enumerate(doc["jsErrors"]):
        stack = jsErr.get("stack", "<none sent>")
        message = jsErr.get("message", "<none sent>")
        errType = jsErr.get("type", "<none sent>")
        filename = jsErr.get("filename", "<none sent>")
        lineno = jsErr.get("lineno", "<none sent>")

        if "file:" in stack:
            stack = "<stacktrace omitted for privacy>"

        if "file:" in message:
            stack = "<message omitted for privacy>"

        if "file:" in errType:
            errType = "<error type omitted for privacy>"

        report += (
            get_template("detailed_report_js_info").format(
                n=n,
                date=jsErr["date"],
                message=message,
                lineno=lineno,
                errType=errType,
                filename=filename,
                stack=stack,
            )
            + "\n"
        )

    return report


code:
import asyncio
import re
import traceback
import time

import aioredis
from bson.objectid import ObjectId
import discord

from prometheus_client import Counter

from .routing import get_report_channels
from .formatting import format_report_notification
from .silence import is_ip_silenced
from . import spam_filter

BOUNTY_ID_REGEX = r"\!\!(\d{6})"

REPORT_COUNTER = Counter(
    "utilities_reports_total",
    "Number of reports delivered by SPNATI Utilities",
    ["type"],
)

async def process_report(client, channel, report):
    if "id" not in report:
        return

    report_id = report["id"]
    cur_time = int(time.time())

    try:
        key = "handled:" + str(report_id)
        handle_time = await client.redis.getset(key, cur_time, encoding="utf-8")
        await client.redis.expire(key, 1800)

        handle_time = int(handle_time)
        if handle_time <= cur_time:
            return
    except (TypeError, ValueError):
        pass

    REPORT_COUNTER.labels(report["type"]).inc()

    if await is_ip_silenced(client.redis, report["ip"]):
        await client.db.bug_reports.update_one(
            {"_id": ObjectId(report_id)}, {"$set": {"status": "spam"}}
        )

    if report["type"] != "auto":
        spam_class, spam_score = spam_filter.classify(report["description"])
        report["spam_class"] = spam_class
        report["spam_score"] = spam_score

    output_channels = await get_report_channels(client, report_id, report)
    formatted_report = await format_report_notification(client, report_id, report)

    if formatted_report is None:
        await client.error_notify(
            "Could not format bug report {}!".format(str(report_id))
        )
        return

    for channel in client.iter_channels(output_channels):
        if isinstance(formatted_report, discord.Embed):
            message = await channel.send(
                "New bug reported: `{}`".format(report_id), embed=formatted_report
            )
        else:
            message = await channel.send(formatted_report)

        client.db.report_notifications.insert_one(
            {
                "msg_id": message.id,
                "msg_channel": message.channel.id,
                "bug_id": ObjectId(report_id),
            }
        )

        if report["type"] == "character" or report["type"] == "feedback":
            await message.add_reaction("✅")
            await message.add_reaction("❎")
            await message.add_reaction("❓")

        if report["type"] != "auto":
            await message.add_reaction("🚫")
            await message.add_reaction("🔇")

    # look for a bounty ID in the description and message TCNATI if necessary.
    bounty_id_match = re.search(BOUNTY_ID_REGEX, report.get("description", ""))
    if bounty_id_match:
        bounty_id = bounty_id_match.group(1)

        report["id"] = str(report_id)
        await client.redis_pub.publish_json(
            "tcnati:bounties", {"bounty-id": bounty_id, "report": report}
        )

async def bug_report_handler(client, channel):
    while True:
        try:
            while await channel.wait_message():
                payload = await channel.get_json()
                await process_report(client, channel, payload)
        except:
            traceback.print_exc()

async def initialize(client):
    res = await client.redis_sub.subscribe("spnati:bug_reports")
    channel = res[0]

    tsk = asyncio.create_task(bug_report_handler(client, channel))
    return tsk


code:
import random

from bson.objectid import ObjectId
import discord

from ..config import config
from ..utils import character_permissions_check
from ..masquerade import get_character_mask
from .formatting import format_report_notification, format_spam_score
from . import silence
from . import spam_filter

REPORT_STATUS = {
    "open": None,
    "resolved": "Resolved",
    "wontfix": "Not a Bug",
    "invalid": "Report Incomplete / Invalid",
    "spam": "Spam",
}

BUG_EMOJI = "🐛"
RESOLVED_EMOJI = "✅"
WONTFIX_EMOJI = "❎"
INVALID_EMOJI = "❓"
SPAM_EMOJI = "🚫"
MUTE_EMOJI = "🔇"

STATUS_EMOJIS = {
    "open": BUG_EMOJI,
    "resolved": RESOLVED_EMOJI,
    "wontfix": WONTFIX_EMOJI,
    "invalid": INVALID_EMOJI,
    "spam": SPAM_EMOJI,
}

EMOJI_STATUS = {}

for k, v in STATUS_EMOJIS.items():
    EMOJI_STATUS[v] = k

async def handle_bug_report_reaction(client, user_id, msg_id, reaction_char):
    if user_id == client.user.id:
        return

    raw_user = client.get_user(user_id)
    if raw_user is None or raw_user.bot:
        return

    dev_server = client.get_guild(config.primary_server_id)
    user = await dev_server.fetch_member(user_id)
    if user is None:
        return

    # Identify what status to set
    status = None
    do_mute = False

    if reaction_char == SPAM_EMOJI:
        status = "spam"
        do_mute = False
    elif reaction_char == MUTE_EMOJI:
        status = "spam"
        do_mute = True
    elif reaction_char in EMOJI_STATUS:
        status = EMOJI_STATUS[reaction_char]
    else:
        return

    # Grab notification data
    doc = await client.db.report_notifications.find_one({"msg_id": msg_id})
    if doc is None:
        return

    # Get the channel and message
    channel = client.get_channel(doc["msg_channel"])
    if channel is None:
        return

    message = await channel.fetch_message(doc["msg_id"])
    if message is None:
        return

    # Check roles:
    if status == "spam" and user_id not in config.authorized_users:
        await client.log_notify(
            "Unauthorized user {}#{} attempted to mark bug report {} as spam in channel <#{:s}>".format(
                user.name, user.discriminator, doc["bug_id"], str(channel.id)
            )
        )
        return

    bug_doc = await client.db.bug_reports.find_one({"_id": ObjectId(doc["bug_id"])})
    authorized = user_id in config.authorized_users

    # For reports with characters in them: do a per-character permissions check
    if "character" in bug_doc and not authorized:
        authorized = await character_permissions_check(
            client, bug_doc["character"], user_id
        )

    if not authorized:
        return await client.log_notify(
            "Unauthorized user {}#{} attempted to close bug report {} in channel <#{:s}>".format(
                user.name, user.discriminator, doc["bug_id"], str(channel.id)
            )
        )

    if do_mute:
        ip_hash = bug_doc["ip"]
        await silence.silence_reports_from_ip(client, ip_hash, 48, user)

    return await set_bug_report_status(client, doc["bug_id"], status, user)

async def set_bug_report_status(client, report_id, status, setting_user):
    await client.db.bug_reports.update_one(
        {"_id": ObjectId(report_id)}, {"$set": {"status": status}}
    )

    cursor = client.db.report_notifications.find({"bug_id": ObjectId(report_id)})
    report = await client.db.bug_reports.find_one({"_id": ObjectId(report_id)})
    mask = None
    mask_channel_id = None
    mask_name = None

    if report["type"] != "auto":
        spam_class, spam_score = spam_filter.classify(report["description"])
        report["spam_class"] = spam_class
        report["spam_score"] = spam_score

    print("Setting bug report {} to status {}".format(report_id, status))

    if "character" in report and report["character"] is not None:
        mask = await get_character_mask(client, report["character"])
        if mask is not None and config.masquerade_enabled:
            mask_channel_id = await mask.get_channel_id()
            mask_name = await mask.get_name()

    pretty_status = REPORT_STATUS[status]
    if status == "open":
        formatted_report = await format_report_notification(client, report_id, report)
    else:
        formatted_report = None

    async for doc in cursor:
        channel = client.get_channel(doc["msg_channel"])
        if channel is None:
            continue

        try:
            message = await channel.fetch_message(doc["msg_id"])
            if message is None:
                continue
        except discord.errors.NotFound:
            continue

        try:
            if status == "open":
                await message.edit(content=formatted_report)
                await message.remove_reaction(STATUS_EMOJIS["open"], client.user)

                for s, emoji in STATUS_EMOJIS.items():
                    if s != "open":
                        await message.add_reaction(emoji)

                await message.add_reaction(MUTE_EMOJI)
            else:
                label = "Issue `{}`:  {}".format(report_id, pretty_status)
                if "character" in report and report["character"] is not None:
                    label = str(report["character"]).capitalize() + " " + label

                if (
                    mask is None
                    or not (await mask.user_in_mask(setting_user))
                    or channel.guild.id in config.masquerade.get("safe_servers", [])
                    or channel.id == mask_channel_id
                ):
                    by_label = "{}#{}".format(
                        setting_user.name, setting_user.discriminator
                    )
                else:
                    by_label = "{}#{}".format(
                        mask_name, "".join(random.choices("0123456789", k=4))
                    )

                close_marker = "**{}** (closed by {}) - React with 🐛 to reopen.".format(
                    label, by_label
                )

                if status == "spam":
                    close_marker += "\n" + format_spam_score(report)

                await message.edit(
                    content=close_marker, embed=None,
                )

                for emoji in STATUS_EMOJIS.values():
                    await message.remove_reaction(emoji, client.user)

                await message.remove_reaction(MUTE_EMOJI, client.user)
                await message.add_reaction(STATUS_EMOJIS["open"])
        except (discord.errors.Forbidden, discord.errors.NotFound):
            pass


code:
import asyncio
from difflib import SequenceMatcher
from bson.objectid import ObjectId

from ..config import config
from ..utils import (
    normalize_channel_name,
    normalize_folder_name,
)
from .silence import is_ip_silenced
from ..masquerade import get_character_mask

def find_character_channel(client, character_id):
    normalized_id = normalize_folder_name(character_id)
    dev_server = client.get_guild(config.primary_server_id)

    for channel in dev_server.text_channels:
        channel_name = normalize_channel_name(channel.name)

        if channel_name == normalized_id:  # prioritize exact matches
            return channel

    return None

async def is_feedback_enabled(client, character_id: str) -> bool:
    feedback_enabled = await client.redis.hget(
        "feedback_data:" + character_id, "enabled", encoding="utf-8",
    )

    try:
        return int(feedback_enabled) > 0
    except (ValueError, TypeError):
        return True

async def get_report_channels(client, report_id, report):
    loop = asyncio.get_event_loop()

    if await is_ip_silenced(client.redis, report["ip"]):
        await client.db.bug_reports.update_one(
            {"_id": ObjectId(report_id)}, {"$set": {"status": "spam"}}
        )

        await client.log_notify(
            "Received report `{}` from silenced IP `{}`".format(
                str(report_id), report["ip"]
            )
        )

        return config.silenced_report_channels

    # Don't post reports from broken rehost
    if "blogofsex" in report["circumstances"]["origin"]:
        return config.silenced_report_channels

    if report["type"] == "character" or report["type"] == "feedback":
        output_channels = list(config.manual_bug_report_channels)

        if not config.character_bug_reporting_enabled:
            return output_channels

        if report["type"] == "feedback":
            output_channels = list(config.feedback_always_channels)
            if report["character"] is not None:
                feedback_enabled = await is_feedback_enabled(
                    client, str(report["character"])
                )

                if not feedback_enabled:
                    await client.log_notify(
                        "Redirected feedback ID {} for `{}`".format(
                            report_id, str(report["character"])
                        )
                    )

                    output_channels.extend(config.feedback_redirect_channels)
                    return output_channels
            else:
                await client.log_notify(
                    "Redirected general feedback ID {}".format(report_id)
                )
                output_channels.extend(config.general_feedback_channels)
                return output_channels

        character_id = str(report["character"])
        character_channel = find_character_channel(client, character_id)
        mask = await get_character_mask(client, character_id)

        if mask is not None and config.masquerade_enabled:
            mask_ch_id = await mask.get_channel_id()

            if report["type"] == "character":
                character_channel = client.get_channel(mask_ch_id)
            else:  # report type == "feedback":
                output_channels.append(mask_ch_id)

        if character_channel is None:
            await client.error_notify(
                "Could not find channel for character {}!".format(character_id)
            )
            return []
        else:
            if report["type"] == "character":
                await client.log_notify(
                    "Routed bug ID {} to <#{}>".format(report_id, character_channel.id)
                )
            elif report["type"] == "feedback":
                await client.log_notify(
                    "Routed feedback ID {} to <#{}>".format(
                        report_id, character_channel.id
                    )
                )

            output_channels.append(character_channel.id)

            for ch_id in config.exclude_character_report_channels:
                try:
                    output_channels.remove(ch_id)
                except ValueError:
                    pass

            return output_channels
    elif report["type"] == "auto":
        stack = report["jsErrors"][-1].get("stack", "<none sent>")

        if report["circumstances"]["origin"] != "https://spnati.net":
            return []

        if (
            ("monika" in report["description"].lower())
            or ("monika" in stack.lower())
            or ("glitch" in stack.lower())
        ):
            return list(config.monika_channels)

        return list(config.auto_bug_report_channels)
    else:
        if len(report["description"].strip()) <= 0:
            return []

        return list(config.manual_bug_report_channels)


code:
import asyncio
from datetime import datetime, timedelta

async def silence_reports_from_ip(client, ip_hash, n_hours, requesting_user):
    ip_hash = ip_hash.lower().strip()
    key = "silence:" + ip_hash

    dt = timedelta(hours=n_hours)
    new_end_time = datetime.utcnow() + dt

    current_end_ts = await client.redis.get(key)
    if current_end_ts is not None:
        cur_end_time = datetime.utcfromtimestamp(float(current_end_ts.decode("utf-8")))
        if new_end_time <= cur_end_time:
            return
    else:
        await client.log_notify(
            "User {}#{} (`{}`) silenced IP-hash `{}` for {} hours".format(
                requesting_user.name,
                requesting_user.discriminator,
                requesting_user.id,
                ip_hash,
                n_hours,
            )
        )

    tr = client.redis.multi_exec()
    tr.set(key, str(new_end_time.timestamp()))
    tr.expire(key, dt.total_seconds())
    await tr.execute()

async def unsilence_ip(redis, ip_hash):
    await redis.delete("silence:" + ip_hash)

async def is_ip_silenced(redis, ip_hash):
    end_ts = await redis.get("silence:" + ip_hash)

    if end_ts is None:
        return False

    end_ts = float(end_ts.decode("utf-8"))

    end_time = datetime.utcfromtimestamp(end_ts)
    now = datetime.utcnow()
    return now <= end_time


code:
import csv
from importlib import resources
from io import StringIO
import re
from typing import Dict, List, Set
import unicodedata

from . import feature_data
from ...config import config

BOUNTY_ID_REGEX = r"\!\!(\d{6})"
URL_REGEX = r"https?://\S+"
CORRECTION_ARROW = r"\S+\s*[\-\=]*\>\s*\S+"
STRANGE_CHARS = r"[]}{|:\\/<"

FEATURE_DATA: Dict[str, float] = None

NOT_SPAM = 3
UNCERTAIN = 2
SPAM = 1
UNKNOWN = 0

def _load_features() -> Dict[str, float]:
    data_str = resources.read_text(feature_data, "features.csv").strip()
    scores = {}

    with StringIO(data_str) as f:
        reader = csv.DictReader(f)
        for row in reader:
            scores[row["feature"]] = float(row["score"])

    return scores

def feature_db() -> Dict[str, float]:
    global FEATURE_DATA

    if FEATURE_DATA is None:
        FEATURE_DATA = _load_features()

    return FEATURE_DATA

def clean(text: str) -> str:
    # Remove bounty IDs and (most) URLs
    text = re.sub(BOUNTY_ID_REGEX, "", text)
    text = re.sub(URL_REGEX, "", text)

    # Perform unicode normalization
    text = unicodedata.normalize("NFKD", text)

    # Remove "{}" and contents
    l = text.find("{")
    r = text.rfind("}")
    if (l > 0) and (r > 0) and (l < r):
        text = text[:l] + text[r + 1 :]

    # Strip everything except token contents
    text = re.sub(r"[^a-zA-Z\'\s\,\.\?\!\*\"\']", "", text).strip()

    return text

def split(text: str) -> List[str]:
    for m in re.finditer(
        r"(?:[a-zA-Z](?:[a-zA-Z\']*[a-zA-Z])?)|,+|\.+|\?+|!+|\*+|[\"\']+", text
    ):
        word = m[0]

        if len(word) == 0:
            continue

        yield word

def extract_features(text: str) -> Set[str]:
    feats = set()
    if re.search(BOUNTY_ID_REGEX, text) is not None:
        feats.add("bounty_id")

    if re.search(CORRECTION_ARROW, text) is not None:
        feats.add("correction_arrow")

    for c in STRANGE_CHARS:
        if c in text:
            feats.add("strange_char")

    words = []
    for word in split(clean(text)):
        init = word[0]
        if init.isupper():
            feats.add("capitalization")

        words.append(word.casefold())

    if len(words) == 0:
        return

    feats.add("bigram:[START]_" + words[0])
    feats.add("bigram:" + words[-1] + "_[END]")
    for i in range(len(words) - 1):
        feats.add("bigram:" + words[i] + "_" + words[i + 1])

    for word in words:
        feats.add("word:" + word)

    return feats

def compute_score(text: str) -> float:
    score = 0
    features = extract_features(text)
    feat_scores = feature_db()

    if features is None:
        return

    for feature in filter(lambda f: f in feat_scores, features):
        score += feat_scores[feature]

    return score

def classify(text: str) -> int:
    if text is None:
        return UNKNOWN

    score = compute_score(text)
    if score is None:
        return (UNKNOWN, score)
    elif score < config.spam_likely_threshold:
        return (SPAM, score)
    elif score < config.spam_uncertain_threshold:
        return (UNCERTAIN, score)
    else:
        return (NOT_SPAM, score)


code:
```
ID: {id}
Type: Automatic Bug Report
User-Agent: {ua}
Origin: {origin}
Reporter IP Hash: {ip}
Version Commit: {commit}
JS Errors: {n_js_err}

JS Error Type: {type}
Message: {message}
Stacktrace:
{stack}
```


code:
**{type} Report {id}:**
**User-Agent**: `{ua}`
**Origin**: `{origin}`
**Reporter IP Hash**: `{ip}`
**Version Commit**: `{commit}`
**JS Errors**: {n_js_err}
{spam_class}

**Description**: {desc}
For more detailed information, DM <@!{self_id}> with `bug {id}`.


code:
ID: {id}
Type: {type}
User-Agent: {ua}
Origin: {origin}
Reporter IP Hash: {ip}
Version Commit: {commit}
Session ID: {session}

Description:
{desc}

Overall Situation: {situation}
Game Round: {currentRound}
Round Turn: {currentTurn}
Game Phase: {gamePhase}
Recent Loser: {recentLoser}
Previous Loser: {previousLoser}
Game Over?: {gameOver}
Visible Screens: {visibleScreens}

Player Gender: {playerGender}
Player Size: {playerSize}


code:
JS Error {n}:
    Timestamp: {date}
    Type: {errType}
    Message: {message},
    Filename: {filename},
    Line: {lineno},
    Stacktrace:
{stack}


code:
Table Slot {slot}:
    Character: {id}
    Stage: {stage}
    Time in Stage: {timeInStage}
    Markers: {markers},
    Current Image: {currentImage}
    Current Dialogue:
    {currentLine}


code:
import asyncio
import time
from datetime import datetime, timedelta
import xml.etree.ElementTree as ET

import aiohttp
from bs4 import BeautifulSoup

from .config import config
from .utils import get_http_session

async def get_deployed_commit(site):
    sess = get_http_session()

    try:
        async with sess.get("https://{}/config.xml".format(site)) as resp:
            if resp.status < 200 or resp.status >= 300:
                return None

            xml_data = await resp.text()
            root = ET.fromstring(xml_data.strip())
            commit_tag = root.find("commit")

            if commit_tag is None:
                return None
            else:
                return commit_tag.text
    except Exception:
        pass

async def get_recent_report_counts(client):
    now_minute = int(time.time() // 60)

    report_keys = [
        "request-minute:" + str(t) + ":reports"
        for t in range(now_minute, now_minute - 15, -1)
    ]
    user_keys = [
        "request-minute:" + str(t) + ":users"
        for t in range(now_minute, now_minute - 15, -1)
    ]

    report_counts = await client.redis.mget(*report_keys, encoding="utf-8")
    user_counts = await asyncio.gather(*(client.redis.scard(k) for k in user_keys))

    total_reports = 0
    for v in report_counts:
        try:
            total_reports += int(v)
        except (ValueError, TypeError):
            pass

    return (total_reports, sum(user_counts))

async def observe_commit_propagation_loop(client):
    client.current_deployed_commit = [None]
    observed_sites = ["spnati.net"]

    while True:
        new_deployed_commits = list(client.current_deployed_commit)

        try:
            observations = []
            for site in observed_sites:
                observations.append(get_deployed_commit(site))

            deployed_commits = await asyncio.gather(*observations)
            for i, site, commit, last_seen in zip(
                range(len(observed_sites)),
                observed_sites,
                deployed_commits,
                client.current_deployed_commit,
            ):
                if commit is None:
                    continue

                if commit != last_seen:
                    if last_seen is None:
                        print(
                            "Currently observed commit on {}: {}".format(
                                site, commit[:8]
                            )
                        )
                    else:
                        await client.log_notify(
                            "Observed new deployed commit on `{}:` `{}`".format(
                                site, commit[:8]
                            )
                        )
                    new_deployed_commits[i] = commit
        except Exception:
            await client.log_exception("in commit propagation loop")

        client.current_deployed_commit = new_deployed_commits

        # Calculate next check time.
        # Check at every multiple of 5 minute
        # (i.e. at :00, :05, :10, ...)
        cur_time = datetime.utcnow()

        last_check_time = datetime(
            cur_time.year,
            cur_time.month,
            cur_time.day,
            cur_time.hour,
            int(cur_time.minute / 5) * 5,
            0,
        )
        next_check_time = last_check_time + timedelta(minutes=5)

        await asyncio.sleep((next_check_time - datetime.utcnow()).total_seconds())


code:
from pathlib import Path
import tempfile

async def handle_tcnati_message(client, msg):
    img_dir = tempfile.mkdtemp()
    img_path = Path(img_dir)

    images = []

    for attachment in msg.attachments:
        if attachment.height is None or attachment.width is None:
            continue

        save_path = img_path.joinpath(attachment.filename)
        with save_path.open("wb") as f:
            await attachment.save(f)

        images.append((str(save_path), attachment.filename, attachment.url))

    report = {
        "author": msg.author.id,
        "content": msg.content,
        "channel": msg.channel.id,
        "category": msg.channel.category_id,
        "server": None,
        "images": images,
        "img_dir": str(img_path),
    }

    if msg.guild is not None:
        report["server"] = msg.guild.id

    await client.redis_pub.publish_json("tcnati:messages", report)


code:
from typing import Dict, List, Optional, Set, AsyncGenerator, Optional
from datetime import datetime, timedelta, timezone
from bs4 import BeautifulSoup
import sys

from . import linecount, permissions
from .config import config
from .git import git_ops
from .git.info import get_merge_requests
from .git.commit_info import ChangedFile
from .utils import format_name
from .reporting.routing import find_character_channel

WARNING_TIMES = {
    timedelta(
        days=31
    ): "{label:s}'s last update was a month ago (<t:{update_ts:d}:D>). They're now hidden by default on the Testing roster, and will soon be taken off entirely due to lack of updates.",
    timedelta(
        days=26
    ): "{label:s}'s last update was 26 days ago (<t:{update_ts:d}:D>). If you don't push an update within five days, they may be taken off the Testing roster.",
}

class UpdateTimeNotFound(Exception):
    pass

class CharacterUpdateStatus:
    def __init__(
        self,
        character: str,
        last_update: datetime,
        pending_mrs: Set[str],
        last_warning: Optional[datetime] = None,
    ):
        cur_time = datetime.now(timezone.utc)
        self.character: str = character
        self.last_update: datetime = last_update
        self.last_warning: Optional[datetime] = (
            last_warning if last_warning is not None else None
        )
        self.threshold: Optional[timedelta] = None
        self.pending_mrs: Set[str] = pending_mrs

        for threshold in sorted(WARNING_TIMES, reverse=True):
            if cur_time >= (last_update + threshold):
                self.threshold = threshold
                break

    @property
    def reached_threshold(self) -> bool:
        return self.threshold is not None

    @property
    def suppressed(self) -> bool:
        return len(self.pending_mrs) > 0

    @property
    def already_warned(self) -> bool:
        return (
            (self.last_warning is not None)
            and (self.threshold is not None)
            and (self.last_warning >= (self.last_update + self.threshold))
        )

    @property
    def should_warn(self) -> bool:
        return self.reached_threshold and not (self.suppressed or self.already_warned)

    @property
    def character_name(self) -> str:
        return format_name(self.character)

    @property
    def warn_msg(self) -> Optional[str]:
        if not self.reached_threshold:
            return None
        return WARNING_TIMES[self.threshold].format(
            update_ts=int(self.last_update.timestamp()),
            label=self.character_name,
        )

    def __str__(self):
        if not self.reached_threshold:
            return "{}: No Warning".format(self.character_name)

        ret = "{}: **{}-Day Warning** ".format(
            self.character_name, int(self.threshold.days)
        )

        if self.suppressed:
            ret += "Suppressed"
            pending_updates = sorted(self.pending_mrs)
            if len(pending_updates) == 1:
                update_str = (
                    "(last update <t:{:d}:R>, update !"
                    + str(pending_updates[0])
                    + " pending)"
                )
            elif len(pending_updates) == 2:
                update_str = (
                    "(last update <t:{:d}:R>, updates !"
                    + str(pending_updates[0])
                    + " and "
                    + str(pending_updates[1])
                    + " pending)"
                )
            elif len(pending_updates) > 2:
                update_str = "(last update <t:{:d}:R>, updates " + ", ".join(
                    "!" + str(iid) for iid in pending_updates[:-1]
                )
                update_str += ", and !" + str(pending_updates[-1]) + " pending)"
            ret += " " + update_str.format(int(self.last_update.timestamp()))
        elif self.already_warned:
            ret += "Already Displayed (last update <t:{:d}:R>, warning triggered <t:{:d}:R>)".format(
                int(self.last_update.timestamp()), int(self.last_warning.timestamp())
            )
        elif self.last_warning is None:
            ret += "Queued (last update <t:{:d}:R>, no previous warnings triggered)".format(
                int(self.last_update.timestamp())
            )
        else:
            ret += "Queued (last update <t:{:d}:R>, last warning triggered <t:{:d}:R>)".format(
                int(self.last_update.timestamp()), int(self.last_warning.timestamp())
            )

        return ret

async def get_last_update(character: str, ref: str = None) -> datetime:
    if ref is None:
        ref = config.upstream_remote_name + "/master"

    meta_data = await git_ops.get_file(
        ref, "opponents/" + character + "/meta.xml", from_ops=True
    )
    soup = BeautifulSoup(meta_data, features="html.parser")

    if soup.lastupdate is None:
        raise UpdateTimeNotFound(character)
    return datetime.fromtimestamp(int(soup.lastupdate.string) / 1000, timezone.utc)

async def get_mr_update_characters(mr_iid: int) -> Set[str]:
    updated_characters = set()
    async for changed_file in ChangedFile.get_tree_diff(
        config.upstream_remote_name + "/master",
        config.upstream_remote_name + "/merge-requests/" + str(mr_iid),
        merge_base=True,
        filter_paths="opponents",
    ):
        if changed_file.dest_path is not None:
            target_path = changed_file.dest_path
        else:
            target_path = changed_file.src_path

        try:
            target_path = target_path.relative_to(config.ops_repo_dir)
        except ValueError:
            continue

        if (
            len(target_path.parts) >= 3
            and target_path.parts[0] == "opponents"
            and (
                target_path.name in ("behaviour.xml", "meta.xml", "collectibles.xml")
                or target_path.suffix == ".png"
            )
        ):
            updated_characters.add(target_path.parts[1])
    return updated_characters

async def get_testing_update_statuses(
    client,
) -> AsyncGenerator[CharacterUpdateStatus, None]:
    if git_ops.GIT_LOCK.locked():
        print(
            "Staleness warning checks are waiting for Git lock...",
            file=sys.stderr,
            flush=True,
        )

    async with git_ops.GIT_LOCK:
        await git_ops.reset_ops_repo()
        roster = await linecount.get_roster("master")

        pending_updates: Dict[str, Set[int]] = dict()
        open_mrs = await get_merge_requests(
            client, config.upstream_project_id, "opened"
        )

        for mr in open_mrs:
            iid = mr["iid"]
            mr_updates = await get_mr_update_characters(iid)
            for character in filter(lambda c: roster[c] == "testing", mr_updates):
                pending_updates.setdefault(character, set()).add(iid)

        for character in filter(lambda c: roster[c] == "testing", roster):
            try:
                last_update = await get_last_update(character)
            except UpdateTimeNotFound:
                await client.error_notify(
                    "Could not get last update time for `{}`".format(character)
                )
                continue

            last_warning = await get_last_warning_time(client, character)
            yield CharacterUpdateStatus(
                character,
                last_update,
                pending_updates.get(character, list()),
                last_warning,
            )

async def get_last_warning_time(client, character: str) -> Optional[datetime]:
    ts = await client.redis.get("testing_warnings:" + character, encoding="utf-8")
    if ts is None or len(ts) == 0:
        return None
    return datetime.fromtimestamp(float(ts), timezone.utc)

async def update_last_warning_time(client, character: str):
    cur_time = datetime.now(timezone.utc)
    await client.redis.set("testing_warnings:" + character, str(cur_time.timestamp()))

async def get_warnings_enabled(client, character: str, user_id: int) -> bool:
    status = await client.redis.get(
        "testing_warning_settings:" + character + ":" + str(user_id), encoding="utf-8"
    )
    return (status is None) or (status.lower() == "true")

async def set_warnings_enabled(client, character: str, user_id: int, enabled: bool):
    await client.redis.set(
        "testing_warning_settings:" + character + ":" + str(user_id), str(enabled)
    )

async def process_staleness_warnings(client):
    print("Processing staleness warnings...", file=sys.stderr, flush=True)

    lines = ["Processed Testing Staleness warnings:"]
    warned_list: List[CharacterUpdateStatus] = []

    async for update_status in get_testing_update_statuses(client):
        if update_status.reached_threshold:
            print(
                "Processed: {} (message: {})".format(
                    update_status.character_name, update_status.warn_msg
                ),
                file=sys.stderr,
                flush=True,
            )
            lines.append(str(update_status))

        if update_status.should_warn:
            warned_list.append(update_status)

    if len(warned_list) > 0:
        for warning in warned_list:
            character_channel = find_character_channel(client, warning.character)
            msg = warning.warn_msg
            if msg is None or character_channel is None:
                continue

            alert_text = "⚠  **Warning:** " + msg
            developers = await permissions.get_developers(
                client.redis, warning.character
            )
            if len(developers) > 0:
                alert_text += "\n" + " ".join(
                    "<@!{}>".format(dev_id) for dev_id in developers
                )

            await character_channel.send(alert_text)
            await update_last_warning_time(client, warning.character)

        lines.append(
            "Warned: " + ", ".join(warning.character_name for warning in warned_list)
        )

    print("Done processing staleness warnings.", file=sys.stderr, flush=True)

    await client.log_notify("\n".join(lines))


code:
import asyncio
from difflib import SequenceMatcher
import re
from typing import Iterable
from itertools import chain

import aiohttp
import discord

from .config import config
from .masquerade import get_character_mask
from . import permissions

version = None
_sess = aiohttp.ClientSession()

def get_http_session():
    return _sess

async def get_cmd_output(cmd, do_strip=True):
    proc = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE)
    stdout, _ = await proc.communicate()
    output = stdout.decode("utf-8")

    if do_strip:
        return output.strip()
    else:
        return output

async def get_service_info(unit):
    raw = await get_cmd_output("systemctl show " + unit)
    svc_data = {}

    for line in raw.split("\n"):
        key, value = line.split("=", 1)
        svc_data[key] = value

    return svc_data

async def get_version():
    global version
    if version is None:
        version = await get_cmd_output("git rev-parse --short HEAD")

    return version

def split_long_message(contents) -> Iterable[str]:
    cur_resp = ""
    if isinstance(contents, str):
        contents = contents.splitlines()

    for line in chain.from_iterable(map(str.splitlines, contents)):
        if len(cur_resp) + len(line) > 1900:
            yield cur_resp
            cur_resp = line + "\n"
        else:
            cur_resp += line + "\n"

    if len(cur_resp) > 0:
        yield cur_resp

def normalize_folder_name(name):
    return re.sub(r"[^a-zA-Z0-9]", "", name).casefold()

def normalize_role_name(name):
    if name == "Chell + Wheatley":
        name = "chell_wheatley"
    elif name == "Corrin (female)":
        name = "corrin_f"
    elif name == "Corrin (male)":
        name = "corrin_m"
    elif name == "Wii Fit Trainer":
        name = "wiifitfemale"
    elif name == "Seven of Nine":
        name = "seven"
    elif name == "Wanda":
        name = "scarlet_witch"
    elif name == "Harley Quinn":
        name = "harley"
    elif name == "Muffins":
        name = "derpy_hooves"

    return normalize_folder_name(name)

def normalize_channel_name(name):
    if name == "seven_of_nine":
        name = "seven"
    elif name == "wii_fit_trainer":
        name = "wiifitfemale"

    return normalize_folder_name(name)

async def has_character_role(
    client: discord.Client, character_id: str, user_id: int
) -> bool:
    if config.masquerade_enabled:
        mask = await get_character_mask(client, character_id)
        if mask is not None:
            return await mask.user_in_mask(user_id)

    dev_server = client.get_guild(config.primary_server_id)
    user = await dev_server.fetch_member(user_id)
    if user is None:
        return False

    normalized_id = normalize_folder_name(character_id)
    return any(normalize_role_name(role.name) == normalized_id for role in user.roles)

async def character_permissions_check(client, character_id: str, user_id: int) -> bool:
    if await permissions.check(client.redis, user_id, character_id):
        return True

    return await has_character_role(client, character_id, user_id)

SPECIAL_CASE_NAMES = {
    "9s": "9S",
    "ae86": "AE86",
    "alice_mgq": "Alice (MGQ)",
    "d.va": "D.Va",
    "hatsune_miku": "Miku",
    "hk416": "HK416",
    "kool-aid": "Kool-Aid Man",
    "larachel": "L'Arachel",
    "miki_miura": "Miki",
    "mom_alola": "Mom (Alola)",
    "ms.fortune": "Ms. Fortune",
    "nami_szs": "Nami (SZS)",
    "pa-15": "PA-15",
    "pauling": "Miss Pauling",
    "penny_polendina": "Penny (RWBY)",
    "pot_of_greed": "Pot of Greed",
    "ringo_ando": "Ringo",
    "saki_zls": "Saki (ZLS)",
    "scm": "Suction Cup Man",
    "wiifitfemale": "Wii Fit Trainer",
    "yshtola": "Y'shtola",
    "yuna_ffx": "Yuna (FFX)",
    "asuna": "Asuna Classic",
    "asuna_yuuki": "Asuna",
    "cammy": "Cammy Classic",
    "cammy_white": "Cammy",
    "chara": "Chara Classic",
    "chara_dreemurr": "Chara",
    "chell": "Chell Classic",
    "chell_wheatley": "Chell + Wheatley",
    # "hermione": "Hermione Classic",
    # "hermione_granger": "Hermione",
    "mettaton": "Mettaton Classic",
    "mettaton_neo": "Mettaton",
    "misty": "Misty Classic",
    "misty_hgss": "Misty",
    "rarity": "Rarity Classic",
    "rarity_eg": "Rarity",
    "samus": "Samus Classic",
    "samus_aran": "Samus",
    "weiss": "Weiss Classic",
    "weiss_schnee": "Weiss",
    # "yang": "Yang Classic",
    # "yang_xiao_long": "Yang",
}

def format_name(name: str) -> str:
    if not isinstance(name, str):
        return "<invalid>"

    if name in SPECIAL_CASE_NAMES:
        return SPECIAL_CASE_NAMES[name]

    return " ".join(map(lambda x: x.capitalize(), name.split("_")))


code:
<!DOCTYPE html>

        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <title>SPNATI Automatic Bug Report Dashboard</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">

SPNATI Automatic Bug Report Dashboard

                    # Errors
                    Message
                    # IPs
                    # Sessions
                    # Games


code:
<!DOCTYPE html>

    <meta charset="utf-8">
    <title>Kisekae ALL-code Splitter</title>
    <link rel="shortcut icon" href="utilities-logo.png" type="image/x-icon">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"
        integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
    <link rel="stylesheet" href="css/code-decompose.css">

            SPNATI Utilities

        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-content"
            aria-controls="navbar-content" aria-expanded="false" aria-label="Toggle navigation">

        </button>

                    Kisekae Importer

                    Kisekae Disassembler

                    Image Cropper

                    Code Splitter(current)

                    Linecounts

                    Creator Index

                    Merge Request Viewer

                    Dialogue Crowdsourcing

Kisekae ALL-code Splitter:

This tool can split a Kisekae ALL-code into its constitutent characters.

Input ALL Code:

                <textarea id="input-code" rows="10"></textarea>

Characters:

                #1:
                <input type="text" class="result-box" id="result-0">

                #2:
                <input type="text" class="result-box" id="result-1">

                #3:
                <input type="text" class="result-box" id="result-2">

                #4:
                <input type="text" class="result-box" id="result-3">

                #5:
                <input type="text" class="result-box" id="result-4">

                #6:
                <input type="text" class="result-box" id="result-5">

                #7:
                <input type="text" class="result-box" id="result-6">

                #8:
                <input type="text" class="result-box" id="result-7">

                #9:
                <input type="text" class="result-box" id="result-8">


code:
<!DOCTYPE html>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Creator Index</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
        integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <link rel="stylesheet" href="css/creators.css">
    <link rel="shortcut icon" href="utilities-logo.png" type="image/x-icon">

            SPNATI Utilities

        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-content"
            aria-controls="navbar-content" aria-expanded="false" aria-label="Toggle navigation">

        </button>

                    Kisekae Importer

                    Kisekae Disassembler

                    Image Cropper

                    Code Splitter

                    Linecounts

                    Creator Index (current)

                    Merge Request Viewer

                    Dialogue Crowdsourcing

Character Creator Index

            This table lists the Discord users who are permitted to submit updates for each character in SPNATI.

            Note that some characters, especially older ones, may not have any configured creators.

                    Character
                    Status
                    Creators