Skip to content

Commit

Permalink
Finish flushing out 'affiliations' abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
jmduke committed Sep 14, 2024
1 parent 1b426e6 commit c025fd8
Show file tree
Hide file tree
Showing 18 changed files with 886 additions and 709 deletions.
61 changes: 39 additions & 22 deletions app/api/domains/[domain]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,48 @@ import fetch from "@/lib/data";
import { reify } from "@/lib/db/domains";

const SOCIAL_MEDIA_SERVICES = [
"facebook",
"twitter",
"instagram",
"linkedin",
"youtube",
"github",
"facebook",
"twitter",
"instagram",
"linkedin",
"youtube",
"github",
];

export async function GET(
request: Request,
context: {
params: {
domain: string;
};
}
request: Request,
context: {
params: {
domain: string;
};
},
) {
const rawResponse = await fetch(context.params.domain);
await reify(context.params.domain, rawResponse);
const rawResponse = await fetch(context.params.domain);
await reify(context.params.domain, rawResponse);
console.log(rawResponse.detected_technologies);

return Response.json({
domain: context.params.domain,
records: rawResponse.data.filter((datum) => datum.label === "DNS").flatMap((datum) => datum.data),
ranking: rawResponse.data.find((datum) => datum.label === "Tranco")?.data[0]?.value,
services: rawResponse.detected_technologies.filter((technology) => technology.identifier !== "subdomain").map((technology) => technology.identifier).sort(),
subdomains: rawResponse.detected_technologies.filter((technology) => technology.identifier === "subdomain").map((technology) => technology.metadata.value).sort(),
social_media: Object.fromEntries(SOCIAL_MEDIA_SERVICES.map(service => [service, rawResponse.detected_technologies.find((note) => note.identifier === service)?.metadata.username]))
});
return Response.json({
domain: context.params.domain,
records: rawResponse.data
.filter((datum) => datum.label === "DNS")
.flatMap((datum) => datum.data),
ranking: rawResponse.data.find((datum) => datum.label === "Tranco")?.data[0]
?.value,
services: rawResponse.detected_technologies
.filter((technology) => technology.identifier !== "subdomain")
.map((technology) => technology.identifier)
.sort(),
subdomains: rawResponse.detected_technologies
.filter((technology) => technology.identifier === "subdomain")
.map((technology) => technology.metadata.value)
.sort(),
social_media: Object.fromEntries(
SOCIAL_MEDIA_SERVICES.map((service) => [
service,
rawResponse.detected_technologies.find(
(note) => note.identifier === service,
)?.metadata.username,
]),
),
});
}
285 changes: 144 additions & 141 deletions app/domain/[domain]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,171 +1,174 @@
import DomainIcon from "@/components/DomainIcon";
import Grid from "@/components/Grid";
import SectionHeader from "@/components/SectionHeader";
import { REGISTRY as AFFILIATIONS_REGISTRY } from "@/lib/affiliations/registry";
import fetch from "@/lib/data";
import { reify } from "@/lib/db/domains";
import { GENRE_REGISTRY, REGISTRY } from "@/lib/services";
import { Metadata, ResolvingMetadata } from "next";
import type { Metadata, ResolvingMetadata } from "next";

type Props = {
params: { domain: string };
params: { domain: string };
};

const SOCIAL_MEDIA_URL_TEMPLATES: { [key: string]: string } = {
twitter: "https://twitter.com/",
linkedin: "https://linkedin.com/in/",
facebook: "https://facebook.com/",
instagram: "https://instagram.com/",
youtube: "https://youtube.com/",
tiktok: "https://tiktok.com/@",
bluesky: "https://bsky.social/",
github: "https://github.com/",
twitter: "https://twitter.com/",
linkedin: "https://linkedin.com/in/",
facebook: "https://facebook.com/",
instagram: "https://instagram.com/",
youtube: "https://youtube.com/",
tiktok: "https://tiktok.com/@",
bluesky: "https://bsky.social/",
github: "https://github.com/",
};

const generateURLForSocialMedia = (
service: string,
username: string
service: string,
username: string,
): string => {
const template = SOCIAL_MEDIA_URL_TEMPLATES[service];
return template ? `${template}${username}` : "";
const template = SOCIAL_MEDIA_URL_TEMPLATES[service];
return template ? `${template}${username}` : "";
};

export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
{ params }: Props,
parent: ResolvingMetadata,
): Promise<Metadata> {
return {
title: params.domain + " - shovel.report",
description:
"Information about " +
params.domain +
" and its DNS records, technologies, social media and more.",
alternates: {
canonical: `/domain/${params.domain}`,
},
};
return {
title: `${params.domain} - shovel.report`,
description: `Information about ${params.domain} and its DNS records, technologies, social media and more.`,
alternates: {
canonical: `/domain/${params.domain}`,
},
};
}

function formatJson(json: string) {
try {
return {
valid: true,
value: JSON.stringify(JSON.parse(json || "{}"), null, 2),
};
} catch {
return { valid: false, value: json };
}
try {
return {
valid: true,
value: JSON.stringify(JSON.parse(json || "{}"), null, 2),
};
} catch {
return { valid: false, value: json };
}
}

export default async function Page({
params,
params,
}: {
params: {
domain: string;
};
params: {
domain: string;
};
}) {
const data = await fetch(params.domain);
if (!process.env.DISABLE_DATABASE) {
await reify(params.domain, data);
}
const data = await fetch(params.domain);
if (!process.env.DISABLE_DATABASE) {
await reify(params.domain, data);
}

const jsonld = data.detected_technologies.find(
(datum) => datum.identifier === "jsonld"
)?.metadata.value;
const formattedJsonLd = formatJson(jsonld ?? "{}");
const jsonld = data.detected_technologies.find(
(datum) => datum.identifier === "jsonld",
)?.metadata.value;
const formattedJsonLd = formatJson(jsonld ?? "{}");

return (
<div className="">
<h1>
<a
href={`https://${params.domain}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 hover:bg-white/20 transition-colors font-black text-xl"
>
<DomainIcon domain={params.domain} />
<span>{params.domain}</span>
</a>
</h1>
<SectionHeader>DNS Records</SectionHeader>
<table className="">
<tbody>
{data.data
.filter((datum) => datum.label === "DNS")
.flatMap((datum) =>
datum.data.map((record) => (
<tr key={record.value}>
<td className="pr-4">{record.type}</td>
<td className="">{record.value}</td>
</tr>
))
)}
</tbody>
</table>
<SectionHeader>Tranco ranking</SectionHeader>
<ul>
{data.data
.filter((datum) => datum.label === "Tranco")
.flatMap((datum) =>
datum.data.map((record) => (
<li key={record.value}>#{record.value}</li>
))
)}
<ul className="only:block hidden opacity-50">No Tranco record found</ul>
</ul>
<Grid.Container title="Subdomains">
{data.detected_technologies
.filter((datum) => datum.identifier === "subdomain")
.map((note, i) => (
<Grid.Item key={i} url={`/domain/${note.metadata.value}`}>
{note.metadata.value}
</Grid.Item>
))}
</Grid.Container>
<Grid.Container title="Services">
{data.detected_technologies
.filter((datum) => datum.identifier !== "subdomain")
.filter((note) => REGISTRY[note.identifier])
.map((note, i) => (
<Grid.Item
key={i}
url={`/technology/${note.identifier}`}
domain={new URL(REGISTRY[note.identifier]?.url).hostname}
>
<div>{REGISTRY[note.identifier]?.name}</div>
<div className="text-gray-400 text-sm">
{GENRE_REGISTRY[REGISTRY[note.identifier]?.genre].name}
</div>
</Grid.Item>
))}
</Grid.Container>
<Grid.Container title="Social media">
{data.detected_technologies
.filter((note) => REGISTRY[note.identifier]?.genre === "social_media")
.map((note, i) => (
<Grid.Item
key={i}
url={generateURLForSocialMedia(
note.identifier,
note.metadata.username
)}
domain={new URL(REGISTRY[note.identifier]?.url).hostname}
>
<div>{note.metadata.username}</div>
<div className="text-gray-400 text-sm">
{REGISTRY[note.identifier]?.name}
</div>
</Grid.Item>
))}
</Grid.Container>
{jsonld && (
<>
<SectionHeader>JSON+LD</SectionHeader>
<pre className="whitespace-pre max-w-full overflow-x-scroll">
{formattedJsonLd.value}
</pre>
{!formattedJsonLd.valid && <p>(this JSON isn&apos;t valid)</p>}
</>
)}
</div>
);
return (
<div className="">
<h1>
<a
href={`https://${params.domain}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 hover:bg-white/20 transition-colors font-black text-xl"
>
<DomainIcon domain={params.domain} />
<span>{params.domain}</span>
</a>
</h1>
<SectionHeader>DNS Records</SectionHeader>
<table className="">
<tbody>
{data.data
.filter((datum) => datum.label === "DNS")
.flatMap((datum) =>
datum.data.map((record) => (
<tr key={record.value}>
<td className="pr-4">{record.type}</td>
<td className="">{record.value}</td>
</tr>
)),
)}
</tbody>
</table>
<Grid.Container title="Affiliations">
{data.detected_technologies
.filter((datum) => datum.identifier in AFFILIATIONS_REGISTRY)
.map((affiliation, i) => (
<Grid.Item
key={affiliation.identifier}
domain={AFFILIATIONS_REGISTRY[affiliation.identifier].domain}
url={`/affiliation/${affiliation.identifier}`}
>
{AFFILIATIONS_REGISTRY[affiliation.identifier].name}
</Grid.Item>
))}
</Grid.Container>
<Grid.Container title="Subdomains">
{data.detected_technologies
.filter((datum) => datum.identifier === "subdomain")
.map((note, i) => (
<Grid.Item
key={note.metadata.value}
url={`/domain/${note.metadata.value}`}
>
{note.metadata.value}
</Grid.Item>
))}
</Grid.Container>
<Grid.Container title="Services">
{data.detected_technologies
.filter((datum) => datum.identifier !== "subdomain")
.filter((note) => REGISTRY[note.identifier])
.map((note, i) => (
<Grid.Item
key={note.identifier}
url={`/technology/${note.identifier}`}
domain={new URL(REGISTRY[note.identifier]?.url).hostname}
>
<div>{REGISTRY[note.identifier]?.name}</div>
<div className="text-gray-400 text-sm">
{GENRE_REGISTRY[REGISTRY[note.identifier]?.genre].name}
</div>
</Grid.Item>
))}
</Grid.Container>
<Grid.Container title="Social media">
{data.detected_technologies
.filter((note) => REGISTRY[note.identifier]?.genre === "social_media")
.map((note, i) => (
<Grid.Item
key={note.identifier}
url={generateURLForSocialMedia(
note.identifier,
note.metadata.username,
)}
domain={new URL(REGISTRY[note.identifier]?.url).hostname}
>
<div>{note.metadata.username}</div>
<div className="text-gray-400 text-sm">
{REGISTRY[note.identifier]?.name}
</div>
</Grid.Item>
))}
</Grid.Container>
{jsonld && (
<>
<SectionHeader>JSON+LD</SectionHeader>
<pre className="whitespace-pre max-w-full overflow-x-scroll">
{formattedJsonLd.value}
</pre>
{!formattedJsonLd.valid && <p>(this JSON isn&apos;t valid)</p>}
</>
)}
</div>
);
}
Loading

0 comments on commit c025fd8

Please sign in to comment.