Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
Currently, only a Discord webhook delivery backend is implemented.
  • Loading branch information
callaa committed Nov 20, 2019
1 parent e645203 commit e76c73e
Show file tree
Hide file tree
Showing 17 changed files with 345 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.git
.env
**/__pycache__

4 changes: 4 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[flake8]

ignore = W391

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.swp
.env
__pycache__
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM python:3.7-alpine3.9

RUN adduser -D -u 1000 python
WORKDIR /home/python

COPY freeze.txt .
RUN pip install -r freeze.txt

COPY report_relay report_relay
USER python
CMD ["gunicorn", "report_relay.main:app", "--bind", "0.0.0.0:8080", "--worker-class", "aiohttp.GunicornWebWorker"]

22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
MIT License

Copyright (c) 2019 Calle Laakkonen

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Drawpile abuse report relay
---------------------------

This is a simple Python script that receives abuse reports from a Drawpile
server and relays them forward.

Currently, only a Discord webhook relay target is implemented.


## Installation

Python 3.7 is required. Check the `requirements.txt` file for a list of
libraries that must be installed.

To deploy as a Docker container, you can build the image by running:

docker build -t drawpile_report_relay .

To run:

docker run -p 8080:8080 --env-file config drawpile_report_relay

Or check `start.sh` for an example.

The application is configured using environment variables.
Check `abusereport/settings.py` for the full list of settings, but at least
the following should be set:

* `AUTH_TOKEN`: the token shared with drawpile-srv
* `SERVER_HOST`: domain name of the server. This is used when generating a link to the session
* `DISCORD_WEBHOOK`: the Discord webhook URL to use.

95 changes: 95 additions & 0 deletions report_relay/abusereport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import asyncio
import logging

from jsonschema import validate, ValidationError
from aiohttp import web

from .settings import settings
from . import discord

logger = logging.getLogger(__name__)

REPORT_SCHEMA = {
"type": "object",
"properties": {
"session": {
"type": "string",
"pattern": r"[0-9a-fA-F-]+"
},
"sessionTitle": {"type": "string"},
"user": {"type": "string"},
"auth": {"type": "boolean"},
"ip": {"type": "string"},
"message": {"type": "string"},
"offset": {"type": "integer"},
"perp": {"type": "integer"},
"users": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"},
"auth": {"type": "boolean"},
"mod": {"type": "boolean"},
"ip": {"type": "string"},
},
"required": ["id", "name", "ip"]
}
}
},
"required": ["session", "sessionTitle", "user", "ip", "message", "users"]
}


async def receive_report(request):
try:
auth_type, auth_token = request.headers\
.get('authorization', '').split(' ', 1)
except ValueError:
return web.Response(text="Bad authorization", status=401)

if auth_type.lower() != 'token':
return web.Response(
text="Authorization type must be Token",
status=401)

if auth_token != settings.AUTH_TOKEN:
return web.Response(text="Incorrect token", status=401)

try:
body = await request.json()
except ValueError:
return web.Response(text="Unparseable JSON", status=400)

try:
validate(instance=body, schema=REPORT_SCHEMA)
except ValidationError as e:
return web.Response(text=e.message, status=400)

targets = []

if settings.DISCORD_WEBHOOK:
targets.append(discord.send_abusereport(
settings.DISCORD_WEBHOOK,
settings.SERVER_HOST,
body))

# We could support other webhook and delivery methods here,
# such as Slack, email or a Telegram bot.

if not targets:
logger.warn(
"Report from %s not relayed, because no targets were configured!",
body['user'])

else:
await asyncio.gather(*targets)

logger.info("Abuse report from %s sent to %d target(s)".format(
body['user'],
len(targets)
))

return web.Response(status=204)

56 changes: 56 additions & 0 deletions report_relay/discord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import aiohttp
import logging


logger = logging.getLogger(__name__)


async def send_abusereport(webhook_url, server_host, report):
embed_fields = []

# Find details about the user being reported on
# (if any: the report may also be about the session in general)
if report.get('perp'):
troller = {
"name": f":japanese_ogre: User #{report['perp']}",
"value": "Unknown user",
}
for user in report['users']:
if user['id'] == report['perp']:
troller = {
"name": f":japanese_ogre: {user['name']}",
"value": user['ip'],
}
break

embed_fields.append(troller)

# The user doing the reporting
embed_fields.append({
"name": f":cold_sweat: {report['user']}",
"value": report["ip"]
})

# Construct the message
message = {
"embeds": [
{
"title": f""":bangbang: Abuse report received from {server_host} session {report["sessionTitle"] or "(Untitled session)"}""",
"description": report["message"] + f"\nSession: drawpile://{server_host}/{report['session']}",
"color": 16711680,
"author": {
"name": report["user"],
},
"fields": embed_fields,
}
]
}

# Send it to the Discord channel webhook
async with aiohttp.ClientSession() as session:
async with session.post(webhook_url, json=message) as response:
if response.status not in (200, 204):
text = await response.text()
logger.warn(f"{webhook_url} responded with {response.status}: {text}")


16 changes: 16 additions & 0 deletions report_relay/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from aiohttp import web

from . import abusereport


async def App():
app = web.Application()

app.router.add_post('/', abusereport.receive_report)

return app


if __name__ == '__main__':
web.run_app(App())

38 changes: 38 additions & 0 deletions report_relay/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import sys
import os

from dotenv import load_dotenv


required = object()


class Settings:
def __init__(self, *defaults):
self._errors = []
for key, default in defaults:
value = os.getenv(key)

if value is None:
if default is required:
self._errors.append(f"Environment variable {key} not set!")
continue
else:
value = default

setattr(self, key, value)


load_dotenv()

settings = Settings(
('AUTH_TOKEN', required), # Token shared with the Drawpile server
('SERVER_HOST', required), # Hostname of the Drawpile server
('DISCORD_WEBHOOK', ''), # URL of the Discord webhook relay target
)

if settings._errors:
for e in settings._errors:
print(e, file=sys.stderr)
sys.exit(1)

4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
aiohttp==3.6.2
python-dotenv==0.10.3
gunicorn==20.0.0
jsonschema==3.2.0
5 changes: 5 additions & 0 deletions start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# production style:
#gunicorn report_relay.main:App --bind localhost:8080 --worker-class aiohttp.GunicornWebWorker

# dev server style:
python3 -m report_relay.main
4 changes: 4 additions & 0 deletions tests/abusereport-invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"session": "93c61170-0800-4a31-8bc2-410ec488702c"
}

11 changes: 11 additions & 0 deletions tests/abusereport-session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"session": "93c61170-0800-4a31-8bc2-410ec488702c",
"sessionTitle": "Test Session",
"user": "reporter",
"auth": false,
"ip": "192.168.1.1",
"message": "This session is breaking the rules",
"offset": 10000,
"users": []
}

27 changes: 27 additions & 0 deletions tests/abusereport-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"session": "93c61170-0800-4a31-8bc2-410ec488702c",
"sessionTitle": "Test Session",
"user": "reporter",
"auth": false,
"ip": "192.168.1.1",
"message": "This guy is trolling!",
"offset": 10000,
"perp": 3,
"users": [
{
"id": 1,
"name": "reporter",
"auth": false,
"mod": false,
"ip": "192.168.1.1"
},
{
"id": 3,
"name": "troller",
"auth": false,
"mod": false,
"ip": "192.168.1.2"
}
]
}

4 changes: 4 additions & 0 deletions tests/invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"hello":
}

8 changes: 8 additions & 0 deletions tests/send_abusereport.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
if [ "$1" == "" ]
then
echo "Usage: $0 <sample file.json>"
exit 1
fi

curl -d "@$1" -H "Content-Type: application/json" -X POST -H "Authorization: Token test" http://localhost:8080/

0 comments on commit e76c73e

Please sign in to comment.