Store and serve local profile icons for user pages #10

Merged
raucao merged 2 commits from feature/icons into master 2024-12-03 17:51:56 +00:00
8 changed files with 69 additions and 19 deletions

View File

@ -36,7 +36,7 @@ export async function profileAtomFeed(
<title>${profile.name} on Nostr (Articles)</title> <title>${profile.name} on Nostr (Articles)</title>
<id>${feedId}</id> <id>${feedId}</id>
<updated>${isoDate(lastUpdate)}</updated> <updated>${isoDate(lastUpdate)}</updated>
<icon>${profile.picture}</icon> <icon>${profile.avatarImageUrl}</icon>
<author> <author>
<name>${name}</name> <name>${name}</name>
</author> </author>

View File

@ -9,7 +9,6 @@ const nprofileHandler = async function (ctx: Context) {
try { try {
data = nip19.decode(nprofile).data as nip19.ProfilePointer; data = nip19.decode(nprofile).data as nip19.ProfilePointer;
console.log(data);
} catch (_e) { } catch (_e) {
notFoundHandler(ctx); notFoundHandler(ctx);
return; return;

View File

@ -27,7 +27,7 @@ const userEventHandler = async function (ctx: Context) {
const article = new Article(articleEvent); const article = new Article(articleEvent);
const profile = new Profile(profileEvent, username); const profile = new Profile(profileEvent, username);
const html = await articleHtml(article, profile); const html = await articleHtml(article, profile);
generateOgProfileImage(profile); await generateOgProfileImage(profile);
ctx.response.body = html; ctx.response.body = html;
} else { } else {

View File

@ -21,7 +21,7 @@ const userProfileHandler = async function (ctx: Context) {
const profile = new Profile(profileEvent, username); const profile = new Profile(profileEvent, username);
const articles = await fetchArticlesByAuthor(pubkey, 210); const articles = await fetchArticlesByAuthor(pubkey, 210);
const html = await profilePageHtml(profile, articles); const html = await profilePageHtml(profile, articles);
generateOgProfileImage(profile); await generateOgProfileImage(profile);
ctx.response.body = html; ctx.response.body = html;
} else { } else {

View File

@ -55,7 +55,7 @@ export async function articleHtml(
${draftLabel} ${draftLabel}
<h1>${titleHtml(article.title)}</h1> <h1>${titleHtml(article.title)}</h1>
<div class="meta"> <div class="meta">
<img class="avatar" src="${profile.picture}" alt="User Avatar" /> <img class="avatar" src="${profile.avatarImageUrl}" alt="User Avatar" />
<div class="content"> <div class="content">
<span class="name"><a href="/@${profile.username}">${profile.name}</a></span> <span class="name"><a href="/@${profile.username}">${profile.name}</a></span>
<span class="date">${publishedAtFormatted}</span> <span class="date">${publishedAtFormatted}</span>
@ -148,7 +148,7 @@ export async function profilePageHtml(
const body = ` const body = `
<main class="profile-page"> <main class="profile-page">
<header> <header>
<img class="avatar" src="${profile.picture}" alt="User Avatar" /> <img class="avatar" src="${profile.avatarImageUrl}" alt="User Avatar" />
<div class="bio"> <div class="bio">
<h1>${profile.name}</h1> <h1>${profile.name}</h1>
${nip05Html} ${nip05Html}
@ -211,6 +211,7 @@ function feedLinksHtml(profile: Profile) {
function profileMetaHtml(profile: Profile) { function profileMetaHtml(profile: Profile) {
return ` return `
<link rel="icon" href="${profile.avatarImageUrl}" type="image/png">
<meta property="og:url" content="${profile.profileUrl}"> <meta property="og:url" content="${profile.profileUrl}">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:title" content="${profile.name} on Nostr"> <meta property="og:title" content="${profile.name} on Nostr">
@ -229,6 +230,7 @@ function articleMetaHtml(article: Article, profile: Profile) {
const imageUrl = article.image || profile.ogImageUrl; const imageUrl = article.image || profile.ogImageUrl;
return ` return `
<link rel="icon" href="${profile.avatarImageUrl}" type="image/png">
<meta property="og:url" content="${article.url}"> <meta property="og:url" content="${article.url}">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:title" content="${article.title}"> <meta property="og:title" content="${article.title}">

View File

@ -8,7 +8,7 @@ if (!magick) {
log("ImageMagick is not installed. Cannot generate preview images", "yellow") log("ImageMagick is not installed. Cannot generate preview images", "yellow")
} }
function createRoundedImage(profile: Profile) { function createProfileImage(profile: Profile) {
if (!magick || !profile.picture) return false; if (!magick || !profile.picture) return false;
const args = [ const args = [
@ -16,24 +16,44 @@ function createRoundedImage(profile: Profile) {
'-resize', '256x256^', '-resize', '256x256^',
'-gravity', 'center', '-gravity', 'center',
'-extent', '256x256', '-extent', '256x256',
'(', '+clone', '-alpha', 'extract', `${tmpImgDir}/p-${profile.event.id}.png`
'-draw', "fill black polygon 0,0 0,128 128,0 fill white circle 128,128 128,0",
'(', '+clone', '-flip', ')', '-compose', 'Multiply', '-composite',
'(', '+clone', '-flop', ')', '-compose', 'Multiply', '-composite',
')',
'-alpha', 'off',
'-compose', 'CopyOpacity',
'-composite',
`${tmpImgDir}/p-${profile.event.id}-rounded.png`
]; ];
return runCommand(magick, args); return runCommand(magick, args);
} }
async function createRoundedProfileImage(profile: Profile) {
if (!magick || !profile.picture) return false;
const status = await generateProfileImage(profile);
if (status && status.success) {
const args = [
`${tmpImgDir}/p-${profile.event.id}.png`,
'-resize', '256x256^',
'-gravity', 'center',
'-extent', '256x256',
'(', '+clone', '-alpha', 'extract',
'-draw', "fill black polygon 0,0 0,128 128,0 fill white circle 128,128 128,0",
'(', '+clone', '-flip', ')', '-compose', 'Multiply', '-composite',
'(', '+clone', '-flop', ')', '-compose', 'Multiply', '-composite',
')',
'-alpha', 'off',
'-compose', 'CopyOpacity',
'-composite',
`${tmpImgDir}/p-${profile.event.id}-rounded.png`
];
return runCommand(magick, args);
} else {
return false;
}
}
async function createOgImage(profile: Profile, ogImagePath: string, backgroundColor: string) { async function createOgImage(profile: Profile, ogImagePath: string, backgroundColor: string) {
if (!magick) return false; if (!magick) return false;
const status = await createRoundedImage(profile); const status = await createRoundedProfileImage(profile);
if (status && status.success) { if (status && status.success) {
const args = [ const args = [
@ -51,6 +71,25 @@ async function createOgImage(profile: Profile, ogImagePath: string, backgroundCo
} }
}; };
export async function generateProfileImage(profile: Profile) {
if (!magick || !profile.picture) return false;
const imagePath = `${tmpImgDir}/p-${profile.event.id}.png`;
const fileExists = await checkFileExists(imagePath);
if (fileExists) {
return { success: true };
} else {
const status = await createProfileImage(profile);
if (status && status.success) {
log(`Created avatar image for ${profile.username}: ${imagePath}`, "blue")
return status;
} else {
log(`Could not create avatar image for ${profile.username}`, "yellow")
}
}
}
export async function generateOgProfileImage(profile: Profile) { export async function generateOgProfileImage(profile: Profile) {
if (!magick || !profile.picture) return false; if (!magick || !profile.picture) return false;
@ -58,10 +97,13 @@ export async function generateOgProfileImage(profile: Profile) {
const backgroundColor = "#333333"; const backgroundColor = "#333333";
const fileExists = await checkFileExists(ogImagePath); const fileExists = await checkFileExists(ogImagePath);
if (!fileExists) { if (fileExists) {
return { success: true };
} else {
const status = await createOgImage(profile, ogImagePath, backgroundColor); const status = await createOgImage(profile, ogImagePath, backgroundColor);
if (status && status.success) { if (status && status.success) {
log(`Created OG image for ${profile.username}: ${ogImagePath}`, "blue") log(`Created OG image for ${profile.username}: ${ogImagePath}`, "blue")
return status;
} else { } else {
log(`Could not create OG image for ${profile.username}`, "yellow") log(`Could not create OG image for ${profile.username}`, "yellow")
} }

View File

@ -63,6 +63,14 @@ export default class Profile {
return `${config.base_url}/@${this.username}`; return `${config.base_url}/@${this.username}`;
} }
get avatarImageUrl(): string {
if (magick) {
return `${config.base_url}/assets/g/img/p-${this.event.id}.png`;
} else {
return this.picture || "";
}
}
get ogImageUrl(): string { get ogImageUrl(): string {
if (magick) { if (magick) {
return `${config.base_url}/assets/g/img/og-p-${this.event.id}.png`; return `${config.base_url}/assets/g/img/og-p-${this.event.id}.png`;

View File

@ -85,7 +85,6 @@ router.get("/:username/:identifier", async (ctx) => {
}); });
router.get("/assets/:path*", async (ctx) => { router.get("/assets/:path*", async (ctx) => {
console.log(import.meta.dirname);
try { try {
let filePath = ctx.params.path || ""; let filePath = ctx.params.path || "";
let root: string; let root: string;