From a62235e925d810ce39059784ff7db7594517b22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20So=C5=9Bnierz?= Date: Fri, 22 Apr 2022 14:37:40 +0200 Subject: [PATCH] Support removing feed connections, deduplicate feed URLs --- src/ConnectionManager.ts | 1 + src/Connections/FeedConnection.ts | 10 ++++--- src/feeds/FeedReader.ts | 44 ++++++++++++++++++++++++++----- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/ConnectionManager.ts b/src/ConnectionManager.ts index 271a4a5d..ffad6e14 100644 --- a/src/ConnectionManager.ts +++ b/src/ConnectionManager.ts @@ -426,6 +426,7 @@ export class ConnectionManager extends EventEmitter { } this.connections.splice(connectionIndex, 1); Metrics.connections.set(this.connections.length); + this.emit('connection-removed', connection); } /** diff --git a/src/Connections/FeedConnection.ts b/src/Connections/FeedConnection.ts index fe40f0b4..362501ad 100644 --- a/src/Connections/FeedConnection.ts +++ b/src/Connections/FeedConnection.ts @@ -3,7 +3,6 @@ import { IConnection, IConnectionState } from "."; import { BridgeConfigFeeds } from "../Config/Config"; import { FeedEntry, FeedError} from "../feeds/FeedReader"; import LogWrapper from "../LogWrapper"; -import { GetConnectionsResponseItem } from "../provisioning/api"; import { IBridgeStorageProvider } from "../Stores/StorageProvider"; import { BaseConnection } from "./BaseConnection"; import markdown from "markdown-it"; @@ -33,11 +32,11 @@ export class FeedConnection extends BaseConnection implements IConnection { private readonly storage: IBridgeStorageProvider ) { super(roomId, stateKey, FeedConnection.CanonicalEventType) - log.info(`FeedConnection created for ${roomId}, ${JSON.stringify(state)}`); + log.info(`Connection ${this.connectionId} created for ${roomId}, ${JSON.stringify(state)}`); } public isInterestedInStateEvent(eventType: string, stateKey: string): boolean { - return false; + return !!FeedConnection.EventTypes.find(e => e === eventType) && stateKey === this.feedUrl; } public async handleFeedEntry(entry: FeedEntry): Promise { @@ -62,6 +61,11 @@ export class FeedConnection extends BaseConnection implements IConnection { } } + // needed to ensure that the connection is removable + public async onRemove(): Promise { + log.info(`Removing connection ${this.connectionId}`); + } + toString(): string { return `FeedConnection ${this.state.url}`; } diff --git a/src/feeds/FeedReader.ts b/src/feeds/FeedReader.ts index 3815e401..2aa66531 100644 --- a/src/feeds/FeedReader.ts +++ b/src/feeds/FeedReader.ts @@ -50,8 +50,16 @@ function stripHtml(input: string): string { return input.replace(/<[^>]*?>/g, ''); } +function normalizeUrl(input: string): string { + const url = new URL(input); + url.hash = ''; + return url.toString(); +} + export class FeedReader { - private observedFeedUrls: string[]; + private connections: FeedConnection[]; + // ts should notice that we do in fact initialize it in constructor, but it doesn't (in this version) + private observedFeedUrls: Set = new Set(); private seenEntries: Map = new Map(); static readonly seenEntriesEventType = "uk.half-shot.matrix-hookshot.feed.reader.seenEntries"; @@ -61,12 +69,21 @@ export class FeedReader { private queue: MessageQueue, private matrixClient: MatrixClient, ) { - const feedConnections = this.connectionManager.getAllConnectionsOfType(FeedConnection); - this.observedFeedUrls = feedConnections.map(c => c.feedUrl); + this.connections = this.connectionManager.getAllConnectionsOfType(FeedConnection); + this.calculateFeedUrls(); connectionManager.on('new-connection', c => { if (c instanceof FeedConnection) { - log.info('New connection tracked:', c.feedUrl); - this.observedFeedUrls.push(c.feedUrl); + log.info('New connection tracked:', c.connectionId); + this.connections.push(c); + this.calculateFeedUrls(); + } + }); + connectionManager.on('connection-removed', removed => { + if (removed instanceof FeedConnection) { + log.info('Connections before removal:', this.connections.map(c => c.connectionId)); + this.connections = this.connections.filter(c => c.connectionId !== removed.connectionId); + log.info('Connections after removal:', this.connections.map(c => c.connectionId)); + this.calculateFeedUrls(); } }); @@ -77,6 +94,19 @@ export class FeedReader { }); } + private calculateFeedUrls(): void { + // just in case we got an invalid URL somehow + const normalizedUrls = []; + for (const conn of this.connections) { + try { + normalizedUrls.push(normalizeUrl(conn.feedUrl)); + } catch (err: unknown) { + log.error(`Invalid feedUrl for connection ${conn.connectionId}: ${conn.feedUrl}. It will not be tracked`); + } + } + this.observedFeedUrls = new Set(normalizedUrls); + } + private async loadSeenEntries(): Promise { try { const accountData = await this.matrixClient.getAccountData(FeedReader.seenEntriesEventType).catch((err: any) => { @@ -108,13 +138,13 @@ export class FeedReader { } private async pollFeeds(): Promise { - log.debug(`Checking for updates in ${this.observedFeedUrls.length} RSS/Atom feeds`); + log.debug(`Checking for updates in ${this.observedFeedUrls.size} RSS/Atom feeds`); let seenEntriesChanged = false; const fetchingStarted = (new Date()).getTime(); - for (const url of this.observedFeedUrls) { + for (const url of this.observedFeedUrls.values()) { try { const res = await axios.get(url.toString()); const feed = await (new Parser()).parseString(res.data);