Post Slack message on publish

Next, we implement a custom finaliser within Maya - a small function that posts a Slack message containing the version ident, the comment and a thumbnail (can be replaced with the full Quicktime reviewable movie if desired), with each publish made.

Functions that run after load and import are called “finalizers” and they are fed all the data from the previous steps and stages.

Save copy of thumbnail image

As a preparation, we need to have the thumbnail publisher to save a copy to be used with the Slack post:

PIPELINE/ftrack-connect-pipeline-definitions/resource/plugins/maya/python/publisher/exporters/maya_thumbnail_publisher_exporter.py

 1import os
 2import shutil
 3
 4..
 5
 6def run(self, context_data=None, data=None, options=None):
 7    ..
 8    # Make a copy of the thumbnail to be used with Slack post
 9    path_slack_thumbnail = os.path.join(os.path.dirname(path), 'slack-{}'.format(os.path.basename(path)))
10    shutil.copy(path, path_slack_thumbnail)
11
12    return [path]

Finaliser

PIPELINE/ftrack-connect-pipeline-definitions/resource/plugins/common/python/publisher/finalisers/common_slack_post_publisher_finalizer.py

 1# :coding: utf-8
 2# :copyright: Copyright (c) 2014-2022 ftrack
 3import os
 4
 5from slack import WebClient
 6
 7import ftrack_api
 8
 9from ftrack_connect_pipeline import plugin
10
11
12class CommonSlackPublisherFinalizerPlugin(plugin.PublisherPostFinalizerPlugin):
13
14    plugin_name = 'common_slack_publisher_finalizer'
15
16    SLACK_CHANNEL = 'test'
17
18    def run(self, context_data=None, data=None, options=None):
19
20        # Harvest publish data
21        reviewable_path = asset_version_id = component_names = None
22        for component_data in data:
23            if component_data['name'] == 'thumbnail':
24                for output in component_data['result']:
25                    if output['name'] == 'exporter':
26                        reviewable_path = output['result'][0]['result'][0]
27            elif component_data['type'] == 'finalizer':
28                for step in component_data['result']:
29                    if step['name'] == 'finalizer':
30                        asset_version_id = step['result'][0]['result'][
31                            'asset_version_id'
32                        ]
33                        component_names = step['result'][0]['result'][
34                            'component_names'
35                        ]
36                        break
37
38        # Fetch version
39        version = self.session.query(
40            'AssetVersion where id={}'.format(asset_version_id)
41        ).one()
42
43        # Fetch path to thumbnail
44        if reviewable_path:
45            # Assume it is on the form /tmp/tmp7vlg8kv5.jpg.0000.jpg, locate our copy
46            reviewable_path = os.path.join(
47                os.path.dirname(reviewable_path),
48                'slack-{}'.format(os.path.basename(reviewable_path)),
49            )
50
51        client = WebClient("<slack-api-key>")
52
53        ident = '|'.join(
54            [cl['name'] for cl in version['asset']['parent']['link']]
55            + [version['asset']['name'], 'v%03d' % (version['version'])]
56        )
57
58        if reviewable_path:
59            self.logger.info(
60                'Posting Slack message "{}" to channel {}, attaching reviewable "{}"'.format(
61                    ident, self.SLACK_CHANNEL, reviewable_path
62                )
63            )
64            try:
65                response = client.files_upload(
66                    channels=self.SLACK_CHANNEL,
67                    file=reviewable_path,
68                    title=ident,
69                    initial_comment=version['comment'],
70                )
71            finally:
72                os.remove(reviewable_path)  # Not needed anymore
73        else:
74            # Just post a message
75            self.logger.info(
76                'Posting Slack message "{}" to channel {}, without reviewable'.format(
77                    ident, self.SLACK_CHANNEL
78                )
79            )
80            client.chat_postMessage(channel=self.SLACK_CHANNEL, text=ident)
81        if response.get('ok') is False:
82            raise Exception(
83                'Slack file upload failed! Details: {}'.format(response)
84            )
85
86        return {}
87
88
89def register(api_object, **kw):
90    if not isinstance(api_object, ftrack_api.Session):
91        # Exit to avoid registering this plugin again.
92        return
93    plugin = CommonSlackPublisherFinalizerPlugin(api_object)
94    plugin.register()

Breakdown of plugin:

  • With the data argument, the finaliser gets passed on the result from the entire publish process. From this data we harvest the temporary path to thumbnail and asset version id.

  • We transcode the path so we locate the thumbnail copy.

  • A Slack client API session is created

  • An human readable asset version identifier is compiled

  • If a thumbnail were found, it is uploaded to Slack. A standard chat message is posted otherwise.

Add Slack finaliser to publishers

Finally we augment the publishers that we wish to use.

PIPELINE/ftrack-connect-pipeline-definition/resource/definitions/publisher/maya/geometry-maya-publish.json

 1{
 2  "type": "publisher",
 3  "name": "Geometry Publisher",
 4  "contexts": [],
 5  "components": [],
 6  "finalizers": [
 7    {
 8      "name": "main",
 9      "stages": [
10        {
11          "name": "pre_finalizer",
12          "visible": false,
13          "plugins":[
14            {
15              "name": "Pre publish to ftrack server",
16              "plugin": "common_passthrough_publisher_pre_finalizer"
17            }
18          ]
19        },
20        {
21          "name": "finalizer",
22          "visible": false,
23          "plugins":[
24            {
25              "name": "Publish to ftrack server",
26              "plugin": "common_passthrough_publisher_finalizer"
27            }
28          ]
29        },
30        {
31          "name": "post_finalizer",
32          "visible": true,
33          "plugins":[
34            {
35              "name": "Post slack message",
36              "plugin": "common_slack_publisher_finalizer"
37            }
38          ]
39        }
40      ]
41    }
42  ]
43}

Repeat this for all publishers that should have the finaliser.

Add Slack library

To be able to use the Slack Python API, we need to add it to our Framework build. We do that by adding the dependency to setup.py:

ftrack-connect-pipeline-definition/setup.py

 1..
 2
 3# Configuration.
 4setup(
 5    name='ftrack-connect-pipeline-definition',
 6    description='Collection of definitions of package and packages.',
 7    long_description=open(README_PATH).read(),
 8    keywords='ftrack',
 9    url='https://bitbucket.org/ftrack/ftrack-connect-pipeline-definition',
10    author='ftrack',
11    author_email='support@ftrack.com',
12    license='Apache License (2.0)',
13    packages=find_packages(SOURCE_PATH),
14    package_dir={'': 'source'},
15    python_requires='<3.10',
16    use_scm_version={
17        'write_to': 'source/ftrack_connect_pipeline_definition/_version.py',
18        'write_to_template': version_template,
19        'version_scheme': 'post-release',
20    },
21    setup_requires=[
22        'sphinx >= 1.8.5, < 4',
23        'sphinx_rtd_theme >= 0.1.6, < 2',
24        'lowdown >= 0.1.0, < 2',
25        'setuptools>=44.0.0',
26        'setuptools_scm',
27        'slackclient'
28    ],
29    install_requires=[
30        'slackclient'
31    ],
32    tests_require=['pytest >= 2.3.5, < 3'],
33    cmdclass={'test': PyTest, 'build_plugin': BuildPlugin},
34    zip_safe=False,
35)

Important

A better approach is to add the dependency to the ftrack-connect-pipeline module where the other pipeline dependencies are defined and built.