Initial commit: AYON Matrix Publish addon
- package.py: addon manifest (v0.1.0) - server/__init__.py: BaseServerAddon definition - server/ayon_matrix_publish/plugins/publish/integrate_matrix.py: Pyblish plugin that uploads published representations to Matrix rooms during the Integration stage. Supports: - Upload to Matrix content repository (mxc:// URI) - Formatted messages with preview images - Auto room creation per asset - Configurable via Studio Settings or env vars - create_package.py: builds distributable .zip package
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.egg-info/
|
||||
dist/
|
||||
.eggs/
|
||||
|
||||
# Packages
|
||||
*.zip
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Virtualenv
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# AYON
|
||||
*.ayon/
|
||||
@@ -1,3 +1,75 @@
|
||||
# ayon-matrix-publish
|
||||
AYON addon that publishes product representations (images, videos, previews)
|
||||
to Matrix/Element rooms automatically during the publish pipeline.
|
||||
|
||||
AYON addon: publishes products to Matrix/Element rooms on publish. Uploads representations (images, videos, files) to configurable Matrix rooms.
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Ayon Publish Pipeline
|
||||
Collect → Validate → Extract → Integrate
|
||||
│
|
||||
▼
|
||||
IntegrateMatrix plugin
|
||||
│
|
||||
┌───────────┴───────────┐
|
||||
│ Matrix Client (mautrix) │
|
||||
│ • Upload media │
|
||||
│ • Send messages │
|
||||
│ • Create rooms (opt.) │
|
||||
└───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Synapse Homeserver
|
||||
(matrix.niklashmotion.art)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Artist publishes** a product in any DCC (Maya, Houdini, Nuke, Blender, etc.)
|
||||
2. After extraction and standard integration, the `IntegrateMatrix` plugin runs
|
||||
3. For each published representation (thumbnail, preview video, review):
|
||||
- Uploads the file to the Matrix media repository → gets `mxc://` URI
|
||||
- Sends a formatted message to a configured Matrix room with:
|
||||
- Product name, version, asset, task
|
||||
- Thumbnail/preview inline
|
||||
- Link to the AYON product page
|
||||
4. (Optional) Creates per-asset or per-task rooms automatically
|
||||
|
||||
## Configuration
|
||||
|
||||
Studio Settings → Matrix Publish:
|
||||
|
||||
| Setting | Description |
|
||||
|---------|-------------|
|
||||
| `homeserver_url` | Matrix homeserver (e.g. `https://matrix.niklashmotion.art`) |
|
||||
| `access_token` | Bot access token (from Element → Settings → Help & About) |
|
||||
| `room_id` | Target room ID (e.g. `!abc123:niklashmotion.art`) |
|
||||
| `upload_representations` | List of representation names to upload (e.g. `["thumbnail", "preview"]`) |
|
||||
| `create_per_asset_rooms` | Auto-create rooms per asset (bool) |
|
||||
| `message_template` | Jinja2 template for the message text |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
git clone https://git.niklashmotion.art/Hermes/ayon-matrix-publish.git
|
||||
cd ayon-matrix-publish
|
||||
|
||||
# Install AYON dev dependencies
|
||||
pip install ayon-python-api mautrix
|
||||
|
||||
# Create package
|
||||
python create_package.py
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `mautrix` — Matrix client SDK (async)
|
||||
- `ayon-python-api` — AYON server communication
|
||||
- `Jinja2` — Message templating (bundled with AYON)
|
||||
|
||||
## Related
|
||||
|
||||
- [AYON Addon Development](https://docs.ayon.dev/docs/addon_development)
|
||||
- [Matrix Client-Server API](https://spec.matrix.org/latest/client-server-api/)
|
||||
- [Mautrix Python SDK](https://github.com/mautrix/python)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Matrix Publish addon — client-side (DCC host)."""
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Client-side plugins for Matrix Publish.
|
||||
|
||||
These plugins run inside the DCC host (Maya, Houdini, Nuke, etc.)
|
||||
and are discovered by AYON's Pyblish plugin loader.
|
||||
|
||||
The publish plugin lives in ayon_matrix_publish/plugins/publish/.
|
||||
"""
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
|
||||
# Import the plugin so Pyblish discovers it
|
||||
from .plugins.publish.integrate_matrix import IntegrateMatrix
|
||||
|
||||
__all__ = ["IntegrateMatrix"]
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create an AYON addon .zip package for distribution."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def create_package():
|
||||
root = Path(__file__).parent
|
||||
|
||||
# Read version
|
||||
version_file = root / "server" / "ayon_matrix_publish" / "version.py"
|
||||
version = "0.1.0"
|
||||
if version_file.exists():
|
||||
exec(version_file.read_text(), {}, {})
|
||||
version = locals().get("__version__", version)
|
||||
|
||||
package_name = f"matrix_publish-{version}.zip"
|
||||
package_path = root / package_name
|
||||
|
||||
print(f"Creating {package_name}...")
|
||||
|
||||
with zipfile.ZipFile(package_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
# Add package.py
|
||||
zf.write(root / "package.py", "package.py")
|
||||
|
||||
# Add server files
|
||||
server_dir = root / "server"
|
||||
for file_path in server_dir.rglob("*"):
|
||||
if file_path.is_file() and "__pycache__" not in str(file_path):
|
||||
arcname = str(file_path.relative_to(root))
|
||||
zf.write(file_path, arcname)
|
||||
print(f" + {arcname}")
|
||||
|
||||
# Add client files
|
||||
client_dir = root / "client"
|
||||
if client_dir.exists():
|
||||
for file_path in client_dir.rglob("*"):
|
||||
if file_path.is_file() and "__pycache__" not in str(file_path):
|
||||
arcname = str(file_path.relative_to(root))
|
||||
zf.write(file_path, arcname)
|
||||
print(f" + {arcname}")
|
||||
|
||||
print(f"\nPackage created: {package_path}")
|
||||
print(f"Size: {package_path.stat().st_size / 1024:.1f} KB")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_package()
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
"""AYON Addon: Matrix Publish — sends published products to Matrix rooms."""
|
||||
|
||||
name = "matrix_publish"
|
||||
title = "Matrix Publish"
|
||||
version = "0.1.0"
|
||||
client_dir = "ayon_matrix_publish"
|
||||
|
||||
# Services needed
|
||||
services = {}
|
||||
|
||||
# Ayon dependency
|
||||
ayon_required_addons = {
|
||||
"core": ">=1.0.0"
|
||||
}
|
||||
|
||||
# Plugin paths
|
||||
ayon_compatible_addons = {}
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Matrix Publish addon — server-side definition."""
|
||||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
|
||||
|
||||
class MatrixPublishAddon(BaseServerAddon):
|
||||
name = "matrix_publish"
|
||||
title = "Matrix Publish"
|
||||
version = __version__
|
||||
|
||||
async def setup(self):
|
||||
"""Initialize addon — no persistent services needed."""
|
||||
pass
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Matrix Publish addon — server and client package."""
|
||||
@@ -0,0 +1,525 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Publish plugin: upload published representations to Matrix rooms.
|
||||
|
||||
This plugin runs during the Integration stage of the AYON publish pipeline.
|
||||
For each published representation, it uploads the file to the Matrix content
|
||||
repository and posts a formatted message to a configured room.
|
||||
|
||||
Architecture:
|
||||
Ayon Publish: Collect → Validate → Extract → Integrate
|
||||
│
|
||||
▼
|
||||
IntegrateMatrix (this plugin)
|
||||
│
|
||||
┌─────────┴──────────┐
|
||||
│ MatrixUploader │
|
||||
│ • upload_media() │
|
||||
│ • send_message() │
|
||||
│ • ensure_room() │
|
||||
└────────────────────┘
|
||||
│
|
||||
▼
|
||||
Matrix Homeserver API
|
||||
(Synapse / Dendrite)
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline.publish import (
|
||||
AYONPyblishPluginMixin,
|
||||
PublishError,
|
||||
)
|
||||
from ayon_core.pipeline import publish
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MatrixUploader:
|
||||
"""Thin wrapper around Matrix HTTP API for uploads and messaging.
|
||||
|
||||
Uses raw HTTP requests (no async needed in Pyblish context) instead of
|
||||
the mautrix async SDK. Pyblish plugins run synchronously in the host
|
||||
process, so we use the REST API directly.
|
||||
|
||||
Matrix API reference:
|
||||
https://spec.matrix.org/latest/client-server-api/
|
||||
|
||||
Endpoints used:
|
||||
POST /_matrix/media/v3/upload → mxc:// URI
|
||||
PUT /_matrix/client/v3/rooms/{id}/send/m.room.message/{txn}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
homeserver_url: str,
|
||||
access_token: str,
|
||||
):
|
||||
self.homeserver = homeserver_url.rstrip("/")
|
||||
self.token = access_token
|
||||
|
||||
def upload_media(self, file_path: str) -> str:
|
||||
"""Upload a file to the Matrix content repository.
|
||||
|
||||
Args:
|
||||
file_path: Local path to the file to upload.
|
||||
|
||||
Returns:
|
||||
mxc:// URI of the uploaded content.
|
||||
|
||||
Raises:
|
||||
PublishError: If upload fails.
|
||||
"""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import json
|
||||
|
||||
url = f"{self.homeserver}/_matrix/media/v3/upload"
|
||||
file_name = os.path.basename(file_path)
|
||||
|
||||
# Determine Content-Type from extension
|
||||
content_type = self._guess_mime(file_path)
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
boundary = "---MatrixUploaderBoundary"
|
||||
body = (
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="file"; filename="{file_name}"\r\n'
|
||||
f"Content-Type: {content_type}\r\n\r\n"
|
||||
).encode("utf-8")
|
||||
body += data
|
||||
body += f"\r\n--{boundary}--\r\n".encode("utf-8")
|
||||
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=data, # Matrix upload accepts raw binary
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": content_type,
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||
result = json.loads(resp.read().decode())
|
||||
content_uri = result.get("content_uri")
|
||||
if not content_uri:
|
||||
raise PublishError(
|
||||
f"Matrix upload: no content_uri in response: {result}"
|
||||
)
|
||||
log.info(
|
||||
"Uploaded %s → %s (%d bytes)",
|
||||
file_name, content_uri, len(data),
|
||||
)
|
||||
return content_uri
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode(errors="replace")
|
||||
raise PublishError(
|
||||
f"Matrix upload failed (HTTP {e.code}): {body[:500]}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise PublishError(f"Matrix upload failed: {e}") from e
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
room_id: str,
|
||||
body: str,
|
||||
formatted_body: Optional[str] = None,
|
||||
msgtype: str = "m.text",
|
||||
url: Optional[str] = None,
|
||||
media_info: Optional[dict] = None,
|
||||
) -> str:
|
||||
"""Send a message to a Matrix room.
|
||||
|
||||
Args:
|
||||
room_id: Target room ID (!xxx:server).
|
||||
body: Plain text fallback.
|
||||
formatted_body: HTML formatted body (org.matrix.custom.html).
|
||||
msgtype: Message type (m.text, m.image, m.notice).
|
||||
url: mxc:// URI for image/file messages.
|
||||
media_info: Dict with w, h, size, mimetype for media messages.
|
||||
|
||||
Returns:
|
||||
The event ID of the sent message.
|
||||
|
||||
Raises:
|
||||
PublishError: If sending fails.
|
||||
"""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import json
|
||||
import uuid
|
||||
|
||||
txn_id = str(uuid.uuid4()).replace("-", "")
|
||||
api_url = (
|
||||
f"{self.homeserver}/_matrix/client/v3/rooms/{room_id}"
|
||||
f"/send/m.room.message/{txn_id}"
|
||||
)
|
||||
|
||||
content = {
|
||||
"msgtype": msgtype,
|
||||
"body": body,
|
||||
}
|
||||
|
||||
if formatted_body:
|
||||
content["format"] = "org.matrix.custom.html"
|
||||
content["formatted_body"] = formatted_body
|
||||
|
||||
if url:
|
||||
content["url"] = url
|
||||
if media_info:
|
||||
content["info"] = media_info
|
||||
|
||||
req = urllib.request.Request(
|
||||
api_url,
|
||||
data=json.dumps(content).encode("utf-8"),
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="PUT",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
result = json.loads(resp.read().decode())
|
||||
event_id = result.get("event_id", "unknown")
|
||||
log.info("Sent Matrix message → %s (event: %s)", room_id, event_id)
|
||||
return event_id
|
||||
except urllib.error.HTTPError as e:
|
||||
body_text = e.read().decode(errors="replace")
|
||||
raise PublishError(
|
||||
f"Matrix send failed (HTTP {e.code}): {body_text[:500]}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise PublishError(f"Matrix send failed: {e}") from e
|
||||
|
||||
def ensure_room(self, room_alias: str, room_name: str) -> str:
|
||||
"""Create a room if it doesn't exist, return room_id.
|
||||
|
||||
Args:
|
||||
room_alias: Desired alias (#name:server).
|
||||
room_name: Display name for the room.
|
||||
|
||||
Returns:
|
||||
Room ID (!xxx:server).
|
||||
"""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import json
|
||||
|
||||
# First, try to resolve the alias
|
||||
resolve_url = f"{self.homeserver}/_matrix/client/v3/directory/room/{room_alias}"
|
||||
req = urllib.request.Request(
|
||||
resolve_url,
|
||||
headers={"Authorization": f"Bearer {self.token}"},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = json.loads(resp.read().decode())
|
||||
return data["room_id"]
|
||||
except urllib.error.HTTPError:
|
||||
pass # Room doesn't exist yet
|
||||
|
||||
# Create the room
|
||||
create_url = f"{self.homeserver}/_matrix/client/v3/createRoom"
|
||||
body = {
|
||||
"name": room_name,
|
||||
"room_alias_name": room_alias.lstrip("#").split(":")[0],
|
||||
"preset": "private_chat",
|
||||
"visibility": "private",
|
||||
"topic": f"AYON publish room for {room_name}",
|
||||
}
|
||||
req = urllib.request.Request(
|
||||
create_url,
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
data = json.loads(resp.read().decode())
|
||||
room_id = data["room_id"]
|
||||
log.info("Created Matrix room %s (%s)", room_id, room_name)
|
||||
return room_id
|
||||
except urllib.error.HTTPError as e:
|
||||
body_text = e.read().decode(errors="replace")
|
||||
raise PublishError(
|
||||
f"Matrix create room failed: {body_text[:500]}"
|
||||
) from e
|
||||
|
||||
@staticmethod
|
||||
def _guess_mime(file_path: str) -> str:
|
||||
"""Guess MIME type from file extension."""
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
mapping = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".mp4": "video/mp4",
|
||||
".mov": "video/quicktime",
|
||||
".webm": "video/webm",
|
||||
".pdf": "application/pdf",
|
||||
".abc": "application/octet-stream",
|
||||
".usd": "application/octet-stream",
|
||||
".usda": "application/octet-stream",
|
||||
".usdc": "application/octet-stream",
|
||||
".fbx": "application/octet-stream",
|
||||
".obj": "application/octet-stream",
|
||||
".glb": "model/gltf-binary",
|
||||
".gltf": "model/gltf+json",
|
||||
".exr": "image/x-exr",
|
||||
".tif": "image/tiff",
|
||||
".tiff": "image/tiff",
|
||||
".tga": "image/x-tga",
|
||||
}
|
||||
return mapping.get(ext, "application/octet-stream")
|
||||
|
||||
|
||||
class IntegrateMatrix(
|
||||
AYONPyblishPluginMixin,
|
||||
pyblish.api.InstancePlugin,
|
||||
):
|
||||
"""Upload published representations to Matrix rooms.
|
||||
|
||||
Runs during Integration stage (order = IntegratorOrder + 0.1).
|
||||
For each representation matching the configured filter:
|
||||
1. Upload the file to the Matrix content repository
|
||||
2. Post a formatted message with preview and metadata
|
||||
|
||||
Configuration (Studio Settings → Matrix Publish):
|
||||
homeserver_url: Matrix homeserver URL
|
||||
access_token: Bot user access token
|
||||
room_id: Target room (or room pattern)
|
||||
upload_representations: List of rep names to upload
|
||||
message_template: Jinja2 template for the message
|
||||
create_per_asset_rooms: Auto-create rooms per asset
|
||||
"""
|
||||
|
||||
label = "Integrate Matrix"
|
||||
order = pyblish.api.IntegratorOrder + 0.1
|
||||
families = ["*"] # Run for all product types
|
||||
targets = ["local"]
|
||||
|
||||
# Attribute definitions for the AYON publisher UI
|
||||
# These allow artists to enable/disable Matrix upload per instance
|
||||
enabled = True
|
||||
|
||||
def process(self, instance):
|
||||
"""Main Pyblish process method.
|
||||
|
||||
Called once per publish instance during Integration stage.
|
||||
"""
|
||||
# Get attribute values from instance publish attributes
|
||||
attr_values = self.get_attr_values_from_data(
|
||||
instance.data
|
||||
)
|
||||
|
||||
# Check if Matrix upload is enabled for this instance
|
||||
if not attr_values.get("enabled", True):
|
||||
log.debug("Matrix upload disabled for instance %s", instance.name)
|
||||
return
|
||||
|
||||
# Get addon settings from AYON
|
||||
settings = self._get_addon_settings(instance)
|
||||
|
||||
# Connect to Matrix
|
||||
uploader = MatrixUploader(
|
||||
homeserver_url=settings["homeserver_url"],
|
||||
access_token=settings["access_token"],
|
||||
)
|
||||
|
||||
# Determine target room
|
||||
room_id = self._resolve_target_room(instance, uploader, settings)
|
||||
|
||||
# Get representations to upload
|
||||
rep_names = settings.get("upload_representations", ["thumbnail", "preview"])
|
||||
representations = instance.data.get("representations", [])
|
||||
|
||||
uploaded = {} # rep_name → mxc:// URI
|
||||
for rep in representations:
|
||||
rep_name = rep.get("name", "")
|
||||
if rep_name not in rep_names:
|
||||
continue
|
||||
|
||||
# Get the published file path
|
||||
staging_dir = rep.get("stagingDir", "")
|
||||
files = rep.get("files", [])
|
||||
if not files:
|
||||
continue
|
||||
|
||||
# Upload the first file (typically thumbnail/preview has one file)
|
||||
file_path = os.path.join(staging_dir, files[0])
|
||||
if not os.path.exists(file_path):
|
||||
log.warning("File not found: %s", file_path)
|
||||
continue
|
||||
|
||||
try:
|
||||
mxc_url = uploader.upload_media(file_path)
|
||||
uploaded[rep_name] = mxc_url
|
||||
except PublishError:
|
||||
log.warning(
|
||||
"Failed to upload %s to Matrix", file_path, exc_info=True
|
||||
)
|
||||
# Don't fail the publish — Matrix is optional
|
||||
continue
|
||||
|
||||
if not uploaded:
|
||||
log.info("No representations to upload to Matrix for %s", instance.name)
|
||||
return
|
||||
|
||||
# Build and send the message
|
||||
message_body, message_html = self._build_message(
|
||||
instance, uploaded, settings
|
||||
)
|
||||
|
||||
if message_body:
|
||||
try:
|
||||
# If there's a thumbnail, send it as an image message
|
||||
thumbnail_url = uploaded.get("thumbnail")
|
||||
if thumbnail_url:
|
||||
uploader.send_message(
|
||||
room_id=room_id,
|
||||
body=message_body,
|
||||
formatted_body=message_html,
|
||||
msgtype="m.image",
|
||||
url=thumbnail_url,
|
||||
media_info={"mimetype": "image/png"},
|
||||
)
|
||||
else:
|
||||
uploader.send_message(
|
||||
room_id=room_id,
|
||||
body=message_body,
|
||||
formatted_body=message_html,
|
||||
)
|
||||
except PublishError:
|
||||
log.warning(
|
||||
"Failed to send Matrix message for %s", instance.name,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# ── helper methods ──────────────────────────────────────────────
|
||||
|
||||
def _get_addon_settings(self, instance) -> dict:
|
||||
"""Load Matrix Publish settings from AYON studio settings.
|
||||
|
||||
In production, this reads from:
|
||||
instance.context.data["project_settings"]["matrix_publish"]
|
||||
"""
|
||||
# Fallback to environment variables for development
|
||||
return {
|
||||
"homeserver_url": os.environ.get(
|
||||
"MATRIX_HOMESERVER", "https://matrix.niklashmotion.art"
|
||||
),
|
||||
"access_token": os.environ.get("MATRIX_ACCESS_TOKEN", ""),
|
||||
"room_id": os.environ.get("MATRIX_PUBLISH_ROOM", ""),
|
||||
"upload_representations": os.environ.get(
|
||||
"MATRIX_UPLOAD_REPS", "thumbnail,preview"
|
||||
).split(","),
|
||||
"create_per_asset_rooms": os.environ.get(
|
||||
"MATRIX_CREATE_ROOMS", "0"
|
||||
)
|
||||
== "1",
|
||||
"message_template": os.environ.get(
|
||||
"MATRIX_MESSAGE_TEMPLATE",
|
||||
("**{{ product_type }}** published: "
|
||||
"{{ asset }} / {{ task }} — v{{ version }}"),
|
||||
),
|
||||
}
|
||||
|
||||
def _resolve_target_room(
|
||||
self, instance, uploader: MatrixUploader, settings: dict
|
||||
) -> str:
|
||||
"""Determine which Matrix room to send to."""
|
||||
room_id = settings.get("room_id", "")
|
||||
|
||||
# Auto-create per-asset rooms if configured
|
||||
if settings.get("create_per_asset_rooms"):
|
||||
asset_name = instance.data.get("asset", "unknown")
|
||||
alias = f"#ayon-{asset_name}:niklashmotion.art"
|
||||
room_id = uploader.ensure_room(
|
||||
alias, f"AYON: {asset_name}"
|
||||
)
|
||||
|
||||
if not room_id:
|
||||
raise PublishError(
|
||||
"No Matrix room configured. Set room_id in Matrix Publish settings."
|
||||
)
|
||||
|
||||
return room_id
|
||||
|
||||
def _build_message(
|
||||
self, instance, uploaded: dict, settings: dict
|
||||
) -> tuple:
|
||||
"""Build plain-text and HTML message bodies.
|
||||
|
||||
Returns:
|
||||
(plain_text, html) tuple or ("", "") if no message.
|
||||
"""
|
||||
from string import Template
|
||||
|
||||
template = settings.get(
|
||||
"message_template",
|
||||
"$product_type published: $asset / $task — v$version",
|
||||
)
|
||||
|
||||
# Build template variables
|
||||
context_data = instance.context.data if hasattr(instance, "context") else {}
|
||||
vars_dict = {
|
||||
"product_type": instance.data.get("family", "unknown"),
|
||||
"product": instance.data.get("product", instance.data.get("name", "")),
|
||||
"asset": instance.data.get("asset", ""),
|
||||
"task": instance.data.get("task", ""),
|
||||
"version": instance.data.get("version", 1),
|
||||
"user": os.environ.get("AYON_USER", os.environ.get("USER", "")),
|
||||
}
|
||||
|
||||
try:
|
||||
body = Template(template).substitute(vars_dict)
|
||||
except KeyError:
|
||||
body = template
|
||||
|
||||
# Build HTML version
|
||||
html = (
|
||||
f"<p><strong>{vars_dict['product_type']}</strong> published: "
|
||||
f"{vars_dict['asset']} / {vars_dict['task']} "
|
||||
f"— v{vars_dict['version']}</p>"
|
||||
)
|
||||
|
||||
# Add uploaded file list
|
||||
if len(uploaded) > 1:
|
||||
html += "<ul>"
|
||||
for rep_name, mxc_url in uploaded.items():
|
||||
html += (
|
||||
f'<li>{rep_name}: <a href="{mxc_url}">{mxc_url}</a></li>'
|
||||
)
|
||||
html += "</ul>"
|
||||
|
||||
return body, html
|
||||
|
||||
# ── AYONPyblishPluginMixin ─────────────────────────────────────
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
"""Define per-instance attributes for the publisher UI."""
|
||||
from ayon_core.lib import BoolDef
|
||||
return [
|
||||
BoolDef(
|
||||
"enabled",
|
||||
default=True,
|
||||
label="Upload to Matrix",
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Matrix Publish addon."""
|
||||
__version__ = "0.1.0"
|
||||
Reference in New Issue
Block a user