Repository: python-discord/bot

Issue:
Don't show infraction total outside staff channels.
Currently, when we deliver an infraction, it will show the infraction total in the bot's response.

![image](https://user-images.githubusercontent.com/2098517/66942037-70113c00-f048-11e9-8867-114b2339a1ac.png)

This is a cool feature, but should not happen in public channels. So let's do something about that.

### Infraction total should be allowed in the following channels:
```
#admins        ID: 365960823622991872
#admin-spam    ID: 563594791770914816
#mod-spam      ID: 620607373828030464
#mods          ID: 305126844661760000
#helpers       ID: 385474242440986624
#organisation  ID: 551789653284356126
#defcon        ID: 464469101889454091
```

If the command is called in any other channel, **do not show the infraction total**. This applies to all moderation commands that currently show the total.

If any of the above channels are not currently registered as constants, please create new constants for them. The above list of channels can be stored as a group constant called `STAFF_CHANNELS`. Make use of [YAML node anchors](https://yaml.org/spec/1.2/spec.html#&%20anchor//) when you do this.



Discussion:

Resolving patch:
diff --git a/bot/__main__.py b/bot/__main__.py
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -6,11 +6,11 @@
 from discord import Game
 from discord.ext.commands import Bot, when_mentioned_or
 
-from bot.api import APIClient
+from bot.api import APIClient, APILoggingHandler
 from bot.constants import Bot as BotConfig, DEBUG_MODE
 
 
-log = logging.getLogger(__name__)
+log = logging.getLogger('bot')
 
 bot = Bot(
     command_prefix=when_mentioned_or(BotConfig.prefix),
@@ -29,6 +29,7 @@
     )
 )
 bot.api_client = APIClient(loop=asyncio.get_event_loop())
+log.addHandler(APILoggingHandler(bot.api_client))
 
 # Internal/debug
 bot.load_extension("bot.cogs.error_handler")
diff --git a/bot/api.py b/bot/api.py
--- a/bot/api.py
+++ b/bot/api.py
@@ -1,9 +1,13 @@
+import asyncio
+import logging
 from urllib.parse import quote as quote_url
 
 import aiohttp
 
 from .constants import Keys, URLs
 
+log = logging.getLogger(__name__)
+
 
 class ResponseCodeError(ValueError):
     def __init__(self, response: aiohttp.ClientResponse):
@@ -58,3 +62,76 @@ async def delete(self, endpoint: str, *args, raise_for_status: bool = True, **kw
 
             self.maybe_raise_for_status(resp, raise_for_status)
             return await resp.json()
+
+
+def loop_is_running() -> bool:
+    # asyncio does not have a way to say "call this when the event
+    # loop is running", see e.g. `callWhenRunning` from twisted.
+
+    try:
+        asyncio.get_running_loop()
+    except RuntimeError:
+        return False
+    return True
+
+
+class APILoggingHandler(logging.StreamHandler):
+    def __init__(self, client: APIClient):
+        logging.StreamHandler.__init__(self)
+        self.client = client
+
+        # internal batch of shipoff tasks that must not be scheduled
+        # on the event loop yet - scheduled when the event loop is ready.
+        self.queue = []
+
+    async def ship_off(self, payload: dict):
+        try:
+            await self.client.post('logs', json=payload)
+        except ResponseCodeError as err:
+            log.warning(
+                "Cannot send logging record to the site, got code %d.",
+                err.response.status,
+                extra={'via_handler': True}
+            )
+        except Exception as err:
+            log.warning(
+                "Cannot send logging record to the site: %r",
+                err,
+                extra={'via_handler': True}
+            )
+
+    def emit(self, record: logging.LogRecord):
+        # Ignore logging messages which are sent by this logging handler
+        # itself. This is required because if we were to not ignore
+        # messages emitted by this handler, we would infinitely recurse
+        # back down into this logging handler, making the reactor run
+        # like crazy, and eventually OOM something. Let's not do that...
+        if not record.__dict__.get('via_handler'):
+            payload = {
+                'application': 'bot',
+                'logger_name': record.name,
+                'level': record.levelname.lower(),
+                'module': record.module,
+                'line': record.lineno,
+                'message': self.format(record)
+            }
+
+            task = self.ship_off(payload)
+            if not loop_is_running():
+                self.queue.append(task)
+            else:
+                asyncio.create_task(task)
+                self.schedule_queued_tasks()
+
+    def schedule_queued_tasks(self):
+        for task in self.queue:
+            asyncio.create_task(task)
+
+        if self.queue:
+            log.debug(
+                "Scheduled %d pending logging tasks.",
+                len(self.queue),
+                extra={'via_handler': True}
+            )
+
+        self.queue.clear()


Test patch: