Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
MONGO_URI=mongodb+srv://urihere
LOG_URL_PREFIX=/logs
# Your MongoDB Connection string, same as your bot's.
CONNECTION_URI=mongodb+srv://urihere
# Where should the logviewer serve your logs. Default: https://example.com/logs/LOGKEY
LOG_URL_PREFIX=/logs
# Listen address and port. Don't change them if you don't know what they do.
HOST=0.0.0.0
PORT=8000
# Whether if the logviewer should use a proxy to view attachments.
# If set to "no" (default), attachments will expire after 1 day and the logviewer won't be able to show the attachment.
# Please be aware that this may violate Discord TOS and the proxy will have full access to your attachments.
# Modmail/Logviewer is not affiliated with the proxy in any way. USE AT YOUR OWN RISK.
USE_ATTACHMENT_PROXY=no
ATTACHMENT_PROXY_URL=https://cdn.discordapp.xyz
42 changes: 42 additions & 0 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

name: Create and publish a Docker image

on:
push:
branches: ['master']

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
FROM python:3.9-slim as py
FROM python:3.12-trixie as py

RUN apt-get update && \
apt-get install -y --no-install-recommends g++ && \
rm -rf /var/lib/apt/lists/*

FROM py as build

RUN apt update && apt install -y g++
COPY requirements.txt /
RUN pip install --prefix=/inst -U -r /requirements.txt

Expand All @@ -14,4 +17,3 @@ COPY --from=build /inst /usr/local
WORKDIR /logviewer
CMD ["python", "app.py"]
COPY . /logviewer

24 changes: 12 additions & 12 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# IMPORTANT: We are no longer using Pipfile for dependency management.
# Please use pip with requirements.txt for installing dependencies.
# This file is retained for backward compatibility only, and will be removed in future releases.

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
jinja2 = "~=3.1.2"
motor = "~=3.0"
natural = "~=0.2.0"
pymongo = {version = "*", extras = ['srv']} # Required by motor
python-dateutil = "~=2.8.2"
python-dotenv = "~=0.18.0"
sanic = "~=22.6.0"
jinja2 = "==3.1.6"
motor = "==3.7.1"
natural = "==0.2.0"
pymongo = "*" # Required by motor
dnspython = "*" # Required for pymongo srv support
python-dateutil = "==2.9.0.post0"
python-dotenv = "==1.2.1"
sanic = "== 25.3.0"

[scripts]
logviewer = "python app.py"

[requires]
python_version = "3.9"
1,034 changes: 634 additions & 400 deletions Pipfile.lock

Large diffs are not rendered by default.

99 changes: 68 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,31 @@
<div align="center">
<h1>Modmail Log Viewer</h1>
<strong><i>A simple webserver to view your selfhosted modmail logs.</i></strong>
<h1>Modmail Logviewer</h1>
<strong><i>A simple webserver to view your self-hosted Modmail logs.</i></strong>
<br />
<br />
<a href="https://heroku.com/deploy?template=https://github.com/kyb3r/logviewer">
<a href="https://heroku.com/deploy?template=https://github.com/modmail-dev/logviewer">
<img src="https://img.shields.io/badge/deploy_to-heroku-997FBC.svg?style=for-the-badge" alt="Deploy to Heroku"/>
</a>
<a href="https://discord.gg/etJNHCQ">
<img src="https://img.shields.io/discord/515071617815019520.svg?label=Discord&logo=Discord&colorB=7289da&style=for-the-badge" alt="Support">
<a href="https://discord.gg/zmdYe3ZVHG">
<img src="https://img.shields.io/discord/1079074933008781362?style=for-the-badge&logo=discord&logoColor=white&label=Discord&color=%235865F2" alt="Discord">
</a>
<a href="https://patreon.com/kyber">
<img src="https://img.shields.io/badge/patreon-donate-orange.svg?style=for-the-badge&logo=Patreon" alt="Donate on Patreon">
<a href="https://buymeacoffee.com/modmaildev">
<img src="https://img.shields.io/badge/buymeacoffee-donate-ff813f.svg?style=for-the-badge&logo=buy-me-a-coffee" alt="Buy Me A Coffee">
</a>
<a href="https://www.python.org/downloads/">
<img src="https://img.shields.io/badge/Made%20With-Python%203.9-blue.svg?style=for-the-badge&logo=Python" alt="Made with Python 3.9">
<img src="https://img.shields.io/badge/Python-3.12-blue.svg?style=for-the-badge&logo=Python" alt="Supports Python 3.12">
</a>
<a href="https://github.com/psf/black">
<img src="https://img.shields.io/badge/Code%20Style-Black-black?style=for-the-badge" alt="Coding Style Black">
</a>
<a href="https://github.com/kyb3r/logviewer/blob/master/LICENSE">
<img src="https://img.shields.io/badge/license-agpl-e74c3c.svg?style=for-the-badge" alt="AGPL License">
<a href="https://github.com/modmail-dev/logviewer/blob/master/LICENSE">
<img src="https://img.shields.io/badge/license-GPLv3-e74c3c.svg?style=for-the-badge" alt="GPLv3 License">
</a>
</div>

## What is this?

In order for you to view your self-hosted logs, you have to deploy this application. Before you deploy the application, create a config var named `MONGO_URI` and put your MongoDB connection URI from the previous section into the value slot. Take the URL of this app after you deploy it and input it as a config var `LOG_URL` in the Modmail bot app.

## Updating

You can automatically update the logviewer in your Heroku account whenever changes are made to this repo.

To enable auto-updates, fork this repo and [install the Pull app in your fork](https://github.com/apps/pull). Then go to the Deploy tab in your Heroku account, select GitHub and connect your fork. Turn on auto-deploy for the master branch.
This is a simple viewer for Modmail logs. It is designed to be self-hosted alongside your Modmail bot instance, allowing you to view your logs in a web interface.

## Self-Hosting Setup

Expand All @@ -41,48 +35,91 @@ Below are some general instructions to help you get started on a Linux machine.

### Prerequisites

- A [Python 3.9 installation](https://www.python.org/downloads/) with `pip`
- A [Python 3.12 installation](https://www.python.org/downloads/) with `pip` included (Python 3.10-3.12 are supported)
- `git` for your system

e.g. on Ubuntu:
e.g. on Debian with pyenv in bash:
```shell
sudo apt install software-properties-common python3.9 python3-dev python3-pip
# Install git and pyenv dependencies
sudo apt update
sudo apt install -y make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev curl git \
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev libzstd-dev
curl https://pyenv.run | bash

# Auto-loads pyenv every time you start a terminal session
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init - bash)"' >> ~/.bashrc
touch .profile
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.profile
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.profile
echo 'eval "$(pyenv init - bash)"' >> ~/.profile

exec "$SHELL"

# Install Python 3.12
pyenv install 3.12
pyenv global 3.12
python -m pip install -U pip
```


### Deployment

Run the following shell commands:
```shell
git clone https://github.com/kyb3r/logviewer
# Clone the repository
git clone https://github.com/modmail-dev/logviewer
cd logviewer
python3 -m pip install pipenv
pipenv install

# Create and activate virtual environment (optional, but strongly recommended)
python -m venv venv
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt
cp .env.example .env
```

Edit the `.env` file (e.g. `nano .env`) and fill in your MongoDB connection URI.

> You can also customize the bind IP and port in the `.env` file.

Then to start the app, run:
```shell
pipenv run logviewer
python app.py
```

> [!NOTE]
> If you are using a virtual environment, ensure you activate it every time you reconnect your terminal session before you run/update the logviewer.
> You can do this by running `source venv/bin/activate` in the logviewer directory.

You can verify the logviewer is working by navigating to `http://<IP_OF_SERVER>:8000` (if you didn't change the bind IP / port) and should be greeted with the Logviewer main page.

To run the program in the background, you can use `screen`. Or you can use a service manager, such as `systemd`, which can also auto-restart the logviewer on failure and after system reboot.
To run the program in the background, you can use `screen`. Or you can use a service manager, such as `systemd`, which can also auto-restart the logviewer on failure and after system reboot. Remember to set the working directory and the correct Python interpreter path in your service file when using a virtual environment.

To update the logviewer, simply `cd` into the logviewer directory and run:
```shell
git pull
pip install -r requirements.txt
```

### Advanced

We recommend setting up Nginx reverse proxy to port forward external port 80 to your internal logviewer port and cache static web contents ([tutorial](https://www.hostinger.com/tutorials/how-to-set-up-nginx-reverse-proxy/)).
We recommend setting up a reverse proxy (e.g. Nginx) to port forward external port 80 to your internal logviewer port and cache static web contents ([tutorial](https://www.hostinger.com/tutorials/how-to-set-up-nginx-reverse-proxy/)).

To accept requests from a domain instead of your server IP, simply set an `A`/`AAAA` record from your DNS provider to forward your domain to your server IP.

To accept requests from a domain instead of your server IP, simply set an `A`/`AAAA` record from your DNS record manager that forwards your domain to your server IP.
## Discord OAuth2

## Discord OAuth2
Protecting your logs with a login (Discord Oauth2 support) is a premium feature, only available to [Premium members](https://buymeacoffee.com/modmaildev).

Protecting your logs with a login (Discord Oauth2 support) is a premium feature, only available to [Patrons](https://patreon.com/kyber).
## Updating on Heroku

You can automatically update the logviewer in your Heroku account whenever changes are made to this repo.

To enable auto-updates, fork this repo and [install the Pull app in your fork](https://github.com/apps/pull). Then go to the Deploy tab in your Heroku account, select GitHub and connect your fork. Turn on auto-deploy for the master branch.

## Contributing

If you can make improvements in the design and presentation of logs, please make a pull request with changes.
If you can make improvements in the design and presentation of logs, please make a pull request with the changes.
24 changes: 17 additions & 7 deletions app.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
{
"name": "Modmail Log Viewer",
"description": "A simple webserver to view self-hosted logs",
"repository": "https://github.com/kyb3r/logviewer",
"env": {
"MONGO_URI": {
"description": "MongoDB connection URI that contains your modmail logs.",
"required": true
"name": "Modmail Logviewer",
"description": "A simple webserver to view Modmail logs.",
"repository": "https://github.com/modmail-dev/logviewer",
"env": {
"CONNECTION_URI": {
"description": "MongoDB connection URI that contains your Modmail logs.",
"required": true
},
"USE_ATTACHMENT_PROXY": {
"description": "Whether if the logviewer should use a proxy to view attachments. If set to 'no', attachments will expire after 1 day. USE AT YOUR OWN RISK.",
"required": false,
"value": "no"
},
"ATTACHMENT_PROXY_URL": {
"description": "Proxy URL for viewing attachments.",
"required": false,
"value": "https://cdn.discordapp.xyz"
}
}
}
31 changes: 28 additions & 3 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
__version__ = "1.1.1"
__version__ = "1.1.3"

import html
import os

from dotenv import load_dotenv
Expand Down Expand Up @@ -41,10 +42,34 @@ def render_template(name, *args, **kwargs):
app.ctx.render_template = render_template


def strtobool(val):
"""
Copied from distutils.strtobool.

Convert a string representation of truth to true (1) or false (0).

True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
'val' is anything else.
"""
val = val.lower()
if val in ('y', 'yes', 't', 'true', 'on', '1'):
return 1
elif val in ('n', 'no', 'f', 'false', 'off', '0'):
return 0
else:
raise ValueError("invalid truth value %r" % (val,))


@app.listener("before_server_start")
async def init(app, loop):
app.ctx.db = AsyncIOMotorClient(MONGO_URI).modmail_bot

use_attachment_proxy = strtobool(os.getenv("USE_ATTACHMENT_PROXY", "no"))
if use_attachment_proxy:
app.ctx.attachment_proxy_url = os.getenv("ATTACHMENT_PROXY_URL", "https://cdn.discordapp.xyz")
app.ctx.attachment_proxy_url = html.escape(app.ctx.attachment_proxy_url).rstrip("/")
else:
app.ctx.attachment_proxy_url = None

@app.exception(NotFound)
async def not_found(request, exc):
Expand Down Expand Up @@ -85,6 +110,6 @@ async def get_logs_file(request, key):
if __name__ == "__main__":
app.run(
host=os.getenv("HOST", "0.0.0.0"),
port=os.getenv("PORT", 8000),
port=int(os.getenv("PORT", 8000)),
debug=bool(os.getenv("DEBUG", False)),
)
2 changes: 1 addition & 1 deletion core/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def decode_codeblock(m):
lang = lang.strip(" \n\r")

result = html.escape(match.group(2))
return f'<div class="pre pre--multiline {lang}">{result}' "</div>"
return f'<div class="pre pre--multiline {html.escape(lang)}">{result}' "</div>"

# Decode and process multiline codeblocks
content = re.sub("\x1AM(.*?)\x1AM", decode_codeblock, content)
Expand Down
Loading