From dd8b9ff8fb3bc0a9fc56ac48bd080edd38f90ec2 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 26 Aug 2020 16:03:52 -0400 Subject: [PATCH] Add support for article dates in link previews. --- .../securesms/components/LinkPreviewView.java | 27 ++++++-- .../securesms/database/MmsDatabase.java | 4 +- .../securesms/jobs/PushProcessMessageJob.java | 2 +- .../securesms/jobs/PushSendJob.java | 2 +- .../securesms/linkpreview/LinkPreview.java | 15 ++++- .../linkpreview/LinkPreviewRepository.java | 23 ++++--- .../linkpreview/LinkPreviewUtil.java | 61 +++++++++++++++++-- app/src/main/res/layout/link_preview.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../api/SignalServiceMessageSender.java | 1 + .../api/messages/SignalServiceContent.java | 1 + .../messages/SignalServiceDataMessage.java | 8 ++- .../src/main/proto/SignalService.proto | 1 + 13 files changed, 124 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java b/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java index 4d17513af..86060b3e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/LinkPreviewView.java @@ -23,6 +23,10 @@ import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Locale; + import okhttp3.HttpUrl; /** @@ -146,14 +150,24 @@ public class LinkPreviewView extends FrameLayout { description.setVisibility(GONE); } + String domain = null; + if (!Util.isEmpty(linkPreview.getUrl())) { HttpUrl url = HttpUrl.parse(linkPreview.getUrl()); if (url != null) { - site.setText(url.topPrivateDomain()); - site.setVisibility(VISIBLE); - } else { - site.setVisibility(GONE); + domain = url.topPrivateDomain(); } + } + + if (domain != null && linkPreview.getDate() > 0) { + site.setText(getContext().getString(R.string.LinkPreviewView_domain_date, domain, formatDate(linkPreview.getDate()))); + site.setVisibility(VISIBLE); + } else if (domain != null) { + site.setText(domain); + site.setVisibility(VISIBLE); + } else if (linkPreview.getDate() > 0) { + site.setText(formatDate(linkPreview.getDate())); + site.setVisibility(VISIBLE); } else { site.setVisibility(GONE); } @@ -187,6 +201,11 @@ public class LinkPreviewView extends FrameLayout { : R.string.LinkPreviewView_no_link_preview_available; } + private static String formatDate(long date) { + DateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()); + return dateFormat.format(date); + } + public interface CloseClickedListener { void onCloseClicked(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index fe9f2c000..3af51c901 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -1118,7 +1118,7 @@ public class MmsDatabase extends MessageDatabase { if (preview.getAttachmentId() != null) { DatabaseAttachment attachment = attachmentIdMap.get(preview.getAttachmentId()); if (attachment != null) { - previews.add(new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), attachment)); + previews.add(new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), preview.getDate(), attachment)); } } else { previews.add(preview); @@ -1526,7 +1526,7 @@ public class MmsDatabase extends MessageDatabase { attachmentId = insertedAttachmentIds.get(preview.getThumbnail().get()); } - LinkPreview updatedPreview = new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), attachmentId); + LinkPreview updatedPreview = new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), preview.getDate(), attachmentId); linkPreviewJson.put(new JSONObject(updatedPreview.serialize())); } catch (JSONException | IOException e) { Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index 57d84b055..94b23d023 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -1696,7 +1696,7 @@ public final class PushProcessMessageJob extends BaseJob { boolean validDomain = url.isPresent() && LinkPreviewUtil.isValidPreviewUrl(url.get()); if (hasTitle && presentInBody && validDomain) { - LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), description.or(""), thumbnail); + LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), description.or(""), preview.getDate(), thumbnail); linkPreviews.add(linkPreview); } else { Log.w(TAG, String.format("Discarding an invalid link preview. hasTitle: %b presentInBody: %b validDomain: %b", hasTitle, presentInBody, validDomain)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index bec17f983..9185a1d5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -315,7 +315,7 @@ public abstract class PushSendJob extends SendJob { List getPreviewsFor(OutgoingMediaMessage mediaMessage) { return Stream.of(mediaMessage.getLinkPreviews()).map(lp -> { SignalServiceAttachment attachment = lp.getThumbnail().isPresent() ? getAttachmentPointerFor(lp.getThumbnail().get()) : null; - return new Preview(lp.getUrl(), lp.getTitle(), lp.getDescription(), Optional.fromNullable(attachment)); + return new Preview(lp.getUrl(), lp.getTitle(), lp.getDescription(), lp.getDate(), Optional.fromNullable(attachment)); }).toList(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreview.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreview.java index 7d1e5c735..c9268994e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreview.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreview.java @@ -25,24 +25,29 @@ public class LinkPreview { @JsonProperty private final String description; + @JsonProperty + private final long date; + @JsonProperty private final AttachmentId attachmentId; @JsonIgnore private final Optional thumbnail; - public LinkPreview(@NonNull String url, @NonNull String title, @NonNull String description, @NonNull DatabaseAttachment thumbnail) { + public LinkPreview(@NonNull String url, @NonNull String title, @NonNull String description, long date, @NonNull DatabaseAttachment thumbnail) { this.url = url; this.title = title; this.description = description; + this.date = date; this.thumbnail = Optional.of(thumbnail); this.attachmentId = thumbnail.getAttachmentId(); } - public LinkPreview(@NonNull String url, @NonNull String title, @NonNull String description, @NonNull Optional thumbnail) { + public LinkPreview(@NonNull String url, @NonNull String title, @NonNull String description, long date, @NonNull Optional thumbnail) { this.url = url; this.title = title; this.description = description; + this.date = date; this.thumbnail = thumbnail; this.attachmentId = null; } @@ -50,11 +55,13 @@ public class LinkPreview { public LinkPreview(@JsonProperty("url") @NonNull String url, @JsonProperty("title") @NonNull String title, @JsonProperty("description") @Nullable String description, + @JsonProperty("date") long date, @JsonProperty("attachmentId") @Nullable AttachmentId attachmentId) { this.url = url; this.title = title; this.description = Optional.fromNullable(description).or(""); + this.date = date; this.attachmentId = attachmentId; this.thumbnail = Optional.absent(); } @@ -71,6 +78,10 @@ public class LinkPreview { return description; } + public long getDate() { + return date; + } + public @NonNull Optional getThumbnail() { return thumbnail; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index a39abd17e..c9334df64 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -106,7 +106,7 @@ public class LinkPreviewRepository { } if (!metadata.getImageUrl().isPresent()) { - callback.onSuccess(new LinkPreview(url, metadata.getTitle().or(""), metadata.getDescription().or(""), Optional.absent())); + callback.onSuccess(new LinkPreview(url, metadata.getTitle().or(""), metadata.getDescription().or(""), metadata.getDate(), Optional.absent())); return; } @@ -114,7 +114,7 @@ public class LinkPreviewRepository { if (!metadata.getTitle().isPresent() && !attachment.isPresent()) { callback.onError(Error.PREVIEW_NOT_AVAILABLE); } else { - callback.onSuccess(new LinkPreview(url, metadata.getTitle().or(""), metadata.getDescription().or(""), attachment)); + callback.onSuccess(new LinkPreview(url, metadata.getTitle().or(""), metadata.getDescription().or(""), metadata.getDate(), attachment)); } }); @@ -153,13 +153,14 @@ public class LinkPreviewRepository { Optional title = openGraph.getTitle(); Optional description = openGraph.getDescription(); Optional imageUrl = openGraph.getImageUrl(); + long date = openGraph.getDate(); if (imageUrl.isPresent() && !LinkPreviewUtil.isValidPreviewUrl(imageUrl.get())) { Log.i(TAG, "Image URL was invalid or for a non-whitelisted domain. Skipping."); imageUrl = Optional.absent(); } - callback.accept(new Metadata(title, description, imageUrl)); + callback.accept(new Metadata(title, description, date, imageUrl)); } }); @@ -227,7 +228,7 @@ public class LinkPreviewRepository { Optional thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.WEBP, MediaUtil.IMAGE_WEBP); - callback.onSuccess(new LinkPreview(packUrl, title, "", thumbnail)); + callback.onSuccess(new LinkPreview(packUrl, title, "", 0, thumbnail)); } else { callback.onError(Error.PREVIEW_NOT_AVAILABLE); } @@ -272,7 +273,7 @@ public class LinkPreviewRepository { thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.WEBP, MediaUtil.IMAGE_WEBP); } - callback.onSuccess(new LinkPreview(groupUrl, title, description, thumbnail)); + callback.onSuccess(new LinkPreview(groupUrl, title, description, 0, thumbnail)); } else { Log.i(TAG, "Group is not locally available for preview generation, fetching from server"); @@ -289,7 +290,7 @@ public class LinkPreviewRepository { if (bitmap != null) bitmap.recycle(); } - callback.onSuccess(new LinkPreview(groupUrl, joinInfo.getTitle(), description, thumbnail)); + callback.onSuccess(new LinkPreview(groupUrl, joinInfo.getTitle(), description, 0, thumbnail)); } } catch (ExecutionException | InterruptedException | IOException | VerificationFailedException e) { Log.w(TAG, "Failed to fetch group link preview.", e); @@ -350,16 +351,18 @@ public class LinkPreviewRepository { private static class Metadata { private final Optional title; private final Optional description; + private final long date; private final Optional imageUrl; - Metadata(Optional title, Optional description, Optional imageUrl) { + Metadata(Optional title, Optional description, long date, Optional imageUrl) { this.title = title; this.description = description; + this.date = date; this.imageUrl = imageUrl; } static Metadata empty() { - return new Metadata(Optional.absent(), Optional.absent(), Optional.absent()); + return new Metadata(Optional.absent(), Optional.absent(), 0, Optional.absent()); } Optional getTitle() { @@ -370,6 +373,10 @@ public class LinkPreviewRepository { return description; } + long getDate() { + return date; + } + Optional getImageUrl() { return imageUrl; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java index caf615545..eb932b0b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java @@ -13,14 +13,19 @@ import android.text.util.Linkify; import com.annimon.stream.Stream; import com.google.android.collect.Sets; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.stickers.StickerUrl; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.util.OptionalUtil; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; @@ -30,10 +35,13 @@ import okhttp3.HttpUrl; public final class LinkPreviewUtil { + private static final String TAG = Log.tag(LinkPreviewUtil.class); + private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$"); private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$"); private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$"); private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>"); + private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>"); private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\""); private static final Pattern TITLE_PATTERN = Pattern.compile("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>"); private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>"); @@ -112,7 +120,22 @@ public final class LinkPreviewUtil { Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag); if (contentMatcher.find() && contentMatcher.groupCount() > 0) { String content = htmlDecoder.fromEncoded(contentMatcher.group(1)); - openGraphTags.put(property, content); + openGraphTags.put(property.toLowerCase(), content); + } + } + } + + Matcher articleMatcher = ARTICLE_TAG_PATTERN.matcher(html); + + while (articleMatcher.find()) { + String tag = articleMatcher.group(); + String property = articleMatcher.groupCount() > 0 ? articleMatcher.group(1) : null; + + if (property != null) { + Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag); + if (contentMatcher.find() && contentMatcher.groupCount() > 0) { + String content = htmlDecoder.fromEncoded(contentMatcher.group(1)); + openGraphTags.put(property.toLowerCase(), content); } } } @@ -154,9 +177,13 @@ public final class LinkPreviewUtil { private final @Nullable String htmlTitle; private final @Nullable String faviconUrl; - private static final String KEY_TITLE = "title"; - private static final String KEY_DESCRIPTION_URL = "description"; - private static final String KEY_IMAGE_URL = "image"; + private static final String KEY_TITLE = "title"; + private static final String KEY_DESCRIPTION_URL = "description"; + private static final String KEY_IMAGE_URL = "image"; + private static final String KEY_PUBLISHED_TIME_1 = "published_time"; + private static final String KEY_PUBLISHED_TIME_2 = "article:published_time"; + private static final String KEY_MODIFIED_TIME_1 = "modified_time"; + private static final String KEY_MODIFIED_TIME_2 = "article:modified_time"; public OpenGraph(@NonNull Map values, @Nullable String htmlTitle, @Nullable String faviconUrl) { this.values = values; @@ -172,9 +199,35 @@ public final class LinkPreviewUtil { return OptionalUtil.absentIfEmpty(Util.getFirstNonEmpty(values.get(KEY_IMAGE_URL), faviconUrl)); } + public long getDate() { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); + + return Stream.of(values.get(KEY_PUBLISHED_TIME_1), + values.get(KEY_PUBLISHED_TIME_2), + values.get(KEY_MODIFIED_TIME_1), + values.get(KEY_MODIFIED_TIME_2)) + .map(dateString -> parseDate(format, dateString)) + .filter(time -> time > 0) + .findFirst() + .orElse(0L); + } + public @NonNull Optional getDescription() { return OptionalUtil.absentIfEmpty(values.get(KEY_DESCRIPTION_URL)); } + + private static long parseDate(DateFormat dateFormat, String dateString) { + if (Util.isEmpty(dateString)) { + return 0; + } + + try { + return dateFormat.parse(dateString).getTime(); + } catch (ParseException e) { + Log.w(TAG, "Failed to parse date.", e); + return 0; + } + } } public interface HtmlDecoder { diff --git a/app/src/main/res/layout/link_preview.xml b/app/src/main/res/layout/link_preview.xml index da8e9868e..a1dc67471 100644 --- a/app/src/main/res/layout/link_preview.xml +++ b/app/src/main/res/layout/link_preview.xml @@ -66,6 +66,7 @@ android:layout_marginStart="8dp" android:layout_marginTop="2dp" android:textColor="?linkpreview_secondary_text_color" + android:maxLines="2" app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail" app:layout_constraintTop_toBottomOf="@+id/linkpreview_description" tools:text="dailybugle.com" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2293e55c5..d604b0df8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -497,6 +497,7 @@ No link preview available This group link is not active + %1$s ยท %2$s diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 369ba6cb8..b52362459 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -629,6 +629,7 @@ public class SignalServiceMessageSender { DataMessage.Preview.Builder previewBuilder = DataMessage.Preview.newBuilder(); previewBuilder.setTitle(preview.getTitle()); previewBuilder.setDescription(preview.getDescription()); + previewBuilder.setDate(preview.getDate()); previewBuilder.setUrl(preview.getUrl()); if (preview.getImage().isPresent()) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index 1148364bb..aa36878a1 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -687,6 +687,7 @@ public final class SignalServiceContent { results.add(new SignalServiceDataMessage.Preview(preview.getUrl(), preview.getTitle(), preview.getDescription(), + preview.getDate(), Optional.fromNullable(attachment))); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java index b883b630d..874ec226e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java @@ -413,12 +413,14 @@ public class SignalServiceDataMessage { private final String url; private final String title; private final String description; + private final long date; private final Optional image; - public Preview(String url, String title, String description, Optional image) { + public Preview(String url, String title, String description, long date, Optional image) { this.url = url; this.title = title; this.description = description; + this.date = date; this.image = image; } @@ -434,6 +436,10 @@ public class SignalServiceDataMessage { return description; } + public long getDate() { + return date; + } + public Optional getImage() { return image; } diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index 282e7ebd4..06bbf608a 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -207,6 +207,7 @@ message DataMessage { optional string title = 2; optional AttachmentPointer image = 3; optional string description = 4; + optional uint64 date = 5; } message Sticker {