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), )
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
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"), ], )
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, )
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(), )
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, )
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/", }, }