18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244 | class SyncsketchAddon(BaseServerAddon):
settings_model: Type[SyncsketchSettings] = SyncsketchSettings
async def resolved_secrets(self):
addon_settings = await self.get_studio_settings()
syncsk_server_config = addon_settings.syncsketch_server_config
syncsk_server_config = dict(syncsk_server_config)
all_secrets = await Secrets.all()
secrets = dict(dict(all_secrets.items()))
# resolve all secrets from the server config
resolved_secrets = {
key_: secrets[syncsk_server_config[key_]]
for key_ in constants.required_secret_keys
if syncsk_server_config[key_] in secrets
}
return resolved_secrets
async def get_default_settings(self):
settings_model_cls = self.get_settings_model()
return settings_model_cls(**DEFAULT_VALUES)
async def setup(self):
need_restart = await self.create_syncsketch_id_attribute()
if need_restart:
self.request_server_restart()
await self.create_syncsketch_webhook()
def initialize(self):
logging.info("Initializing SyncSketch Addon.")
self.add_endpoint(
"syncsketch-event",
self._syncsketch_event,
method="POST",
name="receive-syncsketch-event",
description="Create an Ayon Event, from a SyncSketch event.",
)
logging.info("Added Event Listener Webhook.")
async def create_syncsketch_webhook(self):
"""Create a SyncSketch Webhook for new events.
The Ayon SyncSketch integration will create an end point
that will listen for events coming from SyncSketch and later
process them with the processor.
Here we use the REST API to check if the webhook exists, and if not,
we create it.
"""
addon_settings = await self.get_studio_settings()
syncsk_server_config = addon_settings.syncsketch_server_config
all_secrets = await self.resolved_secrets()
timeout = ayonconfig.http_timeout
# is manageable via server env variable AYON_HTTP_LISTEN_ADDRESS
ayon_endpoint = (f"{ayonconfig.http_listen_address}/api/addons/"
f"{self.name}/{self.version}/syncsketch-event")
if not syncsk_server_config:
logging.error(f"Unable to get Studio Settings: {self.name} addon.")
return
if not all((
syncsk_server_config.url,
)):
logging.error("Missing data in the Addon settings.")
return
# make sure all config secrets are present otherwise return
# TODO: this is just a workaround until we create correct workflow
# for the secrets. We need to first set settings to get secrets.
if len(all_secrets.keys()) != len(constants.required_secret_keys):
logging.warning("Missing secrets in the server config.")
return
syncsketch_endpoint = (
f"{syncsk_server_config.url}/api/v2/notifications/"
f"{all_secrets['account_id']}/webhooks/"
)
headers = {
"Authorization": (
f"apikey {all_secrets['auth_user']}:"
f"{all_secrets['auth_token']}"
),
"Content-Type": "application/json",
}
try:
async with httpx.AsyncClient(timeout=timeout) as client:
res = await client.get(
syncsketch_endpoint,
headers=headers,
)
res.raise_for_status()
existing_webhooks = res.json()
except Exception:
log_traceback("Unable to get existing Webhooks in SyncSketch.")
return
if existing_webhooks:
for webhook in existing_webhooks:
if (
webhook.get("url") == ayon_endpoint
and webhook.get("type") == "all"
):
logging.info(f"AYON Webhook already exists: {webhook}")
return
# Create the Webhook that sends an event when a session ends
try:
async with httpx.AsyncClient(timeout=timeout) as client:
webhook_created = await client.post(
syncsketch_endpoint,
headers=headers,
json={
"url": ayon_endpoint,
"type": "all",
}
)
webhook_created.raise_for_status()
except Exception:
log_traceback("Unable to create a Webhook in SyncSketch.")
return
logging.info(
"Successfully created a Webhook in "
f"SyncSketch {webhook_created.json()}"
)
async def _syncsketch_event(self, request: dict[str, Any]):
"""Dispatch an Ayon event from a SyncSketch one.
"""
if request["action"] in [
"review_session_end",
"item_approval_status_changed"
]:
logging.info(
f"Webhook received with a payload: {pformat(request)}.")
project_name = request["project"]["name"]
event_id = await dispatch_event(
f"syncsketch.{request['action']}",
sender=socket.gethostname(),
project=project_name,
user="",
description=(
"SyncSketch source webhook event "
f"\'{request['action']}\' received."
),
summary=None,
payload=request,
)
logging.info(f"Dispatched event {event_id}")
return event_id
logging.warning(
f"Received a SyncSketch event that we don't handle. {request}"
)
async def create_syncsketch_id_attribute(self) -> bool:
"""Make sure there are required attributes which ftrack addon needs.
Returns:
bool: 'True' if an attribute was created or updated.
"""
query = "SELECT name, position, scope, data from public.attributes"
id_attrib_name = "syncsketchId"
id_attribute_data = {
"type": "string",
"title": "SyncSketch ID",
"inherit": False,
}
id_scope = ["project", "version"]
id_match_position = None
id_matches = False
position = 1
async for row in Postgres.iterate(query):
position += 1
if row["name"] == id_attrib_name:
# Check if scope is matching ftrack addon requirements
if set(row["scope"]) == set(id_scope):
id_matches = True
id_match_position = row["position"]
if id_matches:
return False
postgres_query = "\n".join(
(
"INSERT INTO public.attributes",
" (name, position, scope, data)",
"VALUES",
" ($1, $2, $3, $4)",
"ON CONFLICT (name)",
"DO UPDATE SET",
" scope = $3,",
" data = $4",
)
)
# Reuse position from found attribute
if id_match_position is None:
id_match_position = position
position += 1
await Postgres.execute(
postgres_query,
id_attrib_name,
id_match_position,
id_scope,
id_attribute_data,
)
return True
|