improve favicons
This commit is contained in:
@@ -17,8 +17,10 @@
|
||||
"@mixan/queue": "workspace:*",
|
||||
"@mixan/redis": "workspace:*",
|
||||
"fastify": "^4.25.2",
|
||||
"ico-to-png": "^0.2.1",
|
||||
"pino": "^8.17.2",
|
||||
"ramda": "^0.29.1",
|
||||
"sharp": "^0.33.2",
|
||||
"ua-parser-js": "^1.0.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,83 +1,129 @@
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import icoToPng from 'ico-to-png';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { createHash } from '@mixan/common';
|
||||
import { redis } from '@mixan/redis';
|
||||
|
||||
interface GetFaviconParams {
|
||||
url: string;
|
||||
}
|
||||
|
||||
function toBuffer(arrayBuffer: ArrayBuffer) {
|
||||
const buffer = Buffer.alloc(arrayBuffer.byteLength);
|
||||
const view = new Uint8Array(arrayBuffer);
|
||||
for (let i = 0; i < buffer.length; ++i) {
|
||||
buffer[i] = view[i]!;
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
async function getImageBuffer(url: string) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const contentType = res.headers.get('content-type');
|
||||
|
||||
async function getUrlBuffer(url: string) {
|
||||
const arrayBuffer = await fetch(url).then((res) => {
|
||||
if (res.ok) {
|
||||
return res.arrayBuffer();
|
||||
if (!contentType?.includes('image')) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if (arrayBuffer) {
|
||||
return toBuffer(arrayBuffer);
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (contentType === 'image/x-icon' || url.endsWith('.ico')) {
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
return await icoToPng(buffer, 30);
|
||||
}
|
||||
|
||||
return await sharp(await res.arrayBuffer())
|
||||
.resize(30, 30, {
|
||||
fit: 'cover',
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
} catch (e) {
|
||||
console.log('Failed to get image from url', url);
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageExtensions = ['svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico'];
|
||||
|
||||
export async function getFavicon(
|
||||
request: FastifyRequest<{
|
||||
Querystring: GetFaviconParams;
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
function sendBuffer(buffer: Buffer, cacheKey?: string) {
|
||||
if (cacheKey) {
|
||||
redis.set(`favicon:${cacheKey}`, buffer.toString('base64'));
|
||||
}
|
||||
reply.type('image/png');
|
||||
console.log('buffer', buffer.byteLength);
|
||||
|
||||
return reply.send(buffer);
|
||||
}
|
||||
|
||||
if (!request.query.url) {
|
||||
return reply.status(404).send('Not found');
|
||||
}
|
||||
|
||||
function sendBuffer(buffer: Buffer, hostname?: string) {
|
||||
if (hostname) {
|
||||
redis.set(`favicon:${hostname}`, buffer.toString('base64'));
|
||||
const url = decodeURIComponent(request.query.url);
|
||||
|
||||
// DIRECT IMAGE
|
||||
if (imageExtensions.find((ext) => url.endsWith(ext))) {
|
||||
const cacheKey = createHash(url, 32);
|
||||
const cache = await redis.get(`favicon:${cacheKey}`);
|
||||
if (cache) {
|
||||
return sendBuffer(Buffer.from(cache, 'base64'));
|
||||
}
|
||||
const buffer = await getImageBuffer(url);
|
||||
if (buffer && buffer.byteLength > 0) {
|
||||
return sendBuffer(buffer, cacheKey);
|
||||
}
|
||||
reply.type('image/png');
|
||||
return reply.send(buffer);
|
||||
}
|
||||
|
||||
const url = decodeURIComponent(request.query.url);
|
||||
const { hostname, origin } = new URL(url);
|
||||
|
||||
const cache = await redis.get(`favicon:${hostname}`);
|
||||
if (cache) {
|
||||
return sendBuffer(Buffer.from(cache, 'base64'));
|
||||
}
|
||||
|
||||
// Try just get the favicon.ico
|
||||
const buffer = await getUrlBuffer(`${origin}/favicon.ico`);
|
||||
if (buffer) {
|
||||
// TRY FAVICON.ICO
|
||||
const buffer = await getImageBuffer(`${origin}/favicon.ico`);
|
||||
console.log('buffer', buffer?.length);
|
||||
|
||||
if (buffer && buffer.byteLength > 0) {
|
||||
return sendBuffer(buffer, hostname);
|
||||
}
|
||||
|
||||
// If that didnt work try parse html
|
||||
// PARSE HTML
|
||||
const res = await fetch(url).then((res) => res.text());
|
||||
const favicon =
|
||||
res.match(/<link.*?rel="icon".*?href="(.+?)".*?>/) ||
|
||||
res.match(/<link.*?rel="shortcut icon".*?href="(.+?)".*?>/);
|
||||
|
||||
if (favicon?.[1]) {
|
||||
const faviconUrl = favicon[1].startsWith('http')
|
||||
? favicon[1]
|
||||
: `${origin}${favicon[1]}`;
|
||||
function findFavicon(res: string) {
|
||||
const match = res.match(
|
||||
/(\<link(.+?)image\/x-icon(.+?)\>|\<link(.+?)shortcut\sicon(.+?)\>)/
|
||||
);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const buffer = await getUrlBuffer(faviconUrl);
|
||||
return match[0].match(/href="(.+?)"/)?.[1] ?? null;
|
||||
}
|
||||
|
||||
if (buffer) {
|
||||
const favicon = findFavicon(res);
|
||||
if (favicon) {
|
||||
const buffer = await getImageBuffer(favicon);
|
||||
|
||||
if (buffer && buffer.byteLength > 0) {
|
||||
return sendBuffer(buffer, hostname);
|
||||
}
|
||||
}
|
||||
|
||||
return reply.status(404).send('Not found');
|
||||
}
|
||||
|
||||
export async function clearFavicons(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const keys = await redis.keys('favicon:*');
|
||||
for (const key of keys) {
|
||||
await redis.del(key);
|
||||
}
|
||||
return reply.status(404).send('OK');
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ const miscRouter: FastifyPluginCallback = (fastify, opts, done) => {
|
||||
handler: controller.getFavicon,
|
||||
});
|
||||
|
||||
fastify.route({
|
||||
method: 'GET',
|
||||
url: '/favicon/clear',
|
||||
handler: controller.clearFavicons,
|
||||
});
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"react-in-viewport": "1.0.0-alpha.30",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-social-icons": "^6.12.0",
|
||||
"react-svg-worldmap": "2.0.0-alpha.16",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-use-websocket": "^4.7.0",
|
||||
|
||||
@@ -1,64 +1,108 @@
|
||||
import { useMemo } from 'react';
|
||||
import { NOT_SET_VALUE } from '@/utils/constants';
|
||||
import type { LucideIcon, LucideProps } from 'lucide-react';
|
||||
import {
|
||||
ActivityIcon,
|
||||
ExternalLinkIcon,
|
||||
HelpCircleIcon,
|
||||
MailIcon,
|
||||
MonitorIcon,
|
||||
MonitorPlayIcon,
|
||||
PhoneIcon,
|
||||
PodcastIcon,
|
||||
ScanIcon,
|
||||
SearchIcon,
|
||||
SmartphoneIcon,
|
||||
SquareAsteriskIcon,
|
||||
TabletIcon,
|
||||
TabletSmartphoneIcon,
|
||||
TwitterIcon,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
getKeys,
|
||||
getNetworks,
|
||||
networkFor,
|
||||
register,
|
||||
SocialIcon,
|
||||
} from 'react-social-icons';
|
||||
|
||||
interface SerieIconProps extends LucideProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
function getProxyImage(url: string) {
|
||||
return `${String(process.env.NEXT_PUBLIC_API_URL)}/misc/favicon?url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
|
||||
const createImageIcon = (url: string) => {
|
||||
return function (props: LucideProps) {
|
||||
return <img className="w-4 h-4 object-cover rounded" src={url} />;
|
||||
} as LucideIcon;
|
||||
};
|
||||
|
||||
const mapper: Record<string, LucideIcon> = {
|
||||
// Events
|
||||
screen_view: MonitorPlayIcon,
|
||||
session_start: ActivityIcon,
|
||||
session_end: ActivityIcon,
|
||||
link_out: ExternalLinkIcon,
|
||||
|
||||
// Websites
|
||||
google: createImageIcon(getProxyImage('https://google.com')),
|
||||
facebook: createImageIcon(getProxyImage('https://facebook.com')),
|
||||
bing: createImageIcon(getProxyImage('https://bing.com')),
|
||||
twitter: createImageIcon(getProxyImage('https://x.com')),
|
||||
duckduckgo: createImageIcon(getProxyImage('https://duckduckgo.com')),
|
||||
'yahoo!': createImageIcon(getProxyImage('https://yahoo.com')),
|
||||
instagram: createImageIcon(getProxyImage('https://instagram.com')),
|
||||
gmail: createImageIcon(getProxyImage('https://mail.google.com/')),
|
||||
|
||||
'mobile safari': createImageIcon(
|
||||
getProxyImage(
|
||||
'https://upload.wikimedia.org/wikipedia/commons/5/52/Safari_browser_logo.svg'
|
||||
)
|
||||
),
|
||||
chrome: createImageIcon(
|
||||
getProxyImage(
|
||||
'https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg'
|
||||
)
|
||||
),
|
||||
'samsung internet': createImageIcon(
|
||||
getProxyImage(
|
||||
'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/Samsung_Internet_logo.svg/1024px-Samsung_Internet_logo.svg.png'
|
||||
)
|
||||
),
|
||||
safari: createImageIcon(
|
||||
getProxyImage(
|
||||
'https://upload.wikimedia.org/wikipedia/commons/5/52/Safari_browser_logo.svg'
|
||||
)
|
||||
),
|
||||
edge: createImageIcon(
|
||||
getProxyImage(
|
||||
'https://upload.wikimedia.org/wikipedia/commons/7/7e/Microsoft_Edge_logo_%282019%29.png'
|
||||
)
|
||||
),
|
||||
firefox: createImageIcon(
|
||||
getProxyImage(
|
||||
'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Firefox_logo%2C_2019.svg/1920px-Firefox_logo%2C_2019.svg.png'
|
||||
)
|
||||
),
|
||||
snapchat: createImageIcon(getProxyImage('https://snapchat.com')),
|
||||
|
||||
// Misc
|
||||
mobile: SmartphoneIcon,
|
||||
desktop: MonitorIcon,
|
||||
tablet: TabletIcon,
|
||||
[NOT_SET_VALUE]: HelpCircleIcon,
|
||||
search: SearchIcon,
|
||||
social: PodcastIcon,
|
||||
email: MailIcon,
|
||||
unknown: HelpCircleIcon,
|
||||
[NOT_SET_VALUE]: ScanIcon,
|
||||
};
|
||||
|
||||
const networks = getNetworks();
|
||||
|
||||
register('duckduckgo', {
|
||||
color: 'red',
|
||||
path: 'https://duckduckgo.com/favicon.ico',
|
||||
});
|
||||
|
||||
export function SerieIcon({ name, ...props }: SerieIconProps) {
|
||||
let Icon = mapper[name] ?? null;
|
||||
const Icon = useMemo(() => {
|
||||
const mapped = mapper[name.toLowerCase()] ?? null;
|
||||
|
||||
if (name.includes('http')) {
|
||||
Icon = ((_props) => (
|
||||
<img
|
||||
className="w-4 h-4 object-cover"
|
||||
src={`${String(process.env.NEXT_PUBLIC_API_URL)}/misc/favicon?url=${encodeURIComponent(name)}`}
|
||||
/>
|
||||
)) as LucideIcon;
|
||||
}
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
|
||||
if (Icon === null && networks.includes(name.toLowerCase())) {
|
||||
Icon = ((_props) => (
|
||||
<SocialIcon network={name.toLowerCase()} />
|
||||
)) as LucideIcon;
|
||||
}
|
||||
if (name.includes('http')) {
|
||||
return createImageIcon(getProxyImage(name));
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [name]);
|
||||
|
||||
return Icon ? (
|
||||
<div className="w-4 h-4 flex-shrink-0 relative [&_a]:!w-4 [&_a]:!h-4 [&_svg]:!rounded">
|
||||
|
||||
Reference in New Issue
Block a user