diff --git a/config.example.toml b/config.example.toml index 00115d0..0d38c77 100644 --- a/config.example.toml +++ b/config.example.toml @@ -24,11 +24,13 @@ wget = ["wget", "-q", "-O", "--TARGETPATH--", "--", "--DOWNLOADURL--"] # otherwise it"s disabled. [voctoweb] enable_default = false -# e.g. https://exmaple.com/api/ +# e.g. https://exmaple.com/api/ - with trailing slash api_url = "" api_key = "" -# url your frontend is reachable on, used to build urls in tweets / toots +# url your frontend is reachable on, used to build urls in tweets / toots - no trailing slash frontend_url = "" +# cdn url, used for webhook notifications - no trailing slash +cdn_url = "" # instance name is the name you refer to you"re instead with, its used for the tweets / toots instance_name = "" ssh_host = "" diff --git a/voctopublish/api_client/voctoweb_client.py b/voctopublish/api_client/voctoweb_client.py index 656f5d9..6708867 100644 --- a/voctopublish/api_client/voctoweb_client.py +++ b/voctopublish/api_client/voctoweb_client.py @@ -92,12 +92,12 @@ def _connect_ssh(self): logging.info("SSH connection established to " + str(self.ssh_host)) for dir_type, path in { - 'thumbnail': self.t.voctoweb_thumb_path, - 'video': self.t.voctoweb_path, + "thumbnail": self.t.voctoweb_thumb_path, + "video": self.t.voctoweb_path, }.items(): try: self.sftp.stat(path) - logging.debug(f'{dir_type} directory {path} already exists') + logging.debug(f"{dir_type} directory {path} already exists") except IOError as e: if e.errno == errno.ENOENT: try: diff --git a/voctopublish/api_client/webhook_client.py b/voctopublish/api_client/webhook_client.py new file mode 100644 index 0000000..e08063f --- /dev/null +++ b/voctopublish/api_client/webhook_client.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# Copyright (C) 2023 kunsi +# git@kunsmann.eu +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging + +from requests import RequestException, post +from tools.announcements import EmptyAnnouncementMessage, make_message + +LOG = logging.getLogger("Webhook") + +""" + Webhook gets POSTed to the specified url, format is JSON: + + { + "announcement": "announcement message like it gets posted to social media", # or null + "is_master": true, + "fahrplan": { + "conference": "democon", + "id": 123, + "language": "eng", + "slug": "my-super-cool-talk", + "title": "my super cool talk", + }, + "voctoweb": { + "cdn_url": "https://cdn.example.com/my-video.mp4", + "enabled": true, + "format": "h264-hd", + "frontend_url": "https://example.com/my-video", + "title": "media.ccc.de". + }, + "youtube": { + "enabled": true, + "urls": [ + "https://example.com/asdf", + "https://example.com/uiae", + ], + }, + "rclone": { + "enabled": true, + "destination": "demo:/my-video.mp4", + }, + } + + If "enabled" is false, all other fields are missing. +""" + + +def send(ticket, config, voctoweb_filename, voctoweb_language, rclone): + LOG.info(f"post webhook to {ticket.webhook_url}") + + r = None + result = None + try: + content = _get_json(ticket, config, voctoweb_filename, language, rclone) + LOG.debug(f"{content=}") + + if ticket.webhook_user and ticket.webhook_pass: + r = post( + ticket.webhook_url, + auth=(ticket.webhook_user, ticket.webhook_pass), + json=content, + ) + else: + r = post( + ticket.webhook_url, + json=content, + ) + result = r.status_code + except RequestException as e: + pass + + if r: + LOG.debug(f"{r.status_code=} {r.text=}") + + return result + + +def _get_json(ticket, config, voctoweb_filename, language, rclone): + try: + message = make_message(ticket) + except EmptyAnnouncementMessage: + message = None + + content = { + "announcement": message, + "is_master": ticket.master, + "fahrplan": { + "conference": ticket.acronym, + "id": ticket.fahrplan_id, + "language": language, + "slug": ticket.slug, + "title": ticket.title, + }, + } + + if ticket.voctoweb_enable: + content["voctoweb"] = { + "cdn_url": "{}/{}/{}/{}".format( + config["voctoweb"]["cdn_url"], + ticket.voctoweb_path, + ticket.folder, + voctoweb_filename, + ), + "enabled": True, + "format": self.folder, + "frontend_url": "{}/v/{}".format( + config["voctoweb"]["frontend_url"], + ticket.slug, + ), + "title": config["voctoweb"]["instance_name"], + } + else: + content["voctoweb"] = {"enabled": False} + + if ticket.youtube_enable: + content["youtube"] = { + "enabled": True, + "urls": list(ticket.youtube_urls.values()), + } + else: + content["youtube"] = {"enabled": False} + + if ticket.rclone_enable and rclone: + content["rclone"] = { + "destination": rclone.destination, + "enabled": True, + } + else: + content["rclone"] = {"enabled": False} + + return content diff --git a/voctopublish/model/ticket_module.py b/voctopublish/model/ticket_module.py index c838fca..daa9d9e 100644 --- a/voctopublish/model/ticket_module.py +++ b/voctopublish/model/ticket_module.py @@ -305,17 +305,29 @@ def __init__(self, ticket, ticket_id, config): self.voctoweb_event_id = self._validate_("Voctoweb.EventId", True) # rclone properties - rclone_enabled = self._validate_("Publishing.Rclone.Enable", True) - if rclone_enabled is None: - rclone_enabled = "yes" if config["rclone"]["enable_default"] else "no" - self.rclone_enabled = rclone_enabled == "yes" + rclone_enable = self._validate_("Publishing.Rclone.Enable", True) + if rclone_enable is None: + rclone_enable = "yes" if config["rclone"]["enable_default"] else "no" + self.rclone_enable = rclone_enable == "yes" - if self.rclone_enabled: + if self.rclone_enable: self.rclone_destination = self._validate_("Publishing.Rclone.Destination") self.rclone_only_master = ( self._validate_("Publishing.Rclone.OnlyMaster") == "yes" ) + # generic webhook that gets called on release + self.webhook_url = self._validate_("Publishing.Webhook.Url", True) + if self.webhook_url: + self.webhook_user = self._validate_("Publishing.Webhook.User", True) + self.webhook_pass = self._validate_("Publishing.Webhook.Password", True) + self.webhook_only_master = ( + self._validate_("Publishing.Webhook.OnlyMaster", True) == "yes" + ) + self.webhook_fail_on_error = ( + self._validate_("Publishing.Webhook.FailOnError", True) == "yes" + ) + # twitter properties twitter_enable = self._validate_("Publishing.Twitter.Enable", True) == "yes" if twitter_enable is None: diff --git a/voctopublish/tools/announcements.py b/voctopublish/tools/announcements.py index 8686167..96c8bc6 100644 --- a/voctopublish/tools/announcements.py +++ b/voctopublish/tools/announcements.py @@ -8,7 +8,12 @@ class EmptyAnnouncementMessage(Exception): pass -def make_message(ticket, max_length=200, override_url_length=None): +def make_message(ticket, max_length=None, override_url_length=None): + if max_length is None: + # if max_length is not set, set it to something very big here. + # saves us a bunch of isinstance() calls below + max_length = 1_000_000 + LOG.info(f"generating announcement message with max length of {max_length} chars") targets = [] diff --git a/voctopublish/voctopublish.py b/voctopublish/voctopublish.py index 9ebdfab..eff5234 100755 --- a/voctopublish/voctopublish.py +++ b/voctopublish/voctopublish.py @@ -33,6 +33,7 @@ import api_client.googlechat_client as googlechat import api_client.mastodon_client as mastodon import api_client.twitter_client as twitter +import api_client.webhook_client as webhook from api_client.rclone_client import RCloneClient from api_client.voctoweb_client import VoctowebClient from api_client.youtube_client import YoutubeAPI @@ -203,8 +204,9 @@ def publish(self): else: self._publish_to_youtube() - logging.debug(f"#rclone {self.ticket.rclone_enabled}") - if self.ticket.rclone_enabled: + logging.debug(f"#rclone {self.ticket.rclone_enable}") + rclone = None + if self.ticket.rclone_enable: if self.ticket.master or not self.ticket.rclone_only_master: rclone = RCloneClient(self.ticket, self.config) ret = rclone.upload() @@ -222,6 +224,29 @@ def publish(self): "skipping rclone because Publishing.Rclone.OnlyMaster is set to 'yes'" ) + if self.ticket.webhook_url: + if self.ticket.master or not self.ticket.webhook_only_master: + result = webhook.send( + self.ticket, + self.config, + getattr(self, "voctoweb_filename", None), + getattr(self, "voctoweb_language", ticket.language), + rclone, + ) + if ( + not isinstance(result, int) or result >= 300 + ) and self.ticket.webhook_fail_on_error: + raise PublisherException( + f"POSTing webhook to {self.ticket.webhook_url} failed with http status code {result}" + ) + elif isinstance(result, int): + self.c3tt.set_ticket_properties( + self.ticket_id, + { + "Webhook.StatusCode": result, + }, + ) + self.c3tt.set_ticket_done(self.ticket_id) # Twitter @@ -360,25 +385,27 @@ def _publish_to_voctoweb(self): # audio tracks of the master we need to reflect that in the target filename if self.ticket.language_index: index = int(self.ticket.language_index) - filename = ( + self.voctoweb_filename = ( self.ticket.language_template % self.ticket.languages[index] + "_" + self.ticket.profile_slug + "." + self.ticket.profile_extension ) - language = self.ticket.languages[index] + self.voctoweb_language = self.ticket.languages[index] else: - filename = self.ticket.filename - language = self.ticket.language + self.voctoweb_filename = self.ticket.filename + self.voctoweb_language = self.ticket.language - vw.upload_file(self.ticket.local_filename, filename, self.ticket.folder) + vw.upload_file( + self.ticket.local_filename, self.voctoweb_filename, self.ticket.folder + ) recording_id = vw.create_recording( self.ticket.local_filename, - filename, + self.voctoweb_filename, self.ticket.folder, - language, + self.voctoweb_language, hq, html5, )