aboutsummaryrefslogtreecommitdiffstats
path: root/arthur/exts/systems/system_information.py
blob: 51b0ed26ab1a59d0766e73d30f7d4cc6177ed404 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
"""Return system information on our production 9front infrastructure."""

import asyncio
import base64
import io
import random
from datetime import UTC, datetime
from typing import Literal
from urllib import parse

import aiohttp
from discord import File, Member, Message
from discord.ext.commands import Cog, Context, Converter, command
from loguru import logger
from wand.image import Image

from arthur.apis.systems import lib9front
from arthur.bot import KingArthur
from arthur.config import CONFIG
from arthur.exts.systems.motd import MOTD

BASE_RESOURCE = "https://git.9front.org/plan9front/plan9front/HEAD/{}/raw"
THRESHOLD = 0.01
MIN_MINUTES = 30
BLOG_ABOUT_IT_THRESHOLD = 1000
CORPORATE_FRIENDLY_SMILEYS = (
    ":smile:",
    ":slight_smile:",
    ":grin:",
    ":blush:",
)


class URLConverter(Converter):
    """Validate a passed argument is a URL, for use in optional converters that are looking for URLs."""

    async def convert(self, _ctx: Context, argument: str) -> str | None:
        """Attempt to convert a string to a URL, return the argument if it is a URL, else return None."""
        try:
            parsed = parse.urlparse(argument)

            if parsed.scheme in {"http", "https"}:
                return argument
        except ValueError:
            return None


class SystemInformation(Cog):
    """Utilities for fetching system information from our 9front infrastructure."""

    SUPPORTIVE_PYTHON_DISCORD_COMMENTS = """\
{Lemon|Bella|Chris} would {seriously|really|honestly} love to {have a word|discuss this topic} \
with you! There {seems to be|is|appears to be|is assumed to be} a {tiny|minor|major|large} \
{misunderstanding|miscommunication} here!|
{Python|Erlang|Chris' exhaust} is peace. \
{The DevOps team|Decentralized version control|The 2024 presidential election} is replication. \
Your {message|comment|idea|thought} is strength.|
Who controls {King Arthur|Kubernetes|Netcup|Joe's medication} controls the future. \
Who controls {Bella's rations|the dennis.services mail server|`git push -f` access|edit rights to this message} controls the past.|
The best {messages|comments|ideas|chats}... are those that tell you what you know already.|
If you want to keep {a secret|PGP private keys|Lemoncluster access|access to Joe's secret vacation photo library}, you must also hide it from {yourself|the moderators team|the ethical advisory board|Chris}.|
{:warning:|:information_source:|:no_entry_sign:} Detected a high amount of doublethink in this message. \
The {moderators have|administrators have|DevOps team has|Python Discord ethical advisory board has} been informed.|
Reading your message, I realize: Perhaps a lunatic was simply a minority of one.|
It's a beautiful thing, the destruction of words.|
I enjoy talking to you. Your mind appeals to me. It resembles my own mind except that you happen to be {clinically |absolutely |completely |}insane.
"""

    def __init__(self, bot: KingArthur) -> None:
        self.bot = bot
        self.cached_resources = {}
        self.cached_blogcom = None
        self.cached_bullshit = None
        self.last_sent = None

    async def fetch_resource(self, name: str) -> str:
        """Fetch the file contents of the given filename, starting from ``/``."""
        if name not in self.cached_resources:
            url = BASE_RESOURCE.format(name)
            async with aiohttp.ClientSession() as session, session.get(url) as resp:
                self.cached_resources[name] = await resp.text()
        return self.cached_resources[name]

    @Cog.listener()
    async def on_message(self, msg: Message) -> None:
        """Handler for incoming messages, potentially returning system information."""
        if not msg.guild:
            logger.trace("Ignoring DM")
            return

        if msg.author.id == self.bot.user.id:
            logger.trace("Ignoring own message")
            return

        if msg.channel.id != CONFIG.devops_channel_id:
            logger.trace("Ignoring message outside of DevOps channel")
            return

        if self.last_sent:
            if (datetime.now(tz=UTC) - self.last_sent).seconds // 60 < MIN_MINUTES:
                logger.trace("Ignoring message as within cooldown")
                return

        msg_thresh = THRESHOLD

        if isinstance(msg.author, Member):
            if CONFIG.devops_role not in (r.id for r in msg.author.roles):
                logger.trace("Upping threshold due to non-DevOps member")
                msg_thresh *= 10

        if len(msg.content) > BLOG_ABOUT_IT_THRESHOLD:
            msg_thresh += 0.10

        if random.random() < msg_thresh:
            logger.trace("Criteria hit, generating comment.")
            if random.random() < 0.9:
                blogcom = await self.fetch_resource("lib/blogcom")
            else:
                blogcom = self.SUPPORTIVE_PYTHON_DISCORD_COMMENTS

            comment = lib9front.generate_blog_comment(blogcom).strip()

            self.last_sent = datetime.now(tz=UTC)
            async with msg.channel.typing():
                await asyncio.sleep(3)
                await msg.reply(f"{comment} {random.choice(CORPORATE_FRIENDLY_SMILEYS)}")

    @command(name="software")
    async def software(self, ctx: Context) -> None:
        """Return information on installed and available software."""
        bullshit = await self.fetch_resource("lib/bullshit")
        program = lib9front.generate_buzzwords(bullshit)
        await ctx.reply(program)

    @command(name="face")
    async def face(
        self, ctx: Context, resolution: int | None = 60, *, image_url: URLConverter | None = None
    ) -> None:
        """
        Generate a system-compatible face for the given file.

        If specified, resolution is the integer width and height to use for the generated image, defaulting to 60 (for a 60x60 image).

        The image can be passed in as a URL or attached to the command invocation message.
        """
        image_bytes = io.BytesIO()

        if not image_url:
            if len(ctx.message.attachments) == 0:
                await ctx.reply(":x: Must upload an image or specify image URL")
                return

            await ctx.message.attachments[0].save(image_bytes)
        else:
            async with aiohttp.ClientSession() as session, session.get(image_url) as resp:
                if resp.ok:
                    image_bytes.write(await resp.read())
                    image_bytes.seek(0)
                else:
                    await ctx.reply(
                        f":x: Could not read remote resource, check it exists. (status `{resp.status}`)"
                    )
                    return

        out_bytes = io.BytesIO()

        with Image(file=image_bytes) as img:
            img.resize(resolution, resolution)

            img.quantize(number_colors=2, treedepth=8, dither=True)

            img.type = "grayscale"
            img.format = "png"

            img.save(file=out_bytes)

        out_bytes.seek(0)

        await ctx.reply(file=File(out_bytes, filename="face.png"))

    @command(name="wisdom")
    async def wisdom(
        self, ctx: Context, by: Literal["ken", "rob", "rsc", "theo", "uriel"] | None = None
    ) -> None:
        """Retrieve some software engineering wisdom."""
        if by is None:
            by = random.choice(("ken", "rob", "rsc", "theo", "uriel"))

        contents = await self.fetch_resource(f"lib/{by}")
        result = random.choice(contents.splitlines())
        await ctx.reply(result)

    @command(name="troll")
    async def troll(self, ctx: Context) -> None:
        """Utter statements of utmost importance."""
        contents = await self.fetch_resource("lib/troll")
        result = random.choice(contents.splitlines())
        await ctx.reply(result)

    @command(name="motd")
    async def motd(self, ctx: Context) -> None:
        """Generate an image representing the message of the day."""
        payload = MOTD.replace(b"\n", b"")
        file = File(io.BytesIO(base64.b64decode(payload)), filename="motd.png")
        await ctx.send(file=file)


async def setup(bot: KingArthur) -> None:
    """Add cog to bot."""
    await bot.add_cog(SystemInformation(bot))