Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions app/lib/frontend/handlers/atom_feed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import 'dart:async';
import 'dart:convert';

import 'package:clock/clock.dart';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:pub_dev/shared/changelog.dart';
import 'package:shelf/shelf.dart' as shelf;

import '../../admin/actions/actions.dart';
Expand Down Expand Up @@ -53,7 +55,9 @@ Future<shelf.Response> packageAtomFeedhandler(
Future<String> buildAllPackagesAtomFeedContent() async {
final versions = await packageBackend.latestPackageVersions(limit: 100);
versions.removeWhere((pv) => pv.isNotVisible || pv.isRetracted);
final feed = _allPackagesFeed(versions);
final contents = await Future.wait(
versions.map((v) => _getChangelogReleaseContent(v.package, v.version!)));
final feed = _allPackagesFeed(versions, contents);
return feed.toXmlDocument();
}

Expand All @@ -70,10 +74,34 @@ Future<String> buildPackageAtomFeedContent(String package) async {
)
.toList();
versions.removeWhere((pv) => pv.isNotVisible || pv.isRetracted);
final feed = _packageFeed(package, versions);
final contents = await Future.wait(
versions.map((v) => _getChangelogReleaseContent(package, v.version!)));
final feed = _packageFeed(package, versions, contents);
return feed.toXmlDocument();
}

Future<String> _getChangelogReleaseContent(
String package, String version) async {
final content = await cache
.changelogReleaseContentAsMarkdown(package, version)
.get(() async {
final asset = await packageBackend.lookupPackageVersionAsset(
package, version, AssetKind.changelog);
final content = asset?.textContent;
if (content == null) {
return '';
}
final parsed = ChangelogParser().parseMarkdownText(content);
final release =
parsed.releases.firstWhereOrNull((r) => r.version == version);
if (release == null) {
return '';
}
return release.content.asMarkdownText;
});
return content ?? '';
}

class FeedEntry {
final String id;
final String title;
Expand Down Expand Up @@ -181,7 +209,10 @@ class Feed {
}
}

Feed _allPackagesFeed(List<PackageVersion> versions) {
Feed _allPackagesFeed(
List<PackageVersion> versions,
List<String> releaseContents,
) {
final entries = <FeedEntry>[];
for (var i = 0; i < versions.length; i++) {
final version = versions[i];
Expand All @@ -195,7 +226,14 @@ Feed _allPackagesFeed(List<PackageVersion> versions) {
sha512.convert(utf8.encode('${version.package}/${version.version}'));
final id = createUuid(hash.bytes.sublist(0, 16));
final title = 'v${version.version} of ${version.package}';
final content = version.ellipsizedDescription ?? '[no description]';
final fullReleaseContent = releaseContents[i];
final releaseContent = fullReleaseContent.length > 512
? '${fullReleaseContent.substring(0, 500)}[...]'
: fullReleaseContent;
final content = [
version.ellipsizedDescription ?? '[no description]',
if (releaseContent.isNotEmpty) 'Changelog excerpt:\n$releaseContent',
].join('\n\n');
entries.add(FeedEntry(
id: id,
title: title,
Expand All @@ -216,7 +254,11 @@ Feed _allPackagesFeed(List<PackageVersion> versions) {
);
}

Feed _packageFeed(String package, List<PackageVersion> versions) {
Feed _packageFeed(
String package,
List<PackageVersion> versions,
List<String> releaseContents,
) {
return Feed(
title: 'Recently published versions of package $package on pub.dev',
alternateUrl: activeConfiguration.primarySiteUri
Expand All @@ -227,7 +269,7 @@ Feed _packageFeed(String package, List<PackageVersion> versions) {
.resolve(urls.pkgFeedUrl(package))
.toString(),
author: versions.firstOrNull?.publisherId,
entries: versions.map((v) {
entries: versions.mapIndexed((i, v) {
final hash =
sha512.convert(utf8.encode('package-feed/$package/${v.version}'));
final id = createUuid(hash.bytes.sublist(0, 16));
Expand All @@ -238,13 +280,19 @@ Feed _packageFeed(String package, List<PackageVersion> versions) {
version: v.version,
))
.toString();
final fullReleaseContent = releaseContents[i];
final releaseContent = fullReleaseContent.length > 1024
? '${fullReleaseContent.substring(0, 1000)}[...]'
: fullReleaseContent;
return FeedEntry(
id: id,
title: 'v${v.version} of $package',
alternateUrl: alternateUrl,
alternateTitle: v.version,
content:
'${v.version} was published on ${shortDateFormat.format(v.created!)}.',
content: [
'${v.version} was published on ${shortDateFormat.format(v.created!)}.',
if (releaseContent.isNotEmpty) 'Changelog excerpt:\n$releaseContent',
].join('\n\n'),
updated: v.created!,
);
}).toList(),
Expand Down
7 changes: 7 additions & 0 deletions app/lib/shared/redis_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ class CachePatterns {
decode: (d) => d as bool,
))[package];

Entry<String> changelogReleaseContentAsMarkdown(
String package, String version) =>
_cache
.withPrefix('changelog-release-content-md/')
.withTTL(Duration(hours: 1))
.withCodec(utf8)['$package-$version'];

Entry<List<int>> packageData(String package) => _cache
.withPrefix('api-package-data-by-uri/')
.withTTL(Duration(minutes: 10))['$package'];
Expand Down
8 changes: 5 additions & 3 deletions app/test/frontend/handlers/atom_feed_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,22 @@ void main() {
' <id>urn:uuid:a6a43bff-e1ef-4633-b5ee-e0516b655be9</id>\n'
' <title>v1.2.0 of oxygen</title>\n'
' <updated>(.*)</updated>\n'
' <content>oxygen is awesome</content>\n'
// Note: pretty format + indenting converts the newlines into spaces.
' <content>oxygen is awesome Changelog excerpt: - updated</content>\n'
' <link href="${activeConfiguration.primarySiteUri}/packages/oxygen" rel="alternate" title="oxygen"/>\n'
'</entry>');
expect(
oxygenExpr.hasMatch(entries[1].toXmlString(pretty: true, indent: ' ')),
isTrue,
reason: entries[1].toXmlString(),
reason: entries[1].toXmlString(pretty: true, indent: ' '),
);

final neonExpr = RegExp('<entry>\n'
' <id>urn:uuid:5f920595-c067-404a-bb19-2b0918372eb6</id>\n'
' <title>v1.0.0 of neon</title>\n'
' <updated>(.*)</updated>\n'
' <content>neon is awesome</content>\n'
// Note: pretty format + indenting converts the newlines into spaces.
' <content>neon is awesome Changelog excerpt: - updated</content>\n'
' <link href="${activeConfiguration.primarySiteUri}/packages/neon" rel="alternate" title="neon"/>\n'
'</entry>');
expect(
Expand Down