Compare commits
2
Commits
703c29bc83
...
7a2c0fd4d4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a2c0fd4d4 | ||
|
|
fc7e6ca248 |
No files matched your search
@@ -0,0 +1,27 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { WebsiteSetting } from "@/db/schema";
|
||||
import { db } from "./src/lib/db";
|
||||
|
||||
async function main() {
|
||||
const result = await db
|
||||
.select()
|
||||
.from(WebsiteSetting)
|
||||
.where(eq(WebsiteSetting.key, "habbo_imaging_url"));
|
||||
console.log("Current:", result);
|
||||
|
||||
if (result[0]?.value !== "/imaging") {
|
||||
await db
|
||||
.update(WebsiteSetting)
|
||||
.set({ value: "/imaging" })
|
||||
.where(eq(WebsiteSetting.key, "habbo_imaging_url"));
|
||||
console.log("Updated to /imaging");
|
||||
} else {
|
||||
console.log("Already correct");
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -8,7 +8,6 @@ import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db, MessengerFriendships, User } from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export const metadata = { title: "Friends" };
|
||||
|
||||
@@ -66,26 +65,20 @@ export default async function FriendsPage({
|
||||
),
|
||||
);
|
||||
|
||||
const [friends, imagerBase] = await Promise.all([
|
||||
friendIds.length
|
||||
? db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
look: User.look,
|
||||
motto: User.motto,
|
||||
online: User.online,
|
||||
})
|
||||
.from(User)
|
||||
.where(inArray(User.id, friendIds))
|
||||
.orderBy(asc(User.username))
|
||||
.catch(() => [])
|
||||
: Promise.resolve([]),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
]);
|
||||
const friends = friendIds.length
|
||||
? await db
|
||||
.select({
|
||||
id: User.id,
|
||||
username: User.username,
|
||||
look: User.look,
|
||||
motto: User.motto,
|
||||
online: User.online,
|
||||
})
|
||||
.from(User)
|
||||
.where(inArray(User.id, friendIds))
|
||||
.orderBy(asc(User.username))
|
||||
.catch(() => [])
|
||||
: [];
|
||||
|
||||
const errorMessage =
|
||||
error === "not_found"
|
||||
@@ -125,7 +118,7 @@ export default async function FriendsPage({
|
||||
) : (
|
||||
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
|
||||
{friends.map((friend) => {
|
||||
const avatar = avatarImageUrl(imagerBase ?? "", friend.look, {
|
||||
const avatar = avatarImageUrl(friend.look, {
|
||||
size: "s",
|
||||
headOnly: true,
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
|
||||
import { db, Guilds, GuildsMembers, User } from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
type GuildView = {
|
||||
id: number;
|
||||
@@ -98,11 +97,6 @@ export default async function GuildPage({
|
||||
users = [];
|
||||
}
|
||||
|
||||
const imagerBase = await siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
);
|
||||
|
||||
const usersById = new Map(users.map((u) => [u.id, u]));
|
||||
const sinceById = new Map(memberRows.map((m) => [m.userId, m.memberSince]));
|
||||
|
||||
@@ -211,7 +205,7 @@ export default async function GuildPage({
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
className="avatar"
|
||||
src={avatarImageUrl(imagerBase ?? "", m.look, { size: "l" })}
|
||||
src={avatarImageUrl(m.look, { size: "l" })}
|
||||
alt={`${m.username} avatar`}
|
||||
width={50}
|
||||
height={75}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getTranslations } from "next-intl/server";
|
||||
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui";
|
||||
import { db, User, UsersCurrency, UsersSettings } from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export const metadata = { title: "Leaderboard" };
|
||||
|
||||
@@ -158,20 +157,14 @@ export default async function LeaderboardPage({
|
||||
: "credits";
|
||||
const activeTab = TABS.find((tab) => tab.key === active) ?? TABS[0];
|
||||
|
||||
const [rows, imagerBase] = await Promise.all([
|
||||
active === "credits"
|
||||
? loadCreditsRows()
|
||||
: active === "diamonds" || active === "duckets"
|
||||
? // eslint-disable-next-line security/detect-object-injection -- active validated as "diamonds"|"duckets" in this branch
|
||||
loadCurrencyRows(CURRENCY_TYPE[active])
|
||||
: loadSettingsRows(
|
||||
SETTINGS_FIELD[active as keyof typeof SETTINGS_FIELD],
|
||||
),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
]);
|
||||
const rows = await (active === "credits"
|
||||
? loadCreditsRows()
|
||||
: active === "diamonds" || active === "duckets"
|
||||
? // eslint-disable-next-line security/detect-object-injection -- active validated as "diamonds"|"duckets" in this branch
|
||||
loadCurrencyRows(CURRENCY_TYPE[active])
|
||||
: loadSettingsRows(
|
||||
SETTINGS_FIELD[active as keyof typeof SETTINGS_FIELD],
|
||||
));
|
||||
|
||||
const valueOnly =
|
||||
active === "achievement" ||
|
||||
@@ -213,7 +206,7 @@ export default async function LeaderboardPage({
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, i) => {
|
||||
const avatar = avatarImageUrl(imagerBase ?? "", row.look, {
|
||||
const avatar = avatarImageUrl(row.look, {
|
||||
size: "s",
|
||||
headOnly: true,
|
||||
});
|
||||
|
||||
@@ -18,17 +18,11 @@ import { siteSettings } from "@/lib/services/site-settings";
|
||||
export default async function LoginPage() {
|
||||
const t = await getTranslations("pages.login");
|
||||
const th = await getTranslations("header");
|
||||
const [hotelName, imagerRaw, cfg, logo] = await Promise.all([
|
||||
const [hotelName, cfg, logo] = await Promise.all([
|
||||
resolveHotelName(),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
captchaConfig(),
|
||||
siteSettings.get("radio_social_discord", ""),
|
||||
siteSettings.get("cms_logo", ""),
|
||||
]);
|
||||
const imager = imagerRaw ?? "https://www.habbo.com/habbo-imaging/avatarimage";
|
||||
const nonce = (await headers()).get("x-nonce") ?? undefined;
|
||||
|
||||
const [online, recentUsers, latestUsers] = await Promise.all([
|
||||
@@ -234,7 +228,7 @@ export default async function LoginPage() {
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={avatarImageUrl(imager, u.look, {
|
||||
src={avatarImageUrl(u.look, {
|
||||
headOnly: true,
|
||||
direction: 2,
|
||||
size: "s",
|
||||
@@ -305,7 +299,7 @@ export default async function LoginPage() {
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={avatarImageUrl(imager, u.look, {
|
||||
src={avatarImageUrl(u.look, {
|
||||
headOnly: true,
|
||||
direction: 2,
|
||||
size: "s",
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
WebsiteLoginLogs,
|
||||
WebsiteSetting,
|
||||
} from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { resolveHotelName } from "@/lib/hotel-name";
|
||||
import { getAvatarUrl } from "@/lib/imager";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import CopyReferralButton from "./CopyReferralButton";
|
||||
|
||||
@@ -83,7 +83,6 @@ export default async function MePage({
|
||||
|
||||
const [
|
||||
userRows,
|
||||
imagerBase,
|
||||
hotelName,
|
||||
neededRows,
|
||||
amountRows,
|
||||
@@ -111,10 +110,6 @@ export default async function MePage({
|
||||
.from(User)
|
||||
.where(eq(User.id, userId))
|
||||
.limit(1),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
resolveHotelName(),
|
||||
db
|
||||
.select({ value: WebsiteSetting.value })
|
||||
@@ -273,9 +268,7 @@ export default async function MePage({
|
||||
: "Never";
|
||||
const referralLink = `/register?ref=${encodeURIComponent(user.username)}`;
|
||||
|
||||
const imager =
|
||||
imagerBase ?? "https://www.habbo.com/habbo-imaging/avatarimage";
|
||||
const avatarFull = avatarImageUrl(imager, user.look, {
|
||||
const avatarFull = getAvatarUrl(user.look, {
|
||||
size: "l",
|
||||
direction: 2,
|
||||
});
|
||||
@@ -505,7 +498,7 @@ export default async function MePage({
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={avatarImageUrl(imager, friend.look, {
|
||||
src={getAvatarUrl(friend.look, {
|
||||
size: "s",
|
||||
headOnly: true,
|
||||
})}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
type SearchParams = Promise<{
|
||||
accepted?: string;
|
||||
@@ -65,10 +64,9 @@ export default async function MessagesPage({
|
||||
sendedOn: number;
|
||||
}[] = [];
|
||||
let friendships: { userOneId: number; userTwoId: number }[] = [];
|
||||
let imagerBase = "https://www.habbo.com/habbo-imaging/avatarimage";
|
||||
|
||||
try {
|
||||
const [reqRows, msgRows, friendRows, base] = await Promise.all([
|
||||
const [reqRows, msgRows, friendRows] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: MessengerFriendrequests.id,
|
||||
@@ -101,15 +99,10 @@ export default async function MessagesPage({
|
||||
eq(MessengerFriendships.userTwoId, userId),
|
||||
),
|
||||
),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
]);
|
||||
requests = reqRows;
|
||||
messages = msgRows;
|
||||
friendships = friendRows;
|
||||
if (base) imagerBase = base;
|
||||
} catch {
|
||||
requests = [];
|
||||
messages = [];
|
||||
@@ -277,7 +270,7 @@ export default async function MessagesPage({
|
||||
<div className="grid" style={{ padding: "1rem", gap: "0.75rem" }}>
|
||||
{requests.map((req) => {
|
||||
const from = userById.get(req.userFromId);
|
||||
const avatar = avatarImageUrl(imagerBase, from?.look ?? "", {
|
||||
const avatar = avatarImageUrl(from?.look ?? "", {
|
||||
size: "s",
|
||||
headOnly: true,
|
||||
});
|
||||
@@ -367,7 +360,7 @@ export default async function MessagesPage({
|
||||
<div className="grid" style={{ padding: "1rem", gap: "0.75rem" }}>
|
||||
{messages.map((msg) => {
|
||||
const from = userById.get(msg.userFromId);
|
||||
const avatar = avatarImageUrl(imagerBase, from?.look ?? "", {
|
||||
const avatar = avatarImageUrl(from?.look ?? "", {
|
||||
size: "s",
|
||||
headOnly: true,
|
||||
});
|
||||
|
||||
+3
-10
@@ -22,15 +22,10 @@ import { captchaConfig } from "@/lib/services/captcha";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
async function getHotelData() {
|
||||
const [hotelName, imagerRaw, logo] = await Promise.all([
|
||||
const [hotelName, logo] = await Promise.all([
|
||||
resolveHotelName(),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
siteSettings.get("cms_logo", ""),
|
||||
]);
|
||||
const imager = imagerRaw ?? "https://www.habbo.com/habbo-imaging/avatarimage";
|
||||
|
||||
const [
|
||||
online,
|
||||
@@ -88,7 +83,6 @@ async function getHotelData() {
|
||||
|
||||
return {
|
||||
hotelName,
|
||||
imager,
|
||||
online,
|
||||
users,
|
||||
rooms,
|
||||
@@ -108,7 +102,6 @@ export default async function Home() {
|
||||
const th = await getTranslations("header");
|
||||
const {
|
||||
hotelName,
|
||||
imager,
|
||||
online,
|
||||
users,
|
||||
rooms,
|
||||
@@ -486,7 +479,7 @@ export default async function Home() {
|
||||
>
|
||||
<div className="relative overflow-visible">
|
||||
<Image
|
||||
src={avatarImageUrl(imager, u.look, {
|
||||
src={avatarImageUrl(u.look, {
|
||||
headOnly: true,
|
||||
direction: 3,
|
||||
})}
|
||||
@@ -664,7 +657,7 @@ export default async function Home() {
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={avatarImageUrl(imager, u.look, {
|
||||
src={avatarImageUrl(u.look, {
|
||||
headOnly: true,
|
||||
direction: 2,
|
||||
size: "s",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getTranslations } from "next-intl/server";
|
||||
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui";
|
||||
import { db } from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
type LeaderRow = {
|
||||
username: string;
|
||||
@@ -36,13 +35,7 @@ async function loadRows(): Promise<
|
||||
export default async function RadioLeaderboardPage() {
|
||||
const t = await getTranslations("pages.radioLeaderboard");
|
||||
|
||||
const [rows, imagerBase] = await Promise.all([
|
||||
loadRows(),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
]);
|
||||
const rows = await loadRows();
|
||||
|
||||
return (
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
@@ -63,7 +56,7 @@ export default async function RadioLeaderboardPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, i) => {
|
||||
const avatar = avatarImageUrl(imagerBase ?? "", row.look, {
|
||||
const avatar = avatarImageUrl(row.look, {
|
||||
size: "s",
|
||||
headOnly: true,
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import { auth } from "@/lib/auth";
|
||||
import { db, RadioShouts, User } from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
type SearchParams = Promise<{ posted?: string; error?: string }>;
|
||||
|
||||
@@ -35,17 +34,13 @@ export default async function RadioShoutsPage({
|
||||
const t = await getTranslations("pages.radioShouts");
|
||||
const { posted, error } = await searchParams;
|
||||
|
||||
const [shouts, imagerBase, session] = await Promise.all([
|
||||
const [shouts, session] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(RadioShouts)
|
||||
.orderBy(desc(RadioShouts.createdAt))
|
||||
.limit(50)
|
||||
.catch(() => []),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
auth(),
|
||||
]);
|
||||
|
||||
@@ -141,7 +136,7 @@ export default async function RadioShoutsPage({
|
||||
{shouts.map((s) => {
|
||||
const author = authorById.get(Number(s.userId));
|
||||
const authorAvatar = author
|
||||
? avatarImageUrl(imagerBase ?? "", author.look, {
|
||||
? avatarImageUrl(author.look, {
|
||||
size: "s",
|
||||
headOnly: true,
|
||||
})
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from "@/components/public/ui";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export const metadata = { title: "Rankings" };
|
||||
|
||||
@@ -41,11 +40,6 @@ export default async function RankingsPage() {
|
||||
users = [];
|
||||
}
|
||||
|
||||
const imagerBase = await siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
);
|
||||
|
||||
return (
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
<ContentCard icon="🏆" title={t("title")} subtitle={t("subtitle")} />
|
||||
@@ -56,7 +50,7 @@ export default async function RankingsPage() {
|
||||
) : (
|
||||
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
|
||||
{users.map((u, i) => {
|
||||
const avatar = avatarImageUrl(imagerBase ?? "", u.look, {
|
||||
const avatar = avatarImageUrl(u.look, {
|
||||
size: "m",
|
||||
});
|
||||
return (
|
||||
|
||||
@@ -18,17 +18,11 @@ import { siteSettings } from "@/lib/services/site-settings";
|
||||
export default async function RegisterPage() {
|
||||
const th = await getTranslations("header");
|
||||
const tpr = await getTranslations("pages.register");
|
||||
const [hotelName, imagerRaw, cfg, logo] = await Promise.all([
|
||||
const [hotelName, cfg, logo] = await Promise.all([
|
||||
resolveHotelName(),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
captchaConfig(),
|
||||
siteSettings.get("radio_social_discord", ""),
|
||||
siteSettings.get("cms_logo", ""),
|
||||
]);
|
||||
const imager = imagerRaw ?? "https://www.habbo.com/habbo-imaging/avatarimage";
|
||||
const nonce = (await headers()).get("x-nonce") ?? undefined;
|
||||
|
||||
const [online, recentUsers, latestUsers] = await Promise.all([
|
||||
@@ -231,7 +225,7 @@ export default async function RegisterPage() {
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={avatarImageUrl(imager, u.look, {
|
||||
src={avatarImageUrl(u.look, {
|
||||
headOnly: true,
|
||||
direction: 2,
|
||||
size: "s",
|
||||
@@ -302,7 +296,7 @@ export default async function RegisterPage() {
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={avatarImageUrl(imager, u.look, {
|
||||
src={avatarImageUrl(u.look, {
|
||||
headOnly: true,
|
||||
direction: 2,
|
||||
})}
|
||||
|
||||
@@ -4,7 +4,6 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { db, Rooms, User } from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export const metadata: Metadata = { title: "Search" };
|
||||
|
||||
@@ -18,12 +17,6 @@ export default async function SearchPage({
|
||||
const { q } = await searchParams;
|
||||
const query = q?.trim();
|
||||
|
||||
const imager =
|
||||
(await siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
)) ?? "";
|
||||
|
||||
if (!query) {
|
||||
return (
|
||||
<div
|
||||
@@ -142,7 +135,7 @@ export default async function SearchPage({
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={avatarImageUrl(imager, u.look, {
|
||||
src={avatarImageUrl(u.look, {
|
||||
headOnly: true,
|
||||
direction: 2,
|
||||
})}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Reveal } from "@/components/motion-reveal";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db, User } from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const t = await getTranslations("pages.settings");
|
||||
@@ -41,15 +40,7 @@ export default async function SettingsPage() {
|
||||
|
||||
if (!user) redirect("/login");
|
||||
|
||||
let imagerBase = "https://www.habbo.com/habbo-imaging/avatarimage";
|
||||
try {
|
||||
imagerBase =
|
||||
(await siteSettings.get("habbo_imaging_url", imagerBase)) ?? imagerBase;
|
||||
} catch {
|
||||
/* fall back to default */
|
||||
}
|
||||
|
||||
const avatar = avatarImageUrl(imagerBase, user.look, { size: "l" });
|
||||
const avatar = avatarImageUrl(user.look, { size: "l" });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 pb-14">
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
} from "@/lib/db";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
type SearchParams = Promise<{
|
||||
friend?: string;
|
||||
@@ -87,7 +86,7 @@ export default async function ProfilePage({
|
||||
const { username } = await params;
|
||||
const { friend, guestbook: guestbookStatus, error } = await searchParams;
|
||||
|
||||
const [userRows, imagerBase, session] = await Promise.all([
|
||||
const [userRows, session] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: User.id,
|
||||
@@ -102,17 +101,13 @@ export default async function ProfilePage({
|
||||
.from(User)
|
||||
.where(eq(User.username, username))
|
||||
.limit(1),
|
||||
siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
),
|
||||
auth(),
|
||||
]);
|
||||
const user = userRows[0] ?? null;
|
||||
|
||||
if (!user) notFound();
|
||||
|
||||
const avatar = avatarImageUrl(imagerBase ?? "", user.look, { size: "l" });
|
||||
const avatar = avatarImageUrl(user.look, { size: "l" });
|
||||
const registered = formatDate(new Date(user.accountCreated * 1000), "date");
|
||||
const meId = Number(session?.user?.id);
|
||||
const isLoggedIn = Number.isInteger(meId) && meId > 0;
|
||||
@@ -484,7 +479,7 @@ export default async function ProfilePage({
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={avatarImageUrl(imagerBase ?? "", f.look, {
|
||||
src={avatarImageUrl(f.look, {
|
||||
size: "m",
|
||||
headOnly: true,
|
||||
})}
|
||||
@@ -686,7 +681,7 @@ export default async function ProfilePage({
|
||||
{guestbook.map((g) => {
|
||||
const author = authorById.get(g.userId);
|
||||
const authorAvatar = author
|
||||
? avatarImageUrl(imagerBase ?? "", author.look, {
|
||||
? avatarImageUrl(author.look, {
|
||||
size: "s",
|
||||
headOnly: true,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { resolveUpstreamBase } from "@/lib/imager";
|
||||
import { resolveImagerBase } from "@/lib/imager";
|
||||
|
||||
const FIGURE_RE = /^[a-z]{2}-\d+/i;
|
||||
const FIGURE_RE = /^([a-z]{2}-\d+)(\.[a-z]{2}-\d+)*$/i;
|
||||
const FIGURE_MAX_LEN = 512;
|
||||
const FIGURE_MAX_PARTS = 24;
|
||||
const UPSTREAM_TIMEOUT_MS = 10_000;
|
||||
@@ -49,6 +49,7 @@ export async function GET(request: NextRequest) {
|
||||
searchParams.get("headOnly") === "1";
|
||||
const gesture = searchParams.get("gesture") ?? undefined;
|
||||
const action = searchParams.get("action") ?? undefined;
|
||||
const effect = searchParams.get("effect") ?? undefined;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
figure,
|
||||
@@ -60,11 +61,12 @@ export async function GET(request: NextRequest) {
|
||||
if (headOnly) params.set("headonly", "1");
|
||||
if (gesture) params.set("gesture", gesture);
|
||||
if (action) params.set("action", action);
|
||||
if (effect) params.set("effect", effect);
|
||||
|
||||
const imgFormat = searchParams.get("img_format");
|
||||
if (imgFormat) params.set("img_format", imgFormat);
|
||||
|
||||
const upstream = resolveUpstreamBase();
|
||||
const upstream = resolveImagerBase();
|
||||
const upstreamUrl = `${upstream}?${params.toString()}`;
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { resolveUpstreamBase } from "@/lib/imager";
|
||||
import { resolveImagerBase } from "@/lib/imager";
|
||||
|
||||
const FIGURE_RE = /^[a-z]{2}-\d+/i;
|
||||
const FIGURE_RE = /^([a-z]{2}-\d+)(\.[a-z]{2}-\d+)*$/i;
|
||||
const FIGURE_MAX_LEN = 512;
|
||||
const FIGURE_MAX_PARTS = 24;
|
||||
const UPSTREAM_TIMEOUT_MS = 10_000;
|
||||
@@ -49,6 +49,7 @@ export async function GET(request: NextRequest) {
|
||||
searchParams.get("headOnly") === "1";
|
||||
const gesture = searchParams.get("gesture") ?? undefined;
|
||||
const action = searchParams.get("action") ?? undefined;
|
||||
const effect = searchParams.get("effect") ?? undefined;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
figure,
|
||||
@@ -60,11 +61,12 @@ export async function GET(request: NextRequest) {
|
||||
if (headOnly) params.set("headonly", "1");
|
||||
if (gesture) params.set("gesture", gesture);
|
||||
if (action) params.set("action", action);
|
||||
if (effect) params.set("effect", effect);
|
||||
|
||||
const imgFormat = searchParams.get("img_format");
|
||||
if (imgFormat) params.set("img_format", imgFormat);
|
||||
|
||||
const upstream = resolveUpstreamBase();
|
||||
const upstream = resolveImagerBase();
|
||||
const upstreamUrl = `${upstream}?${params.toString()}`;
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
|
||||
export interface OnlineUser {
|
||||
id: number;
|
||||
@@ -55,7 +56,7 @@ export function OnlineUsersWidget({
|
||||
>
|
||||
<div className="relative flex-none w-[50px] h-[62px]">
|
||||
<Image
|
||||
src={`https://www.habbo.com/habbo-imaging/avatarimage?width=50&height=62&direction=2&headonly=1&crop=face&action=wav&gender=m&look=${encodeURIComponent(user.look)}`}
|
||||
src={avatarImageUrl(user.look, { size: "s", headOnly: true })}
|
||||
alt={user.username}
|
||||
fill
|
||||
className="rounded object-cover border border-[var(--admin-border)]"
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { avatarImageUrl } from "@/lib/format";
|
||||
import { resolveHotelName } from "@/lib/hotel-name";
|
||||
import { canAccess, getApiAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
function Currency({
|
||||
icon,
|
||||
@@ -78,12 +77,7 @@ export async function TopHeader({ session }: { session: Session | null }) {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const imager =
|
||||
(await siteSettings.get(
|
||||
"habbo_imaging_url",
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
)) ?? "";
|
||||
const avatar = avatarImageUrl(imager, look, { headOnly: true, direction: 2 });
|
||||
const avatar = avatarImageUrl(look, { headOnly: true, direction: 2 });
|
||||
let showAdmin = false;
|
||||
let showMod = false;
|
||||
const ctx = await getApiAdminContext();
|
||||
@@ -394,7 +388,7 @@ export async function TopHeader({ session }: { session: Session | null }) {
|
||||
className="flex items-center gap-2 rounded-lg px-2 py-1 text-xs transition-colors hover:bg-white/10"
|
||||
>
|
||||
<Image
|
||||
src={avatarImageUrl(imager, u.look, {
|
||||
src={avatarImageUrl(u.look, {
|
||||
headOnly: true,
|
||||
direction: 2,
|
||||
size: "s",
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
convertFigurePart,
|
||||
convertFigureString,
|
||||
getNewFormatFigure,
|
||||
} from "./figure-convert";
|
||||
|
||||
describe("convertFigurePart", () => {
|
||||
it("converts head part", () => {
|
||||
expect(convertFigurePart("hr-11782-40-40")).toBe("hd-180-1");
|
||||
expect(convertFigurePart("hr-11782-41-41")).toBe("hd-180-2");
|
||||
});
|
||||
|
||||
it("converts hair part", () => {
|
||||
expect(convertFigurePart("hd-180-7-14")).toBe("hd-180-1");
|
||||
expect(convertFigurePart("hd-180-8-14")).toBe("hd-180-2");
|
||||
});
|
||||
|
||||
it("converts face part", () => {
|
||||
expect(convertFigurePart("ch-11592-66")).toBe("ch-255-66");
|
||||
});
|
||||
|
||||
it("converts shirt part", () => {
|
||||
expect(convertFigurePart("lg-10726-79-1408")).toBe("lg-280-110");
|
||||
});
|
||||
|
||||
it("converts shorts part", () => {
|
||||
expect(convertFigurePart("sh-11764-1408")).toBe("sh-305-62");
|
||||
});
|
||||
|
||||
it("converts shoes part", () => {
|
||||
expect(convertFigurePart("ha-11958-70-1408")).toBe("ha-310-70");
|
||||
});
|
||||
|
||||
it("converts waist part", () => {
|
||||
expect(convertFigurePart("wa-2007-0")).toBe("wa-200-0");
|
||||
});
|
||||
|
||||
it("returns original for unknown parts", () => {
|
||||
expect(convertFigurePart("unknown-part")).toBe("unknown-part");
|
||||
expect(convertFigurePart("hd-999-99")).toBe("hd-999-99");
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertFigureString", () => {
|
||||
it("converts full figure string from old to new format", () => {
|
||||
const oldFigure =
|
||||
"hr-11782-40-40.hd-180-7-14.ch-11592-66.lg-10726-79-1408.sh-11764-1408.ha-11958-70-1408.wa-2007-0";
|
||||
const newFigure =
|
||||
"hd-180-1.hd-180-1.ch-255-66.lg-280-110.sh-305-62.ha-310-70.wa-200-0";
|
||||
expect(convertFigureString(oldFigure)).toBe(newFigure);
|
||||
});
|
||||
|
||||
it("handles empty string", () => {
|
||||
expect(convertFigureString("")).toBe("");
|
||||
});
|
||||
|
||||
it("handles already converted figure", () => {
|
||||
const newFigure = "hd-180-1.ch-255-66.lg-280-110";
|
||||
expect(convertFigureString(newFigure)).toBe(newFigure);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNewFormatFigure", () => {
|
||||
it("converts figure using getNewFormatFigure", () => {
|
||||
const oldFigure = "hr-11782-40-40.hd-180-7-14.ch-11592-66";
|
||||
expect(getNewFormatFigure(oldFigure)).toBe("hd-180-1.hd-180-1.ch-255-66");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Figure string converter: old Habbo format -> epicnabbo.nl format.
|
||||
*
|
||||
* Old format uses long IDs (e.g., hr-11782-40-40, hd-180-7-14)
|
||||
* New format uses short IDs (e.g., hd-180-1, ch-255-66)
|
||||
*/
|
||||
|
||||
interface FigurePartMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const HEAD_MAP: FigurePartMap = {
|
||||
"hr-11782-40-40": "hd-180-1",
|
||||
"hr-11782-41-41": "hd-180-2",
|
||||
"hr-11782-42-42": "hd-180-3",
|
||||
"hr-11782-43-43": "hd-180-4",
|
||||
"hr-11782-44-44": "hd-180-5",
|
||||
"hr-11782-45-45": "hd-180-6",
|
||||
"hr-11782-46-46": "hd-180-7",
|
||||
"hr-11782-47-47": "hd-180-8",
|
||||
"hr-11782-48-48": "hd-180-9",
|
||||
"hr-11782-49-49": "hd-180-10",
|
||||
};
|
||||
|
||||
const HAIR_MAP: FigurePartMap = {
|
||||
"hd-180-7-14": "hd-180-1",
|
||||
"hd-180-8-14": "hd-180-2",
|
||||
"hd-180-9-14": "hd-180-3",
|
||||
"hd-180-10-14": "hd-180-4",
|
||||
"hd-180-11-14": "hd-180-5",
|
||||
"hd-180-12-14": "hd-180-6",
|
||||
"hd-180-13-14": "hd-180-7",
|
||||
"hd-180-14-14": "hd-180-8",
|
||||
"hd-180-15-14": "hd-180-9",
|
||||
"hd-180-16-14": "hd-180-10",
|
||||
};
|
||||
|
||||
const FACE_MAP: FigurePartMap = {
|
||||
"ch-11592-66": "ch-255-66",
|
||||
"ch-11593-66": "ch-255-67",
|
||||
"ch-11594-66": "ch-255-68",
|
||||
"ch-11595-66": "ch-255-69",
|
||||
};
|
||||
|
||||
const SHIRT_MAP: FigurePartMap = {
|
||||
"lg-10726-79-1408": "lg-280-110",
|
||||
"lg-10727-79-1408": "lg-280-111",
|
||||
"lg-10728-79-1408": "lg-280-112",
|
||||
"lg-10729-79-1408": "lg-280-113",
|
||||
"lg-10730-79-1408": "lg-280-114",
|
||||
};
|
||||
|
||||
const SHORTS_MAP: FigurePartMap = {
|
||||
"sh-11764-1408": "sh-305-62",
|
||||
"sh-11765-1408": "sh-305-63",
|
||||
"sh-11766-1408": "sh-305-64",
|
||||
"sh-11767-1408": "sh-305-65",
|
||||
};
|
||||
|
||||
const SHOES_MAP: FigurePartMap = {
|
||||
"ha-11958-70-1408": "ha-310-70",
|
||||
"ha-11959-70-1408": "ha-310-71",
|
||||
"ha-11960-70-1408": "ha-310-72",
|
||||
};
|
||||
|
||||
const WAIST_MAP: FigurePartMap = {
|
||||
"wa-2007-0": "wa-200-0",
|
||||
};
|
||||
|
||||
function getMapForPrefix(prefix: string): FigurePartMap | null {
|
||||
if (prefix === "hr") return HEAD_MAP;
|
||||
if (prefix === "hd") return HAIR_MAP;
|
||||
if (prefix === "ch") return FACE_MAP;
|
||||
if (prefix === "lg") return SHIRT_MAP;
|
||||
if (prefix === "sh") return SHORTS_MAP;
|
||||
if (prefix === "ha") return SHOES_MAP;
|
||||
if (prefix === "wa") return WAIST_MAP;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a single figure part from old format to new format.
|
||||
* Example: "hr-11782-40-40" -> "hd-180-1"
|
||||
*/
|
||||
export function convertFigurePart(part: string): string {
|
||||
const dashIndex = part.indexOf("-");
|
||||
if (dashIndex === -1) return part;
|
||||
|
||||
const prefix = part.slice(0, dashIndex);
|
||||
const map = getMapForPrefix(prefix);
|
||||
if (!map) return part;
|
||||
|
||||
return map[part] ?? part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a full figure string from old format to new format.
|
||||
* Example: "hr-11782-40-40.hd-180-7-14.ch-11592-66.lg-10726-79-1408.sh-11764-1408.ha-11958-70-1408.wa-2007-0"
|
||||
* -> "hd-180-1.hd-180-1.ch-255-66.lg-280-110.sh-305-62.ha-310-70.wa-200-0"
|
||||
*/
|
||||
export function convertFigureString(figure: string): string {
|
||||
if (!figure) return figure;
|
||||
|
||||
const parts = figure.split(".");
|
||||
const converted = parts.map(convertFigurePart);
|
||||
return converted.join(".");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the new format figure string for a user's look.
|
||||
* Handles both old and new format input.
|
||||
*/
|
||||
export function getNewFormatFigure(look: string): string {
|
||||
return convertFigureString(look);
|
||||
}
|
||||
+20
-7
@@ -1,4 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const envMock = vi.hoisted(() => ({
|
||||
env: {
|
||||
NEXT_PUBLIC_IMAGER_URL: "https://img.example.com" as string | undefined,
|
||||
NEXT_PUBLIC_APP_URL: undefined as string | undefined,
|
||||
IMAGING_UPSTREAM_URL: "https://up.example.com" as string | undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/env", () => envMock);
|
||||
|
||||
import { avatarImageUrl, excerpt, slugify } from "./format";
|
||||
|
||||
describe("slugify", () => {
|
||||
@@ -14,20 +25,22 @@ describe("slugify", () => {
|
||||
});
|
||||
|
||||
describe("avatarImageUrl", () => {
|
||||
const base = "https://www.habbo.com/habbo-imaging/avatarimage";
|
||||
beforeEach(() => {
|
||||
envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com";
|
||||
});
|
||||
|
||||
it("appends the figure and options", () => {
|
||||
const url = avatarImageUrl(base, "hr-100", { size: "l", headOnly: true });
|
||||
const url = avatarImageUrl("hr-100", { size: "l", headOnly: true });
|
||||
expect(url).toContain("figure=hr-100");
|
||||
expect(url).toContain("size=l");
|
||||
expect(url).toContain("headonly=1");
|
||||
expect(url.startsWith(`${base}?`)).toBe(true);
|
||||
expect(url.startsWith("https://img.example.com?")).toBe(true);
|
||||
});
|
||||
|
||||
it("uses & when the base already has a query string", () => {
|
||||
expect(
|
||||
avatarImageUrl(`${base}?x=1`, "hr-100").includes("?x=1&figure=hr-100"),
|
||||
).toBe(true);
|
||||
// This test is no longer applicable since avatarImageUrl doesn't accept a base parameter
|
||||
// The base URL is now resolved from env
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+6
-21
@@ -1,26 +1,11 @@
|
||||
import { resolveImagerBase } from "@/lib/imager";
|
||||
import { getNewFormatFigure } from "@/lib/figure-convert";
|
||||
import { getAvatarUrl } from "@/lib/imager";
|
||||
import type { AvatarOptions } from "@/types/admin";
|
||||
|
||||
/** Build a Habbo avatar-imager URL for a figure string. */
|
||||
export function avatarImageUrl(
|
||||
base: string,
|
||||
look: string,
|
||||
opts: {
|
||||
size?: "s" | "m" | "l";
|
||||
headOnly?: boolean;
|
||||
direction?: number;
|
||||
headDirection?: number;
|
||||
} = {},
|
||||
): string {
|
||||
const resolved = resolveImagerBase(base);
|
||||
const params = new URLSearchParams({ figure: look, img_format: "png" });
|
||||
if (opts.size) params.set("size", opts.size);
|
||||
if (opts.headOnly) params.set("headonly", "1");
|
||||
if (opts.direction !== undefined)
|
||||
params.set("direction", String(opts.direction));
|
||||
if (opts.headDirection !== undefined)
|
||||
params.set("head_direction", String(opts.headDirection));
|
||||
const sep = resolved.includes("?") ? "&" : "?";
|
||||
return `${resolved}${sep}${params.toString()}`;
|
||||
export function avatarImageUrl(look: string, opts: AvatarOptions = {}): string {
|
||||
const figure = getNewFormatFigure(look);
|
||||
return getAvatarUrl(figure, opts);
|
||||
}
|
||||
|
||||
/** URL-safe slug from a title (lowercase, ascii, hyphenated). */
|
||||
|
||||
+35
-57
@@ -2,69 +2,34 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const envMock = vi.hoisted(() => ({
|
||||
env: {
|
||||
NEXT_PUBLIC_IMAGER_URL: undefined as string | undefined,
|
||||
NEXT_PUBLIC_IMAGER_URL: "https://img.example.com" as string | undefined,
|
||||
NEXT_PUBLIC_APP_URL: undefined as string | undefined,
|
||||
IMAGING_UPSTREAM_URL: undefined as string | undefined,
|
||||
IMAGING_UPSTREAM_URL: "https://up.example.com" as string | undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/env", () => envMock);
|
||||
|
||||
import { getAvatarUrl, resolveImagerBase, resolveUpstreamBase } from "./imager";
|
||||
import { getAvatarUrl, resolveImagerBase } from "./imager";
|
||||
|
||||
describe("resolveImagerBase", () => {
|
||||
beforeEach(() => {
|
||||
envMock.env.NEXT_PUBLIC_IMAGER_URL = undefined;
|
||||
envMock.env.NEXT_PUBLIC_APP_URL = undefined;
|
||||
envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com";
|
||||
});
|
||||
|
||||
it("prefers a configured imager env var", () => {
|
||||
it("returns configured imager env var", () => {
|
||||
expect(resolveImagerBase()).toBe("https://img.example.com");
|
||||
});
|
||||
|
||||
it("strips trailing slashes", () => {
|
||||
envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com/";
|
||||
expect(resolveImagerBase()).toBe("https://img.example.com");
|
||||
});
|
||||
|
||||
it("ignores the broken self-hosted proxy env route", () => {
|
||||
envMock.env.NEXT_PUBLIC_IMAGER_URL =
|
||||
"https://img.example.com/api/imaging/avatar";
|
||||
envMock.env.NEXT_PUBLIC_APP_URL = "https://app.example.com/";
|
||||
expect(resolveImagerBase()).toBe("https://app.example.com/imaging");
|
||||
});
|
||||
|
||||
it("falls back to app URL imaging route", () => {
|
||||
envMock.env.NEXT_PUBLIC_APP_URL = "https://app.example.com/";
|
||||
expect(resolveImagerBase()).toBe("https://app.example.com/imaging");
|
||||
});
|
||||
|
||||
it("falls back to relative /imaging", () => {
|
||||
expect(resolveImagerBase()).toBe("/imaging");
|
||||
});
|
||||
|
||||
it("uses configured setting when valid", () => {
|
||||
expect(resolveImagerBase("https://cdn.example.com/imager/")).toBe(
|
||||
"https://cdn.example.com/imager",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects configured setting that is the broken proxy route", () => {
|
||||
expect(
|
||||
resolveImagerBase("https://cdn.example.com/api/imaging/avatar"),
|
||||
).toBe("/imaging");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveUpstreamBase", () => {
|
||||
beforeEach(() => {
|
||||
envMock.env.IMAGING_UPSTREAM_URL = undefined;
|
||||
});
|
||||
|
||||
it("uses env upstream when configured", () => {
|
||||
envMock.env.IMAGING_UPSTREAM_URL = "https://up.example.com/";
|
||||
expect(resolveUpstreamBase()).toBe("https://up.example.com");
|
||||
});
|
||||
|
||||
it("falls back to Habbo public imager", () => {
|
||||
expect(resolveUpstreamBase()).toBe(
|
||||
"https://www.habbo.com/habbo-imaging/avatarimage",
|
||||
it("falls back to epicnabbo.nl when env var is not set", () => {
|
||||
envMock.env.NEXT_PUBLIC_IMAGER_URL = undefined;
|
||||
expect(resolveImagerBase()).toBe(
|
||||
"https://epicnabbo.nl/imaging/avatarimage",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -72,7 +37,6 @@ describe("resolveUpstreamBase", () => {
|
||||
describe("getAvatarUrl", () => {
|
||||
beforeEach(() => {
|
||||
envMock.env.NEXT_PUBLIC_IMAGER_URL = "https://img.example.com";
|
||||
envMock.env.NEXT_PUBLIC_APP_URL = undefined;
|
||||
});
|
||||
|
||||
it("builds a query string with defaults", () => {
|
||||
@@ -82,7 +46,8 @@ describe("getAvatarUrl", () => {
|
||||
expect(url).toContain("direction=2");
|
||||
expect(url).toContain("head_direction=3");
|
||||
expect(url).toContain("size=m");
|
||||
expect(url).toContain("img_format=png");
|
||||
expect(url).toContain("img_format=apng");
|
||||
expect(url).toContain("effect=14");
|
||||
});
|
||||
|
||||
it("honors custom options", () => {
|
||||
@@ -102,15 +67,28 @@ describe("getAvatarUrl", () => {
|
||||
expect(url).toContain("action=sit");
|
||||
});
|
||||
|
||||
it("uses app URL imaging route when imager env absent", () => {
|
||||
envMock.env.NEXT_PUBLIC_IMAGER_URL = undefined;
|
||||
envMock.env.NEXT_PUBLIC_APP_URL = "https://app.example.com/";
|
||||
const url = getAvatarUrl("hd-180-1");
|
||||
expect(url.startsWith("https://app.example.com/imaging?")).toBe(true);
|
||||
});
|
||||
|
||||
it("escapes figure values in the query string", () => {
|
||||
const url = getAvatarUrl("hd-180-1&x=1");
|
||||
expect(url).not.toContain("&x=1");
|
||||
});
|
||||
|
||||
it("allows custom img_format", () => {
|
||||
const url = getAvatarUrl("hd-180-1", { img_format: "png" });
|
||||
expect(url).toContain("img_format=png");
|
||||
});
|
||||
|
||||
it("allows custom effect", () => {
|
||||
const url = getAvatarUrl("hd-180-1", { effect: 0 });
|
||||
expect(url).toContain("effect=0");
|
||||
});
|
||||
|
||||
it("uses epicnabbo.nl when no env var is set", () => {
|
||||
envMock.env.NEXT_PUBLIC_IMAGER_URL = undefined;
|
||||
const url = getAvatarUrl("hd-180-1");
|
||||
expect(url.startsWith("https://epicnabbo.nl/imaging/avatarimage?")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(url).toContain("img_format=apng");
|
||||
expect(url).toContain("effect=14");
|
||||
});
|
||||
});
|
||||
+15
-35
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* Avatar imager helpers.
|
||||
*
|
||||
* The public-facing imager URL resolves to the site's own `/imaging` endpoint
|
||||
* by default. That endpoint proxies the image from the configured upstream
|
||||
* (defaults to Habbo's public imager).
|
||||
* The public-facing imager URL is configured via NEXT_PUBLIC_IMAGER_URL env var.
|
||||
* Defaults to epicnabbo.nl imager with effect=14 and img_format=apng.
|
||||
*/
|
||||
|
||||
export type { AvatarOptions } from "@/types/admin";
|
||||
@@ -11,45 +10,23 @@ export type { AvatarOptions } from "@/types/admin";
|
||||
import { env } from "@/env";
|
||||
import type { AvatarOptions } from "@/types/admin";
|
||||
|
||||
/** Build an absolute default imager URL from public env vars. */
|
||||
function defaultImagerUrl(): string {
|
||||
const pub = env.NEXT_PUBLIC_IMAGER_URL?.trim();
|
||||
if (pub && !pub.includes("/api/imaging/avatar"))
|
||||
return pub.replace(/\/+$/, "");
|
||||
const app = env.NEXT_PUBLIC_APP_URL?.trim();
|
||||
if (app) return `${app.replace(/\/+$/, "")}/imaging`;
|
||||
return "/imaging";
|
||||
}
|
||||
const DEFAULT_IMAGER_URL = "https://epicnabbo.nl/imaging/avatarimage";
|
||||
|
||||
/**
|
||||
* Resolve the public-facing imager base URL.
|
||||
* Rejects the broken self-hosted proxy route if stored in settings.
|
||||
* Resolve the public-facing imager base URL from env.
|
||||
* Falls back to epicnabbo.nl if not configured.
|
||||
*/
|
||||
export function resolveImagerBase(configured?: string | null): string {
|
||||
export function resolveImagerBase(): string {
|
||||
const fromEnv = env.NEXT_PUBLIC_IMAGER_URL?.trim();
|
||||
if (fromEnv && !fromEnv.includes("/api/imaging/avatar")) {
|
||||
return fromEnv.replace(/\/+$/, "");
|
||||
if (!fromEnv) {
|
||||
return DEFAULT_IMAGER_URL;
|
||||
}
|
||||
|
||||
const v = (configured ?? "").trim();
|
||||
if (!v || v.includes("/api/imaging/avatar")) {
|
||||
return defaultImagerUrl();
|
||||
}
|
||||
return v.replace(/\/+$/, "");
|
||||
return fromEnv.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the upstream URL used by the proxy endpoint.
|
||||
* Falls back to Habbo's public imager when nothing else is configured.
|
||||
*/
|
||||
export function resolveUpstreamBase(): string {
|
||||
const fromEnv = env.IMAGING_UPSTREAM_URL?.trim();
|
||||
if (fromEnv) return fromEnv.replace(/\/+$/, "");
|
||||
return "https://www.habbo.com/habbo-imaging/avatarimage";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an avatar image URL (site's own `/imaging` endpoint by default).
|
||||
* Build an avatar image URL using the configured imager.
|
||||
* Uses effect=14 and img_format=apng by default for epicnabbo.nl compatibility.
|
||||
*/
|
||||
export function getAvatarUrl(
|
||||
figure: string,
|
||||
@@ -62,6 +39,8 @@ export function getAvatarUrl(
|
||||
headOnly = false,
|
||||
gesture,
|
||||
action,
|
||||
effect = 14,
|
||||
img_format = "apng",
|
||||
} = options;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
@@ -69,7 +48,8 @@ export function getAvatarUrl(
|
||||
direction: String(direction),
|
||||
head_direction: String(headDirection),
|
||||
size,
|
||||
img_format: "png",
|
||||
img_format,
|
||||
effect: String(effect),
|
||||
});
|
||||
|
||||
if (headOnly) params.set("headonly", "1");
|
||||
|
||||
@@ -40,4 +40,8 @@ export interface AvatarOptions {
|
||||
gesture?: string;
|
||||
/** Action: wav, sit, lay, etc. */
|
||||
action?: string;
|
||||
/** Effect ID (e.g., 14 for ghost) */
|
||||
effect?: string | number;
|
||||
/** Image format (e.g., "png", "apng") */
|
||||
img_format?: string;
|
||||
}
|
||||
Reference in new issue
Block a user