Hoppa till innehållet

Memos - Gotify manual relay installment

Från Plutten

Memos → Gotify Relay Manual Installation

[redigera | redigera wikitext]

Version: 1.0.0
Author: vibbe
Date: 2025-10-08

This guide explains how to manually install and configure the Memos → Gotify relay service on Ubuntu without using an automated installer.

---

  • Sends all Memos events (memo.created, memo.updated, memo.deleted, memo.shared, user.created, user.updated, resource.uploaded, system.backup) to Gotify.
  • Preserves multi-line formatting for better readability.
  • Logs all events in memos_gotify.log.
  • Runs as a systemd service for automatic startup.
  • Can be fully uninstalled manually.

---

Manual Installation Steps

[redigera | redigera wikitext]

1. Create installation directory

[redigera | redigera wikitext]
mkdir -p ~/bots/memos-gotify
cd ~/bots/memos-gotify

---

2. Create the Python script

[redigera | redigera wikitext]
  • Create file memos_to_gotify.py inside the directory.
  • Paste the full Python script below into that file:
#!/usr/bin/env python3
from flask import Flask, request
import requests
import logging
from datetime import datetime
import os
import signal
import sys

# -----------------------------
# Configuration
# -----------------------------
GOTIFY_URL = "http://192.168.1.43:444/message"
GOTIFY_TOKEN = "ACcigBM7UHUE_5Z"
LISTEN_PORT = 5000
# -----------------------------

# Base directory for logs
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_FILE = os.path.join(BASE_DIR, "memos_gotify.log")

# Logging configuration
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    handlers=[
        logging.FileHandler(LOG_FILE),
        logging.StreamHandler()
    ]
)

# Signal handling for graceful shutdown
def handle_sigterm(signal_number, frame):
    logging.info("Received termination signal, shutting down gracefully...")
    sys.exit(0)

signal.signal(signal.SIGTERM, handle_sigterm)
signal.signal(signal.SIGINT, handle_sigterm)

app = Flask(__name__)

# Map activityType to friendly title + emoji
ACTIVITY_MAP = {
    "memos.memo.created": "🟢 Memo Created",
    "memos.memo.updated": "🟡 Memo Updated",
    "memos.memo.deleted": "🔴 Memo Deleted",
    "memos.memo.shared": "🔵 Memo Shared",
    "memos.user.created": "🟢 New User",
    "memos.user.updated": "🟡 User Updated",
    "memos.resource.uploaded": "📎 File Uploaded",
    "memos.system.backup": "💾 Backup Completed"
}

@app.route("/memos", methods=["POST"])
def memos_webhook():
    try:
        data = request.get_json(force=True)
    except Exception as e:
        logging.error(f"Failed to parse JSON: {e}")
        return "Invalid JSON", 400

    logging.info(f"Incoming webhook JSON: {data}")

    activity = data.get("activityType", "unknown")
    friendly_title = ACTIVITY_MAP.get(activity, activity)

    # Get creator ID for events where we show it
    creator_ref = data.get("creator", "Unknown")
    creator_id = creator_ref.split("/")[-1]  # numeric ID
    creator_display = f"User ID: {creator_id}"

    # Extract timestamp
    ts_seconds = data.get("memo", {}).get("create_time", {}).get("seconds") or \
                 data.get("memo", {}).get("update_time", {}).get("seconds")
    timestamp = datetime.utcfromtimestamp(ts_seconds).strftime("%Y-%m-%d %H:%M:%S UTC") if ts_seconds else "Unknown time"

    # Extract memo content
    memo_obj = data.get("memo", {})
    content = memo_obj.get("snippet") or memo_obj.get("content") or "<No content>"

    # Build multi-line message for all events
    if activity in ["memos.memo.created", "memos.memo.updated", "memos.memo.shared"]:
        # Include content + User ID + timestamp
        message = (
            f"{friendly_title}\n\n"
            f"{content}\n\n"
            f"By: {creator_display}\n"
            f"Time: {timestamp}"
        )
    elif activity == "memos.memo.deleted":
        message = (
            f"{friendly_title}\n\n"
            f"{creator_display}\n"
            f"Memo ID: {memo_obj.get('name', 'unknown')}\n"
            f"Time: {timestamp}"
        )
    elif activity.startswith("user"):
        message = (
            f"{friendly_title}\n\n"
            f"{creator_display}\n"
            f"Time: {timestamp}"
        )
    elif activity == "memos.resource.uploaded":
        resource_name = data.get("data", {}).get("name") or data.get("data", {}).get("filename") or "Unknown file"
        message = (
            f"{friendly_title}\n\n"
            f"File: {resource_name}\n"
            f"Uploaded by: {creator_display}\n"
            f"Time: {timestamp}"
        )
    elif activity == "memos.system.backup":
        message = f"{friendly_title}\n\nTime: {timestamp}"
    else:
        message = str(data)

    # Build Gotify payload
    payload = {
        "title": friendly_title,
        "message": message,
        "priority": 5
    }

    try:
        response = requests.post(f"{GOTIFY_URL}?token={GOTIFY_TOKEN}", json=payload)
        if response.status_code == 200:
            logging.info(f"Sent notification to Gotify:\n{message}")
            return "OK", 200
        else:
            logging.error(f"Gotify returned error {response.status_code}: {response.text}")
            return response.text, response.status_code
    except Exception as e:
        logging.exception(f"Error sending to Gotify: {e}")
        return str(e), 500

if __name__ == "__main__":
    try:
        logging.info(f"Starting Memos→Gotify relay on port {LISTEN_PORT} with full multi-line formatting...")
        app.run(host="0.0.0.0", port=LISTEN_PORT)
    except Exception as e:
        logging.exception(f"Fatal error in Memos→Gotify relay: {e}")
        sys.exit(1)
  • Make it executable:
chmod +x memos_to_gotify.py

---

3. Set up a Python virtual environment and install dependencies

[redigera | redigera wikitext]
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install flask requests
deactivate

---

4. Create systemd service

[redigera | redigera wikitext]
  • Create file /etc/systemd/system/memos-gotify.service with the following contents:
[Unit]
Description=Memos → Gotify Relay Service
After=network.target

[Service]
Type=simple
User=$USER
WorkingDirectory=/home/$USER/bots/memos-gotify
ExecStart=/usr/bin/python3 /home/$USER/bots/memos-gotify/memos_to_gotify.py
Restart=on-failure
RestartSec=5
StandardOutput=append:/home/$USER/bots/memos-gotify/memos_gotify.log
StandardError=append:/home/$USER/bots/memos-gotify/memos_gotify.log

[Install]
WantedBy=multi-user.target

---

5. Reload systemd and start the service

[redigera | redigera wikitext]
sudo systemctl daemon-reload
sudo systemctl start memos-gotify.service
sudo systemctl enable memos-gotify.service

---

6. Configure Memos webhook

[redigera | redigera wikitext]
  • Webhook URL:
http://<SERVER_LAN_IP>:5000/memos
gotify api token

Replace <SERVER_LAN_IP> with your server IP address.

  • Use the Gotify API token in the script configuration section.
Memos webhook


---

Logs & Troubleshooting

[redigera | redigera wikitext]
  • View logs:
cat ~/bots/memos-gotify/memos_gotify.log
tail -f ~/bots/memos-gotify/memos_gotify.log
  • Check service status:
sudo systemctl status memos-gotify.service
sudo journalctl -u memos-gotify.service -f
  • Stop and restart service manually if needed:
sudo systemctl stop memos-gotify.service
sudo systemctl start memos-gotify.service
sudo systemctl enable memos-gotify.service
  • Find process using port 5000 and kill it if necessary:
lsof -i :5000
kill -9 <PID>

---

Uninstallation

[redigera | redigera wikitext]
  • Stop the service:
sudo systemctl stop memos-gotify.service
sudo systemctl disable memos-gotify.service
  • Remove systemd service file:
sudo rm /etc/systemd/system/memos-gotify.service
sudo systemctl daemon-reload
  • Remove installation directory:
rm -rf ~/bots/memos-gotify

---

Also a install sh file can be used

[redigera | redigera wikitext]
nano install.sh

add this:

#!/usr/bin/env bash
set -e

# ----------------------------
# Configuration
# ----------------------------
USER_NAME="$USER"
INSTALL_DIR="/home/$USER_NAME/bots/memos-gotify"
SERVICE_FILE="/etc/systemd/system/memos-gotify.service"
VENV_DIR="$INSTALL_DIR/venv"
PYTHON_SCRIPT="$INSTALL_DIR/memos_to_gotify.py"
LOG_FILE="$INSTALL_DIR/memos_gotify.log"
LISTEN_PORT=5000
# ----------------------------

function install_service() {
    echo "Installing Memos→Gotify relay service..."

    read -p "Enter Gotify server IP (local or LAN): " GOTIFY_IP
    read -p "Enter Gotify server port (default 444): " GOTIFY_PORT
    GOTIFY_PORT=${GOTIFY_PORT:-444}
    read -p "Enter Gotify API token: " GOTIFY_TOKEN

    GOTIFY_URL="http://$GOTIFY_IP:$GOTIFY_PORT/message"
    LOCAL_IP=$(hostname -I | awk '{print $1}')
    echo "Detected local server IP: $LOCAL_IP"

    mkdir -p "$INSTALL_DIR"

    echo "Writing Python script..."
    cat > "$PYTHON_SCRIPT" <<'EOL'
#!/usr/bin/env python3
from flask import Flask, request
import requests, logging, os, signal, sys
from datetime import datetime

GOTIFY_URL = "$GOTIFY_URL"
GOTIFY_TOKEN = "$GOTIFY_TOKEN"
LISTEN_PORT = 5000

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_FILE = os.path.join(BASE_DIR, "memos_gotify.log")

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler()]
)

def handle_sigterm(sig, frame):
    logging.info("Received termination signal, shutting down...")
    sys.exit(0)

signal.signal(signal.SIGTERM, handle_sigterm)
signal.signal(signal.SIGINT, handle_sigterm)

app = Flask(__name__)

@app.route("/memos", methods=["POST"])
def memos_webhook():
    try:
        data = request.get_json(force=True)
    except Exception as e:
        logging.error(f"Invalid JSON: {e}")
        return "Invalid JSON", 400

    activity = data.get("activityType", "unknown")
    creator_ref = data.get("creator", "Unknown")
    creator_id = creator_ref.split("/")[-1]
    creator_display = f"User ID: {creator_id}"

    memo_obj = data.get("memo", {})
    content = memo_obj.get("snippet") or memo_obj.get("content") or "<No content>"
    ts_seconds = memo_obj.get("create_time", {}).get("seconds") or memo_obj.get("update_time", {}).get("seconds")
    timestamp = datetime.utcfromtimestamp(ts_seconds).strftime("%Y-%m-%d %H:%M:%S UTC") if ts_seconds else "Unknown time"

    message = f"{activity}\n\n{content}\n\nBy: {creator_display}\nTime: {timestamp}"

    payload = {"title": activity, "message": message, "priority": 5}

    try:
        resp = requests.post(f"{GOTIFY_URL}?token={GOTIFY_TOKEN}", json=payload)
        if resp.status_code == 200:
            logging.info(f"Sent to Gotify:\n{message}")
            return "OK", 200
        else:
            logging.error(f"Gotify returned {resp.status_code}: {resp.text}")
            return resp.text, resp.status_code
    except Exception as e:
        logging.exception(f"Error sending to Gotify: {e}")
        return str(e), 500

if __name__ == "__main__":
    logging.info(f"Starting Memos→Gotify relay on 0.0.0.0:{LISTEN_PORT}")
    app.run(host="0.0.0.0", port=LISTEN_PORT)
EOL

    chmod +x "$PYTHON_SCRIPT"

    echo "Setting up Python virtual environment..."
    python3 -m venv "$VENV_DIR"
    source "$VENV_DIR/bin/activate"
    pip install --upgrade pip flask requests
    deactivate

    echo "Creating systemd service..."
    sudo tee "$SERVICE_FILE" > /dev/null <<EOL
[Unit]
Description=Memos → Gotify Relay Service
After=network.target

[Service]
Type=simple
User=$USER_NAME
WorkingDirectory=$INSTALL_DIR
ExecStart=/usr/bin/python3 $PYTHON_SCRIPT
Restart=on-failure
RestartSec=5
StandardOutput=append:$LOG_FILE
StandardError=append:$LOG_FILE

[Install]
WantedBy=multi-user.target
EOL

    echo "Reloading systemd and starting service..."
    sudo systemctl daemon-reload
    sudo systemctl start memos-gotify.service
    sudo systemctl enable memos-gotify.service

    echo "✅ Installation complete!"
    echo "Webhook URL for Memos: http://$LOCAL_IP:$LISTEN_PORT/memos"
    echo "Service status:"
    sudo systemctl status memos-gotify.service
}

function uninstall_service() {
    echo "Stopping and removing Memos→Gotify service..."

    sudo systemctl stop memos-gotify.service || true
    sudo systemctl disable memos-gotify.service || true
    sudo rm -f "$SERVICE_FILE"
    sudo systemctl daemon-reload

    echo "Removing install directory..."
    rm -rf "$INSTALL_DIR"

    echo "✅ Uninstallation complete!"
}

# ----------------------------
# Main
# ----------------------------
if [[ "$1" == "uninstall" ]]; then
    uninstall_service
else
    install_service
fi

---

Notes About install.sh

[redigera | redigera wikitext]

The install.sh script automates the full setup of the Memos → Gotify relay. Here’s what it does step by step:

  1. Detects your local server IP for generating the correct webhook URL.
  2. Asks for Gotify server IP, port, and API token.
  3. Creates the installation directory:
~/bots/memos-gotify
  1. Writes the full Python script memos_to_gotify.py into the directory.
  2. Sets up a Python virtual environment inside the directory and installs dependencies (Flask and requests).
  3. Creates a systemd service file memos-gotify.service pointing to the Python script.
  4. Starts the service immediately and enables it to run on boot.

Caveats / Tips

[redigera | redigera wikitext]
  • If the service is already running on the same port, install.sh will fail. Stop the service first:
sudo systemctl stop memos-gotify.service
lsof -i :5000    # find PID if necessary
kill -9 <PID>
  • You can safely re-run install.sh to update the Python script or service configuration.
  • To uninstall, run:
./install.sh uninstall
  • Logs are always written to:
~/bots/memos-gotify/memos_gotify.log
  • The script does not modify your Memos instance; it only sets up the webhook listener so its able to send it over to gotify on the port you specify.

---

  • Ensure the Gotify server is reachable from the Memos server.
  • The Python script must have the correct Gotify URL and token.
  • Logs are stored at ~/bots/memos-gotify/memos_gotify.log.
  • This guide allows full manual control of installation, startup, and troubleshooting.

Please note to setup the gotify and memos also

[redigera | redigera wikitext]

[1] [2]