Memos - Gotify manual relay installment
Utseende
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.
---
Features
[redigera | redigera wikitext]- 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

Replace <SERVER_LAN_IP> with your server IP address.
- Use the Gotify API token in the script configuration section.

---
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:
- Detects your local server IP for generating the correct webhook URL.
- Asks for Gotify server IP, port, and API token.
- Creates the installation directory:
~/bots/memos-gotify
- Writes the full Python script memos_to_gotify.py into the directory.
- Sets up a Python virtual environment inside the directory and installs dependencies (Flask and requests).
- Creates a systemd service file memos-gotify.service pointing to the Python script.
- 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.
---
Notes
[redigera | redigera wikitext]- 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.