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>
|
<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>
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
6
html.ts
6
html.ts
@ -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}">
|
||||||
|
48
magick.ts
48
magick.ts
@ -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,6 +16,23 @@ function createRoundedImage(profile: Profile) {
|
|||||||
'-resize', '256x256^',
|
'-resize', '256x256^',
|
||||||
'-gravity', 'center',
|
'-gravity', 'center',
|
||||||
'-extent', '256x256',
|
'-extent', '256x256',
|
||||||
|
`${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',
|
'(', '+clone', '-alpha', 'extract',
|
||||||
'-draw', "fill black polygon 0,0 0,128 128,0 fill white circle 128,128 128,0",
|
'-draw', "fill black polygon 0,0 0,128 128,0 fill white circle 128,128 128,0",
|
||||||
'(', '+clone', '-flip', ')', '-compose', 'Multiply', '-composite',
|
'(', '+clone', '-flip', ')', '-compose', 'Multiply', '-composite',
|
||||||
@ -28,12 +45,15 @@ function createRoundedImage(profile: Profile) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return runCommand(magick, args);
|
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")
|
||||||
}
|
}
|
||||||
|
@ -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`;
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user