Skip to content

Commit

Permalink
MediaEmbed: added Bluesky
Browse files Browse the repository at this point in the history
Closes #238
  • Loading branch information
JoshyPHP committed Nov 11, 2024
1 parent 0ad2008 commit 922ca9e
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 84 deletions.
26 changes: 26 additions & 0 deletions docs/Plugins/MediaEmbed/Federated_sites.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
<h2>Federated sites</h2>


### Bluesky

The default Bluesky media site can be customized with different hosts. This can be done using the `BlueskyHelper` class. In the example below, we configure the Bluesky media site to support both the main `bsky.app` domain, as well as our own hypothetical instance hosted on `bluesky.local`.

```php
$configurator = new s9e\TextFormatter\Configurator;

// Use the Bluesky helper to set 'bsky.app' and 'bluesky.local' as supported instances
$mastodonHelper = $configurator->MediaEmbed->getSiteHelper('bluesky');
$mastodonHelper->setHosts(['bsky.app', 'bluesky.local']);

// Get an instance of the parser and the renderer
extract($configurator->finalize());

$text = 'https://bsky.app/profile/bsky.app/post/3kkrqzuydho2v';
$xml = $parser->parse($text);
$html = $renderer->render($xml);

echo $html;
```
```html
<iframe data-s9e-mediaembed="bluesky" allowfullscreen="" loading="lazy" onload="let c=new MessageChannel;c.port1.onmessage=e=&gt;this.style.height=e.data+'px';this.contentWindow.postMessage('s9e:init','*',[c.port2])" scrolling="no" src="https://s9e.github.io/iframe/2/bluesky.min.html#at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3kkrqzuydho2v#embed.bsky.app" style="border:0;height:600px;max-width:600px;width:100%"></iframe>
```


### Mastodon

The default Mastodon media site can be customized with additional hosts. This can be done using the `MastodonHelper` class. In the example below, we add support for toots published by the `infosec.exchange` instance.
Expand Down
4 changes: 4 additions & 0 deletions docs/Plugins/MediaEmbed/Sites.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
<td><code>bitchute</code></td>
<td>https://www.bitchute.com/video/y9AejeZ2vD0/</td>
</tr>
<tr title="Bluesky" id="bluesky">
<td><code>bluesky</code></td>
<td>https://bsky.app/profile/bsky.app/post/3kkrqzuydho2v<br/>https://bsky.app/profile/bnewbold.net/post/3kxjq2auebs2f</td>
</tr>
<tr title="Brightcove" id="brightcove">
<td><code>brightcove</code></td>
<td>https://players.brightcove.net/219646971/default_default/index.html?videoId=4815779906001<br/>https://link.brightcove.com/services/player/bcpid4773906090001?bckey=AQ~~,AAAAAA0Xi_s~,r1xMuE8k5Nyz5IyYc0Hzhl5eZ5cEEvlm&amp;bctid=4815779906001</td>
Expand Down
2 changes: 1 addition & 1 deletion docs/Plugins/MediaEmbed/Using_default_sites.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ echo count($configurator->MediaEmbed->defaultSites), " sites remaining.\n";
```
Does YouTube exist? yes
What about now? no
133 sites remaining.
134 sites remaining.
0 sites remaining.
```

Expand Down
7 changes: 7 additions & 0 deletions docs/testdox.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6757,6 +6757,11 @@ Xml File Definition Collection (s9e\TextFormatter\Tests\Plugins\MediaEmbed\Confi
[x] Other default attribute values are left as strings
[x] Attributes' "required" property is cast to bool

Bluesky Helper (s9e\TextFormatter\Tests\Plugins\MediaEmbed\Configurator\SiteHelpers\BlueskyHelper)
[x] addHost() normalizes the host
[x] addHost() adds the Bluesky media site if it's not enabled yet
[x] addHost() updates the embedded regexp

Mastodon Helper (s9e\TextFormatter\Tests\Plugins\MediaEmbed\Configurator\SiteHelpers\MastodonHelper)
[x] addHost() normalizes the host
[x] addHost() adds the Mastodon media site if it's not enabled yet
Expand Down Expand Up @@ -6896,6 +6901,8 @@ Parser (s9e\TextFormatter\Tests\Plugins\MediaEmbed\Parser)
[x] Scraping tests with data set #54
[x] Scraping tests with data set #55
[x] Scraping tests with data set #56
[x] Scraping tests with data set #57
[x] Scraping tests with data set #58
[x] Scraping+rendering tests with data set #0
[x] Scraping+rendering tests with data set #1
[x] Scraping+rendering tests with data set #2
Expand Down
160 changes: 80 additions & 80 deletions src/Bundles/MediaPack.php

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/Bundles/MediaPack/Renderer.php

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class CachedDefinitionCollection extends SiteDefinitionCollection
'bandcamp'=>['attributes'=>[],'example'=>['https://proleter.bandcamp.com/album/curses-from-past-times-ep','https://proleter.bandcamp.com/track/downtown-irony','https://therunons.bandcamp.com/track/still-feel'],'extract'=>[],'homepage'=>'https://bandcamp.com/','host'=>['bandcamp.com'],'iframe'=>['height'=>400,'src'=>'//bandcamp.com/EmbeddedPlayer/size=large/minimal=true/<xsl:choose><xsl:when test="@album_id">album=<xsl:value-of select="@album_id"/><xsl:if test="@track_num">/t=<xsl:value-of select="@track_num"/></xsl:if></xsl:when><xsl:otherwise>track=<xsl:value-of select="@track_id"/></xsl:otherwise></xsl:choose><xsl:if test="$MEDIAEMBED_THEME=\'dark\'">/bgcol=333333/linkcol=0f91ff</xsl:if>','width'=>400],'name'=>'Bandcamp','scrape'=>[['extract'=>['!/album=(?\'album_id\'\\d+)!'],'header'=>'User-agent: PHP (not Mozilla)','match'=>['!bandcamp\\.com/album/.!']],['extract'=>['!(?:"|&quot;)album_id(?:"|&quot;):(?\'album_id\'\\d+)!','!(?:"|&quot;)track_num(?:"|&quot;):(?\'track_num\'\\d+)!','!/track=(?\'track_id\'\\d+)!'],'header'=>'User-agent: PHP (not Mozilla)','match'=>['!bandcamp\\.com/track/.!']]],'tags'=>['music']],
'bbcnews'=>['attributes'=>[],'example'=>['https://www.bbc.com/news/video_and_audio/must_see/42847060/calls-to-clean-off-banksy-mural-in-hull','https://www.bbc.co.uk/news/av/world-africa-48141248/fifteen-year-old-nigerian-builds-small-scale-construction-machines','https://www.bbc.co.uk/news/av/embed/p0783sg7/48125671'],'extract'=>['@bbc\\.co(?:m|\\.uk)/news/(?:av|video_and_audio)/(?:\\w+-)+(?\'id\'\\d+)@','@bbc\\.co(?:m|\\.uk)/news/(?:av|video_and_audio)/embed/(?\'id\'\\w+/\\d+)@','@bbc\\.co(?:m|\\.uk)/news/(?:av|video_and_audio)/\\w+/(?\'id\'\\d+)@','@bbc\\.co(?:m|\\.uk)/news/av-embeds/(?\'id\'\\d+)@'],'homepage'=>'https://www.bbc.com/news/video_and_audio/headlines/','host'=>['bbc.co.uk','bbc.com'],'iframe'=>['src'=>'//www.bbc.com/news/av-embeds/<xsl:choose><xsl:when test="starts-with(@playlist,\'/news/\')"><xsl:choose><xsl:when test="contains(@playlist,\'-\')"><xsl:value-of select="substring-after(substring-after(translate(@playlist,\'A\',\'#\'),\'news/\'),\'-\')"/></xsl:when><xsl:otherwise><xsl:value-of select="substring-after(translate(@playlist,\'A\',\'/\'),\'/news/\')"/></xsl:otherwise></xsl:choose></xsl:when><xsl:when test="contains(@id,\'/\')"><xsl:value-of select="substring-after(@id,\'/\')"/></xsl:when><xsl:otherwise><xsl:value-of select="@id"/></xsl:otherwise></xsl:choose>'],'name'=>'BBC News','scrape'=>[],'tags'=>['news']],
'bitchute'=>['attributes'=>[],'example'=>'https://www.bitchute.com/video/y9AejeZ2vD0/','extract'=>['@bitchute\\.com/(?:embed|video)/(?\'id\'[-\\w]+)@'],'homepage'=>'https://www.bitchute.com/','host'=>['bitchute.com'],'iframe'=>['src'=>'https://www.bitchute.com/embed/{@id}/'],'name'=>'BitChute','oembed'=>['endpoint'=>'https://www.bitchute.com/oembed/','scheme'=>'https://www.bitchute.com/video/{@id}/'],'scrape'=>[],'tags'=>['videos']],
'bluesky'=>['attributes'=>['embedder'=>['filterChain'=>['#regexp(/^(?:[-\\w]*\\.)*bsky\\.app$/)'],'required'=>true],'url'=>['filterChain'=>['urldecode','#regexp(/^at:\\/\\/[.:\\w]+\\/[.\\w]+\\/\\w+$/)'],'required'=>true]],'example'=>['https://bsky.app/profile/bsky.app/post/3kkrqzuydho2v','https://bsky.app/profile/bnewbold.net/post/3kxjq2auebs2f'],'extract'=>['#https://(?\'embedder\'[.\\w]+)/oembed.*?url=(?\'url\'[\\w%.]+)#'],'helper'=>'s9e\\TextFormatter\\Plugins\\MediaEmbed\\Configurator\\SiteHelpers\\BlueskyHelper','homepage'=>'https://bsky.app/','host'=>['bsky.app'],'iframe'=>['data-s9e-livepreview-ignore-attrs'=>'style','height'=>600,'onload'=>'let c=new MessageChannel;c.port1.onmessage=e=>this.style.height=e.data+\'px\';this.contentWindow.postMessage(\'s9e:init\',\'*\',[c.port2])','src'=>'https://s9e.github.io/iframe/2/bluesky.min.html#<xsl:value-of select="@url"/>#<xsl:value-of select="@embedder"/>','width'=>600],'name'=>'Bluesky','scrape'=>[['extract'=>['#https://(?\'embedder\'[.\\w]+)/oembed.*?url=(?\'url\'[\\w%.]+)#'],'match'=>['#/profile/[^/]+/post/.#']]],'source'=>'https://embed.bsky.app/','tags'=>['social']],

This comment has been minimized.

Copy link
@rxu

rxu Nov 15, 2024

@JoshyPHP Would it work on 2.11.5 while translated into Yaml within phpBB mediaembed extension, and how xsl part would look then? Just looking into the possibility of adding it in extension.

This comment has been minimized.

Copy link
@JoshyPHP

JoshyPHP Nov 15, 2024

Author Member

Off the top of my head, I can't think of a reason it wouldn't work on a recent-ish version, so I'd give it a good 95% chance.

You can dump a site's configuration if you want to save it in another format, and you can dump a tag's template if you want to inspect it too.

$configurator = new s9e\TextFormatter\Configurator;

// Dump the site's config, could be saved as any other format
print_r($configurator->MediaEmbed->defaultSites['bluesky']);

// Display the site's tag's template
$configurator->MediaEmbed->add('bluesky');
echo $configurator->tags['BLUESKY']->template, "\n";

This comment has been minimized.

Copy link
@rxu

rxu Nov 16, 2024

Should it work without helper which doesn't exist in 2.11.5?

EDIT: tested and figured out it requires the helper code to work.

This comment has been minimized.

Copy link
@JoshyPHP

JoshyPHP Nov 16, 2024

Author Member

Open an issue in the phpBB repo so we can regroup your and @iMattPro's thread and I'll post there asap.

'brightcove'=>['attributes'=>['bckey'=>['required'=>false]],'example'=>['https://players.brightcove.net/219646971/default_default/index.html?videoId=4815779906001','https://link.brightcove.com/services/player/bcpid4773906090001?bckey=AQ~~,AAAAAA0Xi_s~,r1xMuE8k5Nyz5IyYc0Hzhl5eZ5cEEvlm&bctid=4815779906001'],'extract'=>['@link\\.brightcove\\.com/services/player/bcpid(?\'bcpid\'\\d+).*?bckey=(?\'bckey\'[-,~\\w]+).*?bctid=(?\'bctid\'\\d+)@','@players\\.brightcove\\.net/(?\'bcpid\'\\d+)/.*?videoId=(?\'bctid\'\\d+)@'],'homepage'=>'https://www.brightcove.com/','host'=>['link.brightcove.com','players.brightcove.net'],'iframe'=>['src'=>'https://<xsl:choose><xsl:when test="@bckey">link.brightcove.com/services/player/bcpid<xsl:value-of select="@bcpid"/>?bckey=<xsl:value-of select="@bckey"/>&amp;bctid=<xsl:value-of select="@bctid"/>&amp;secureConnections=true&amp;secureHTMLConnections=true&amp;autoStart=false&amp;height=360&amp;width=640</xsl:when><xsl:otherwise>players.brightcove.net/<xsl:value-of select="@bcpid"/>/default_default/index.html?videoId=<xsl:value-of select="@bctid"/></xsl:otherwise></xsl:choose>'],'name'=>'Brightcove','scrape'=>[],'tags'=>['videos']],
'bunny'=>['attributes'=>[],'example'=>'https://video.bunnycdn.com/play/759/eb1c4f77-0cda-46be-b47d-1118ad7c2ffe','extract'=>['@/(?:embed|play)/(?\'video_library_id\'\\d+)/(?\'video_id\'[-\\w]+)@'],'homepage'=>'https://bunny.net/stream/','host'=>['iframe.mediadelivery.net','video.bunnycdn.com'],'iframe'=>['src'=>'https://iframe.mediadelivery.net/embed/{@video_library_id}/{@video_id}?autoplay=false'],'name'=>'Bunny Stream','scrape'=>[],'tags'=>['videos']],
'captivate'=>['attributes'=>[],'example'=>['https://player.captivate.fm/episode/03f47eef-4a8f-4616-8922-c77cb3d1edfa','https://decoding-the-gurus.captivate.fm/episode/sam-harris'],'extract'=>['@//player\\.captivate\\.fm/episode/(?\'id\'[-\\w]+)(?:\\?t=(?\'t\'\\d+))?@'],'homepage'=>'https://www.captivate.fm/','host'=>['captivate.fm'],'iframe'=>['height'=>200,'max-width'=>900,'src'=>'https://player.captivate.fm/episode/{@id}?t={@t}','style'=>['border-radius'=>'6px'],'width'=>'100%'],'name'=>'Captivate','scrape'=>[['extract'=>['@//player\\.captivate\\.fm/episode/(?\'id\'[-\\w]+)@'],'match'=>['@//(?!player\\.)[-\\w]+\\.captivate\\.fm/episode/.@']]],'tags'=>['podcasts']],
Expand Down
37 changes: 37 additions & 0 deletions src/Plugins/MediaEmbed/Configurator/SiteHelpers/BlueskyHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types=1);

/**
* @package s9e\TextFormatter
* @copyright Copyright (c) The s9e authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Plugins\MediaEmbed\Configurator\SiteHelpers;

use s9e\RegexpBuilder\Builder;

class BlueskyHelper extends AbstractConfigurableHostHelper
{
protected Builder $builder;

public function addHosts(array $hosts): void
{
parent::addHosts($hosts);

if (!isset($this->builder))
{
$this->builder = new Builder;
}

$siteId = $this->getSiteId();
$hosts = $this->getHosts();

$this->configurator->tags[$siteId]->attributes['embedder']->filterChain[0]->setRegexp(
'/^(?:[-\w]*\.)*' . $this->builder->build($hosts) . '$/'
);
}

protected function getSiteId(): string
{
return 'bluesky';
}
}
35 changes: 35 additions & 0 deletions src/Plugins/MediaEmbed/Configurator/sites/bluesky.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<site name="Bluesky" homepage="https://bsky.app/" helper="s9e\TextFormatter\Plugins\MediaEmbed\Configurator\SiteHelpers\BlueskyHelper">
<source>https://embed.bsky.app/</source>
<example>https://bsky.app/profile/bsky.app/post/3kkrqzuydho2v</example>
<example>https://bsky.app/profile/bnewbold.net/post/3kxjq2auebs2f</example>
<!-- https://web.syu.is/profile/syui.syu.is/post/3krfuqkpfns27 -->

<tags><tag>social</tag></tags>

<attributes>
<embedder required="true">
<filterChain>#regexp(/^(?:[-\w]*\.)*bsky\.app$/)</filterChain>
</embedder>
<url required="true">
<filterChain>urldecode</filterChain>
<filterChain>#regexp(/^at:\/\/[.:\w]+\/[.\w]+\/\w+$/)</filterChain>
</url>
</attributes>

<!-- https://atproto.com/specs/at-uri-scheme -->
<host>bsky.app</host>
<extract>#https://(?'embedder'[.\w]+)/oembed.*?url=(?'url'[\w%.]+)#</extract>
<scrape>
<match>#/profile/[^/]+/post/.#</match>
<extract>#https://(?'embedder'[.\w]+)/oembed.*?url=(?'url'[\w%.]+)#</extract>
</scrape>

<iframe width="600" height="600" onload="let c=new MessageChannel;c.port1.onmessage=e=&gt;this.style.height=e.data+'px';this.contentWindow.postMessage('s9e:init','*',[c.port2])" data-s9e-livepreview-ignore-attrs="style">
<src><![CDATA[
<xsl:text>https://s9e.github.io/iframe/2/bluesky.min.html#</xsl:text>
<xsl:value-of select="@url"/>
<xsl:text>#</xsl:text>
<xsl:value-of select="@embedder"/>
]]></src>
</iframe>
</site>
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php declare(strict_types=1);

namespace s9e\TextFormatter\Tests\Plugins\MediaEmbed\Configurator\SiteHelpers;

use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\SiteHelpers\BlueskyHelper;
use s9e\TextFormatter\Tests\Test;

/**
* @covers s9e\TextFormatter\Plugins\MediaEmbed\Configurator\SiteHelpers\AbstractConfigurableHostHelper
* @covers s9e\TextFormatter\Plugins\MediaEmbed\Configurator\SiteHelpers\AbstractSiteHelper
* @covers s9e\TextFormatter\Plugins\MediaEmbed\Configurator\SiteHelpers\BlueskyHelper
*/
class BlueskyHelperTest extends Test
{
/**
* @testdox addHost() normalizes the host
*/
public function testAddHostNormalize()
{
$this->configurator->MediaEmbed->add('bluesky');

$blueskyHelper = new BlueskyHelper($this->configurator);
$blueskyHelper->addHost('BLUESKY.LOCAL');

$parser = $this->getParser();
$actual = $parser->parse('https://embed.bluesky.local/oembed?format=json&url=at%3A%2F%2Fdid%3Aplc%3Az72i7hdynmk6r22z27h6tvur%2Fapp.bsky.feed.post%2F3kkrqzuydho2v');
$expected = '<r><BLUESKY embedder="embed.bluesky.local" url="at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3kkrqzuydho2v">https://embed.bluesky.local/oembed?format=json&amp;url=at%3A%2F%2Fdid%3Aplc%3Az72i7hdynmk6r22z27h6tvur%2Fapp.bsky.feed.post%2F3kkrqzuydho2v</BLUESKY></r>';

$this->assertXmlStringEqualsXmlString($expected, $actual);
}

/**
* @testdox addHost() adds the Bluesky media site if it's not enabled yet
*/
public function testAddHostCreate()
{
$blueskyHelper = new BlueskyHelper($this->configurator);
$blueskyHelper->addHost('BLUESKY.LOCAL');

$parser = $this->getParser();
$actual = $parser->parse('https://embed.bluesky.local/oembed?format=json&url=at%3A%2F%2Fdid%3Aplc%3Az72i7hdynmk6r22z27h6tvur%2Fapp.bsky.feed.post%2F3kkrqzuydho2v');
$expected = '<r><BLUESKY embedder="embed.bluesky.local" url="at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3kkrqzuydho2v">https://embed.bluesky.local/oembed?format=json&amp;url=at%3A%2F%2Fdid%3Aplc%3Az72i7hdynmk6r22z27h6tvur%2Fapp.bsky.feed.post%2F3kkrqzuydho2v</BLUESKY></r>';

$this->assertXmlStringEqualsXmlString($expected, $actual);
}

/**
* @testdox addHost() updates the embedded regexp
*/
public function testAddHostRegexp()
{
$blueskyHelper = new BlueskyHelper($this->configurator);
$blueskyHelper->addHost('BLUESKY.LOCAL');

$parser = $this->getParser();
$actual = $parser->parse('https://embed.bluesky.local/oembed?format=json&url=at%3A%2F%2Fdid%3Aplc%3Az72i7hdynmk6r22z27h6tvur%2Fapp.bsky.feed.post%2F3kkrqzuydho2v');
$expected = '<r><BLUESKY embedder="embed.bluesky.local" url="at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3kkrqzuydho2v">https://embed.bluesky.local/oembed?format=json&amp;url=at%3A%2F%2Fdid%3Aplc%3Az72i7hdynmk6r22z27h6tvur%2Fapp.bsky.feed.post%2F3kkrqzuydho2v</BLUESKY></r>';

$actual = $parser->parse('https://embed.bluesky.local.evil/oembed?format=json&url=at%3A%2F%2Fdid%3Aplc%3Az72i7hdynmk6r22z27h6tvur%2Fapp.bsky.feed.post%2F3kkrqzuydho2v');
$expected = '<t>https://embed.bluesky.local.evil/oembed?format=json&amp;url=at%3A%2F%2Fdid%3Aplc%3Az72i7hdynmk6r22z27h6tvur%2Fapp.bsky.feed.post%2F3kkrqzuydho2v</t>';

$this->assertXmlStringEqualsXmlString($expected, $actual);
}
}
20 changes: 20 additions & 0 deletions tests/Plugins/MediaEmbed/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,26 @@ function ($configurator)
$configurator->MediaEmbed->add('bandcamp');
}
],
[
'https://bsky.app/profile/bsky.app/post/3kkrqzuydho2v',
'<r><BLUESKY embedder="embed.bsky.app" url="at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3kkrqzuydho2v">https://bsky.app/profile/bsky.app/post/3kkrqzuydho2v</BLUESKY></r>',
[],
function ($configurator)
{
$configurator->registeredVars['cacheDir'] = __DIR__ . '/../../.cache';
$configurator->MediaEmbed->add('bluesky');
}
],
[
'https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur/post/3kkrqzuydho2v',
'<r><BLUESKY embedder="embed.bsky.app" url="at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3kkrqzuydho2v">https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur/post/3kkrqzuydho2v</BLUESKY></r>',
[],
function ($configurator)
{
$configurator->registeredVars['cacheDir'] = __DIR__ . '/../../.cache';
$configurator->MediaEmbed->add('bluesky');
}
],
[
'https://decoding-the-gurus.captivate.fm/episode/sam-harris',
'<r><CAPTIVATE id="ec119d4f-acc4-464a-8976-1fafc3e2d23b">https://decoding-the-gurus.captivate.fm/episode/sam-harris</CAPTIVATE></r>',
Expand Down

0 comments on commit 922ca9e

Please sign in to comment.