Example #1
0
    async def announce_post(self, post: Post) -> Optional[Announcement]:
        path = post.path.parent / "webmentions.yaml"

        tasks = []
        with update_yaml(path) as webmentions:
            async with ClientSession() as session:
                for target in extract_links(post):
                    for builder in self.builders:
                        source = builder.absolute_url(post)
                        task = asyncio.create_task(
                            collect_webmentions(session, source, target,
                                                webmentions), )
                        tasks.append(task)

                await asyncio.gather(*tasks)

        if any(webmention["status"] == "queue"
               for webmention in webmentions.values()):
            # return nothing so we can check queued webmentions on next publish
            return None

        return Announcement(
            url=post.url,
            timestamp=datetime.now(timezone.utc),
        )
Example #2
0
 async def announce_site(self) -> Optional[Announcement]:
     urls = [builder.home for builder in self.builders]
     saved_urls = await archive_urls(urls)
     if saved_urls:
         return Announcement(
             url="https://web.archive.org/save/",
             timestamp=datetime.now(timezone.utc),
         )
     return None
Example #3
0
 async def announce_post(self, post: Post) -> Optional[Announcement]:
     urls = [builder.absolute_url(post) for builder in self.builders]
     saved_urls = await archive_urls(urls)
     if saved_urls:
         return Announcement(
             url="https://web.archive.org/save/",
             timestamp=datetime.now(timezone.utc),
         )
     return None
async def test_announcer_announce_post(
    mocker: MockerFixture,
    root: Path,
    config: Config,
    post: Post,
) -> None:
    """
    Test the announcer saving a post.
    """
    gemini_builder = Builder(root, config, "gemini://example.com/", "gemini")
    gemini_builder.extension = ".gmi"
    html_builder = Builder(root, config, "https://example.com/", "www")
    html_builder.extension = ".html"

    mocker.patch(
        "nefelibata.announcers.archive_blog.archive_urls",
        return_value={},
    )

    announcer = ArchiveBlogAnnouncer(
        root,
        config,
        builders=[gemini_builder, html_builder],
    )

    announcement = await announcer.announce_post(post)
    assert announcement is None

    archive_urls = mocker.patch(
        "nefelibata.announcers.archive_blog.archive_urls",
        return_value={
            URL("https://nefelibata.readthedocs.io/"):
            URL(
                "https://web.archive.org/web/20211003154602/https://nefelibata.readthedocs.io/",
            ),
        },
    )

    with freeze_time("2021-01-01T00:00:00Z"):
        announcement = await announcer.announce_post(post)
    assert announcement == Announcement(
        url="https://web.archive.org/save/",
        timestamp=datetime(2021, 1, 1, 0, 0, tzinfo=timezone.utc),
        grace_seconds=0,
    )

    archive_urls.assert_called_with([
        URL("gemini://example.com/first/index.gmi"),
        URL("https://example.com/first/index.html"),
    ], )
Example #5
0
    async def announce_post(self, post: Post) -> Optional[Announcement]:
        valid_enclosures = [
            enclosure
            for enclosure in post.enclosures
            if enclosure.type in VALID_MIMETYPES
        ]
        if len(valid_enclosures) > MAX_NUMBER_OF_ENCLOSURES:
            _logger.warning(
                "Found more than %d media enclosures in post %s. Only the first "
                "%d will be uploaded.",
                MAX_NUMBER_OF_ENCLOSURES,
                post.path,
                MAX_NUMBER_OF_ENCLOSURES,
            )

        media_ids = []
        for enclosure in valid_enclosures[:MAX_NUMBER_OF_ENCLOSURES]:
            _logger.info("Uploading post enclosure %s", enclosure.path)

            media_dict = self.client.media_post(
                str(enclosure.path),
                enclosure.type,
                enclosure.description,
            )
            media_ids.append(media_dict)

        language = post.metadata.get("language") or self.config.language
        summary = post.metadata.get("summary") or post.title
        urls = "\n".join(
            builder.absolute_url(post).human_repr() for builder in self.builders
        )
        status = f"{summary}\n\n{urls}"

        _logger.info("Announcing post %s on Mastodon", post.path)
        toot_dict = self.client.status_post(
            status=status,
            visibility="public",
            media_ids=media_ids,
            language=language,
            idempotency_key=str(post.path),
        )

        return Announcement(
            url=toot_dict.url,
            timestamp=toot_dict.created_at,
        )
Example #6
0
    async def announce_site(self) -> Optional[Announcement]:
        """
        Add capsule to Geminispace.
        """
        for builder in self.builders:
            if builder.home.scheme != "gemini":
                raise Exception(
                    "Geminispace announcer only works with `gemini://` builds",
                )

            capsule_url = urllib.parse.quote_plus(str(builder.home))
            url = f"gemini://geminispace.info/add-seed?{capsule_url}"

            _logger.info("Announcing capsule %s to Geminispace", capsule_url)
            await self.client.get(URL(url))

        return Announcement(
            url="gemini://geminispace.info/",
            timestamp=datetime.now(timezone.utc),
            grace_seconds=timedelta(days=365).total_seconds(),
        )
Example #7
0
    async def announce_site(self) -> Optional[Announcement]:
        """
        Send the link.
        """
        for builder in self.builders:
            if builder.home.scheme != "gemini":
                raise Exception(
                    f"{self.name} announcer only works with `gemini://` builds",
                )

            feed_url = urllib.parse.quote_plus(
                f"{builder.home}/feed{builder.extension}", )
            url = self.submit_url + feed_url

            self.logger.info("Announcing feed %s to %s", feed_url, self.name)
            await self.client.get(URL(url))

        return Announcement(
            url=self.url,
            timestamp=datetime.now(timezone.utc),
            grace_seconds=self.grace_seconds,
        )
Example #8
0
async def test_run(
    mocker: MockerFixture,
    root: Path,
    config: Config,
    post: Post,
) -> None:
    """
    Test ``publish``.
    """
    publisher = mocker.MagicMock()
    publisher.publish = mocker.AsyncMock(side_effect=[
        Publishing(timestamp=datetime(2021, 1, 1)),
        None,
        Publishing(timestamp=datetime(2021, 1, 3)),
    ], )

    announcer = mocker.MagicMock()
    announcer.announce_post = mocker.AsyncMock(side_effect=[
        None,
        Announcement(
            url="https://host1.example.com/",
            timestamp=datetime(2021, 1, 1),
        ),
    ], )
    announcer.announce_site = mocker.AsyncMock(side_effect=[
        Announcement(
            url="https://host2.example.com/",
            timestamp=datetime(2021, 1, 2),
        ),
        None,
    ], )

    mocker.patch(
        "nefelibata.cli.publish.get_announcers",
        return_value={"announcer": announcer},
    )
    mocker.patch(
        "nefelibata.cli.publish.get_publishers",
        return_value={"publisher": publisher},
    )

    # On the first publish we should announce site and post.
    await publish.run(root)
    publisher.publish.assert_called_with(None, False)
    announcer.announce_site.assert_called_with()
    announcer.announce_post.assert_called()

    # Publish again, should have a ``since`` value. Because ``publish``
    # returns ``None`` the second time we shouldn't announce the site.
    # And because the first time ``publish_post`` returned ``None``,
    # it should try again now and be called.
    announcer.announce_site.reset_mock()
    announcer.announce_post.reset_mock()
    await publish.run(root)
    publisher.publish.assert_called_with(datetime(2021, 1, 1), False)
    announcer.announce_site.assert_not_called()
    announcer.announce_post.assert_called()

    # Publish again. This time ``publish`` returns a new data, so we
    # expect to call ``publish_site``. Because the post was already
    # published last time it shouldn't be announced this time.
    announcer.announce_post.reset_mock()
    await publish.run(root)
    publisher.publish.assert_called_with(datetime(2021, 1, 1), False)
    announcer.announce_site.assert_called_with()
    announcer.announce_post.assert_not_called()
async def test_announcer_announce(
    mocker: MockerFixture,
    root: Path,
    config: Config,
    post: Post,
) -> None:
    """
    Test announcing posts.
    """
    gemini_builder = Builder(root, config, "gemini://example.com/", "gemini")
    gemini_builder.extension = ".gmi"
    html_builder = Builder(root, config, "https://example.com/", "www")
    html_builder.extension = ".html"

    send_webmention = mocker.AsyncMock(side_effect=[
        Webmention(
            source="gemini://example.com/first/index.html",
            target="https://nefelibata.readthedocs.io/",
            status="invalid",
        ),
        Webmention(
            source="https://example.com/first/index.html",
            target="https://nefelibata.readthedocs.io/",
            status="queue",
            location="https://bob.example.com/webmention.php?id=42",
        ),
    ], )
    mocker.patch("nefelibata.announcers.webmention.send_webmention",
                 send_webmention)

    announcer = WebmentionAnnouncer(
        root,
        config,
        builders=[gemini_builder, html_builder],
    )

    announcement = await announcer.announce_post(post)
    assert announcement is None

    path = post.path.parent / "webmentions.yaml"
    with open(path, encoding="utf-8") as input_:
        webmentions = yaml.load(input_, Loader=yaml.SafeLoader)
    assert webmentions == {
        "gemini://example.com/first/index.gmi => https://nefelibata.readthedocs.io/":
        {
            "location": None,
            "source": "gemini://example.com/first/index.html",
            "status": "invalid",
            "target": "https://nefelibata.readthedocs.io/",
        },
        "https://example.com/first/index.html => https://nefelibata.readthedocs.io/":
        {
            "location": "https://bob.example.com/webmention.php?id=42",
            "source": "https://example.com/first/index.html",
            "status": "queue",
            "target": "https://nefelibata.readthedocs.io/",
        },
    }

    update_webmention = mocker.AsyncMock(return_value=Webmention(
        source="https://example.com/first/index.html",
        target="https://nefelibata.readthedocs.io/",
        status="success",
        location=None,
    ), )
    mocker.patch(
        "nefelibata.announcers.webmention.update_webmention",
        update_webmention,
    )

    with freeze_time("2021-01-01T00:00:00Z"):
        announcement = await announcer.announce_post(post)
    assert announcement == Announcement(
        url="first/index",
        timestamp=datetime(2021, 1, 1, tzinfo=timezone.utc),
        grace_seconds=0,
    )

    path = post.path.parent / "webmentions.yaml"
    with open(path, encoding="utf-8") as input_:
        webmentions = yaml.load(input_, Loader=yaml.SafeLoader)
    assert webmentions == {
        "gemini://example.com/first/index.gmi => https://nefelibata.readthedocs.io/":
        {
            "location": None,
            "source": "gemini://example.com/first/index.html",
            "status": "invalid",
            "target": "https://nefelibata.readthedocs.io/",
        },
        "https://example.com/first/index.html => https://nefelibata.readthedocs.io/":
        {
            "location": None,
            "source": "https://example.com/first/index.html",
            "status": "success",
            "target": "https://nefelibata.readthedocs.io/",
        },
    }