-
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: SeDemal <[email protected]>
- Loading branch information
Showing
23 changed files
with
528 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.scroll-area-w100 .mantine-ScrollArea-viewport > div:nth-of-type(1) { | ||
width: 100%; | ||
display: inherit !important; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { TRPCError } from "@trpc/server"; | ||
|
||
import { and, eq } from "@homarr/db"; | ||
import { items } from "@homarr/db/schema/sqlite"; | ||
import type { WidgetKind } from "@homarr/definitions"; | ||
import { z } from "@homarr/validation"; | ||
|
||
import { publicProcedure } from "../trpc"; | ||
|
||
export const createOneItemMiddleware = (kind: WidgetKind) => { | ||
return publicProcedure.input(z.object({ itemId: z.string() })).use(async ({ input, ctx, next }) => { | ||
const item = await ctx.db.query.items.findFirst({ | ||
where: and(eq(items.id, input.itemId), eq(items.kind, kind)), | ||
}); | ||
|
||
if (!item) { | ||
throw new TRPCError({ | ||
code: "NOT_FOUND", | ||
message: `Item with id ${input.itemId} not found`, | ||
}); | ||
} | ||
|
||
return next({ | ||
ctx: { | ||
item, | ||
}, | ||
}); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import type { RssFeed } from "@homarr/cron-jobs"; | ||
import { createItemChannel } from "@homarr/redis"; | ||
|
||
import { createOneItemMiddleware } from "../../middlewares/item"; | ||
import { createTRPCRouter, publicProcedure } from "../../trpc"; | ||
|
||
export const rssFeedRouter = createTRPCRouter({ | ||
getFeeds: publicProcedure.unstable_concat(createOneItemMiddleware("rssFeed")).query(async ({ input }) => { | ||
const channel = createItemChannel<RssFeed[]>(input.itemId); | ||
return await channel.getAsync(); | ||
}), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import type { FeedData, FeedEntry } from "@extractus/feed-extractor"; | ||
import { extract } from "@extractus/feed-extractor"; | ||
import SuperJSON from "superjson"; | ||
|
||
import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; | ||
import { db, eq } from "@homarr/db"; | ||
import { items } from "@homarr/db/schema/sqlite"; | ||
import { logger } from "@homarr/log"; | ||
import { createItemChannel } from "@homarr/redis"; | ||
import { z } from "@homarr/validation"; | ||
|
||
// This import is done that way to avoid circular dependencies. | ||
import type { WidgetComponentProps } from "../../../widgets"; | ||
import { createCronJob } from "../lib"; | ||
|
||
export const rssFeedsJob = createCronJob("rssFeeds", EVERY_5_MINUTES).withCallback(async () => { | ||
const itemsForIntegration = await db.query.items.findMany({ | ||
where: eq(items.kind, "rssFeed"), | ||
}); | ||
|
||
for (const item of itemsForIntegration) { | ||
const options = SuperJSON.parse<WidgetComponentProps<"rssFeed">["options"]>(item.options); | ||
|
||
const feeds = await Promise.all( | ||
options.feedUrls.map(async (feedUrl) => ({ | ||
feedUrl, | ||
feed: (await extract(feedUrl, { | ||
getExtraEntryFields: (feedEntry) => { | ||
const media = attemptGetImageFromEntry(feedUrl, feedEntry); | ||
if (!media) { | ||
return {}; | ||
} | ||
return { | ||
enclosure: media, | ||
}; | ||
}, | ||
})) as ExtendedFeedData, | ||
})), | ||
); | ||
|
||
const channel = createItemChannel<RssFeed[]>(item.id); | ||
await channel.publishAndUpdateLastStateAsync(feeds); | ||
} | ||
}); | ||
|
||
const attemptGetImageFromEntry = (feedUrl: string, entry: object) => { | ||
const media = getFirstMediaProperty(entry); | ||
if (media !== null) { | ||
return media; | ||
} | ||
return getImageFromStringAsFallback(feedUrl, JSON.stringify(entry)); | ||
}; | ||
|
||
const getImageFromStringAsFallback = (feedUrl: string, content: string) => { | ||
const regex = /https?:\/\/\S+?\.(jpg|jpeg|png|gif|bmp|svg|webp|tiff)/i; | ||
const result = regex.exec(content); | ||
|
||
if (result == null) { | ||
return null; | ||
} | ||
|
||
console.debug( | ||
`Falling back to regex image search for '${feedUrl}'. Found ${result.length} matches in content: ${content}`, | ||
); | ||
return result[0]; | ||
}; | ||
|
||
const mediaProperties = [ | ||
{ | ||
path: ["enclosure", "@_url"], | ||
}, | ||
{ | ||
path: ["media:content", "@_url"], | ||
}, | ||
]; | ||
|
||
/** | ||
* The RSS and Atom standards are poorly adhered to in most of the web. | ||
* We want to show pretty background images on the posts and therefore need to extract | ||
* the enclosure (aka. media images). This function uses the dynamic properties defined above | ||
* to search through the possible paths and detect valid image URLs. | ||
* @param feedObject The object to scan for. | ||
* @returns the value of the first path that is found within the object | ||
*/ | ||
const getFirstMediaProperty = (feedObject: object) => { | ||
for (const mediaProperty of mediaProperties) { | ||
let propertyIndex = 0; | ||
let objectAtPath: object = feedObject; | ||
while (propertyIndex < mediaProperty.path.length) { | ||
const key = mediaProperty.path[propertyIndex]; | ||
if (key === undefined) { | ||
break; | ||
} | ||
const propertyEntries = Object.entries(objectAtPath); | ||
const propertyEntry = propertyEntries.find(([entryKey]) => entryKey === key); | ||
if (!propertyEntry) { | ||
break; | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const [_, propertyEntryValue] = propertyEntry; | ||
objectAtPath = propertyEntryValue as object; | ||
propertyIndex++; | ||
} | ||
|
||
const validationResult = z.string().url().safeParse(objectAtPath); | ||
if (!validationResult.success) { | ||
continue; | ||
} | ||
|
||
logger.debug(`Found an image in the feed entry: ${validationResult.data}`); | ||
return validationResult.data; | ||
} | ||
return null; | ||
}; | ||
|
||
/** | ||
* We extend the feed with custom properties. | ||
* This interface adds properties on top of the default ones. | ||
*/ | ||
interface ExtendedFeedEntry extends FeedEntry { | ||
enclosure?: string; | ||
} | ||
|
||
/** | ||
* We extend the feed with custom properties. | ||
* This interface omits the default entries with our custom definition. | ||
*/ | ||
interface ExtendedFeedData extends Omit<FeedData, "entries"> { | ||
entries?: ExtendedFeedEntry; | ||
} | ||
|
||
export interface RssFeed { | ||
feedUrl: string; | ||
feed: ExtendedFeedData; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.