feat(integrations): add app linking (#4338)
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
import { decryptSecret } from "@homarr/common/server";
|
||||
import type { Modify } from "@homarr/common/types";
|
||||
import type { Integration as DbIntegration } from "@homarr/db/schema";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
|
||||
import { AdGuardHomeIntegration } from "../adguard-home/adguard-home-integration";
|
||||
import { CodebergIntegration } from "../codeberg/codeberg-integration";
|
||||
@@ -62,20 +59,6 @@ export const createIntegrationAsync = async <TKind extends keyof typeof integrat
|
||||
return new creator(integration) as IntegrationInstanceOfKind<TKind>;
|
||||
};
|
||||
|
||||
export const createIntegrationAsyncFromSecrets = <TKind extends keyof typeof integrationCreators>(
|
||||
integration: Modify<DbIntegration, { kind: TKind }> & {
|
||||
secrets: { kind: IntegrationSecretKind; value: `${string}.${string}` }[];
|
||||
},
|
||||
) => {
|
||||
return createIntegrationAsync({
|
||||
...integration,
|
||||
decryptedSecrets: integration.secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
type IntegrationInstance = new (integration: IntegrationInput) => Integration;
|
||||
|
||||
// factories are an array, to differentiate in js between class constructors and functions
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface IntegrationInput {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
externalUrl: string | null;
|
||||
decryptedSecrets: IntegrationSecret[];
|
||||
}
|
||||
|
||||
@@ -54,8 +55,12 @@ export abstract class Integration {
|
||||
return this.integration.decryptedSecrets.some((secret) => secret.kind === kind);
|
||||
}
|
||||
|
||||
protected url(path: `/${string}`, queryParams?: Record<string, string | Date | number | boolean>) {
|
||||
const baseUrl = removeTrailingSlash(this.integration.url);
|
||||
private createUrl(
|
||||
inputUrl: string,
|
||||
path: `/${string}`,
|
||||
queryParams?: Record<string, string | Date | number | boolean>,
|
||||
) {
|
||||
const baseUrl = removeTrailingSlash(inputUrl);
|
||||
const url = new URL(`${baseUrl}${path}`);
|
||||
|
||||
if (queryParams) {
|
||||
@@ -66,6 +71,13 @@ export abstract class Integration {
|
||||
|
||||
return url;
|
||||
}
|
||||
protected url(path: `/${string}`, queryParams?: Record<string, string | Date | number | boolean>) {
|
||||
return this.createUrl(this.integration.url, path, queryParams);
|
||||
}
|
||||
|
||||
protected externalUrl(path: `/${string}`, queryParams?: Record<string, string | Date | number | boolean>) {
|
||||
return this.createUrl(this.integration.externalUrl ?? this.integration.url, path, queryParams);
|
||||
}
|
||||
|
||||
public async testConnectionAsync(): Promise<TestingResult> {
|
||||
try {
|
||||
|
||||
@@ -125,7 +125,7 @@ export class EmbyIntegration extends Integration implements IMediaServerIntegrat
|
||||
sessionId: `${sessionInfo.Id}`,
|
||||
sessionName: `${sessionInfo.Client} (${sessionInfo.DeviceName})`,
|
||||
user: {
|
||||
profilePictureUrl: super.url(`/Users/${sessionInfo.UserId}/Images/Primary`).toString(),
|
||||
profilePictureUrl: super.externalUrl(`/Users/${sessionInfo.UserId}/Images/Primary`).toString(),
|
||||
userId: sessionInfo.UserId ?? "",
|
||||
username: sessionInfo.UserName ?? "",
|
||||
},
|
||||
@@ -169,13 +169,13 @@ export class EmbyIntegration extends Integration implements IMediaServerIntegrat
|
||||
description: item.Overview,
|
||||
releaseDate: item.PremiereDate ?? item.DateCreated,
|
||||
imageUrls: {
|
||||
poster: super.url(`/Items/${item.Id}/Images/Primary?maxHeight=492&maxWidth=328&quality=90`).toString(),
|
||||
backdrop: super.url(`/Items/${item.Id}/Images/Backdrop/0?maxWidth=960&quality=70`).toString(),
|
||||
poster: super.externalUrl(`/Items/${item.Id}/Images/Primary?maxHeight=492&maxWidth=328&quality=90`).toString(),
|
||||
backdrop: super.externalUrl(`/Items/${item.Id}/Images/Backdrop/0?maxWidth=960&quality=70`).toString(),
|
||||
},
|
||||
producer: item.Studios.at(0)?.Name,
|
||||
rating: item.CommunityRating?.toFixed(1),
|
||||
tags: item.Genres,
|
||||
href: super.url(`/web/index.html#!/item?id=${item.Id}&serverId=${item.ServerId}`).toString(),
|
||||
href: super.externalUrl(`/web/index.html#!/item?id=${item.Id}&serverId=${item.ServerId}`).toString(),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -54,4 +54,4 @@ export type { Notification } from "./interfaces/notifications/notification-types
|
||||
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
|
||||
|
||||
// Helpers
|
||||
export { createIntegrationAsync, createIntegrationAsyncFromSecrets } from "./base/creator";
|
||||
export { createIntegrationAsync } from "./base/creator";
|
||||
|
||||
@@ -94,7 +94,7 @@ export class JellyfinIntegration extends Integration implements IMediaServerInte
|
||||
sessionId: `${sessionInfo.Id}`,
|
||||
sessionName: `${sessionInfo.Client} (${sessionInfo.DeviceName})`,
|
||||
user: {
|
||||
profilePictureUrl: this.url(`/Users/${sessionInfo.UserId}/Images/Primary`).toString(),
|
||||
profilePictureUrl: this.externalUrl(`/Users/${sessionInfo.UserId}/Images/Primary`).toString(),
|
||||
userId: sessionInfo.UserId ?? "",
|
||||
username: sessionInfo.UserName ?? "",
|
||||
},
|
||||
@@ -130,13 +130,13 @@ export class JellyfinIntegration extends Integration implements IMediaServerInte
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
releaseDate: new Date(item.PremiereDate ?? item.DateCreated!),
|
||||
imageUrls: {
|
||||
poster: super.url(`/Items/${item.Id}/Images/Primary?maxHeight=492&maxWidth=328&quality=90`).toString(),
|
||||
backdrop: super.url(`/Items/${item.Id}/Images/Backdrop/0?maxWidth=960&quality=70`).toString(),
|
||||
poster: super.externalUrl(`/Items/${item.Id}/Images/Primary?maxHeight=492&maxWidth=328&quality=90`).toString(),
|
||||
backdrop: super.externalUrl(`/Items/${item.Id}/Images/Backdrop/0?maxWidth=960&quality=70`).toString(),
|
||||
},
|
||||
producer: item.Studios?.at(0)?.Name ?? undefined,
|
||||
rating: item.CommunityRating?.toFixed(1),
|
||||
tags: item.Genres ?? [],
|
||||
href: super.url(`/web/index.html#!/details?id=${item.Id}&serverId=${item.ServerId}`).toString(),
|
||||
href: super.externalUrl(`/web/index.html#!/details?id=${item.Id}&serverId=${item.ServerId}`).toString(),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export class RadarrIntegration extends Integration implements ICalendarIntegrati
|
||||
private getLinksForRadarrCalendarEvent = (event: z.infer<typeof radarrCalendarEventSchema>) => {
|
||||
const links: CalendarLink[] = [
|
||||
{
|
||||
href: this.url(`/movie/${event.titleSlug}`).toString(),
|
||||
href: this.externalUrl(`/movie/${event.titleSlug}`).toString(),
|
||||
name: "Radarr",
|
||||
logo: "/images/apps/radarr.svg",
|
||||
color: undefined,
|
||||
|
||||
@@ -74,7 +74,7 @@ export class ReadarrIntegration extends Integration implements ICalendarIntegrat
|
||||
private getLinksForReadarrCalendarEvent = (event: z.infer<typeof readarrCalendarEventSchema>) => {
|
||||
return [
|
||||
{
|
||||
href: this.url(`/author/${event.author.foreignAuthorId}`).toString(),
|
||||
href: this.externalUrl(`/author/${event.author.foreignAuthorId}`).toString(),
|
||||
color: "#f5c518",
|
||||
isDark: false,
|
||||
logo: "/images/apps/readarr.svg",
|
||||
@@ -101,7 +101,7 @@ export class ReadarrIntegration extends Integration implements ICalendarIntegrat
|
||||
if (!bestImage) {
|
||||
return undefined;
|
||||
}
|
||||
return this.url(bestImage.url as `/${string}`).toString();
|
||||
return this.externalUrl(bestImage.url as `/${string}`).toString();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export class SonarrIntegration extends Integration implements ICalendarIntegrati
|
||||
private getLinksForSonarrCalendarEvent = (event: z.infer<typeof sonarrCalendarEventSchema>) => {
|
||||
const links: CalendarLink[] = [
|
||||
{
|
||||
href: this.url(`/series/${event.series.titleSlug}`).toString(),
|
||||
href: this.externalUrl(`/series/${event.series.titleSlug}`).toString(),
|
||||
name: "Sonarr",
|
||||
logo: "/images/apps/sonarr.svg",
|
||||
color: undefined,
|
||||
|
||||
@@ -87,7 +87,7 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr
|
||||
"color" in veventObject && typeof veventObject.color === "string" ? veventObject.color : "#ff8600",
|
||||
links: [
|
||||
{
|
||||
href: this.url(
|
||||
href: this.externalUrl(
|
||||
`/apps/calendar/timeGridWeek/now/edit/sidebar/${eventSlug}/${dateInMillis / 1000}`,
|
||||
).toString(),
|
||||
name: "Nextcloud",
|
||||
|
||||
@@ -54,7 +54,7 @@ export class NTFYIntegration extends Integration implements INotificationsIntegr
|
||||
return notifications
|
||||
.filter((notification) => notification !== null)
|
||||
.map((notification): Notification => {
|
||||
const topicURL = this.url(`/${notification.topic}`);
|
||||
const topicURL = this.externalUrl(`/${notification.topic}`);
|
||||
return {
|
||||
id: notification.id,
|
||||
time: new Date(notification.time * 1000),
|
||||
|
||||
@@ -43,7 +43,7 @@ export class OverseerrIntegration
|
||||
return schemaData.results.map((result) => ({
|
||||
id: result.id,
|
||||
name: "name" in result ? result.name : result.title,
|
||||
link: this.url(`/${result.mediaType}/${result.id}`).toString(),
|
||||
link: this.externalUrl(`/${result.mediaType}/${result.id}`).toString(),
|
||||
image: constructSearchResultImage(result),
|
||||
text: "overview" in result ? result.overview : undefined,
|
||||
type: result.mediaType,
|
||||
@@ -144,7 +144,7 @@ export class OverseerrIntegration
|
||||
availability: request.media.status,
|
||||
backdropImageUrl: `https://image.tmdb.org/t/p/original/${information.backdropPath}`,
|
||||
posterImagePath: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${information.posterPath}`,
|
||||
href: this.url(`/${request.type}/${request.media.tmdbId}`).toString(),
|
||||
href: this.externalUrl(`/${request.type}/${request.media.tmdbId}`).toString(),
|
||||
type: request.type,
|
||||
createdAt: request.createdAt,
|
||||
airDate: new Date(information.airDate),
|
||||
@@ -152,7 +152,7 @@ export class OverseerrIntegration
|
||||
? ({
|
||||
...request.requestedBy,
|
||||
displayName: request.requestedBy.displayName,
|
||||
link: this.url(`/users/${request.requestedBy.id}`).toString(),
|
||||
link: this.externalUrl(`/users/${request.requestedBy.id}`).toString(),
|
||||
avatar: this.constructAvatarUrl(request.requestedBy.avatar).toString(),
|
||||
} satisfies Omit<RequestUser, "requestCount">)
|
||||
: undefined,
|
||||
@@ -180,7 +180,7 @@ export class OverseerrIntegration
|
||||
return users.map((user): RequestUser => {
|
||||
return {
|
||||
...user,
|
||||
link: this.url(`/users/${user.id}`).toString(),
|
||||
link: this.externalUrl(`/users/${user.id}`).toString(),
|
||||
avatar: this.constructAvatarUrl(user.avatar).toString(),
|
||||
};
|
||||
});
|
||||
@@ -255,7 +255,7 @@ export class OverseerrIntegration
|
||||
return avatar;
|
||||
}
|
||||
|
||||
return this.url(`/${avatar}`);
|
||||
return this.externalUrl(`/${avatar}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user