This commit is contained in:
eric
2026-06-07 23:57:52 -05:00
parent 6d479c9f94
commit 47b1ae8514
56 changed files with 4163 additions and 874 deletions

View File

@@ -1,5 +1,15 @@
const MEDIA_PROXY_PATH = "/api/media/proxy";
/** 浏览器可直接加载的公开图床,无需经 MinIO 代理缓存 */
const DIRECT_MEDIA_HOSTS = [
"images.unsplash.com",
"unsplash.com",
"i.pravatar.cc",
"api.dicebear.com",
"logo.clearbit.com",
"avatars.githubusercontent.com",
];
function configuredMinioPublicUrl(): string {
return (
process.env.NEXT_PUBLIC_MINIO_PUBLIC_URL ||
@@ -8,6 +18,13 @@ function configuredMinioPublicUrl(): string {
).replace(/\/$/, "");
}
function isDirectMediaHost(hostname: string): boolean {
const host = hostname.toLowerCase();
return DIRECT_MEDIA_HOSTS.some(
(allowed) => host === allowed || host.endsWith(`.${allowed}`)
);
}
export function isExternalMediaUrl(value: string): boolean {
return /^https?:\/\//i.test(value);
}
@@ -22,8 +39,23 @@ export function mediaUrl(value?: string | null): string {
if (!url) return "";
if (/^(data:|blob:|#|mailto:|tel:)/i.test(url)) return url;
if (/^file:/i.test(url)) return "";
if (url.startsWith("/") && !url.startsWith("//")) {
const publicUrl = configuredMinioPublicUrl();
return publicUrl ? `${publicUrl}${url}` : url;
}
if (!isExternalMediaUrl(url)) return url;
if (isMinioUrl(url)) return url;
try {
const parsed = new URL(url);
if (isDirectMediaHost(parsed.hostname)) return url;
} catch {
return "";
}
return `${MEDIA_PROXY_PATH}?src=${encodeURIComponent(url)}`;
}

View File

@@ -7,13 +7,42 @@ export interface MeetupFeatureOption {
en: string;
}
export interface MiroTalkMeetupLike {
export interface MeetupLike {
id: string;
city?: string;
date?: string;
time?: string;
meetingUrl?: string;
mirotalkRoom?: string;
loungeChannel?: string;
mode?: MeetupMode | string;
accessLevel?: MeetupAccessLevel | string;
}
export interface MeetupViewer {
loaded?: boolean;
user?: {
id?: string;
email?: string;
name?: string;
vip?: boolean;
role?: string;
memberRole?: string;
siteRole?: string;
} | null;
vip?: boolean;
}
export interface MeetupSession {
canJoin: boolean;
lockReason?: string;
displayName: string;
videoUrl: string;
chatUrl: string;
loungeChannel: string;
mirotalkRoom: string;
meetingProvider: string;
user?: MeetupViewer["user"];
}
export const DEFAULT_MIROTALK_URL = "https://mirotalk.nomadro.cn";
@@ -41,13 +70,27 @@ export function normalizeMiroTalkRoom(value: string): string {
.slice(0, 80);
}
export function createMiroTalkRoomSlug(meetup: MiroTalkMeetupLike): string {
export function normalizeLoungeChannel(value: string): string {
const channel = value.trim();
if (!channel) return "";
return channel.startsWith("#") ? channel : `#${channel}`;
}
export function createMiroTalkRoomSlug(meetup: MeetupLike): string {
const datePart = String(meetup.date || "").slice(0, 10).replace(/[^0-9]/g, "");
const timePart = String(meetup.time || "").slice(0, 5).replace(/[^0-9]/g, "");
const cityPart = normalizeMiroTalkRoom(meetup.city || "online");
return normalizeMiroTalkRoom(["nomadcna", cityPart, datePart, timePart, meetup.id].filter(Boolean).join("-"));
}
export function createLoungeChannel(meetup: MeetupLike): string {
const explicit = normalizeLoungeChannel(meetup.loungeChannel || "");
if (explicit) return explicit;
const room = normalizeMiroTalkRoom(meetup.mirotalkRoom || "");
if (room) return `#${room}`;
return `#${createMiroTalkRoomSlug(meetup)}`;
}
export function getMiroTalkBaseUrl(): string {
return (
process.env.NEXT_PUBLIC_MIROTALK_URL ||
@@ -66,7 +109,7 @@ export function getMiroTalkServerLabel(): string {
}
}
export function buildMiroTalkJoinUrl(meetup: MiroTalkMeetupLike, displayName = "NomadCNA"): string {
export function buildMiroTalkJoinUrl(meetup: MeetupLike, displayName = "NomadCNA"): string {
if (meetup.meetingUrl && /^https?:\/\//i.test(meetup.meetingUrl)) {
return meetup.meetingUrl;
}
@@ -89,8 +132,35 @@ export function getLoungeServerLabel(): string {
}
}
export function buildMeetupChatUrl(): string {
return getLoungeBaseUrl();
export function buildMeetupChatUrl(meetup: MeetupLike): string {
const channel = createLoungeChannel(meetup);
return `${getLoungeBaseUrl()}/#/chan-${channel}`;
}
export function buildMeetupLivePath(meetupId: string): string {
return `/meetups/${meetupId}/live`;
}
export function viewerDisplayName(viewer: MeetupViewer): string {
const name = String(viewer.user?.name || "").trim();
if (name) return name;
const email = String(viewer.user?.email || "").trim();
return email ? email.split("@")[0] : "NomadCNA";
}
export function isHostViewer(viewer: MeetupViewer): boolean {
const role = String(
viewer.user?.role || viewer.user?.memberRole || viewer.user?.siteRole || ""
).toLowerCase();
return ["admin", "owner", "host", "organizer", "moderator"].includes(role);
}
export function canAccessMeetup(meetup: Pick<MeetupLike, "accessLevel">, viewer: MeetupViewer): boolean {
const access = String(meetup.accessLevel || "public").toLowerCase();
if (access === "public") return true;
if (access === "members") return Boolean(viewer.user);
if (access === "vip") return Boolean(viewer.vip || viewer.user?.vip);
return isHostViewer(viewer);
}
export function featureLabels(keys: string[], locale: string): string[] {
@@ -99,3 +169,7 @@ export function featureLabels(keys: string[], locale: string): string[] {
locale === "zh" ? feature.zh : feature.en
);
}
export function isLiveMeetup(meetup: Pick<MeetupLike, "mode">): boolean {
return meetup.mode === "online" || meetup.mode === "hybrid";
}