Compare commits
10 commits
main
...
ari/CleanA
Author | SHA1 | Date | |
---|---|---|---|
d92f6f6514 | |||
04de4c1841 | |||
824fcf2575 | |||
5da1494081 | |||
1e04628fdf | |||
cb4189ee51 | |||
cef9060948 | |||
e1665b6c55 | |||
9fe004f4a4 | |||
412e18f628 |
15 changed files with 148 additions and 362 deletions
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
|||
# MIT License
|
||||
|
||||
Copyright (c) 2025 Witchcraft Systems
|
||||
|
||||
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.
|
61
README.md
61
README.md
|
@ -1,62 +1,3 @@
|
|||
# pds-dash
|
||||
|
||||
a frontend dashboard with stats for your ATProto PDS.
|
||||
|
||||
## setup
|
||||
|
||||
### prerequisites
|
||||
|
||||
- [deno](https://deno.com/manual/getting_started/installation)
|
||||
|
||||
### installing
|
||||
|
||||
clone the repo, install dependencies using deno:
|
||||
|
||||
```sh
|
||||
deno install
|
||||
```
|
||||
|
||||
### development server
|
||||
|
||||
local develompent server with hot reloading:
|
||||
|
||||
```sh
|
||||
deno task dev
|
||||
```
|
||||
|
||||
### building
|
||||
|
||||
to build the optimized bundle run:
|
||||
|
||||
```sh
|
||||
deno task build
|
||||
```
|
||||
|
||||
the output will be in the `dist/` directory.
|
||||
|
||||
## deploying
|
||||
|
||||
we use our own CI/CD workflow at [`.forgejo/workflows/deploy.yaml`](.forgejo/workflows/deploy.yaml), but it boils down to building the project bundle and deploying it to a web server. it'll probably make more sense to host it on the same domain as your PDS, but it doesn't affect anything if you host it somewhere else.
|
||||
|
||||
## configuring
|
||||
|
||||
[`config.ts`](config.ts) is the main configuration file, you can find more information in the file itself.
|
||||
|
||||
## theming
|
||||
|
||||
the colors are designated in [`src/app.css`](src/app.css) as variables, go crazy with them
|
||||
|
||||
the rest is done by editing the css files and style tags directly, good luck
|
||||
|
||||
relevant files:
|
||||
|
||||
- [`src/App.svelte`](src/App.svelte)
|
||||
- [`src/app.css`](src/app.css)
|
||||
- [`src/lib/AccountComponent.svelte`](src/lib/AccountComponent.svelte)
|
||||
- [`src/lib/PostComponent.svelte`](src/lib/PostComponent.svelte)
|
||||
|
||||
the favicon is located at [`public/favicon.ico`](public/favicon.ico)
|
||||
|
||||
## license
|
||||
|
||||
MIT
|
||||
Frontend with stats for your ATProto PDS
|
23
config.ts
23
config.ts
|
@ -9,29 +9,20 @@ export class Config {
|
|||
static readonly PDS_URL: string = "https://pds.witchcraft.systems";
|
||||
|
||||
/**
|
||||
* The base URL of the frontend service for linking to replies/quotes/accounts etc.
|
||||
* The base URL of the frontend service for linking to replies
|
||||
* @default "https://deer.social"
|
||||
*/
|
||||
static readonly FRONTEND_URL: string = "https://deer.social";
|
||||
|
||||
/**
|
||||
* Maximum number of posts to fetch from the PDS per request
|
||||
* Should be around 20 for about 10 users on the pds
|
||||
* The more users you have, the lower the number should be
|
||||
* since sorting is slow and is done on the frontend
|
||||
* @default 20
|
||||
* Maximum number of posts to show in the feed (across all users)
|
||||
* @default 100
|
||||
*/
|
||||
static readonly MAX_POSTS: number = 20;
|
||||
static readonly MAX_POSTS: number = 100;
|
||||
|
||||
/**
|
||||
* Footer text for the dashboard, you probably want to change this
|
||||
* Footer text for the dashboard
|
||||
* @default "Astrally projected from witchcraft.systems"
|
||||
*/
|
||||
static readonly FOOTER_TEXT: string =
|
||||
"Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a><br><br><a href='https://git.witchcraft.systems/scientific-witchery/pds-dash' target='_blank'>Source</a> (<a href='https://github.com/witchcraft-systems/pds-dash/' target='_blank'>github mirror</a>)";
|
||||
|
||||
/**
|
||||
* Whether to show the posts that are in the future
|
||||
* @default false
|
||||
*/
|
||||
static readonly SHOW_FUTURE_POSTS: boolean = false;
|
||||
static readonly FOOTER_TEXT: string = "Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a>";
|
||||
}
|
5
deno.lock
generated
5
deno.lock
generated
|
@ -8,7 +8,6 @@
|
|||
"npm:@tsconfig/svelte@^5.0.4": "5.0.4",
|
||||
"npm:moment@^2.30.1": "2.30.1",
|
||||
"npm:svelte-check@^4.1.5": "4.1.6_svelte@5.28.1__acorn@8.14.1_typescript@5.7.3",
|
||||
"npm:svelte-infinite-loading@^1.4.0": "1.4.0",
|
||||
"npm:svelte@^5.23.1": "5.28.1_acorn@8.14.1",
|
||||
"npm:typescript@~5.7.2": "5.7.3",
|
||||
"npm:vite@^6.3.1": "6.3.2_picomatch@4.0.2"
|
||||
|
@ -416,9 +415,6 @@
|
|||
"typescript"
|
||||
]
|
||||
},
|
||||
"svelte-infinite-loading@1.4.0": {
|
||||
"integrity": "sha512-Jo+f/yr/HmZQuIiiKKzAHVFXdAUWHW2RBbrcQTil8JVk1sCm/riy7KTJVzjBgQvHasrFQYKF84zvtc9/Y4lFYg=="
|
||||
},
|
||||
"svelte@5.28.1_acorn@8.14.1": {
|
||||
"integrity": "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q==",
|
||||
"dependencies": [
|
||||
|
@ -480,7 +476,6 @@
|
|||
"npm:@tsconfig/svelte@^5.0.4",
|
||||
"npm:moment@^2.30.1",
|
||||
"npm:svelte-check@^4.1.5",
|
||||
"npm:svelte-infinite-loading@^1.4.0",
|
||||
"npm:svelte@^5.23.1",
|
||||
"npm:typescript@~5.7.2",
|
||||
"npm:vite@^6.3.1"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
"@atcute/bluesky": "^2.0.2",
|
||||
"@atcute/client": "^3.0.1",
|
||||
"@atcute/identity-resolver": "^0.1.2",
|
||||
"moment": "^2.30.1",
|
||||
"svelte-infinite-loading": "^1.4.0"
|
||||
"moment": "^2.30.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
|
|
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -1,36 +1,10 @@
|
|||
<script lang="ts">
|
||||
import PostComponent from "./lib/PostComponent.svelte";
|
||||
import AccountComponent from "./lib/AccountComponent.svelte";
|
||||
import InfiniteLoading from "svelte-infinite-loading";
|
||||
import { getNextPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
|
||||
import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
|
||||
import { Config } from "../config";
|
||||
const postsPromise = fetchAllPosts();
|
||||
const accountsPromise = getAllMetadataFromPds();
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let posts: Post[] = [];
|
||||
|
||||
onMount(() => {
|
||||
// Fetch initial posts
|
||||
getNextPosts().then((initialPosts) => {
|
||||
posts = initialPosts;
|
||||
});
|
||||
});
|
||||
// Infinite loading function
|
||||
const onInfinite = ({
|
||||
detail: { loaded, complete },
|
||||
}: {
|
||||
detail: { loaded: () => void; complete: () => void };
|
||||
}) => {
|
||||
getNextPosts().then((newPosts) => {
|
||||
console.log("Loading next posts...");
|
||||
if (newPosts.length > 0) {
|
||||
posts = [...posts, ...newPosts];
|
||||
loaded();
|
||||
} else {
|
||||
complete();
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<main>
|
||||
|
@ -52,18 +26,22 @@
|
|||
<p>Error: {error.message}</p>
|
||||
{/await}
|
||||
|
||||
{#await postsPromise}
|
||||
<p>Loading...</p>
|
||||
{:then postsData}
|
||||
<div id="Feed">
|
||||
<div id="spacer"></div>
|
||||
{#each posts as postObject}
|
||||
{#each postsData as postObject}
|
||||
<PostComponent post={postObject as Post} />
|
||||
{/each}
|
||||
<InfiniteLoading on:infinite={onInfinite} distance={3000} />
|
||||
<div id="spacer"></div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
|
||||
/* desktop style */
|
||||
|
||||
#Content {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@font-face {
|
||||
font-family: "ProggyClean";
|
||||
font-family: 'ProggyClean';
|
||||
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ body {
|
|||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background-color: var(--background-color);
|
||||
font-family: "ProggyClean", monospace;
|
||||
font-family: 'ProggyClean', monospace;
|
||||
font-size: 24px;
|
||||
color: var(--text-color);
|
||||
border-color: var(--border-color);
|
||||
|
|
1
src/assets/svelte.svg
Normal file
1
src/assets/svelte.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -113,7 +113,7 @@
|
|||
<div id="carouselControls">
|
||||
<button
|
||||
id="prevBtn"
|
||||
onclick={prevImage}
|
||||
on:click={prevImage}
|
||||
disabled={currentImageIndex === 0}>←</button
|
||||
>
|
||||
<div id="carouselIndicators">
|
||||
|
@ -125,7 +125,7 @@
|
|||
</div>
|
||||
<button
|
||||
id="nextBtn"
|
||||
onclick={nextImage}
|
||||
on:click={nextImage}
|
||||
disabled={currentImageIndex === post.imagesCid.length - 1}
|
||||
>→</button
|
||||
>
|
||||
|
@ -145,6 +145,7 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
@ -196,7 +197,6 @@
|
|||
background-color: var(--content-background-color);
|
||||
color: var(--text-color);
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
}
|
||||
#replyingText {
|
||||
font-size: 0.7em;
|
||||
|
@ -213,10 +213,6 @@
|
|||
#postText {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: normal;
|
||||
word-break: break-word;
|
||||
hyphens: none;
|
||||
}
|
||||
#headerText {
|
||||
margin-left: 10px;
|
||||
|
|
|
@ -18,15 +18,11 @@ import { Config } from "../../config";
|
|||
// import { AppBskyActorDefs } from "@atcute/client/lexicons";
|
||||
|
||||
interface AccountMetadata {
|
||||
did: At.Did;
|
||||
did: string;
|
||||
displayName: string;
|
||||
handle: string;
|
||||
avatarCid: string | null;
|
||||
currentCursor?: string;
|
||||
}
|
||||
|
||||
let accountsMetadata: AccountMetadata[] = [];
|
||||
|
||||
interface atUriObject {
|
||||
repo: string;
|
||||
collection: string;
|
||||
|
@ -71,8 +67,8 @@ class Post {
|
|||
this.videosLinkCid = null;
|
||||
switch (post.embed?.$type) {
|
||||
case "app.bsky.embed.images":
|
||||
this.imagesCid = post.embed.images.map(
|
||||
(imageRecord: any) => imageRecord.image.ref.$link,
|
||||
this.imagesCid = post.embed.images.map((imageRecord: any) =>
|
||||
imageRecord.image.ref.$link
|
||||
);
|
||||
break;
|
||||
case "app.bsky.embed.video":
|
||||
|
@ -85,8 +81,8 @@ class Post {
|
|||
this.quotingUri = processAtUri(post.embed.record.record.uri);
|
||||
switch (post.embed.media.$type) {
|
||||
case "app.bsky.embed.images":
|
||||
this.imagesCid = post.embed.media.images.map(
|
||||
(imageRecord) => imageRecord.image.ref.$link,
|
||||
this.imagesCid = post.embed.media.images.map((imageRecord) =>
|
||||
imageRecord.image.ref.$link
|
||||
);
|
||||
|
||||
break;
|
||||
|
@ -115,15 +111,13 @@ const rpc = new XRPC({
|
|||
}),
|
||||
});
|
||||
|
||||
const getDidsFromPDS = async (): Promise<At.Did[]> => {
|
||||
const getDidsFromPDS = async () : Promise<At.Did[]> => {
|
||||
const { data } = await rpc.get("com.atproto.sync.listRepos", {
|
||||
params: {},
|
||||
});
|
||||
return data.repos.map((repo: any) => repo.did) as At.Did[];
|
||||
return data.repos.map((repo: any) => (repo.did)) as At.Did[];
|
||||
};
|
||||
const getAccountMetadata = async (
|
||||
did: `did:${string}:${string}`,
|
||||
) => {
|
||||
const getAccountMetadata = async (did: `did:${string}:${string}`) : Promise<AccountMetadata> => {
|
||||
// gonna assume self exists in the app.bsky.actor.profile
|
||||
try {
|
||||
const { data } = await rpc.get("com.atproto.repo.getRecord", {
|
||||
|
@ -145,20 +139,50 @@ const getAccountMetadata = async (
|
|||
account.avatarCid = value.avatar.ref["$link"];
|
||||
}
|
||||
return account;
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`Error fetching metadata for ${did}:`, e);
|
||||
return null;
|
||||
return {
|
||||
did: "error",
|
||||
displayName: "",
|
||||
avatarCid: null,
|
||||
handle: "error",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => {
|
||||
const getAllMetadataFromPds = async () : Promise<AccountMetadata[]> => {
|
||||
const dids = await getDidsFromPDS();
|
||||
const metadata = await Promise.all(
|
||||
dids.map(async (repo: `did:${string}:${string}`) => {
|
||||
return await getAccountMetadata(repo);
|
||||
}),
|
||||
);
|
||||
return metadata.filter((account) => account !== null) as AccountMetadata[];
|
||||
return metadata.filter(account => account.did !== "error");
|
||||
};
|
||||
|
||||
const fetchPosts = async (did: string) => {
|
||||
try {
|
||||
const { data } = await rpc.get("com.atproto.repo.listRecords", {
|
||||
params: {
|
||||
repo: did as At.Identifier,
|
||||
collection: "app.bsky.feed.post",
|
||||
limit: Config.MAX_POSTS,
|
||||
},
|
||||
});
|
||||
return {
|
||||
records: data.records as ComAtprotoRepoListRecords.Record[],
|
||||
did: did,
|
||||
error: false
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`Error fetching posts for ${did}:`, e);
|
||||
return {
|
||||
records: [],
|
||||
did: did,
|
||||
error: true
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const identityResolve = async (did: At.Did) => {
|
||||
|
@ -194,146 +218,27 @@ const blueskyHandleFromDid = async (did: At.Did) => {
|
|||
}
|
||||
};
|
||||
|
||||
interface PostsAcc {
|
||||
posts: ComAtprotoRepoListRecords.Record[];
|
||||
account: AccountMetadata;
|
||||
}
|
||||
const getCutoffDate = (postAccounts: PostsAcc[]) => {
|
||||
const now = Date.now();
|
||||
let cutoffDate: Date | null = null;
|
||||
postAccounts.forEach((postAcc) => {
|
||||
const latestPost = new Date(
|
||||
(postAcc.posts[postAcc.posts.length - 1].value as AppBskyFeedPost.Record)
|
||||
.createdAt,
|
||||
const fetchAllPosts = async () => {
|
||||
const users: AccountMetadata[] = await getAllMetadataFromPds();
|
||||
const postRecords = await Promise.all(
|
||||
users.map(async (metadata: AccountMetadata) =>
|
||||
await fetchPosts(metadata.did)
|
||||
),
|
||||
);
|
||||
if (!cutoffDate) {
|
||||
cutoffDate = latestPost;
|
||||
} else {
|
||||
if (latestPost > cutoffDate) {
|
||||
cutoffDate = latestPost;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (cutoffDate) {
|
||||
return cutoffDate;
|
||||
} else {
|
||||
return new Date(now);
|
||||
const validPostRecords = postRecords.filter(record => !record.error);
|
||||
const posts: Post[] = validPostRecords.flatMap((userFetch) =>
|
||||
userFetch.records.map((record) => {
|
||||
const user = users.find((user: AccountMetadata) =>
|
||||
user.did == userFetch.did
|
||||
);
|
||||
if (!user) {
|
||||
throw new Error(`User with DID ${userFetch.did} not found`);
|
||||
}
|
||||
return new Post(record, user);
|
||||
})
|
||||
);
|
||||
posts.sort((a, b) => b.timestamp - a.timestamp);
|
||||
return posts.slice(0, Config.MAX_POSTS);
|
||||
};
|
||||
|
||||
const filterPostsByDate = (posts: PostsAcc[], cutoffDate: Date) => {
|
||||
// filter posts for each account that are older than the cutoff date and save the cursor of the last post included
|
||||
const filteredPosts: PostsAcc[] = posts.map((postAcc) => {
|
||||
const filtered = postAcc.posts.filter((post) => {
|
||||
const postDate = new Date(
|
||||
(post.value as AppBskyFeedPost.Record).createdAt,
|
||||
);
|
||||
return postDate >= cutoffDate;
|
||||
});
|
||||
if (filtered.length > 0) {
|
||||
postAcc.account.currentCursor = processAtUri(filtered[filtered.length - 1].uri).rkey;
|
||||
}
|
||||
return {
|
||||
posts: filtered,
|
||||
account: postAcc.account,
|
||||
};
|
||||
});
|
||||
return filteredPosts;
|
||||
};
|
||||
// nightmare function. However it works so I am not touching it
|
||||
const getNextPosts = async () => {
|
||||
if (!accountsMetadata.length) {
|
||||
accountsMetadata = await getAllMetadataFromPds();
|
||||
}
|
||||
|
||||
const postsAcc: PostsAcc[] = await Promise.all(
|
||||
accountsMetadata.map(async (account) => {
|
||||
const posts = await fetchPostsForUser(
|
||||
account.did,
|
||||
account.currentCursor || null,
|
||||
);
|
||||
if (posts) {
|
||||
return {
|
||||
posts: posts,
|
||||
account: account,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
posts: [],
|
||||
account: account,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
const recordsFiltered = postsAcc.filter((postAcc) =>
|
||||
postAcc.posts.length > 0
|
||||
);
|
||||
const cutoffDate = getCutoffDate(recordsFiltered);
|
||||
const recordsCutoff = filterPostsByDate(recordsFiltered, cutoffDate);
|
||||
// update the accountMetadata with the new cursor
|
||||
accountsMetadata = accountsMetadata.map((account) => {
|
||||
const postAcc = recordsCutoff.find(
|
||||
(postAcc) => postAcc.account.did == account.did,
|
||||
);
|
||||
if (postAcc) {
|
||||
account.currentCursor = postAcc.account.currentCursor;
|
||||
}
|
||||
return account;
|
||||
}
|
||||
);
|
||||
// throw the records in a big single array
|
||||
let records = recordsCutoff.flatMap((postAcc) => postAcc.posts);
|
||||
// sort the records by timestamp
|
||||
records = records.sort((a, b) => {
|
||||
const aDate = new Date(
|
||||
(a.value as AppBskyFeedPost.Record).createdAt,
|
||||
).getTime();
|
||||
const bDate = new Date(
|
||||
(b.value as AppBskyFeedPost.Record).createdAt,
|
||||
).getTime();
|
||||
return bDate - aDate;
|
||||
});
|
||||
// filter out posts that are in the future
|
||||
if (!Config.SHOW_FUTURE_POSTS) {
|
||||
const now = Date.now();
|
||||
records = records.filter((post) => {
|
||||
const postDate = new Date(
|
||||
(post.value as AppBskyFeedPost.Record).createdAt,
|
||||
).getTime();
|
||||
return postDate <= now;
|
||||
});
|
||||
}
|
||||
|
||||
const newPosts = records.map((record) => {
|
||||
const account = accountsMetadata.find(
|
||||
(account) => account.did == processAtUri(record.uri).repo,
|
||||
);
|
||||
if (!account) {
|
||||
throw new Error(
|
||||
`Account with DID ${processAtUri(record.uri).repo} not found`,
|
||||
);
|
||||
}
|
||||
return new Post(record, account);
|
||||
});
|
||||
return newPosts;
|
||||
};
|
||||
|
||||
const fetchPostsForUser = async (did: At.Did, cursor: string | null) => {
|
||||
try {
|
||||
const { data } = await rpc.get("com.atproto.repo.listRecords", {
|
||||
params: {
|
||||
repo: did as At.Identifier,
|
||||
collection: "app.bsky.feed.post",
|
||||
limit: Config.MAX_POSTS,
|
||||
cursor: cursor || undefined,
|
||||
},
|
||||
});
|
||||
return data.records as ComAtprotoRepoListRecords.Record[];
|
||||
} catch (e) {
|
||||
console.error(`Error fetching posts for ${did}:`, e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export { getAllMetadataFromPds, getNextPosts, Post };
|
||||
export { fetchAllPosts, getAllMetadataFromPds, Post };
|
||||
export type { AccountMetadata };
|
||||
|
|
12
src/main.ts
12
src/main.ts
|
@ -1,9 +1,9 @@
|
|||
import { mount } from "svelte";
|
||||
import "./app.css";
|
||||
import App from "./App.svelte";
|
||||
import { mount } from 'svelte'
|
||||
import './app.css'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById("app")!,
|
||||
});
|
||||
target: document.getElementById('app')!,
|
||||
})
|
||||
|
||||
export default app;
|
||||
export default app
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
});
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue