Skip to content

Commit

Permalink
feat: add 1px transparent image in RSS item descriptions for telemetr…
Browse files Browse the repository at this point in the history
…y content views
  • Loading branch information
guqing committed Dec 10, 2024
1 parent d55dd55 commit f375f8d
Show file tree
Hide file tree
Showing 13 changed files with 696 additions and 12 deletions.
45 changes: 45 additions & 0 deletions api/src/main/java/run/halo/feed/TelemetryEventInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package run.halo.feed;

import java.util.Objects;
import lombok.Data;
import lombok.Getter;
import lombok.experimental.Accessors;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.NonNull;

@Data
@Accessors(chain = true)
public class TelemetryEventInfo {
private String pageUrl;
private String screen;
private String language;
private String languageRegion;
private String title;
private String referrer;
private String ip;
private String userAgent;
private String browser;
private String os;

@Getter(onMethod_ = @NonNull)
private HttpHeaders headers;

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TelemetryEventInfo that = (TelemetryEventInfo) o;
return Objects.equals(pageUrl, that.pageUrl) && Objects.equals(title, that.title)
&& Objects.equals(referrer, that.referrer) && Objects.equals(ip, that.ip)
&& Objects.equals(userAgent, that.userAgent);
}

@Override
public int hashCode() {
return Objects.hash(pageUrl, title, referrer, ip, userAgent);
}
}
8 changes: 8 additions & 0 deletions api/src/main/java/run/halo/feed/TelemetryRecorder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package run.halo.feed;

import org.pf4j.ExtensionPoint;

public interface TelemetryRecorder extends ExtensionPoint {

void record(TelemetryEventInfo eventInfo);
}
19 changes: 10 additions & 9 deletions app/src/main/java/run/halo/feed/RssCacheManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,23 @@ public Mono<String> get(String key, Mono<RSS2> loader) {
}

private Mono<String> generateRssXml(Mono<RSS2> loader) {
var builder = new RssXmlBuilder();
var builder = new RssXmlBuilder()
.withGenerator("Halo v2.0");

var rssMono = loader.doOnNext(builder::withRss2);
var generatorMono = getRssGenerator()
.doOnNext(builder::withGenerator);

var generatorMono = systemInfoGetter.get()
.doOnNext(info -> builder.withExternalUrl(info.getUrl().toString())
.withGenerator("Halo v" + info.getVersion().toStableVersion().toString())
);

var extractTagsMono = BasicProp.getBasicProp(settingFetcher)
.doOnNext(prop -> builder.withExtractRssTags(prop.getRssExtraTags()));

return Mono.when(rssMono, generatorMono, extractTagsMono)
.then(Mono.fromSupplier(builder::toXmlString));
}

private Mono<String> getRssGenerator() {
return systemInfoGetter.get()
.map(info -> "Halo v" + info.getVersion().toStableVersion().toString())
.defaultIfEmpty("Halo v2.0");
}

@EventListener(PluginConfigUpdatedEvent.class)
public void onPluginConfigUpdated() {
cache.invalidateAll();
Expand Down
40 changes: 37 additions & 3 deletions app/src/main/java/run/halo/feed/RssXmlBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.common.base.Throwables;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
Expand All @@ -14,13 +15,17 @@
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import run.halo.feed.telemetry.TelemetryEndpoint;

@Slf4j
public class RssXmlBuilder {
private RSS2 rss2;
private String generator = "Halo v2.0";
private String extractRssTags;
private Instant lastBuildDate = Instant.now();
private String externalUrl;

public RssXmlBuilder withRss2(RSS2 rss2) {
this.rss2 = rss2;
Expand Down Expand Up @@ -48,6 +53,11 @@ RssXmlBuilder withLastBuildDate(Instant lastBuildDate) {
return this;
}

RssXmlBuilder withExternalUrl(String externalUrl) {
this.externalUrl = externalUrl;
return this;
}

public String toXmlString() {
Document document = DocumentHelper.createDocument();

Expand Down Expand Up @@ -127,18 +137,19 @@ private Element parseXmlString(String xml) throws DocumentException {
}
}

private static void createItemElementsToChannel(Element channel, List<RSS2.Item> items) {
private void createItemElementsToChannel(Element channel, List<RSS2.Item> items) {
if (CollectionUtils.isEmpty(items)) {
return;
}
items.forEach(item -> createItemElementToChannel(channel, item));
}

private static void createItemElementToChannel(Element channel, RSS2.Item item) {
private void createItemElementToChannel(Element channel, RSS2.Item item) {
Element itemElement = channel.addElement("item");
itemElement.addElement("title").addCDATA(item.getTitle());
itemElement.addElement("link").addText(item.getLink());
itemElement.addElement("description").addCDATA(item.getDescription());
var description = getDescriptionWithTelemetry(item);
itemElement.addElement("description").addCDATA(description);
itemElement.addElement("guid")
.addAttribute("isPermaLink", "false")
.addText(item.getGuid());
Expand Down Expand Up @@ -201,6 +212,29 @@ private static void createItemElementToChannel(Element channel, RSS2.Item item)
});
}

private String getDescriptionWithTelemetry(RSS2.Item item) {
if (StringUtils.isBlank(externalUrl)) {
return item.getDescription();
}
var uri = UriComponentsBuilder.fromUriString(item.getLink())
.build();
var telemetryBaseUri = externalUrl + TelemetryEndpoint.TELEMETRY_PATH;
var telemetryUri = UriComponentsBuilder.fromUriString(telemetryBaseUri)
.queryParam("title", UriUtils.encode(item.getTitle(), StandardCharsets.UTF_8))
.queryParam("url", uri.getPath())
.build(true)
.toUriString();

// Build the telemetry image HTML
var telemetryImageHtml = String.format(
"<img src=\"%s\" width=\"1\" height=\"1\" alt=\"\" style=\"opacity:0;\" />",
telemetryUri
);

// Append telemetry image to description
return telemetryImageHtml + item.getDescription();
}

static <T> List<T> nullSafeList(List<T> list) {
return list == null ? List.of() : list;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package run.halo.feed.telemetry;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.experimental.UtilityClass;
import org.springframework.lang.NonNull;

@UtilityClass
public class AcceptLanguageParser {

public record Language(String code, String script, String region, double quality) {
@Override
public String toString() {
return "Language{" +
"code='" + code + '\'' +
", script='" + script + '\'' +
", region='" + region + '\'' +
", quality=" + quality +
'}';
}
}

private static final Pattern REGEX = Pattern.compile(
"((([a-zA-Z]+(-[a-zA-Z0-9]+){0,2})|\\*)(;q=[0-1](\\.[0-9]+)?)?)*");

@NonNull
public static List<Language> parseAcceptLanguage(String acceptLanguage) {
if (acceptLanguage == null || acceptLanguage.isEmpty()) {
return Collections.emptyList();
}

List<Language> languages = new ArrayList<>();
Matcher matcher = REGEX.matcher(acceptLanguage);

while (matcher.find()) {
String match = matcher.group();
if (match == null || match.isEmpty()) {
continue;
}

String[] parts = match.split(";");
String ietfTag = parts[0];
String[] ietfComponents = ietfTag.split("-");
String code = ietfComponents[0];
String script = ietfComponents.length == 3 ? ietfComponents[1] : null;
String region = ietfComponents.length == 3 ? ietfComponents[2]
: ietfComponents.length == 2 ? ietfComponents[1] : null;

double quality = 1.0;
if (parts.length > 1 && parts[1].startsWith("q=")) {
try {
quality = Double.parseDouble(parts[1].substring(2));
} catch (NumberFormatException e) {
// ignore
}
}

languages.add(new Language(code, script, region, quality));
}

return languages.stream()
.filter(Objects::nonNull)
.sorted(Comparator.comparingDouble((Language l) -> l.quality).reversed())
.collect(Collectors.toList());
}
}
Loading

0 comments on commit f375f8d

Please sign in to comment.