Fediverse: Add support for videos

This commit is contained in:
Valentin Lorentz 2022-07-20 17:53:00 +02:00
parent d67fb2a8b2
commit 2df2bc28d0
4 changed files with 287 additions and 7 deletions

View File

@ -38,6 +38,7 @@ from supybot.commands import urlSnarfer, wrap
from supybot.i18n import PluginInternationalization
from . import activitypub as ap
from .utils import parse_xsd_duration
importlib.reload(ap)
@ -222,18 +223,29 @@ class Fediverse(callbacks.PluginRegexp):
name = actor.get("name", username)
return "\x02%s\x02 (@%s@%s)" % (name, username, hostname)
def _format_author(self, irc, author):
if isinstance(author, str):
# it's an URL
try:
author = self._get_actor(irc, author)
except ap.ActivityPubError as e:
return _("<error: %s>") % str(e)
else:
return self._format_actor_fullname(author)
elif isinstance(author, dict):
if author.get("id"):
return self._format_author(irc, author["id"])
elif isinstance(author, list):
return format("%L", [self._format_author(irc, item) for item in author])
else:
return "<unknown>"
def _format_status(self, irc, msg, status):
if status["type"] == "Create":
return self._format_status(irc, msg, status["object"])
elif status["type"] == "Note":
author_url = status["attributedTo"]
try:
author = self._get_actor(irc, author_url)
except ap.ActivityPubError as e:
author_fullname = _("<error: %s>") % str(e)
else:
author_fullname = self._format_actor_fullname(author)
cw = status.get("summary")
author_fullname = self._format_author(irc, status.get("attributedTo"))
if cw:
if self.registryValue(
"format.statuses.showContentWithCW",
@ -275,6 +287,15 @@ class Fediverse(callbacks.PluginRegexp):
return self._format_status(irc, msg, status)
except ap.ActivityPubProtocolError as e:
return "<Could not fetch status: %s>" % e.args[0]
elif status["type"] == "Video":
author_fullname = self._format_author(irc, status.get("attributedTo"))
return format(
_("\x02%s\x02 (%T) by %s: %s"),
status["name"],
abs(parse_xsd_duration(status["duration"]).total_seconds()),
author_fullname,
status["content"],
)
else:
assert False, "Unknown status type %s: %r" % (
status["type"],

View File

@ -60,6 +60,10 @@ from .test_data import (
BOOSTED_DATA,
BOOSTED_ACTOR_URL,
BOOSTED_ACTOR_DATA,
PEERTUBE_VIDEO_URL,
PEERTUBE_VIDEO_DATA,
PEERTUBE_ACTOR_URL,
PEERTUBE_ACTOR_DATA,
)
@ -430,6 +434,21 @@ class NetworklessFediverseTestCase(BaseFediverseTestCase):
+ "<https://example.net/system/media_attachments/image.png>",
)
def testVideo(self):
expected_requests = [
(PEERTUBE_VIDEO_URL, PEERTUBE_VIDEO_DATA),
(PEERTUBE_ACTOR_URL, PEERTUBE_ACTOR_DATA),
(ACTOR_URL, ACTOR_DATA),
]
with self.mockRequests(expected_requests):
self.assertResponse(
"status https://example.org/w/gABde9e210FGHre",
"\x02name of video\x02 (1 hour, 26 minutes, and 0 seconds) "
"by \x02chocobozzz\x02 (@chocobozzz@peertube.cpy.re) "
"and \x02someuser\x02 (@someuser@example.org): description of video"
)
def testStatusUrlSnarferDisabled(self):
with self.mockWebfingerSupport("not called"), self.mockRequests([]):
self.assertSnarfNoResponse(

View File

@ -384,3 +384,180 @@ BOOSTED_ACTOR_VALUE = {
"endpoints": {"sharedInbox": "https://example.net/inbox"},
}
BOOSTED_ACTOR_DATA = json.dumps(BOOSTED_ACTOR_VALUE).encode()
PEERTUBE_ACTOR_VALUE = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"RsaSignature2017": "https://w3id.org/security#RsaSignature2017"
},
{
"pt": "https://joinpeertube.org/ns#",
"sc": "http://schema.org/",
"playlists": {
"@id": "pt:playlists",
"@type": "@id"
}
}
],
"type": "Person",
"id": "https://peertube.cpy.re/accounts/chocobozzz",
"following": "https://peertube.cpy.re/accounts/chocobozzz/following",
"followers": "https://peertube.cpy.re/accounts/chocobozzz/followers",
"playlists": "https://peertube.cpy.re/accounts/chocobozzz/playlists",
"inbox": "https://peertube.cpy.re/accounts/chocobozzz/inbox",
"outbox": "https://peertube.cpy.re/accounts/chocobozzz/outbox",
"preferredUsername": "chocobozzz",
"url": "https://peertube.cpy.re/accounts/chocobozzz",
"name": "chocobozzz",
"published": "2017-11-28T08:48:24.271Z",
"summary": None
}
PEERTUBE_ACTOR_DATA = json.dumps(PEERTUBE_ACTOR_VALUE).encode()
PEERTUBE_ACTOR_URL = "https://peertube.cpy.re/accounts/chocobozzz"
PEERTUBE_VIDEO_VALUE = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"RsaSignature2017": "https://w3id.org/security#RsaSignature2017"
},
{
"pt": "https://joinpeertube.org/ns#",
"sc": "http://schema.org/",
"Hashtag": "as:Hashtag",
"uuid": "sc:identifier",
"category": "sc:category",
"licence": "sc:license",
"subtitleLanguage": "sc:subtitleLanguage",
"sensitive": "as:sensitive",
"language": "sc:inLanguage",
"icons": "as:icon",
"isLiveBroadcast": "sc:isLiveBroadcast",
"liveSaveReplay": {
"@type": "sc:Boolean",
"@id": "pt:liveSaveReplay"
},
"permanentLive": {
"@type": "sc:Boolean",
"@id": "pt:permanentLive"
},
"latencyMode": {
"@type": "sc:Number",
"@id": "pt:latencyMode"
},
"Infohash": "pt:Infohash",
"originallyPublishedAt": "sc:datePublished",
"views": {
"@type": "sc:Number",
"@id": "pt:views"
},
"state": {
"@type": "sc:Number",
"@id": "pt:state"
},
"size": {
"@type": "sc:Number",
"@id": "pt:size"
},
"fps": {
"@type": "sc:Number",
"@id": "pt:fps"
},
"commentsEnabled": {
"@type": "sc:Boolean",
"@id": "pt:commentsEnabled"
},
"downloadEnabled": {
"@type": "sc:Boolean",
"@id": "pt:downloadEnabled"
},
"waitTranscoding": {
"@type": "sc:Boolean",
"@id": "pt:waitTranscoding"
},
"support": {
"@type": "sc:Text",
"@id": "pt:support"
},
"likes": {
"@id": "as:likes",
"@type": "@id"
},
"dislikes": {
"@id": "as:dislikes",
"@type": "@id"
},
"shares": {
"@id": "as:shares",
"@type": "@id"
},
"comments": {
"@id": "as:comments",
"@type": "@id"
}
}
],
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Video",
"name": "name of video",
"duration": "PT5160S",
"tag": [
{
"type": "Hashtag",
"name": "vostfr"
}
],
"category": {
"identifier": "2",
"name": "Films"
},
"licence": {
"identifier": "4",
"name": "Attribution - Non Commercial"
},
"language": {
"identifier": "en",
"name": "English"
},
"views": 13718,
"sensitive": False,
"waitTranscoding": False,
"state": 1,
"commentsEnabled": True,
"downloadEnabled": True,
"published": "2017-10-23T07:54:38.155Z",
"originallyPublishedAt": None,
"updated": "2022-07-13T07:03:12.373Z",
"mediaType": "text/markdown",
"content": "description of video",
"support": None,
"subtitleLanguage": [],
"icon": [
# redacted
],
"url": [
# redacted
],
"attributedTo": [
{
"type": "Person",
"id": PEERTUBE_ACTOR_URL
},
{
"type": "Group",
"id": ACTOR_URL,
}
],
"isLiveBroadcast": False,
"liveSaveReplay": None,
"permanentLive": None,
"latencyMode": None,
}
PEERTUBE_VIDEO_DATA = json.dumps(PEERTUBE_VIDEO_VALUE).encode()
PEERTUBE_VIDEO_URL = "https://example.org/w/gABde9e210FGHre"

View File

@ -0,0 +1,63 @@
###
# Copyright (c) 2022, Valentin Lorentz
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import re
import datetime
# Credits for the regexp and function: https://stackoverflow.com/a/2765366/539465
_XSD_DURATION_RE = re.compile(
"(?P<sign>-?)P"
"(?:(?P<years>\d+)Y)?"
"(?:(?P<months>\d+)M)?"
"(?:(?P<days>\d+)D)?"
"(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?"
)
def parse_xsd_duration(s):
"""Parses this format to a timedelta:
https://www.w3.org/TR/xmlschema11-2/#duration"""
# Fetch the match groups with default value of 0 (not None)
duration = _XSD_DURATION_RE.match(s).groupdict(0)
# Create the timedelta object from extracted groups
delta = datetime.timedelta(
days=int(duration["days"])
+ (int(duration["months"]) * 30)
+ (int(duration["years"]) * 365),
hours=int(duration["hours"]),
minutes=int(duration["minutes"]),
seconds=int(duration["seconds"]),
)
if duration["sign"] == "-":
delta *= -1
return delta