mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 00:29:11 +01:00
Add support for article dates in link previews.
This commit is contained in:
committed by
Alan Evans
parent
bfed03b7b5
commit
dd8b9ff8fb
@@ -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<Attachment> 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<Attachment> thumbnail) {
|
||||
public LinkPreview(@NonNull String url, @NonNull String title, @NonNull String description, long date, @NonNull Optional<Attachment> 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<Attachment> getThumbnail() {
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
@@ -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<String> title = openGraph.getTitle();
|
||||
Optional<String> description = openGraph.getDescription();
|
||||
Optional<String> 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<Attachment> 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<String> title;
|
||||
private final Optional<String> description;
|
||||
private final long date;
|
||||
private final Optional<String> imageUrl;
|
||||
|
||||
Metadata(Optional<String> title, Optional<String> description, Optional<String> imageUrl) {
|
||||
Metadata(Optional<String> title, Optional<String> description, long date, Optional<String> 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<String> getTitle() {
|
||||
@@ -370,6 +373,10 @@ public class LinkPreviewRepository {
|
||||
return description;
|
||||
}
|
||||
|
||||
long getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
Optional<String> getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
@@ -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<String, String> 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<String> 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 {
|
||||
|
||||
Reference in New Issue
Block a user