+26
-19
config.ts
+26
-19
config.ts
···
2
* Configuration module for the PDS Dashboard
3
*/
4
export class Config {
5
-
/**
6
-
* The base URL of the PDS (Personal Data Server)
7
-
* @default "https://pds.witchcraft.systems"
8
-
*/
9
-
static readonly PDS_URL: string = "https://pds.witchcraft.systems";
10
11
-
/**
12
-
* The base URL of the frontend service for linking to replies
13
-
* @default "https://deer.social"
14
-
*/
15
-
static readonly FRONTEND_URL: string = "https://deer.social";
16
17
-
/**
18
-
* Maximum number of posts to show in the feed (across all users)
19
-
* @default 100
20
-
*/
21
-
static readonly MAX_POSTS: number = 100;
22
23
/**
24
-
* Footer text for the dashboard
25
-
* @default "Astrally projected from witchcraft.systems"
26
*/
27
-
static readonly FOOTER_TEXT: string = "Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a>";
28
-
}
···
2
* Configuration module for the PDS Dashboard
3
*/
4
export class Config {
5
+
/**
6
+
* The base URL of the PDS (Personal Data Server)
7
+
* @default "https://pds.witchcraft.systems"
8
+
*/
9
+
static readonly PDS_URL: string = "https://pds.witchcraft.systems";
10
+
11
+
/**
12
+
* The base URL of the frontend service for linking to replies
13
+
* @default "https://deer.social"
14
+
*/
15
+
static readonly FRONTEND_URL: string = "https://deer.social";
16
17
+
/**
18
+
* Maximum number of posts to show in the feed (across all users)
19
+
* @default 100
20
+
*/
21
+
static readonly MAX_POSTS: number = 100;
22
23
+
/**
24
+
* Footer text for the dashboard
25
+
* @default "Astrally projected from witchcraft.systems"
26
+
*/
27
+
static readonly FOOTER_TEXT: string =
28
+
"Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a>";
29
30
/**
31
+
* Whether to show the posts that are in the future
32
+
* @default false
33
*/
34
+
static readonly SHOW_FUTURE_POSTS: boolean = false;
35
+
}
+1
-1
index.html
+1
-1
index.html
+27
-28
src/App.svelte
+27
-28
src/App.svelte
···
9
10
<main>
11
<div id="Content">
12
-
{#await accountsPromise}
13
-
<p>Loading...</p>
14
-
{:then accountsData}
15
-
<div id="Account">
16
-
<h1 id="Header">ATProto PDS</h1>
17
-
<p>Home to {accountsData.length} accounts</p>
18
-
<div id="accountsList">
19
-
{#each accountsData as accountObject}
20
-
<AccountComponent account={accountObject} />
21
-
{/each}
22
</div>
23
-
<p>{@html Config.FOOTER_TEXT}</p>
24
-
</div>
25
-
{:catch error}
26
-
<p>Error: {error.message}</p>
27
-
{/await}
28
29
-
{#await postsPromise}
30
-
<p>Loading...</p>
31
-
{:then postsData}
32
-
<div id="Feed">
33
-
<div id="spacer"></div>
34
-
{#each postsData as postObject}
35
-
<PostComponent post={postObject as Post} />
36
-
{/each}
37
-
<div id="spacer"></div>
38
-
</div>
39
-
{/await}
40
</div>
41
</main>
42
43
<style>
44
-
45
/* desktop style */
46
47
#Content {
···
74
#Account {
75
width: 35%;
76
display: flex;
77
-
flex-direction: column;
78
border: 1px solid var(--border-color);
79
background-color: var(--content-background-color);
80
height: 80vh;
···
9
10
<main>
11
<div id="Content">
12
+
{#await accountsPromise}
13
+
<p>Loading...</p>
14
+
{:then accountsData}
15
+
<div id="Account">
16
+
<h1 id="Header">ATProto PDS</h1>
17
+
<p>Home to {accountsData.length} accounts</p>
18
+
<div id="accountsList">
19
+
{#each accountsData as accountObject}
20
+
<AccountComponent account={accountObject} />
21
+
{/each}
22
+
</div>
23
+
<p>{@html Config.FOOTER_TEXT}</p>
24
</div>
25
+
{:catch error}
26
+
<p>Error: {error.message}</p>
27
+
{/await}
28
29
+
{#await postsPromise}
30
+
<p>Loading...</p>
31
+
{:then postsData}
32
+
<div id="Feed">
33
+
<div id="spacer"></div>
34
+
{#each postsData as postObject}
35
+
<PostComponent post={postObject as Post} />
36
+
{/each}
37
+
<div id="spacer"></div>
38
+
</div>
39
+
{/await}
40
</div>
41
</main>
42
43
<style>
44
/* desktop style */
45
46
#Content {
···
73
#Account {
74
width: 35%;
75
display: flex;
76
+
flex-direction: column;
77
border: 1px solid var(--border-color);
78
background-color: var(--content-background-color);
79
height: 80vh;
+3
-3
src/app.css
+3
-3
src/app.css
···
1
@font-face {
2
-
font-family: 'ProggyClean';
3
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
4
}
5
···
62
min-width: 320px;
63
min-height: 100vh;
64
background-color: var(--background-color);
65
-
font-family: 'ProggyClean', monospace;
66
font-size: 24px;
67
color: var(--text-color);
68
border-color: var(--border-color);
···
80
margin-left: auto;
81
margin-right: auto;
82
text-align: center;
83
-
}
···
1
@font-face {
2
+
font-family: "ProggyClean";
3
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
4
}
5
···
62
min-width: 320px;
63
min-height: 100vh;
64
background-color: var(--background-color);
65
+
font-family: "ProggyClean", monospace;
66
font-size: 24px;
67
color: var(--text-color);
68
border-color: var(--border-color);
···
80
margin-left: auto;
81
margin-right: auto;
82
text-align: center;
83
+
}
+1
-1
src/lib/PostComponent.svelte
+1
-1
src/lib/PostComponent.svelte
+51
-41
src/lib/pdsfetch.ts
+51
-41
src/lib/pdsfetch.ts
···
45
46
constructor(
47
record: ComAtprotoRepoListRecords.Record,
48
-
account: AccountMetadata,
49
) {
50
this.postCid = record.cid;
51
this.recordName = processAtUri(record.uri).rkey;
···
67
this.videosLinkCid = null;
68
switch (post.embed?.$type) {
69
case "app.bsky.embed.images":
70
-
this.imagesCid = post.embed.images.map((imageRecord: any) =>
71
-
imageRecord.image.ref.$link
72
);
73
break;
74
case "app.bsky.embed.video":
···
81
this.quotingUri = processAtUri(post.embed.record.record.uri);
82
switch (post.embed.media.$type) {
83
case "app.bsky.embed.images":
84
-
this.imagesCid = post.embed.media.images.map((imageRecord) =>
85
-
imageRecord.image.ref.$link
86
);
87
88
break;
···
111
}),
112
});
113
114
-
const getDidsFromPDS = async () : Promise<At.Did[]> => {
115
const { data } = await rpc.get("com.atproto.sync.listRepos", {
116
params: {},
117
});
118
-
return data.repos.map((repo: any) => (repo.did)) as At.Did[];
119
};
120
-
const getAccountMetadata = async (did: `did:${string}:${string}`) : Promise<AccountMetadata> => {
121
// gonna assume self exists in the app.bsky.actor.profile
122
try {
123
-
const { data } = await rpc.get("com.atproto.repo.getRecord", {
124
-
params: {
125
-
repo: did,
126
-
collection: "app.bsky.actor.profile",
127
-
rkey: "self",
128
-
},
129
-
});
130
-
const value = data.value as AppBskyActorProfile.Record;
131
-
const handle = await blueskyHandleFromDid(did);
132
-
const account: AccountMetadata = {
133
-
did: did,
134
-
handle: handle,
135
-
displayName: value.displayName || "",
136
-
avatarCid: null,
137
-
};
138
-
if (value.avatar) {
139
-
account.avatarCid = value.avatar.ref["$link"];
140
-
}
141
-
return account;
142
-
}
143
-
catch (e) {
144
console.error(`Error fetching metadata for ${did}:`, e);
145
return {
146
did: "error",
···
151
}
152
};
153
154
-
const getAllMetadataFromPds = async () : Promise<AccountMetadata[]> => {
155
const dids = await getDidsFromPDS();
156
const metadata = await Promise.all(
157
dids.map(async (repo: `did:${string}:${string}`) => {
158
return await getAccountMetadata(repo);
159
-
}),
160
);
161
-
return metadata.filter(account => account.did !== "error");
162
};
163
164
const fetchPosts = async (did: string) => {
···
173
return {
174
records: data.records as ComAtprotoRepoListRecords.Record[],
175
did: did,
176
-
error: false
177
};
178
} catch (e) {
179
console.error(`Error fetching posts for ${did}:`, e);
180
return {
181
records: [],
182
did: did,
183
-
error: true
184
};
185
}
186
};
···
195
196
if (did.startsWith("did:plc:") || did.startsWith("did:web:")) {
197
const doc = await resolver.resolve(
198
-
did as `did:plc:${string}` | `did:web:${string}`,
199
);
200
return doc;
201
} else {
···
221
const fetchAllPosts = async () => {
222
const users: AccountMetadata[] = await getAllMetadataFromPds();
223
const postRecords = await Promise.all(
224
-
users.map(async (metadata: AccountMetadata) =>
225
-
await fetchPosts(metadata.did)
226
-
),
227
);
228
-
const validPostRecords = postRecords.filter(record => !record.error);
229
const posts: Post[] = validPostRecords.flatMap((userFetch) =>
230
userFetch.records.map((record) => {
231
-
const user = users.find((user: AccountMetadata) =>
232
-
user.did == userFetch.did
233
);
234
if (!user) {
235
throw new Error(`User with DID ${userFetch.did} not found`);
···
237
return new Post(record, user);
238
})
239
);
240
posts.sort((a, b) => b.timestamp - a.timestamp);
241
return posts.slice(0, Config.MAX_POSTS);
242
};
243
export { fetchAllPosts, getAllMetadataFromPds, Post };
···
45
46
constructor(
47
record: ComAtprotoRepoListRecords.Record,
48
+
account: AccountMetadata
49
) {
50
this.postCid = record.cid;
51
this.recordName = processAtUri(record.uri).rkey;
···
67
this.videosLinkCid = null;
68
switch (post.embed?.$type) {
69
case "app.bsky.embed.images":
70
+
this.imagesCid = post.embed.images.map(
71
+
(imageRecord: any) => imageRecord.image.ref.$link
72
);
73
break;
74
case "app.bsky.embed.video":
···
81
this.quotingUri = processAtUri(post.embed.record.record.uri);
82
switch (post.embed.media.$type) {
83
case "app.bsky.embed.images":
84
+
this.imagesCid = post.embed.media.images.map(
85
+
(imageRecord) => imageRecord.image.ref.$link
86
);
87
88
break;
···
111
}),
112
});
113
114
+
const getDidsFromPDS = async (): Promise<At.Did[]> => {
115
const { data } = await rpc.get("com.atproto.sync.listRepos", {
116
params: {},
117
});
118
+
return data.repos.map((repo: any) => repo.did) as At.Did[];
119
};
120
+
const getAccountMetadata = async (
121
+
did: `did:${string}:${string}`
122
+
): Promise<AccountMetadata> => {
123
// gonna assume self exists in the app.bsky.actor.profile
124
try {
125
+
const { data } = await rpc.get("com.atproto.repo.getRecord", {
126
+
params: {
127
+
repo: did,
128
+
collection: "app.bsky.actor.profile",
129
+
rkey: "self",
130
+
},
131
+
});
132
+
const value = data.value as AppBskyActorProfile.Record;
133
+
const handle = await blueskyHandleFromDid(did);
134
+
const account: AccountMetadata = {
135
+
did: did,
136
+
handle: handle,
137
+
displayName: value.displayName || "",
138
+
avatarCid: null,
139
+
};
140
+
if (value.avatar) {
141
+
account.avatarCid = value.avatar.ref["$link"];
142
+
}
143
+
return account;
144
+
} catch (e) {
145
console.error(`Error fetching metadata for ${did}:`, e);
146
return {
147
did: "error",
···
152
}
153
};
154
155
+
const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => {
156
const dids = await getDidsFromPDS();
157
const metadata = await Promise.all(
158
dids.map(async (repo: `did:${string}:${string}`) => {
159
return await getAccountMetadata(repo);
160
+
})
161
);
162
+
return metadata.filter((account) => account.did !== "error");
163
};
164
165
const fetchPosts = async (did: string) => {
···
174
return {
175
records: data.records as ComAtprotoRepoListRecords.Record[],
176
did: did,
177
+
error: false,
178
};
179
} catch (e) {
180
console.error(`Error fetching posts for ${did}:`, e);
181
return {
182
records: [],
183
did: did,
184
+
error: true,
185
};
186
}
187
};
···
196
197
if (did.startsWith("did:plc:") || did.startsWith("did:web:")) {
198
const doc = await resolver.resolve(
199
+
did as `did:plc:${string}` | `did:web:${string}`
200
);
201
return doc;
202
} else {
···
222
const fetchAllPosts = async () => {
223
const users: AccountMetadata[] = await getAllMetadataFromPds();
224
const postRecords = await Promise.all(
225
+
users.map(
226
+
async (metadata: AccountMetadata) => await fetchPosts(metadata.did)
227
+
)
228
);
229
+
const validPostRecords = postRecords.filter((record) => !record.error);
230
const posts: Post[] = validPostRecords.flatMap((userFetch) =>
231
userFetch.records.map((record) => {
232
+
const user = users.find(
233
+
(user: AccountMetadata) => user.did == userFetch.did
234
);
235
if (!user) {
236
throw new Error(`User with DID ${userFetch.did} not found`);
···
238
return new Post(record, user);
239
})
240
);
241
+
242
posts.sort((a, b) => b.timestamp - a.timestamp);
243
+
244
+
if(!Config.SHOW_FUTURE_POSTS) {
245
+
// Filter out posts that are in the future
246
+
const now = Date.now();
247
+
const filteredPosts = posts.filter((post) => post.timestamp <= now);
248
+
return filteredPosts.slice(0, Config.MAX_POSTS);
249
+
}
250
+
251
return posts.slice(0, Config.MAX_POSTS);
252
};
253
export { fetchAllPosts, getAllMetadataFromPds, Post };
+6
-6
src/main.ts
+6
-6
src/main.ts
+2
-2
svelte.config.js
+2
-2
svelte.config.js
+3
-3
vite.config.ts
+3
-3
vite.config.ts