server/presenters/unfurl.ts
Rama main · réplica de lectura del repositorio original.
import { differenceInMinutes, formatDistanceToNowStrict } from "date-fns";
import { t } from "i18next";
import type { UnfurlResponse } from "@shared/types";
import { UnfurlResourceType } from "@shared/types";
import { dateLocale } from "@shared/utils/date";
import { Hour } from "@shared/utils/time";
import type { Document, User, Group } from "@server/models";
import { View } from "@server/models";
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
import { TextHelper } from "@server/models/helpers/TextHelper";
import { opts } from "@server/utils/i18n";
// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- heterogeneous payload from internal callers and third-party unfurl plugins.
type UnfurlData = Record<string, any>;
interface UnfurlOptions {
/** Whether the mentioned user's email may be included in the response. */
includeEmail: boolean;
/** Whether the mentioned user's viewing activity may be included in the response. */
includeLastViewed: boolean;
}
async function presentUnfurl(data: UnfurlData, options?: UnfurlOptions) {
switch (data.type) {
case UnfurlResourceType.Mention:
return presentMention(data, options);
case UnfurlResourceType.Group:
return presentGroup(data);
case UnfurlResourceType.Document:
return presentDocument(data);
case UnfurlResourceType.PR:
return presentPR(data);
case UnfurlResourceType.Issue:
return presentIssue(data);
case UnfurlResourceType.Project:
return presentProject(data);
default:
return presentURL(data);
}
}
// The data will have been transformed by the unfurl plugin, fields are picked
// individually so that additional metadata is never exposed in the response.
const presentURL = (
data: UnfurlData
): UnfurlResponse[UnfurlResourceType.URL] => ({
type: UnfurlResourceType.URL,
url: data.url,
title: data.title,
description: data.description,
color: data.color,
thumbnailUrl: data.thumbnailUrl,
faviconUrl: data.faviconUrl,
});
const presentMention = async (
data: UnfurlData,
options?: UnfurlOptions
): Promise<UnfurlResponse[UnfurlResourceType.Mention]> => {
const user: User = data.user;
const document: Document = data.document;
const lastOnlineInfo = presentLastOnlineInfoFor(user);
const lastViewedInfo =
options && options.includeLastViewed
? await presentLastViewedInfoFor(user, document)
: undefined;
return {
type: UnfurlResourceType.Mention,
name: user.name,
email: options && options.includeEmail ? user.email : null,
avatarUrl: user.avatarUrl,
color: user.color,
lastActive: lastViewedInfo
? `${lastOnlineInfo} • ${lastViewedInfo}`
: lastOnlineInfo,
};
};
const presentGroup = async (
data: UnfurlData
): Promise<UnfurlResponse[UnfurlResourceType.Group]> => {
const group: Group = data.group;
const memberCount = await group.memberCount;
return {
type: UnfurlResourceType.Group,
name: group.name,
description: group.description,
memberCount,
users: (data.users as User[]).map((user) => ({
id: user.id,
name: user.name,
avatarUrl: user.avatarUrl,
color: user.color,
})),
};
};
const presentDocument = async (
data: UnfurlData
): Promise<UnfurlResponse[UnfurlResourceType.Document]> => {
const document: Document = data.document;
const viewer: User | undefined = data.viewer;
const url: string | undefined = data.url;
const anchor: string | undefined = data.anchor;
// When the URL targets a specific heading, preview the content of that
// section rather than the top of the document.
const sectionSummary = anchor
? DocumentHelper.getAnchorContent(document, anchor)
: undefined;
return {
url: url ?? `${document.url}${anchor ?? ""}`,
type: UnfurlResourceType.Document,
id: document.id,
title: document.displayTitle,
summary: sectionSummary || document.getSummary(),
thumbnailUrl: await presentThumbnailUrl(document, !!data.signedUrls),
lastActivityByViewer: viewer
? presentLastActivityInfoFor(document, viewer)
: undefined,
};
};
const presentPR = (data: UnfurlData): UnfurlResponse[UnfurlResourceType.PR] =>
data as UnfurlResponse[UnfurlResourceType.PR]; // this would have been transformed by the unfurl plugin.
const presentIssue = (
data: UnfurlData
): UnfurlResponse[UnfurlResourceType.Issue] =>
data as UnfurlResponse[UnfurlResourceType.Issue]; // this would have been transformed by the unfurl plugin.
const presentProject = (
data: UnfurlData
): UnfurlResponse[UnfurlResourceType.Project] =>
data as UnfurlResponse[UnfurlResourceType.Project]; // this would have been transformed by the unfurl plugin.
const presentLastOnlineInfoFor = (user: User) => {
const locale = dateLocale(user.language);
let info: string;
if (!user.lastActiveAt) {
info = t("Never logged in", { ...opts(user) });
} else if (differenceInMinutes(new Date(), user.lastActiveAt) < 5) {
info = t("Online now", { ...opts(user) });
} else {
info = t("Online {{ timeAgo }}", {
timeAgo: formatDistanceToNowStrict(user.lastActiveAt, {
addSuffix: true,
locale,
}),
...opts(user),
});
}
return info;
};
const presentLastViewedInfoFor = async (user: User, document: Document) => {
const lastView = await View.findOne({
where: {
userId: user.id,
documentId: document.id,
},
order: [["updatedAt", "DESC"]],
});
const lastViewedAt = lastView ? lastView.updatedAt : undefined;
const locale = dateLocale(user.language);
let info: string;
if (!lastViewedAt) {
info = t("Never viewed", { ...opts(user) });
} else if (differenceInMinutes(new Date(), lastViewedAt) < 5) {
info = t("Viewed just now", { ...opts(user) });
} else {
info = t("Viewed {{ timeAgo }}", {
timeAgo: formatDistanceToNowStrict(lastViewedAt, {
addSuffix: true,
locale,
}),
...opts(user),
});
}
return info;
};
const presentLastActivityInfoFor = (document: Document, viewer: User) => {
const locale = dateLocale(viewer.language);
const wasUpdated = document.createdAt !== document.updatedAt;
let info: string;
if (wasUpdated) {
const lastUpdatedByViewer = document.updatedBy.id === viewer.id;
if (lastUpdatedByViewer) {
info = t("You updated {{ timeAgo }}", {
timeAgo: formatDistanceToNowStrict(document.updatedAt, {
addSuffix: true,
locale,
}),
...opts(viewer),
});
} else {
info = t("{{ user }} updated {{ timeAgo }}", {
user: document.updatedBy.name,
timeAgo: formatDistanceToNowStrict(document.updatedAt, {
addSuffix: true,
locale,
}),
...opts(viewer),
});
}
} else {
const lastCreatedByViewer = document.createdById === viewer.id;
if (lastCreatedByViewer) {
info = t("You created {{ timeAgo }}", {
timeAgo: formatDistanceToNowStrict(document.createdAt, {
addSuffix: true,
locale,
}),
...opts(viewer),
});
} else {
info = t("{{ user }} created {{ timeAgo }}", {
user: document.createdBy.name,
timeAgo: formatDistanceToNowStrict(document.createdAt, {
addSuffix: true,
locale,
}),
...opts(viewer),
});
}
}
return info;
};
/**
* Returns the preview image of a document. Signed URLs are used for viewers of
* a public share, who cannot load attachments through the redirect URL.
*/
const presentThumbnailUrl = async (
document: Document,
signedUrls: boolean
): Promise<string | undefined> => {
const src = DocumentHelper.getPreviewImage(document);
if (!src || !signedUrls) {
return src;
}
return TextHelper.attachmentsToSignedUrls(src, document.teamId, Hour.seconds);
};
export default presentUnfurl;