Store and serve local profile icons for user pages #10
2
feeds.ts
2
feeds.ts
@ -36,7 +36,7 @@ export async function profileAtomFeed(
|
||||
<title>${profile.name} on Nostr (Articles)</title>
|
||||
<id>${feedId}</id>
|
||||
<updated>${isoDate(lastUpdate)}</updated>
|
||||
<icon>${profile.picture}</icon>
|
||||
<icon>${profile.avatarImageUrl}</icon>
|
||||
<author>
|
||||
<name>${name}</name>
|
||||
</author>
|
||||
|
@ -9,7 +9,6 @@ const nprofileHandler = async function (ctx: Context) {
|
||||
|
||||
try {
|
||||
data = nip19.decode(nprofile).data as nip19.ProfilePointer;
|
||||
console.log(data);
|
||||
} catch (_e) {
|
||||
notFoundHandler(ctx);
|
||||
return;
|
||||
|
@ -27,7 +27,7 @@ const userEventHandler = async function (ctx: Context) {
|
||||
const article = new Article(articleEvent);
|
||||
const profile = new Profile(profileEvent, username);
|
||||
const html = await articleHtml(article, profile);
|
||||
generateOgProfileImage(profile);
|
||||
await generateOgProfileImage(profile);
|
||||
|
||||
ctx.response.body = html;
|
||||
} else {
|
||||
|
@ -21,7 +21,7 @@ const userProfileHandler = async function (ctx: Context) {
|
||||
const profile = new Profile(profileEvent, username);
|
||||
const articles = await fetchArticlesByAuthor(pubkey, 210);
|
||||
const html = await profilePageHtml(profile, articles);
|
||||
generateOgProfileImage(profile);
|
||||
await generateOgProfileImage(profile);
|
||||
|
||||
ctx.response.body = html;
|
||||
} else {
|
||||
|
6
html.ts
6
html.ts
@ -55,7 +55,7 @@ export async function articleHtml(
|
||||
${draftLabel}
|
||||
<h1>${titleHtml(article.title)}</h1>
|
||||
<div class="meta">
|
||||
<img class="avatar" src="${profile.picture}" alt="User Avatar" />
|
||||
<img class="avatar" src="${profile.avatarImageUrl}" alt="User Avatar" />
|
||||
<div class="content">
|
||||
<span class="name"><a href="/@${profile.username}">${profile.name}</a></span>
|
||||
<span class="date">${publishedAtFormatted}</span>
|
||||
@ -148,7 +148,7 @@ export async function profilePageHtml(
|
||||
const body = `
|
||||
<main class="profile-page">
|
||||
<header>
|
||||
<img class="avatar" src="${profile.picture}" alt="User Avatar" />
|
||||
<img class="avatar" src="${profile.avatarImageUrl}" alt="User Avatar" />
|
||||
<div class="bio">
|
||||
<h1>${profile.name}</h1>
|
||||
${nip05Html}
|
||||
@ -211,6 +211,7 @@ function feedLinksHtml(profile: Profile) {
|
||||
|
||||
function profileMetaHtml(profile: Profile) {
|
||||
return `
|
||||
<link rel="icon" href="${profile.avatarImageUrl}" type="image/png">
|
||||
<meta property="og:url" content="${profile.profileUrl}">
|
||||
<meta property="og:type" content="website">
|
||||
<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;
|
||||
|
||||
return `
|
||||
<link rel="icon" href="${profile.avatarImageUrl}" type="image/png">
|
||||
<meta property="og:url" content="${article.url}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="${article.title}">
|
||||
|
66
magick.ts
66
magick.ts
@ -8,7 +8,7 @@ if (!magick) {
|
||||
log("ImageMagick is not installed. Cannot generate preview images", "yellow")
|
||||
}
|
||||
|
||||
function createRoundedImage(profile: Profile) {
|
||||
function createProfileImage(profile: Profile) {
|
||||
if (!magick || !profile.picture) return false;
|
||||
|
||||
const args = [
|
||||
@ -16,24 +16,44 @@ function createRoundedImage(profile: Profile) {
|
||||
'-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`
|
||||
`${tmpImgDir}/p-${profile.event.id}.png`
|
||||
];
|
||||
|
||||
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) {
|
||||
if (!magick) return false;
|
||||
|
||||
const status = await createRoundedImage(profile);
|
||||
const status = await createRoundedProfileImage(profile);
|
||||
|
||||
if (status && status.success) {
|
||||
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) {
|
||||
if (!magick || !profile.picture) return false;
|
||||
|
||||
@ -58,10 +97,13 @@ export async function generateOgProfileImage(profile: Profile) {
|
||||
const backgroundColor = "#333333";
|
||||
const fileExists = await checkFileExists(ogImagePath);
|
||||
|
||||
if (!fileExists) {
|
||||
if (fileExists) {
|
||||
return { success: true };
|
||||
} else {
|
||||
const status = await createOgImage(profile, ogImagePath, backgroundColor);
|
||||
if (status && status.success) {
|
||||
log(`Created OG image for ${profile.username}: ${ogImagePath}`, "blue")
|
||||
return status;
|
||||
} else {
|
||||
log(`Could not create OG image for ${profile.username}`, "yellow")
|
||||
}
|
||||
|
@ -63,6 +63,14 @@ export default class Profile {
|
||||
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 {
|
||||
if (magick) {
|
||||
return `${config.base_url}/assets/g/img/og-p-${this.event.id}.png`;
|
||||
|
Loading…
x
Reference in New Issue
Block a user