first commit
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 ATScan
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,16 @@
|
||||||
|
.PHONY: all
|
||||||
|
|
||||||
|
all: plc-crawl
|
||||||
|
|
||||||
|
plc-daemon:
|
||||||
|
deno run --unstable --allow-net ./backend/plc-crawler.js daemon
|
||||||
|
|
||||||
|
plc-init:
|
||||||
|
deno run --unstable --allow-net ./backend/plc-crawler.js init
|
||||||
|
|
||||||
|
plc-crawl:
|
||||||
|
deno run --unstable --allow-net ./backend/plc-crawler.js
|
||||||
|
|
||||||
|
test:
|
||||||
|
deno test --unstable --allow-read ./backend/test.js
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# ATScan
|
||||||
|
|
||||||
|
This is a monorepo containing the backend and frontend of ATScan. The current version is hosted on [atscan.net](https://atscan.net).
|
||||||
|
|
||||||
|
## Authors
|
||||||
|
- Tree ([burningtree](https://github.com/burningtree))
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT
|
|
@ -0,0 +1 @@
|
||||||
|
IPINFO_TOKEN=XXXXX
|
|
@ -0,0 +1 @@
|
||||||
|
.env
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { ATScan } from "./lib/atscan.js";
|
||||||
|
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
|
||||||
|
import { oakCors } from "https://deno.land/x/cors/mod.ts";
|
||||||
|
|
||||||
|
const ats = new ATScan();
|
||||||
|
await ats.init();
|
||||||
|
|
||||||
|
const HTTP_PORT = 6677;
|
||||||
|
const app = new Application();
|
||||||
|
|
||||||
|
const router = new Router();
|
||||||
|
router
|
||||||
|
.get("/pds", async (ctx) => {
|
||||||
|
const out = []
|
||||||
|
for (const item of (await ats.db.pds.find({}).toArray())) {
|
||||||
|
item.host = item.url.replace(/^https?:\/\//, "");
|
||||||
|
item.env = (ats.BSKY_OFFICIAL_PDS.includes(item.url) &&
|
||||||
|
item.plcs.includes("https://plc.directory"))
|
||||||
|
? "bsky"
|
||||||
|
: (item.plcs.includes("https://plc.bsky-sandbox.dev")
|
||||||
|
? "sandbox"
|
||||||
|
: null);
|
||||||
|
//item.didsCount = await ats.db.did.countDocuments({ 'pds': { $in: [ item.url ] }})
|
||||||
|
out.push(item)
|
||||||
|
}
|
||||||
|
ctx.response.body = out.filter((i) => i.env)
|
||||||
|
})
|
||||||
|
.get("/plc", async (ctx) => {
|
||||||
|
const out = []
|
||||||
|
for (const plc of ats.defaultPLC) {
|
||||||
|
plc.host = plc.url.replace(/^https?:\/\//, "");
|
||||||
|
plc.didsCount = await ats.db.did.countDocuments({ src: plc.url })
|
||||||
|
plc.lastUpdate = (await ats.db.meta.findOne({ key: `lastUpdate:${plc.url}` })).value
|
||||||
|
out.push(plc)
|
||||||
|
}
|
||||||
|
ctx.response.body = out;
|
||||||
|
})
|
||||||
|
.get("/did", async (ctx) => {
|
||||||
|
const out = []
|
||||||
|
for (const did of (await ats.db.did.find({}).sort({ time: -1 }).limit(100).toArray())) {
|
||||||
|
did.srcHost = did.src.replace(/^https?:\/\//, "");
|
||||||
|
out.push(did)
|
||||||
|
}
|
||||||
|
ctx.response.body = out;
|
||||||
|
})
|
||||||
|
.get("/:id", async (ctx) => {
|
||||||
|
if (!ctx.params.id.match(/^did\:/)) {
|
||||||
|
return ctx.status = 404;
|
||||||
|
}
|
||||||
|
const did = ctx.params.id
|
||||||
|
const item = await ats.db.did.findOne({ did })
|
||||||
|
item.env = ((item.src === "https://plc.directory")
|
||||||
|
? "bsky"
|
||||||
|
: (item.src === "https://plc.bsky-sandbox.dev")
|
||||||
|
? "sbox"
|
||||||
|
: null);
|
||||||
|
ctx.response.body = item
|
||||||
|
})
|
||||||
|
.get("/pds/:host", async (ctx) => {
|
||||||
|
const host = ctx.params.host
|
||||||
|
const item = await ats.db.pds.findOne({ url: `https://${host}` })
|
||||||
|
item.host = item.url.replace(/^https?:\/\//, "");
|
||||||
|
item.env = (ats.BSKY_OFFICIAL_PDS.includes(item.url) &&
|
||||||
|
item.plcs.includes("https://plc.directory"))
|
||||||
|
? "bsky"
|
||||||
|
: (item.plcs.includes("https://plc.bsky-sandbox.dev")
|
||||||
|
? "sandbox"
|
||||||
|
: null);
|
||||||
|
ctx.response.body = item
|
||||||
|
})
|
||||||
|
.get("/", ctx => {
|
||||||
|
ctx.response.body = "ATScan API"
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(oakCors()); // Enable CORS for All Routes
|
||||||
|
app.use(router.routes());
|
||||||
|
app.listen({ port: HTTP_PORT });
|
||||||
|
|
||||||
|
console.log(`ATScan API started at port ${HTTP_PORT}`);
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { Bson, MongoClient } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
||||||
|
import { parse, stringify } from "https://deno.land/std@0.184.0/yaml/mod.ts";
|
||||||
|
|
||||||
|
const BSKY_OFFICIAL_PDS = [
|
||||||
|
"https://bsky.social",
|
||||||
|
];
|
||||||
|
|
||||||
|
export class ATScan {
|
||||||
|
constructor(opts = {}) {
|
||||||
|
this.verbose = opts.verbose;
|
||||||
|
this.debug = opts.debug;
|
||||||
|
this.defaultPLC = parse(Deno.readTextFileSync('./spec/plc.yaml'))
|
||||||
|
this.BSKY_OFFICIAL_PDS = BSKY_OFFICIAL_PDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.client = new MongoClient();
|
||||||
|
await this.client.connect("mongodb://127.0.0.1:27017");
|
||||||
|
this.dbRaw = this.client.database("test");
|
||||||
|
this.db = {
|
||||||
|
did: this.dbRaw.collection("did"),
|
||||||
|
pds: this.dbRaw.collection("pds"),
|
||||||
|
meta: this.dbRaw.collection("meta"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async processPlcExport(plc, after = null) {
|
||||||
|
const url = plc.url + "/export?after=" + (after || "");
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(`ProcessPlcExport: ${url}`);
|
||||||
|
}
|
||||||
|
const req = await fetch(url);
|
||||||
|
const lines = await req.text();
|
||||||
|
const arr = lines.split("\n").map((l) => JSON.parse(l));
|
||||||
|
|
||||||
|
for (const data of arr) {
|
||||||
|
const pdsUrl = data.operation.services?.atproto_pds?.endpoint;
|
||||||
|
const matcher = { did: data.did, src: plc.url };
|
||||||
|
const obj = {
|
||||||
|
did: data.did,
|
||||||
|
src: plc.url,
|
||||||
|
revs: [data],
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
pds: pdsUrl ? [pdsUrl] : [],
|
||||||
|
};
|
||||||
|
let didRev = 0;
|
||||||
|
const found = await this.db.did.findOne(matcher);
|
||||||
|
if (found) {
|
||||||
|
const revFound = found.revs.find((r) => r.cid === data.cid);
|
||||||
|
let updated = false;
|
||||||
|
if (!revFound) {
|
||||||
|
updated = true;
|
||||||
|
didRev = found.revs.length;
|
||||||
|
found.revs.push(data);
|
||||||
|
//found.time = new Date().toISOString()
|
||||||
|
console.log(`DID: Adding new DID revision: ${data.did}@${didRev}`);
|
||||||
|
}
|
||||||
|
if (pdsUrl && !found.pds.includes(pdsUrl)) {
|
||||||
|
updated = true;
|
||||||
|
found.pds.push(pdsUrl);
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
await this.db.did.updateOne(matcher, {
|
||||||
|
$set: {
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
revs: found.revs,
|
||||||
|
pds: found.pds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`DID: Adding new DID revision: ${data.did}@0 (init)`);
|
||||||
|
await this.db.did.insertOne(obj);
|
||||||
|
}
|
||||||
|
const pdsFound = await this.db.pds.findOne({ url: pdsUrl });
|
||||||
|
const didId = [data.did, didRev].join("@");
|
||||||
|
if (pdsFound) {
|
||||||
|
if (!pdsFound.plcs.includes(plc.url)) {
|
||||||
|
pdsFound.plcs.push(plcUrl);
|
||||||
|
console.log(`PDS [${pdsUrl}]: Adding new PLC: ${plc.url}`);
|
||||||
|
await this.db.pds.updateOne({ url: pdsUrl }, {
|
||||||
|
$set: { plcs: pdsFound.plcs },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.db.pds.insertOne({
|
||||||
|
url: pdsUrl,
|
||||||
|
plcs: [plc.url],
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const key = `lastUpdate:${plc.url}`;
|
||||||
|
await this.db.meta.updateOne({ key }, {
|
||||||
|
$set: { key, value: arr[arr.length - 1].createdAt },
|
||||||
|
}, { upsert: true });
|
||||||
|
return arr.length !== 1 ? arr[arr.length - 1].createdAt : false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { ATScan } from "./lib/atscan.js";
|
||||||
|
import { pooledMap } from "https://deno.land/std/async/mod.ts";
|
||||||
|
import "https://deno.land/std@0.192.0/dotenv/load.ts";
|
||||||
|
|
||||||
|
function timeout(ms, promise) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
const start = performance.now();
|
||||||
|
setTimeout(function () {
|
||||||
|
reject(new Error("timeout"));
|
||||||
|
}, ms);
|
||||||
|
promise.then((v) => {
|
||||||
|
const end = performance.now();
|
||||||
|
return resolve([v, end - start]);
|
||||||
|
}, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function crawl(ats) {
|
||||||
|
const arr = await ats.db.pds.find().toArray();
|
||||||
|
const results = pooledMap(25, arr.slice(0, 1000), async (i) => {
|
||||||
|
let err = null;
|
||||||
|
let res, data, ms;
|
||||||
|
|
||||||
|
const host = i.url.replace(/^https?:\/\//, "");
|
||||||
|
if (!i.dns) {
|
||||||
|
console.log('sending dns request: ', i.url)
|
||||||
|
let dns =
|
||||||
|
await (await fetch(`https://dns.google/resolve?name=${host}&type=A`))
|
||||||
|
.json();
|
||||||
|
i.dns = dns;
|
||||||
|
ats.db.pds.updateOne({ url: i.url }, { $set: { dns } });
|
||||||
|
}
|
||||||
|
if (!i.ip && (i.dns.Answer && i.dns.Answer.filter(a => a.type === 1).length > 0)) {
|
||||||
|
const ipAddr = i.dns.Answer.filter(a => a.type === 1)[0].data
|
||||||
|
let ip;
|
||||||
|
try {
|
||||||
|
ip =
|
||||||
|
await (await fetch(`http://ipinfo.io/${ipAddr}?token=${Deno.env.get('IPINFO_TOKEN')}`))
|
||||||
|
.json();
|
||||||
|
} catch (e) {}
|
||||||
|
if (ip || (i.ip && i.ip.Question && i.ip && i.ip.Question[0].name !== host+'.') || !i.ip) {
|
||||||
|
i.ip = ip;
|
||||||
|
ats.db.pds.updateOne({ url: i.url }, { $set: { ip } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i.url.match(/^https?:\/\/(localhost|example.com)/)) {
|
||||||
|
err = "not allowed domain";
|
||||||
|
}
|
||||||
|
if (!i.dns.Answer) {
|
||||||
|
err = "not existing domain";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!err) {
|
||||||
|
const url = `${i.url}/xrpc/com.atproto.server.describeServer`;
|
||||||
|
try {
|
||||||
|
[res, ms] = await timeout(
|
||||||
|
5000,
|
||||||
|
fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent":
|
||||||
|
"ATScan Crawler",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (res) {
|
||||||
|
data = await res.json();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
err = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inspect = {
|
||||||
|
err,
|
||||||
|
data,
|
||||||
|
ms,
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
const dbSet = { "inspect.current": inspect };
|
||||||
|
if (!err && data) {
|
||||||
|
dbSet["inspect.lastOnline"] = (new Date()).toISOString();
|
||||||
|
}
|
||||||
|
await ats.db.pds.updateOne({ url: i.url }, {
|
||||||
|
$set: dbSet,
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
`-> ${i.url} ${ms ? "[" + ms + "ms]" : ""} ${
|
||||||
|
err ? "error = " + err : ""
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
for await (const value of results) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Deno.args[0] === "daemon") {
|
||||||
|
const wait = 60 * 10;
|
||||||
|
|
||||||
|
console.log("Initializing ATScan ..");
|
||||||
|
const ats = new ATScan();
|
||||||
|
ats.debug = true;
|
||||||
|
await ats.init();
|
||||||
|
console.log("pds-crawl daemon started");
|
||||||
|
console.log("Performing initial crawl ..");
|
||||||
|
// initial crawl
|
||||||
|
await crawl(ats);
|
||||||
|
console.log(`Initial crawl done`);
|
||||||
|
ats.debug = false;
|
||||||
|
console.log(`Processing events [wait=${wait}s] ..`);
|
||||||
|
setInterval(() => crawl(ats), wait * 1000);
|
||||||
|
} else {
|
||||||
|
const ats = new ATScan({ debug: true });
|
||||||
|
await ats.init();
|
||||||
|
await crawl(ats);
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { ATScan } from "./lib/atscan.js";
|
||||||
|
|
||||||
|
async function crawl(ats) {
|
||||||
|
for (const plc of ats.defaultPLC) {
|
||||||
|
let start = 0;
|
||||||
|
if (Deno.args[0] !== "init") {
|
||||||
|
const item = await ats.db.meta.findOne({ key: `lastUpdate:${plc.url}` });
|
||||||
|
if (item) {
|
||||||
|
start = item.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let after = await ats.processPlcExport(plc, start);
|
||||||
|
while (after) {
|
||||||
|
after = await ats.processPlcExport(plc, after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Deno.args[0] === "daemon") {
|
||||||
|
const wait = 60;
|
||||||
|
|
||||||
|
console.log("Initializing ATScan ..");
|
||||||
|
const ats = new ATScan();
|
||||||
|
ats.debug = true;
|
||||||
|
await ats.init();
|
||||||
|
console.log("plc-crawl daemon started");
|
||||||
|
console.log("Performing initial crawl ..");
|
||||||
|
// initial crawl
|
||||||
|
await crawl(ats);
|
||||||
|
console.log(`Initial crawl done`);
|
||||||
|
ats.debug = false;
|
||||||
|
console.log(`Processing events [wait=${wait}s] ..`);
|
||||||
|
setInterval(() => crawl(ats), wait * 1000);
|
||||||
|
} else {
|
||||||
|
const ats = new ATScan({ debug: true });
|
||||||
|
await ats.init();
|
||||||
|
await crawl(ats);
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
module.exports = {
|
||||||
|
apps: [{
|
||||||
|
name: "atscan-plc-crawler",
|
||||||
|
script: "./backend/plc-crawler.js",
|
||||||
|
args: "daemon",
|
||||||
|
interpreter: "deno",
|
||||||
|
interpreterArgs: "run --unstable --allow-net --allow-read",
|
||||||
|
}, {
|
||||||
|
name: "atscan-pds-crawler",
|
||||||
|
script: "./backend/pds-crawler.js",
|
||||||
|
args: "daemon",
|
||||||
|
interpreter: "mullvad-exclude",
|
||||||
|
interpreterArgs: "deno run --unstable --allow-net --allow-read",
|
||||||
|
}, {
|
||||||
|
name: "atscan-fe-dev",
|
||||||
|
interpreter: "mullvad-exclude",
|
||||||
|
interpreterArgs: "npm run dev",
|
||||||
|
env: {
|
||||||
|
HOST: "127.0.0.1",
|
||||||
|
PORT: 4010,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
name: "atscan-fe",
|
||||||
|
script: "./frontend/build/index.js",
|
||||||
|
interpreter: "mullvad-exclude",
|
||||||
|
interpreterArgs: "node",
|
||||||
|
env: {
|
||||||
|
HOST: "127.0.0.1",
|
||||||
|
PORT: 4000,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
name: "atscan-api",
|
||||||
|
script: "./backend/api.js",
|
||||||
|
//args : "daemon",
|
||||||
|
interpreter: "deno",
|
||||||
|
interpreterArgs: "run --unstable -A",
|
||||||
|
watch: true,
|
||||||
|
}],
|
||||||
|
};
|
|
@ -0,0 +1,13 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ["eslint:recommended", "plugin:svelte/recommended", "prettier"],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
extraFileExtensions: [".svelte"],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
|
@ -0,0 +1,2 @@
|
||||||
|
engine-strict=true
|
||||||
|
resolution-mode=highest
|
|
@ -0,0 +1,13 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"pluginSearchDirs": ["."],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
{
|
||||||
|
"prettier.documentSelectors": [
|
||||||
|
"**/*.svelte"
|
||||||
|
],
|
||||||
|
"tailwindCSS.classAttributes": [
|
||||||
|
"class",
|
||||||
|
"accent",
|
||||||
|
"active",
|
||||||
|
"background",
|
||||||
|
"badge",
|
||||||
|
"bgBackdrop",
|
||||||
|
"bgDark",
|
||||||
|
"bgDrawer",
|
||||||
|
"bgLight",
|
||||||
|
"blur",
|
||||||
|
"border",
|
||||||
|
"button",
|
||||||
|
"buttonAction",
|
||||||
|
"buttonBack",
|
||||||
|
"buttonClasses",
|
||||||
|
"buttonComplete",
|
||||||
|
"buttonDismiss",
|
||||||
|
"buttonNeutral",
|
||||||
|
"buttonNext",
|
||||||
|
"buttonPositive",
|
||||||
|
"buttonTextCancel",
|
||||||
|
"buttonTextConfirm",
|
||||||
|
"buttonTextFirst",
|
||||||
|
"buttonTextLast",
|
||||||
|
"buttonTextNext",
|
||||||
|
"buttonTextPrevious",
|
||||||
|
"buttonTextSubmit",
|
||||||
|
"caretClosed",
|
||||||
|
"caretOpen",
|
||||||
|
"chips",
|
||||||
|
"color",
|
||||||
|
"controlSeparator",
|
||||||
|
"controlVariant",
|
||||||
|
"cursor",
|
||||||
|
"display",
|
||||||
|
"element",
|
||||||
|
"fill",
|
||||||
|
"fillDark",
|
||||||
|
"fillLight",
|
||||||
|
"flex",
|
||||||
|
"gap",
|
||||||
|
"gridColumns",
|
||||||
|
"height",
|
||||||
|
"hover",
|
||||||
|
"justify",
|
||||||
|
"meter",
|
||||||
|
"padding",
|
||||||
|
"position",
|
||||||
|
"regionBackdrop",
|
||||||
|
"regionBody",
|
||||||
|
"regionCaption",
|
||||||
|
"regionCaret",
|
||||||
|
"regionCell",
|
||||||
|
"regionCone",
|
||||||
|
"regionContent",
|
||||||
|
"regionControl",
|
||||||
|
"regionDefault",
|
||||||
|
"regionDrawer",
|
||||||
|
"regionFoot",
|
||||||
|
"regionFootCell",
|
||||||
|
"regionFooter",
|
||||||
|
"regionHead",
|
||||||
|
"regionHeadCell",
|
||||||
|
"regionHeader",
|
||||||
|
"regionIcon",
|
||||||
|
"regionInterface",
|
||||||
|
"regionInterfaceText",
|
||||||
|
"regionLabel",
|
||||||
|
"regionLead",
|
||||||
|
"regionLegend",
|
||||||
|
"regionList",
|
||||||
|
"regionNavigation",
|
||||||
|
"regionPage",
|
||||||
|
"regionPanel",
|
||||||
|
"regionRowHeadline",
|
||||||
|
"regionRowMain",
|
||||||
|
"regionTab",
|
||||||
|
"regionTrail",
|
||||||
|
"ring",
|
||||||
|
"rounded",
|
||||||
|
"select",
|
||||||
|
"shadow",
|
||||||
|
"slotDefault",
|
||||||
|
"slotFooter",
|
||||||
|
"slotHeader",
|
||||||
|
"slotLead",
|
||||||
|
"slotMessage",
|
||||||
|
"slotMeta",
|
||||||
|
"slotPageContent",
|
||||||
|
"slotPageFooter",
|
||||||
|
"slotPageHeader",
|
||||||
|
"slotSidebarLeft",
|
||||||
|
"slotSidebarRight",
|
||||||
|
"slotTrail",
|
||||||
|
"spacing",
|
||||||
|
"text",
|
||||||
|
"track",
|
||||||
|
"width",
|
||||||
|
"zIndex"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
# create-svelte
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by
|
||||||
|
[`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npm create svelte@latest
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npm create svelte@latest my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or
|
||||||
|
`pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an
|
||||||
|
> [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "atscan-fe",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
|
"format": "prettier --plugin-search-dir . --write ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.4.2",
|
||||||
|
"@skeletonlabs/skeleton": "^1.8.0",
|
||||||
|
"@sveltejs/adapter-auto": "^2.0.0",
|
||||||
|
"@sveltejs/adapter-node": "^1.2.4",
|
||||||
|
"@sveltejs/kit": "^1.5.0",
|
||||||
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"date-fns": "^2.30.0",
|
||||||
|
"eslint": "^8.28.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-svelte": "^2.26.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"postcss": "^8.4.24",
|
||||||
|
"prettier": "^2.8.0",
|
||||||
|
"prettier-plugin-svelte": "^2.8.1",
|
||||||
|
"svelte": "^3.54.0",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
|
"vite": "^4.3.0"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"highlight.js": "^11.8.0",
|
||||||
|
"minidenticons": "^4.2.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
// and what to do when importing types
|
||||||
|
declare namespace App {
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface Error {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover" data-theme="skeleton">
|
||||||
|
<div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,5 @@
|
||||||
|
/*place global styles here */
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
@apply h-full overflow-hidden;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script>
|
||||||
|
export let data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
{#each data as crumb, i}
|
||||||
|
<!-- If crumb index is less than the breadcrumb length minus 1 -->
|
||||||
|
{#if i < data.length - 1}
|
||||||
|
<li class="crumb"><a class="btn variant-soft-secondary" href={crumb.link}>{crumb.label}</a></li>
|
||||||
|
<li class="crumb-separator" aria-hidden>›</li>
|
||||||
|
{:else}
|
||||||
|
<li class="crumb">
|
||||||
|
{#if crumb.link}
|
||||||
|
<a class="btn variant-soft-secondary" href={crumb.link}>{@html crumb.label}</a>
|
||||||
|
{:else}
|
||||||
|
{@html crumb.label}
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ol>
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
import { minidenticon } from 'minidenticons';
|
||||||
|
|
||||||
|
export function dateDistance (date) {
|
||||||
|
return formatDistanceToNow(new Date(date))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function identicon (...args) {
|
||||||
|
return 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(...args))
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
<script>
|
||||||
|
// The ordering of these imports is critical to your app working properly
|
||||||
|
import '@skeletonlabs/skeleton/themes/theme-skeleton.css';
|
||||||
|
// If you have source.organizeImports set to true in VSCode, then it will auto change this ordering
|
||||||
|
import '@skeletonlabs/skeleton/styles/skeleton.css';
|
||||||
|
// Most of your app wide CSS should be put in this file
|
||||||
|
import '../app.postcss';
|
||||||
|
import { AppShell, AppBar, LightSwitch } from '@skeletonlabs/skeleton';
|
||||||
|
|
||||||
|
import hljs from 'highlight.js';
|
||||||
|
import 'highlight.js/styles/github-dark.css';
|
||||||
|
import { storeHighlightJs } from '@skeletonlabs/skeleton';
|
||||||
|
|
||||||
|
storeHighlightJs.set(hljs);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- App Shell -->
|
||||||
|
<AppShell>
|
||||||
|
<svelte:fragment slot="header">
|
||||||
|
<!-- App Bar -->
|
||||||
|
<AppBar>
|
||||||
|
<svelte:fragment slot="lead">
|
||||||
|
<a href="/"><strong class="text-xl uppercase">ATScan</strong></a>
|
||||||
|
<div class="lg:ml-8 flex">
|
||||||
|
<div class="relative hidden lg:block">
|
||||||
|
<a href="/did" class="btn hover:variant-soft-primary"><span>DIDs</span></a>
|
||||||
|
</div>
|
||||||
|
<div class="relative hidden lg:block">
|
||||||
|
<a href="/pds" class="btn hover:variant-soft-primary"><span>PDS Instances</span></a>
|
||||||
|
</div>
|
||||||
|
<div class="relative hidden lg:block">
|
||||||
|
<a href="/plc" class="btn hover:variant-soft-primary"><span>PLC Directories</span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</svelte:fragment>
|
||||||
|
<svelte:fragment slot="trail">
|
||||||
|
<!--
|
||||||
|
<a
|
||||||
|
class="btn btn-sm variant-ghost-surface"
|
||||||
|
href="https://discord.gg/EXqV7W8MtY"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Discord
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="btn btn-sm variant-ghost-surface"
|
||||||
|
href="https://twitter.com/SkeletonUI"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Twitter
|
||||||
|
</a>
|
||||||
|
-->
|
||||||
|
<a
|
||||||
|
class="btn btn-sm variant-ghost-surface hover:variant-soft-primary"
|
||||||
|
href="https://github.com/burningtree/atscan"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
<LightSwitch />
|
||||||
|
</svelte:fragment>
|
||||||
|
</AppBar>
|
||||||
|
</svelte:fragment>
|
||||||
|
<!-- Page Route Content -->
|
||||||
|
<slot />
|
||||||
|
</AppShell>
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export function load() {
|
||||||
|
// ...
|
||||||
|
throw redirect(302, '/did');
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
<div class="w-full h-full bg-ats-bsky"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/** dark:bg-ats-bsky dark:bg-ats-sbox */
|
||||||
|
</script>
|
|
@ -0,0 +1,13 @@
|
||||||
|
import * as _ from "lodash";
|
||||||
|
|
||||||
|
/** @type {import('./$types').PageLoad} */
|
||||||
|
export async function load({ params }) {
|
||||||
|
const res = await fetch("https://api.atscan.net/did");
|
||||||
|
const did = _.orderBy(await res.json(), ["time"], [
|
||||||
|
"desc",
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
did,
|
||||||
|
plc: await (await fetch("https://api.atscan.net/plc")).json()
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
<script>
|
||||||
|
import { Table } from '@skeletonlabs/skeleton';
|
||||||
|
import { tableMapperValues, tableSourceValues } from '@skeletonlabs/skeleton';
|
||||||
|
import { dateDistance, identicon } from '$lib/utils.js';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
let search = ""
|
||||||
|
/*search.subscribe(val => {
|
||||||
|
console.log(`/did?q=${val}`)
|
||||||
|
setTimeout(() => {
|
||||||
|
throw redirect(307, `/did?q=${''}`);
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
//*/
|
||||||
|
|
||||||
|
function formSubmit () {
|
||||||
|
console.log(search)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectionHandler (i) {
|
||||||
|
return goto(`/${i.detail[0]}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function tableMapperValuesLocal (source, keys) {
|
||||||
|
return tableSourceValues(source.map((row) => {
|
||||||
|
const mappedRow = {};
|
||||||
|
keys.forEach((key) => {
|
||||||
|
let val = row[key]
|
||||||
|
if (key === 'world') {
|
||||||
|
}
|
||||||
|
if (key === 'srcHost') {
|
||||||
|
val = `<a href="/plc/${val}" class="hover:underline">${val}</a>`
|
||||||
|
}
|
||||||
|
if (key === 'pds') {
|
||||||
|
const host =
|
||||||
|
val = val.map(i => {
|
||||||
|
const host = i.replace(/^https?:\/\//, '')
|
||||||
|
return `<a href="/pds/${host}" class='hover:underline'>${host}</a>`
|
||||||
|
}).join(', ')
|
||||||
|
}
|
||||||
|
if (key === 'did') {
|
||||||
|
const did = val
|
||||||
|
const plc = data.plc.find(i => i.url === row.src)
|
||||||
|
val = `<div class="flex gap-6">`
|
||||||
|
val += ` <div>`
|
||||||
|
val += ` <div class="text-lg inline-block"><a href="/${did}" class="hover:underline"><span class="opacity-50">did:plc:</span><span class="font-semibold opacity-100">${did.replace(/^did:plc:/, '')}</span></a></div>`
|
||||||
|
const handles = row.revs[row.revs.length-1].operation.alsoKnownAs.map(h => h.replace(/^at:\/\//, ''))
|
||||||
|
val += ` <div class="mt-1.5">`
|
||||||
|
val += ` <span class="mr-2 badge text-xs variant-filled ${plc.color} dark:${plc.color} opacity-70 text-white dark:text-black">${plc.name}</span>`
|
||||||
|
val += ` <span>${handles.map(h => `<a href="https://bsky.app/profile/${h}" target="_blank" class="hover:underline">@${h}</a>`).join(', ')}</span>`
|
||||||
|
val += ` </div>`
|
||||||
|
val += ` </div>`
|
||||||
|
val += "</div>"
|
||||||
|
}
|
||||||
|
if (key === 'time') {
|
||||||
|
val = dateDistance(val)
|
||||||
|
}
|
||||||
|
if (key === 'deep') {
|
||||||
|
val = row.revs.length
|
||||||
|
}
|
||||||
|
if (key === 'img') {
|
||||||
|
val = `<div class="text-right w-full"><div class="inline-block"><a href="/${row.did}"><img src="${identicon(row.did)}" class="w-16 h-16 rounded-lg bg-gray-200 dark:bg-gray-800 float-left" /></a></div></div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappedRow[key] = val
|
||||||
|
})
|
||||||
|
return mappedRow;
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceData = data.did;
|
||||||
|
const tableSimple = {
|
||||||
|
// A list of heading labels.
|
||||||
|
head: ['', 'DID', '#', 'PLC', 'PDS', 'Last mod'],
|
||||||
|
body: tableMapperValuesLocal(sourceData, ['img', 'did', 'deep', 'srcHost', 'pds', 'time']),
|
||||||
|
meta: tableMapperValues(sourceData, ['did']),
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-8 space-y-8">
|
||||||
|
<h1 class="h1">DIDs</h1>
|
||||||
|
|
||||||
|
<form on:submit={formSubmit}>
|
||||||
|
<input class="input" title="Input (text)" type="text" placeholder="Search for DID .." bind:value={search} />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!--p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p-->
|
||||||
|
|
||||||
|
<!--h2 class="h2">Active DIDs ({sourceData.length})</h2-->
|
||||||
|
<Table source={tableSimple} interactive={true} on:selected={selectionHandler} />
|
||||||
|
</div>
|
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
export async function load({ params }) {
|
||||||
|
const did = `did:${params.id}`
|
||||||
|
const res = await fetch(`https://api.atscan.net/${did}`);
|
||||||
|
return {
|
||||||
|
item: res.json(),
|
||||||
|
did
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
<script>
|
||||||
|
import { CodeBlock } from '@skeletonlabs/skeleton';
|
||||||
|
import Breadcrumb from '$lib/components/Breadcrumb.svelte';
|
||||||
|
import { dateDistance, identicon } from '$lib/utils.js';
|
||||||
|
import { Table } from '@skeletonlabs/skeleton';
|
||||||
|
import { tableMapperValues, tableSourceValues } from '@skeletonlabs/skeleton';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
const item = data.item;
|
||||||
|
const handles = item.revs[item.revs.length-1].operation.alsoKnownAs.map(h => h.replace(/^at:\/\//, ''))
|
||||||
|
|
||||||
|
function tableMapperValuesLocal (source, keys) {
|
||||||
|
let i = 0
|
||||||
|
return tableSourceValues(source.map((row) => {
|
||||||
|
const mappedRow = {};
|
||||||
|
keys.forEach((key) => {
|
||||||
|
let val = row[key]
|
||||||
|
if (key === 'num') {
|
||||||
|
val = String('#'+i)
|
||||||
|
}
|
||||||
|
if (key === 'handle') {
|
||||||
|
val = row.operation.alsoKnownAs.map(a => a.replace(/^at:\/\//,'@')).join(', ')
|
||||||
|
}
|
||||||
|
if (key === 'createdAt') {
|
||||||
|
val = `<span title="${val}" alt="${val}">${dateDistance(val)}</span>`
|
||||||
|
}
|
||||||
|
return mappedRow[key] = val
|
||||||
|
})
|
||||||
|
i++
|
||||||
|
return mappedRow;
|
||||||
|
}).reverse())
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceData = item.revs
|
||||||
|
const historyTable = {
|
||||||
|
head: [ '#', 'Handle', 'CID', 'Age' ],
|
||||||
|
body: tableMapperValuesLocal(sourceData, ['num', 'handle', 'cid', 'createdAt']),
|
||||||
|
meta: tableMapperValues(sourceData, ['cid']),
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-8 space-y-8">
|
||||||
|
<Breadcrumb data={[
|
||||||
|
{ label: 'DIDs', link: '/did' },
|
||||||
|
{ label: `<span class="mr-2 badge ${item.env ? 'bg-ats-'+item.env : 'bg-gray-500'} text-white dark:text-black">${item.env}</span> federation`, link: `/did?federation=${item.env}` }
|
||||||
|
]} />
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<div>
|
||||||
|
<img src="{identicon(item.did)}" class="w-40 h-40 rounded-2xl bg-gray-200 dark:bg-gray-800 float-left" />
|
||||||
|
</div>
|
||||||
|
<div class="grow">
|
||||||
|
<h1 class="h1">
|
||||||
|
<span class="opacity-50 font-normal">did:plc:</span><span class="font-semibold opacity-100">{item.did.replace(/^did:plc:/, '')}</span>
|
||||||
|
</h1>
|
||||||
|
<div class="text-2xl mt-4">
|
||||||
|
{@html handles.map(h => `<a href="https://bsky.app/profile/${h}" target="_blank" class="hover:underline">@${h}</a>`).join(', ')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="h2">Revisions <span class="font-normal text-2xl">({sourceData.length})</span></h2>
|
||||||
|
<Table source={historyTable} />
|
||||||
|
|
||||||
|
<h2 class="h2">Source</h2>
|
||||||
|
<CodeBlock code={JSON.stringify(item, null, 2)} language="json" />
|
||||||
|
|
||||||
|
<!--p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p-->
|
||||||
|
</div>
|
|
@ -0,0 +1,14 @@
|
||||||
|
import * as _ from "lodash";
|
||||||
|
|
||||||
|
/** @type {import('./$types').PageLoad} */
|
||||||
|
export async function load({ params }) {
|
||||||
|
const res = await fetch("https://api.atscan.net/pds");
|
||||||
|
const pds = _.orderBy(await res.json(), ["env", "inspect.current.ms"], [
|
||||||
|
"asc",
|
||||||
|
"asc",
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
pds,
|
||||||
|
plc: await (await fetch("https://api.atscan.net/plc")).json()
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
<script>
|
||||||
|
import { Table } from '@skeletonlabs/skeleton';
|
||||||
|
import { tableMapperValues, tableSourceValues } from '@skeletonlabs/skeleton';
|
||||||
|
import { dateDistance } from '$lib/utils.js';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
const search = writable($page.url.searchParams.get('q'))
|
||||||
|
|
||||||
|
function selectionHandler (i) {
|
||||||
|
return goto(`/pds/${i.detail[0]}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function tableMapperValuesLocal (source, keys) {
|
||||||
|
return tableSourceValues(source.map((row) => {
|
||||||
|
const mappedRow = {};
|
||||||
|
keys.forEach((key) => {
|
||||||
|
let val = row[key]
|
||||||
|
if (key === 'plcs' && val) {
|
||||||
|
val = val.map(i => i.replace(/^https?:\/\//, '')).join(', ')
|
||||||
|
}
|
||||||
|
if (key === 'env') {
|
||||||
|
let arr = []
|
||||||
|
const plc = (val === "sandbox") ? data.plc.find(i => i.code === 'sbox') : ((val === "bsky") ? data.plc.find(i => i.code === 'bsky') : null)
|
||||||
|
if (plc) {
|
||||||
|
arr.push(`<span class="badge variant-filled ${plc.color} dark:${plc.color} opacity-70 text-white dark:text-black">${plc.name}</span>`)
|
||||||
|
}
|
||||||
|
val = arr.reverse().join(' ')
|
||||||
|
}
|
||||||
|
if (key === 'host') {
|
||||||
|
val = `<a href="/pds/${val}" class="hover:underline"><span class="font-semibold text-lg">${val}</span></a>`
|
||||||
|
}
|
||||||
|
if (key === 'ms') {
|
||||||
|
val = row.inspect?.current.err
|
||||||
|
? `<a href="${row.url}/xrpc/com.atproto.server.describeServer" target="_blank" title="${row.inspect.current.err}" class="hover:underline">error</a>`
|
||||||
|
: row.inspect?.current.ms ? `<a href="${row.url}/xrpc/com.atproto.server.describeServer" target="_blank" class="hover:underline">${row.inspect.current.ms + 'ms'}</a>` : '-'
|
||||||
|
}
|
||||||
|
if (key === 'userDomains') {
|
||||||
|
val = row.inspect?.current.data?.availableUserDomains.join(', ')
|
||||||
|
}
|
||||||
|
if (key === 'location') {
|
||||||
|
val = row.ip && row.ip.country
|
||||||
|
? `<img src="/cc/${row.ip.country.toLowerCase()}.png" alt="${row.ip.country}" title="${row.ip.country}" class="inline-block mr-2" />`
|
||||||
|
: ''
|
||||||
|
if (row.ip && row.ip.city) {
|
||||||
|
val += `${row.ip.city} - `
|
||||||
|
}
|
||||||
|
const dnsIp = row.dns ? row.dns.Answer?.filter(a => a.type === 1)[0].data : null
|
||||||
|
val += `<a href="http://ipinfo.io/${dnsIp}" target="_blank" class="hover:underline">${dnsIp}</a>` || '-'
|
||||||
|
if (row.ip && row.ip.regionName) {
|
||||||
|
val += ' ('+row.ip.regionName+')'
|
||||||
|
}
|
||||||
|
val += `<br /><span class="text-xs">${row.ip?.org || 'n/a'}</span>`
|
||||||
|
}
|
||||||
|
if (key === 'didsCount') {
|
||||||
|
val = `<a href="/did?pds=${row.host}" class="hover:underline">${val}</a>`
|
||||||
|
}
|
||||||
|
if (key === 'lastOnline' && row.inspect) {
|
||||||
|
val = row.inspect?.lastOnline ? dateDistance(row.inspect?.lastOnline) : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappedRow[key] = val
|
||||||
|
});
|
||||||
|
return mappedRow;
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = data.pds.filter(d => d.inspect?.lastOnline)
|
||||||
|
let sourceData = JSON.parse(JSON.stringify(filtered))
|
||||||
|
let sourceDataOffline = data.pds.filter(d => !d.inspect?.lastOnline)
|
||||||
|
|
||||||
|
|
||||||
|
function formSubmit() {
|
||||||
|
const url = '?q='+$search
|
||||||
|
//goto(url)
|
||||||
|
// refilter
|
||||||
|
sourceData = data.pds.filter(d => d.inspect?.lastOnline).filter(i => i.url.match(new RegExp($search, 'i')))
|
||||||
|
genTableSimple()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let tableSimple;
|
||||||
|
function genTableSimple () {
|
||||||
|
tableSimple = {
|
||||||
|
// A list of heading labels.
|
||||||
|
head: ['Federation', 'Host', 'Delay', 'Location', 'DIDs', 'PLCs', 'User Domains', 'Last mod'],
|
||||||
|
// The data visibly shown in your table body UI.
|
||||||
|
//body: mapper(sourceData, ['host', 'type', 'plc']),
|
||||||
|
body: tableMapperValuesLocal(sourceData, ['env', 'host', 'ms', 'location', 'didsCount', 'plcs', 'userDomains', 'lastOnline' ]),
|
||||||
|
// Optional: The data returned when interactive is enabled and a row is clicked.
|
||||||
|
meta: tableMapperValues(sourceData, ['host']),
|
||||||
|
// Optional: A list of footer labels.
|
||||||
|
//foot: ['Total', '', '<code class="code">5</code>']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
genTableSimple()
|
||||||
|
|
||||||
|
const tableSimpleOffline = {
|
||||||
|
// A list of heading labels.
|
||||||
|
head: ['Federation', 'Host', 'Delay', 'Location', 'DIDs', 'PLCs',],
|
||||||
|
body: tableMapperValuesLocal(sourceDataOffline, ['env', 'host', 'ms', 'location', 'didsCount', 'plcs']),
|
||||||
|
meta: tableMapperValues(sourceDataOffline, ['host']),
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-8 space-y-8">
|
||||||
|
<h1 class="h1">PDS Instances ({sourceData.length})</h1>
|
||||||
|
|
||||||
|
<form on:submit={formSubmit} class="flex gap-4">
|
||||||
|
<input class="input" title="Input (text)" type="text" placeholder="Search for PDS .." bind:value={$search} />
|
||||||
|
<button type="submit" class="btn variant-filled">Search</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!--p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p-->
|
||||||
|
|
||||||
|
<!--h2 class="h2">Active instances ({sourceData.length})</h2-->
|
||||||
|
<Table source={tableSimple} interactive={true} on:selected={selectionHandler} />
|
||||||
|
|
||||||
|
<!--<h2 class="h2">Inactive instances ({sourceDataOffline.length})</h2>
|
||||||
|
<Table source={tableSimpleOffline} />-->
|
||||||
|
</div>
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
export async function load({ params }) {
|
||||||
|
const res = await fetch(`https://api.atscan.net/pds/${params.host}`);
|
||||||
|
return {
|
||||||
|
item: res.json()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script>
|
||||||
|
import { CodeBlock } from '@skeletonlabs/skeleton';
|
||||||
|
import Breadcrumb from '$lib/components/Breadcrumb.svelte';
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
const item = data.item;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-8 space-y-8">
|
||||||
|
<Breadcrumb data={[
|
||||||
|
{ label: 'PDS Instances', link: '/pds' },
|
||||||
|
{ label: `<span class="mr-2 badge ${item.env === 'bsky' ? 'bg-ats-bsky' : (item.env === 'sandbox' ? 'bg-ats-sbox' : 'bg-gray-500')} text-white dark:text-black">${item.env}</span> federation`, link: `/pds?federation=${item.env}` }
|
||||||
|
]} />
|
||||||
|
<h1 class="h1">
|
||||||
|
{item.host}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h2 class="h2">Source</h2>
|
||||||
|
<CodeBlock code={JSON.stringify(item, null, 2)} language="json" />
|
||||||
|
|
||||||
|
<!--p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p-->
|
||||||
|
</div>
|
|
@ -0,0 +1,10 @@
|
||||||
|
import * as _ from "lodash";
|
||||||
|
|
||||||
|
/** @type {import('./$types').PageLoad} */
|
||||||
|
export async function load({ params }) {
|
||||||
|
const res = await fetch("https://api.atscan.net/plc");
|
||||||
|
const plc = _.orderBy(await res.json(), ["code"], [
|
||||||
|
"asc",
|
||||||
|
]);
|
||||||
|
return { plc };
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script>
|
||||||
|
import { Table } from '@skeletonlabs/skeleton';
|
||||||
|
import { tableMapperValues, tableSourceValues } from '@skeletonlabs/skeleton';
|
||||||
|
import { dateDistance } from '$lib/utils.js';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
function tableMapperValuesLocal (source, keys) {
|
||||||
|
return tableSourceValues(source.map((row) => {
|
||||||
|
const mappedRow = {};
|
||||||
|
keys.forEach((key) => {
|
||||||
|
let val = row[key]
|
||||||
|
if (key === 'world') {
|
||||||
|
val = `<span class="badge variant-filled ${row.color} dark:${row.color} opacity-70 text-white dark:text-black">${row.name}</span>`
|
||||||
|
}
|
||||||
|
if (key === 'host') {
|
||||||
|
val = `<span class="text-xl font-semibold">${val}</span>`
|
||||||
|
}
|
||||||
|
if (key === 'code') {
|
||||||
|
val = `<div class="inline-block font-mono">${val}</div>`
|
||||||
|
}
|
||||||
|
if (key === 'lastUpdate') {
|
||||||
|
val = dateDistance(val)
|
||||||
|
}
|
||||||
|
if (key === 'didsCount') {
|
||||||
|
val = `<a href="/did?plc=${row.host}" class="hover:underline">${val}</a>`
|
||||||
|
}
|
||||||
|
return mappedRow[key] = val
|
||||||
|
})
|
||||||
|
return mappedRow;
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceData = data.plc;
|
||||||
|
const tableSimple = {
|
||||||
|
// A list of heading labels.
|
||||||
|
head: ['Federation', 'Host', 'DIDs', 'Last mod'],
|
||||||
|
body: tableMapperValuesLocal(sourceData, ['world', 'host', 'didsCount', 'lastUpdate']),
|
||||||
|
meta: tableMapperValues(sourceData, ['position', 'name', 'symbol', 'weight']),
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="container mx-auto p-8 space-y-8">
|
||||||
|
<h1 class="h1">PLC Directories ({sourceData.length})</h1>
|
||||||
|
|
||||||
|
<!--p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p-->
|
||||||
|
|
||||||
|
<!--<h2 class="h2">Active directories </h2>-->
|
||||||
|
<Table source={tableSimple} interactive={true} />
|
||||||
|
</div>
|
Za Šířka: | Výška: | Velikost: 442 B |
Za Šířka: | Výška: | Velikost: 445 B |
Za Šířka: | Výška: | Velikost: 493 B |
Za Šířka: | Výška: | Velikost: 587 B |
Za Šířka: | Výška: | Velikost: 327 B |
Za Šířka: | Výška: | Velikost: 444 B |
Za Šířka: | Výška: | Velikost: 560 B |
Za Šířka: | Výška: | Velikost: 417 B |
Za Šířka: | Výška: | Velikost: 355 B |
Za Šířka: | Výška: | Velikost: 353 B |
Za Šířka: | Výška: | Velikost: 407 B |
Za Šířka: | Výška: | Velikost: 469 B |
Za Šířka: | Výška: | Velikost: 315 B |
Za Šířka: | Výška: | Velikost: 472 B |
Za Šířka: | Výška: | Velikost: 441 B |
Za Šířka: | Výška: | Velikost: 287 B |
Za Šířka: | Výška: | Velikost: 507 B |
Za Šířka: | Výška: | Velikost: 393 B |
Za Šířka: | Výška: | Velikost: 577 B |
Za Šířka: | Výška: | Velikost: 528 B |
Za Šířka: | Výška: | Velikost: 386 B |
Za Šířka: | Výška: | Velikost: 528 B |
Za Šířka: | Výška: | Velikost: 410 B |
Za Šířka: | Výška: | Velikost: 502 B |
Za Šířka: | Výška: | Velikost: 467 B |
Za Šířka: | Výška: | Velikost: 293 B |
Za Šířka: | Výška: | Velikost: 361 B |
Za Šířka: | Výška: | Velikost: 416 B |
Za Šířka: | Výška: | Velikost: 516 B |
Za Šířka: | Výška: | Velikost: 389 B |
Za Šířka: | Výška: | Velikost: 560 B |
Za Šířka: | Výška: | Velikost: 295 B |
Za Šířka: | Výška: | Velikost: 442 B |
Za Šířka: | Výška: | Velikost: 368 B |
Za Šířka: | Výška: | Velikost: 362 B |
Za Šířka: | Výška: | Velikost: 437 B |
Za Šířka: | Výška: | Velikost: 520 B |
Za Šířka: | Výška: | Velikost: 379 B |
Za Šířka: | Výška: | Velikost: 420 B |
Za Šířka: | Výška: | Velikost: 413 B |
Za Šířka: | Výška: | Velikost: 343 B |
Za Šířka: | Výška: | Velikost: 287 B |
Za Šířka: | Výška: | Velikost: 449 B |
Za Šířka: | Výška: | Velikost: 552 B |
Za Šířka: | Výška: | Velikost: 387 B |
Za Šířka: | Výška: | Velikost: 578 B |
Za Šířka: | Výška: | Velikost: 491 B |
Za Šířka: | Výška: | Velikost: 581 B |
Za Šířka: | Výška: | Velikost: 418 B |
Za Šířka: | Výška: | Velikost: 529 B |
Za Šířka: | Výška: | Velikost: 435 B |
Za Šířka: | Výška: | Velikost: 581 B |
Za Šířka: | Výška: | Velikost: 512 B |
Za Šířka: | Výška: | Velikost: 292 B |
Za Šířka: | Výška: | Velikost: 436 B |
Za Šířka: | Výška: | Velikost: 473 B |
Za Šířka: | Výška: | Velikost: 389 B |
Za Šířka: | Výška: | Velikost: 462 B |
Za Šířka: | Výška: | Velikost: 573 B |
Za Šířka: | Výška: | Velikost: 358 B |
Za Šířka: | Výška: | Velikost: 471 B |