From 841124e87c9aa888b89f4318feeab51b6302aa64 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Jun 2021 11:48:32 +0200 Subject: [PATCH] client/#75 - Added Slack module Added collector Added integrator --- openpype/modules/__init__.py | 5 +- openpype/modules/slack/README.md | 46 ++++++++++++++++ openpype/modules/slack/__init__.py | 7 +++ openpype/modules/slack/manifest.yml | 22 ++++++++ .../plugins/publish/collect_slack_family.py | 55 +++++++++++++++++++ .../plugins/publish/integrate_slack_api.py | 54 ++++++++++++++++++ openpype/modules/slack/slack_module.py | 26 +++++++++ 7 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 openpype/modules/slack/README.md create mode 100644 openpype/modules/slack/__init__.py create mode 100644 openpype/modules/slack/manifest.yml create mode 100644 openpype/modules/slack/plugins/publish/collect_slack_family.py create mode 100644 openpype/modules/slack/plugins/publish/integrate_slack_api.py create mode 100644 openpype/modules/slack/slack_module.py diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index debeeed6bf..d6fb9c0aef 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -39,6 +39,7 @@ from .deadline import DeadlineModule from .project_manager_action import ProjectManagerAction from .standalonepublish_action import StandAlonePublishAction from .sync_server import SyncServerModule +from .slack import SlackIntegrationModule __all__ = ( @@ -77,5 +78,7 @@ __all__ = ( "ProjectManagerAction", "StandAlonePublishAction", - "SyncServerModule" + "SyncServerModule", + + "SlackIntegrationModule" ) diff --git a/openpype/modules/slack/README.md b/openpype/modules/slack/README.md new file mode 100644 index 0000000000..5e099be9e7 --- /dev/null +++ b/openpype/modules/slack/README.md @@ -0,0 +1,46 @@ +Slack notification for publishing +--------------------------------- + +This module allows configuring profiles(when to trigger, for which combination of task, host and family) +and templates(could contain {} placeholder, as "{asset} published"). + +These need to be configured in +```Project settings > Slack > Publish plugins > Notification to Slack``` + +Slack module must be enabled in System Setting, could be configured per Project. + +## App installation + +Slack app needs to be installed to company's workspace. Attached .yaml file could be +used, follow instruction https://api.slack.com/reference/manifests#using + +## Settings + +### Token +Most important for module to work is to fill authentication token +```Project settings > Slack > Publish plugins > Token``` + +This token should be available after installation of app in Slack dashboard. +It is possible to create multiple tokens and configure different scopes for them. + +### Profiles +Profiles are used to select when to trigger notification. One or multiple profiles +could be configured, 'family', 'task name' (regex available) and host combination is needed. + +Eg. If I want to be notified when render is published from Maya, setting is: + +- family: 'render' +- host: 'Maya' + +### Channel +Message could be delivered to one or multiple channels, by default app allows Slack bot +to send messages to 'public' channels (eg. bot doesn't need to join channel first). + +This could be configured in Slack dashboard and scopes might be modified. + +### Message +Placeholders {} could be used in message content which will be filled during runtime. +Only keys available in 'anatomyData' are currently implemented. + +Example of message content: +```{SUBSET} for {Asset} was published.``` \ No newline at end of file diff --git a/openpype/modules/slack/__init__.py b/openpype/modules/slack/__init__.py new file mode 100644 index 0000000000..3c2a50aa35 --- /dev/null +++ b/openpype/modules/slack/__init__.py @@ -0,0 +1,7 @@ +from .slack_module import ( + SlackIntegrationModule +) + +__all__ = ( + "SlackIntegrationModule" +) diff --git a/openpype/modules/slack/manifest.yml b/openpype/modules/slack/manifest.yml new file mode 100644 index 0000000000..290093388a --- /dev/null +++ b/openpype/modules/slack/manifest.yml @@ -0,0 +1,22 @@ +_metadata: + major_version: 1 + minor_version: 1 +display_information: + name: OpenPypeNotifier +features: + app_home: + home_tab_enabled: false + messages_tab_enabled: true + messages_tab_read_only_enabled: true + bot_user: + display_name: OpenPypeNotifier + always_online: false +oauth_config: + scopes: + bot: + - chat:write + - chat:write.public +settings: + org_deploy_enabled: false + socket_mode_enabled: false + is_hosted: false diff --git a/openpype/modules/slack/plugins/publish/collect_slack_family.py b/openpype/modules/slack/plugins/publish/collect_slack_family.py new file mode 100644 index 0000000000..51eebac052 --- /dev/null +++ b/openpype/modules/slack/plugins/publish/collect_slack_family.py @@ -0,0 +1,55 @@ +from avalon import io +import pyblish.api + +from openpype.lib.profiles_filtering import filter_profiles + + +class CollectSlackFamilies(pyblish.api.InstancePlugin): + """Collect family for Slack notification + + Expects configured profile in + Project settings > Slack > Publish plugins > Notification to Slack + + Add Slack family to those instance that should be messaged to Slack + """ + order = pyblish.api.CollectorOrder + 0.4999 + label = 'Collect Slack family' + + profiles = None + + def process(self, instance): + task_name = io.Session.get("AVALON_TASK") + family = self.main_family_from_instance(instance) + + key_values = { + "families": family, + "tasks": task_name, + "hosts": instance.data["anatomyData"]["app"], + } + self.log.debug("key_values {}".format(key_values)) + profile = filter_profiles(self.profiles, key_values, + logger=self.log) + + # make slack publishable + if profile: + if instance.data.get('families'): + instance.data['families'].append('slack') + else: + instance.data['families'] = ['slack'] + + instance.data["slack_channel"] = profile["channel"] + instance.data["slack_message"] = profile["message"] + + slack_token = (instance.context.data["project_settings"] + ["slack"] + ["publish"] + ["CollectSlackFamilies"] + ["token"]) + instance.data["slack_token"] = slack_token + + def main_family_from_instance(self, instance): # TODO yank from integrate + """Returns main family of entered instance.""" + family = instance.data.get("family") + if not family: + family = instance.data["families"][0] + return family diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py new file mode 100644 index 0000000000..d9a172b89b --- /dev/null +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -0,0 +1,54 @@ +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError + +import pyblish.api +from openpype.lib.plugin_tools import prepare_template_data + + +class IntegrateSlackAPI(pyblish.api.InstancePlugin): + """ Send message notification to a channel. + + Triggers on instances with "slack" family, filled by + 'collect_slack_family'. + + Expects configured profile in + Project settings > Slack > Publish plugins > Notification to Slack + + Message template can contain {} placeholders from anatomyData. + """ + order = pyblish.api.IntegratorOrder+0.499 + label = "Integrate Slack Api" + families = ["slack"] + + optional = True + + def process(self, instance): + message_templ = instance.data["slack_message"] + + fill_pairs = set() + for key, value in instance.data["anatomyData"].items(): + if not isinstance(value, str): + continue + fill_pairs.add((key, value)) + self.log.debug("fill_pairs:: {}".format(fill_pairs)) + + message = message_templ.format(**prepare_template_data(fill_pairs)) + + self.log.debug("message:: {}".format(message)) + if '{' in message: + self.log.warning( + "Missing values to fill message properly {}".format(message)) + + return + + for channel in instance.data["slack_channel"]: + try: + client = WebClient(token=instance.data["slack_token"]) + _response = client.chat_postMessage( + channel=channel, + text=message + ) + except SlackApiError as e: + # You will get a SlackApiError if "ok" is False + self.log.warning("Error happened {}".format(e.response[ + "error"])) diff --git a/openpype/modules/slack/slack_module.py b/openpype/modules/slack/slack_module.py new file mode 100644 index 0000000000..53173f6cd0 --- /dev/null +++ b/openpype/modules/slack/slack_module.py @@ -0,0 +1,26 @@ +import os +from openpype.modules import ( + PypeModule, IPluginPaths) + + +class SlackIntegrationModule(PypeModule, IPluginPaths): + """Allows sending notification to Slack channels during publishing.""" + + name = "slack" + + def initialize(self, modules_settings): + slack_settings = modules_settings[self.name] + self.enabled = slack_settings["enabled"] + + def connect_with_modules(self, _enabled_modules): + """Nothing special.""" + return + + def get_plugin_paths(self): + """Deadline plugin paths.""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + return { + "publish": [os.path.join(current_dir, "plugins", "publish")] + } + +