postherousimages.ts105 matches
1import { blob } from "https://esm.town/v/std/blob";
2import { createImage, generateUniqueImageFilename, getImageByFilename, deleteImage } from "../database/queries.ts";
3import { BlogImage, CreateImageData, EmailAttachment } from "../../shared/types.ts";
45// Supported image MIME types
6const SUPPORTED_IMAGE_TYPES = [
7'image/jpeg',
8'image/jpg',
9'image/png',
10'image/gif',
11'image/webp',
12'image/svg+xml'
13];
141718/**
19* Check if a MIME type is a supported image format
20*/
21export function isSupportedImageType(mimeType: string): boolean {
22return SUPPORTED_IMAGE_TYPES.includes(mimeType.toLowerCase());
23}
2425/**
26* Validate image file
27*/
28export function validateImageFile(file: { size: number; type: string }): { valid: boolean; error?: string } {
29if (file.size > MAX_FILE_SIZE) {
30return { valid: false, error: `File size too large. Maximum size is ${MAX_FILE_SIZE / (1024 * 1024)}MB` };
31}
3233if (!isSupportedImageType(file.type)) {
34return { valid: false, error: `Unsupported file type. Supported types: ${SUPPORTED_IMAGE_TYPES.join(', ')}` };
35}
363940/**
41* Store an image from a File object (for uploads)
42*/
43export async function storeImageFromFile(
44file: File,
45uploadedByEmail: string,
48postSlug?: string;
49} = {}
50): Promise<BlogImage> {
51// Validate the file
52const validation = validateImageFile(file);
53if (!validation.valid) {
54throw new Error(validation.error);
5657// Generate unique filename
58const filename = await generateUniqueImageFilename(file.name);
59const blobKey = `images/${filename}`;
6061// Read file as array buffer and store in blob
63await blob.set(blobKey, new Uint8Array(arrayBuffer));
6465// Try to get image dimensions (basic approach)
66let width: number | undefined;
67let height: number | undefined;
6869// For now, we'll skip dimension detection as it requires additional libraries
70// In a production environment, you might want to use a library like 'image-size'
7172// Create image record
73const imageData: CreateImageData = {
74filename,
75original_filename: file.name,
84};
8586return await createImage(imageData);
87}
8889/**
90* Store an image from base64 data (for email attachments)
91*/
92export async function storeImageFromBase64(
93base64Data: string,
94originalFilename: string,
99postSlug?: string;
100} = {}
101): Promise<BlogImage> {
102// Convert base64 to Uint8Array
103const binaryString = atob(base64Data);
110111// Validate the file
112const validation = validateImageFile({ size: fileSize, type: mimeType });
113if (!validation.valid) {
114throw new Error(validation.error);
116117// Generate unique filename
118const filename = await generateUniqueImageFilename(originalFilename);
119const blobKey = `images/${filename}`;
120121// Store in blob
122await blob.set(blobKey, bytes);
123124// Create image record
125const imageData: CreateImageData = {
126filename,
127original_filename: originalFilename,
134};
135136return await createImage(imageData);
137}
138139/**
140* Get image data from blob storage
141*/
142export async function getImageData(filename: string): Promise<{ data: Uint8Array; mimeType: string } | null> {
143const imageRecord = await getImageByFilename(filename);
144if (!imageRecord) {
145return null;
146}
147148const blobData = await blob.get(imageRecord.blob_key);
149if (!blobData) {
150return null;
169return {
170data,
171mimeType: imageRecord.mime_type
172};
173}
174175/**
176* Delete an image (both record and blob data)
177*/
178export async function deleteImageCompletely(filename: string): Promise<boolean> {
179const imageRecord = await getImageByFilename(filename);
180if (!imageRecord) {
181return false;
182}
183184// Delete from blob storage
185await blob.delete(imageRecord.blob_key);
186187// Delete from database
188return await deleteImage(filename);
189}
190191/**
192* Process email attachments and extract images
193*/
194export async function processEmailAttachments(
196uploadedByEmail: string,
197postSlug?: string
198): Promise<BlogImage[]> {
199const images: BlogImage[] = [];
200201for (const attachment of attachments) {
202if (isSupportedImageType(attachment.contentType)) {
203try {
204const image = await storeImageFromBase64(
205attachment.content,
206attachment.filename,
209{ postSlug }
210);
211images.push(image);
212console.log(`✅ Processed email attachment image: ${attachment.filename} -> ${image.filename}`);
213} catch (error) {
214console.error(`❌ Failed to process email attachment ${attachment.filename}:`, error);
217}
218219return images;
220}
221222/**
223* Generate image URL for serving
224*/
225export function getImageUrl(filename: string, baseUrl?: string): string {
226const base = baseUrl || '';
227return `${base}/images/${filename}`;
228}
229230/**
231* Replace image references in content with proper URLs and add unreferenced attachments
232*
233* This function handles two main scenarios:
234* 1. Inline images: Replaces various email client image reference patterns (cid:, src=, etc.) with proper URLs
235* 2. Attachment images: Adds images that were attached but not referenced in the email content
236*
237* @param content - The email content (HTML or text)
238* @param images - Array of processed images from email attachments
239* @param baseUrl - Base URL for generating image URLs
240* @returns Updated content with proper image references and unreferenced attachments
241*/
242export function replaceImageReferencesInContent(content: string, images: BlogImage[], baseUrl?: string): string {
243let updatedContent = content;
244const referencedImages = new Set<string>();
245246// If no images, return content as-is
247if (images.length === 0) {
248return updatedContent;
249}
250251for (const image of images) {
252const imageUrl = getImageUrl(image.filename, baseUrl);
253const originalFilename = image.original_filename;
254
255// Escape special regex characters in filename
258// Replace various possible references to the original filename
259const patterns = [
260// Content-ID references (most common for inline images)
261new RegExp(`cid:${escapedFilename}`, 'gi'),
262new RegExp(`cid:"${escapedFilename}"`, 'gi'),
277
278// Email client specific patterns
279new RegExp(`\\[image: ${escapedFilename}\\]`, 'gi'),
280new RegExp(`\\[Inline image ${escapedFilename}\\]`, 'gi'),
281new RegExp(`<${escapedFilename}>`, 'gi'),
282];
283284let imageWasReferenced = false;
285286for (const pattern of patterns) {
287const matches = updatedContent.match(pattern);
288if (matches && matches.length > 0) {
289imageWasReferenced = true;
290referencedImages.add(image.filename);
291
292if (pattern.source.includes('src=')) {
293// Replace src attributes
294updatedContent = updatedContent.replace(pattern, `src="${imageUrl}"`);
295} else if (pattern.source.includes('cid:')) {
296// Replace Content-ID references
297updatedContent = updatedContent.replace(pattern, imageUrl);
298} else if (pattern.source.includes('alt=') || pattern.source.includes('title=')) {
299// For alt/title attributes, just update the reference but keep the structure
300updatedContent = updatedContent.replace(pattern, (match) => {
301return match.replace(originalFilename, imageUrl);
302});
303} else {
304// Replace other references with proper img tags
305updatedContent = updatedContent.replace(pattern, `<img src="${imageUrl}" alt="${image.alt_text || image.original_filename}" style="max-width: 100%; height: auto;" />`);
306}
307}
313if (brokenMatches) {
314for (const match of brokenMatches) {
315// Check if this img tag might be referencing our image
316if (match.includes(originalFilename) ||
317match.includes('src=""') ||
320match.includes("src='cid:")) {
321
322imageWasReferenced = true;
323referencedImages.add(image.filename);
324
325// Extract alt text if present
326const altMatch = match.match(/alt=["']([^"']*)["']/i);
327const altText = altMatch ? altMatch[1] : (image.alt_text || image.original_filename);
328
329// Replace with working img tag
330const newImgTag = `<img src="${imageUrl}" alt="${altText}" style="max-width: 100%; height: auto;" />`;
331updatedContent = updatedContent.replace(match, newImgTag);
332}
341const cidMatches = [...updatedContent.matchAll(cidPattern)];
342
343if (cidMatches.length > 0 && images.length > 0) {
344console.log(`🔍 Found ${cidMatches.length} unmatched CID references, using first available image`);
345
346// Use the first available image for unmatched CID references
347const firstImage = images[0];
348const firstImageUrl = getImageUrl(firstImage.filename, baseUrl);
349
350for (const match of cidMatches) {
352const cidId = match[1];
353
354console.log(`🔄 Replacing unmatched CID: ${cidId} with ${firstImageUrl}`);
355updatedContent = updatedContent.replace(fullMatch, `src="${firstImageUrl}"`);
356referencedImages.add(firstImage.filename);
357}
358}
359360// Add unreferenced attachment images at the end of the content
361const unreferencedImages = images.filter(image => !referencedImages.has(image.filename));
362
363if (unreferencedImages.length > 0) {
364console.log(`📎 Adding ${unreferencedImages.length} unreferenced attachment images to content`);
365
366// Add a section for attachments
368attachmentSection += '<h3 style="margin-bottom: 1rem; color: #374151;">Attachments:</h3>\n';
369
370for (const image of unreferencedImages) {
371const imageUrl = getImageUrl(image.filename, baseUrl);
372attachmentSection += `<div style="margin-bottom: 1rem;">\n`;
373attachmentSection += ` <img src="${imageUrl}" alt="${image.alt_text || image.original_filename}" style="max-width: 100%; height: auto; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);" />\n`;
374if (image.alt_text || image.original_filename !== image.filename) {
375attachmentSection += ` <p style="margin-top: 0.5rem; font-size: 0.875rem; color: #6b7280; font-style: italic;">${image.alt_text || image.original_filename}</p>\n`;
376}
377attachmentSection += `</div>\n`;
postherousindex.ts113 matches
5deletePostsByAuthorEmail,
6getActiveFollowers,
7getAllImages,
8getAllPosts,
9getEmailVerificationByToken,
10getFollowerCount,
11getImagesByPostSlug,
12getImagesByUser,
13getPostActivityCounts,
14getPostBySlug,
29import { isEmailAllowed } from "./services/email-security.ts";
30import {
31deleteImageCompletely,
32getImageData,
33getImageUrl,
34storeImageFromFile,
35validateImageFile,
36} from "./services/images.ts";
37import { generateRSSFeed } from "./services/rss.ts";
38import { extractDomain, generateWebFingerResponse, isValidWebFingerResource } from "./services/webfinger.ts";
215
216<!-- Twitter -->
217<meta property="twitter:card" content="summary_large_image">
218<meta property="twitter:url" content="${baseUrl}">
219<meta property="twitter:title" content="${escapeHtml(config.blog_title)}">
225<link rel="alternate" type="application/activity+json" href="${baseUrl}/actor" />
226<!-- Favicon -->
227<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${config.blog_icon}</text></svg>">
228
229<!-- Styles -->
319
320<!-- Twitter -->
321<meta property="twitter:card" content="summary_large_image">
322<meta property="twitter:url" content="${postUrl}">
323<meta property="twitter:title" content="${escapeHtml(post.title)}">
329
330<!-- Favicon -->
331<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${config.blog_icon}</text></svg>">
332
333<!-- Styles -->
1417icon: follower.actor_icon_url
1418? {
1419type: "Image",
1420url: follower.actor_icon_url,
1421}
1454icon: follower.actor_icon_url
1455? {
1456type: "Image",
1457url: follower.actor_icon_url,
1458}
1750});
17511752// ===== IMAGE ROUTES =====
17531754// Serve image upload page
1755app.get("/upload", async c => {
1756try {
1769<div class="max-w-md mx-auto bg-white rounded-lg shadow-md p-6 text-center">
1770<h1 class="text-2xl font-bold text-gray-900 mb-4">Upload Disabled</h1>
1771<p class="text-gray-600 mb-4">Image upload is currently disabled.</p>
1772<p class="text-sm text-gray-500">Administrator: Set the UPLOAD_PASSWORD environment variable to enable uploads.</p>
1773<a href="/" class="text-blue-600 hover:text-blue-800 text-sm">← Back to Blog</a>
1922});
19231924// Serve images
1925app.get("/images/:filename", async c => {
1926try {
1927await ensureDbInitialized();
1928const filename = c.req.param("filename");
19291930const imageData = await getImageData(filename);
1931if (!imageData) {
1932return c.text("Image not found", 404);
1933}
19341935// Return the image data as a proper Response
1936return new Response(imageData.data, {
1937headers: {
1938"Content-Type": imageData.mimeType,
1939"Cache-Control": "public, max-age=31536000", // Cache for 1 year
1940},
1941});
1942} catch (error) {
1943console.error("Error serving image:", error);
1944return c.text("Error serving image", 500);
1945}
1946});
19471948// Upload image endpoint
1949app.post("/api/images/upload", async c => {
1950try {
1951await ensureDbInitialized();
1953// Get the uploaded file and form data
1954const body = await c.req.formData();
1955const file = body.get("image") as File;
1956const password = body.get("password") as string;
1957const uploaderEmail = body.get("email") as string;
19601961if (!file) {
1962return c.json({ error: "No image file provided" }, 400);
1963}
19641979console.log(`✅ Valid upload password from ${uploaderEmail}`);
19801981// Store the image
1982const image = await storeImageFromFile(file, uploaderEmail, {
1983altText: altText || undefined,
1984postSlug: postSlug || undefined,
19861987const baseUrl = getBaseUrl(c);
1988const imageUrl = getImageUrl(image.filename, baseUrl);
19891990return c.json({
1991success: true,
1992image: {
1993id: image.id,
1994filename: image.filename,
1995originalFilename: image.original_filename,
1996url: imageUrl,
1997mimeType: image.mime_type,
1998fileSize: image.file_size,
1999altText: image.alt_text,
2000postSlug: image.post_slug,
2001createdAt: image.created_at,
2002},
2003});
2004} catch (error) {
2005console.error("Error uploading image:", error);
2006return c.json({ error: error.message || "Failed to upload image" }, 500);
2007}
2008});
20092010// Get images for a specific post
2011app.get("/api/posts/:slug/images", async c => {
2012try {
2013await ensureDbInitialized();
2014const slug = c.req.param("slug");
20152016const images = await getImagesByPostSlug(slug);
2017const baseUrl = getBaseUrl(c);
20182019return c.json({
2020images: images.map(image => ({
2021id: image.id,
2022filename: image.filename,
2023originalFilename: image.original_filename,
2024url: getImageUrl(image.filename, baseUrl),
2025mimeType: image.mime_type,
2026fileSize: image.file_size,
2027altText: image.alt_text,
2028createdAt: image.created_at,
2029})),
2030});
2031} catch (error) {
2032console.error("Error fetching post images:", error);
2033return c.json({ error: "Failed to fetch images" }, 500);
2034}
2035});
20362037// Get images uploaded by a user
2038app.get("/api/images/user/:email", async c => {
2039try {
2040await ensureDbInitialized();
2047}
20482049const images = await getImagesByUser(email);
2050const baseUrl = getBaseUrl(c);
20512052return c.json({
2053images: images.map(image => ({
2054id: image.id,
2055filename: image.filename,
2056originalFilename: image.original_filename,
2057url: getImageUrl(image.filename, baseUrl),
2058mimeType: image.mime_type,
2059fileSize: image.file_size,
2060altText: image.alt_text,
2061postSlug: image.post_slug,
2062createdAt: image.created_at,
2063})),
2064});
2065} catch (error) {
2066console.error("Error fetching user images:", error);
2067return c.json({ error: "Failed to fetch images" }, 500);
2068}
2069});
20702071// Delete an image
2072app.delete("/api/images/:filename", async c => {
2073try {
2074await ensureDbInitialized();
2080}
20812082const success = await deleteImageCompletely(filename);
20832084if (!success) {
2085return c.json({ error: "Image not found" }, 404);
2086}
20872088return c.json({ success: true, message: "Image deleted successfully" });
2089} catch (error) {
2090console.error("Error deleting image:", error);
2091return c.json({ error: "Failed to delete image" }, 500);
2092}
2093});
20942095// Get all images (admin endpoint)
2096app.get("/api/images", async c => {
2097try {
2098await ensureDbInitialized();
2106const offset = parseInt(c.req.query("offset") || "0");
21072108const images = await getAllImages(limit, offset);
2109const baseUrl = getBaseUrl(c);
21102111return c.json({
2112images: images.map(image => ({
2113id: image.id,
2114filename: image.filename,
2115originalFilename: image.original_filename,
2116url: getImageUrl(image.filename, baseUrl),
2117mimeType: image.mime_type,
2118fileSize: image.file_size,
2119altText: image.alt_text,
2120postSlug: image.post_slug,
2121uploadedByEmail: image.uploaded_by_email,
2122createdAt: image.created_at,
2123})),
2124pagination: {
2125limit,
2126offset,
2127hasMore: images.length === limit,
2128},
2129});
2130} catch (error) {
2131console.error("Error fetching all images:", error);
2132return c.json({ error: "Failed to fetch images" }, 500);
2133}
2134});
21352136// Serve images from blob storage
2137app.get("/images/:filename", async c => {
2138try {
2139const filename = c.req.param("filename");
2143}
21442145console.log(`📷 Serving image: ${filename}`);
2146
2147const imageData = await getImageData(filename);
2148
2149if (!imageData) {
2150console.log(`❌ Image not found: ${filename}`);
2151return c.text("Image not found", 404);
2152}
21532154console.log(`✅ Image found: ${filename}, size: ${imageData.data.length} bytes, type: ${imageData.mimeType}`);
21552156// Set appropriate headers
2157const headers = {
2158"Content-Type": imageData.mimeType,
2159"Content-Length": imageData.data.length.toString(),
2160"Cache-Control": "public, max-age=31536000", // Cache for 1 year
2161"ETag": `"${filename}"`,
2162};
21632164return new Response(imageData.data, { headers });
2165} catch (error) {
2166console.error("Error serving image:", error);
2167return c.text("Error serving image", 500);
2168}
2169});
13}
1415function createManifest(images: any[], label: string, uuid: string) {
16const builder = new IIIFBuilder();
17const id = iiifBaseUrl + uuid;
31profile: "https://djehuty.4tu.nl/#x1-490005",
32}]);
33if (images.length) {
34for (const [index, item] of images.entries()) {
35manifest.createCanvas(id + "/canvas/" + item.meta.uuid, (canvas) => {
36canvas.height = item.height;
39canvas.addRendering({
40id: item.meta.download_url,
41type: "Image",
42label: { "en": ["Download original image"] },
43format: "image/tiff",
44});
45canvas.createAnnotation(id + "/annotation/" + item.meta.uuid, {
49body: {
50id: item.id + "/full/max/0/default.jpg",
51type: "Image",
52format: "image/jpeg",
53height: item.height,
54width: item.width,
83return Response.json({ error: "Please provide a valid identifier" });
84}
85const images = await Promise.all(
86metadata.files.map((i: any) =>
87fetchJson(iiifBaseUrl + i.uuid + "/info.json").then((resp) => {
96);
97const manifest = createManifest(
98images.filter((img: any) => img),
99metadata.title,
100datasetUuid,
219onFrame: async () => {
220if (videoElement.readyState >= 3) {
221await hands.send({ image: videoElement });
222}
223},
FFS_Frame_Custom_actionsmain.ts4 matches
137],
138};
139case "generate.image":
140return {
141title: "Generate Image",
142description: "Firefly text-to-image.",
143fields: [{
144type: "textarea",
151return {
152title: "Remove Background",
153description: "Remove the background of the selected image.",
154fields: [{
155type: "boolean",
Townie-Al2val-summary.ts3 matches
16SUM(cache_write_tokens) as total_cache_write_tokens,
17SUM(price) as total_price,
18SUM(num_images) as total_images
19FROM ${USAGE_TABLE}
20WHERE val_id = ? AND our_api_token = 1
54total_cache_write_tokens: 0,
55total_price: 0,
56total_images: 0
57};
58
85// Always include inference price for comparison
86inference_price: inferenceSummary.inference_price || 0,
87total_images: usageSummary.total_images,
88// Add flag to indicate inference data usage
89used_inference_data: !!inferenceSummary.inference_price,
Townie-Al2val-detail.ts6 matches
17price?: number;
18finish_reason?: string;
19num_images?: number;
20our_api_token: boolean;
21}
31inference_price: number;
32original_price?: number;
33total_images: number;
34used_inference_data?: boolean;
35inference_price_primary?: boolean;
66<th>Cache Write</th>
67<th>Total Price</th>
68<th>Images</th>
69</tr>
70</thead>
76<td>${formatNumber(summary.total_cache_write_tokens)}</td>
77<td class="price">${formatPrice(summary.total_price)}</td>
78<td>${formatNumber(summary.total_images)}</td>
79</tr>
80</tbody>
97<th>Price</th>
98<th>Finish</th>
99<th>Images</th>
100</tr>
101</thead>
114<td class="price">${formatPrice(row.price)}</td>
115<td>${row.finish_reason || '-'}</td>
116<td>${formatNumber(row.num_images)}</td>
117</tr>
118`).join("")}
Townie-Al2user-summary.ts2 matches
18SUM(cache_write_tokens) as total_cache_write_tokens,
19SUM(price) as total_price,
20SUM(num_images) as total_images
21FROM ${USAGE_TABLE}
22WHERE our_api_token = 1
151total_price: userData.price,
152inference_price: inferencePriceByUser.get(userId) || 0,
153total_images: 0,
154used_inference_data: true
155});
Townie-Al2user-detail.ts7 matches
13total_price: number;
14inference_price: number;
15total_images: number;
16used_inference_data?: boolean;
17}
32price?: number;
33finish_reason?: string;
34num_images?: number;
35our_api_token: boolean;
36}
48total_price: 0,
49inference_price: 0,
50total_images: 0
51};
52
77<th>Total Price</th>
78<th>Inference Price</th>
79<th>Images</th>
80</tr>
81</thead>
88<td class="price">${formatPrice(userData.total_price)} ${userData.used_inference_data ? '<span class="badge badge-info" title="Using inference data">I</span>' : ''}</td>
89<td class="price">${formatPrice(userData.inference_price || 0)}</td>
90<td>${formatNumber(userData.total_images)}</td>
91</tr>
92</tbody>
135<th>Price</th>
136<th>Finish</th>
137<th>Images</th>
138</tr>
139</thead>
152<td class="price">${formatPrice(row.price)}</td>
153<td>${row.finish_reason || '-'}</td>
154<td>${formatNumber(row.num_images)}</td>
155</tr>
156`).join("")}
Townie-Al2useChatLogic.ts4 matches
7branchId: string | undefined;
8selectedFiles: string[];
9images: (string | null)[];
10soundEnabled: boolean;
11}
20// bearerToken,
21selectedFiles,
22images,
23soundEnabled,
24}: UseChatLogicProps) {
44branchId,
45selectedFiles,
46images: images
47.filter((img): img is string => {
48const isValid = typeof img === "string" && img.startsWith("data:");
49if (!isValid && img !== null) {
50console.warn(
51"Invalid image format:",
52img?.substring(0, 50) + "..."
53);