+15
-2
atproto-notifications/package-lock.json
+15
-2
atproto-notifications/package-lock.json
···
8
8
"name": "atproto-notifications",
9
9
"version": "0.0.0",
10
10
"dependencies": {
11
+
"@atcute/client": "^4.0.3",
11
12
"@atcute/identity-resolver": "^1.1.3",
12
13
"@uidotdev/usehooks": "^2.4.1",
13
14
"lexicons": "file:../lexicons",
···
33
34
}
34
35
},
35
36
"../lexicons": {
36
-
"version": "0.0.1"
37
+
"version": "0.0.1",
38
+
"dependencies": {
39
+
"psl": "^1.15.0"
40
+
}
37
41
},
38
42
"node_modules/@ampproject/remapping": {
39
43
"version": "2.3.0",
···
49
53
"node": ">=6.0.0"
50
54
}
51
55
},
56
+
"node_modules/@atcute/client": {
57
+
"version": "4.0.3",
58
+
"resolved": "https://registry.npmjs.org/@atcute/client/-/client-4.0.3.tgz",
59
+
"integrity": "sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==",
60
+
"license": "MIT",
61
+
"dependencies": {
62
+
"@atcute/identity": "^1.0.2",
63
+
"@atcute/lexicons": "^1.0.3"
64
+
}
65
+
},
52
66
"node_modules/@atcute/identity": {
53
67
"version": "1.0.3",
54
68
"resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-1.0.3.tgz",
55
69
"integrity": "sha512-mNMxbKHFGys03A8JXKk0KfMBzdd0vrYMzZZWjpw1nYTs0+ea6bo5S1hwqVUZxHdo1gFHSe/t63jxQIF4yL9aKw==",
56
70
"license": "0BSD",
57
-
"peer": true,
58
71
"dependencies": {
59
72
"@atcute/lexicons": "^1.0.4",
60
73
"@badrap/valita": "^0.4.5"
+1
atproto-notifications/package.json
+1
atproto-notifications/package.json
+12
-3
atproto-notifications/src/components/Notification.tsx
+12
-3
atproto-notifications/src/components/Notification.tsx
···
1
+
import { useState, useEffect } from 'react';
1
2
import ReactTimeAgo from 'react-time-ago';
2
3
import psl from 'psl';
3
-
import lexicons from 'lexicons';
4
+
import { default as lexicons, getLink } from 'lexicons';
4
5
import { resolveDid } from '../atproto/resolve';
5
6
import { Fetch } from './Fetch';
6
7
7
8
import './Notification.css';
8
9
9
10
export function Notification({ app, group, source, source_record, source_did, subject, timestamp }) {
11
+
const [resolvedLink, setResolvedLink] = useState(null);
12
+
13
+
useEffect(() => {
14
+
(async () => {
15
+
const link = await getLink(source, source_record, subject);
16
+
if (link) setResolvedLink(link);
17
+
})();
18
+
}, [source, source_record, subject]);
10
19
11
20
// TODO: clean up / move this to lexicons package?
12
21
let title = source;
···
48
57
49
58
directLink = lex
50
59
?.clients[0]
51
-
?.direct_links[`at_uri:${sourceRemainder}`]
60
+
?.direct_links?.[`at_uri:${sourceRemainder}`]
52
61
?.replace('{subject.did}', did)
53
62
?.replace('{subject.collection}', collection)
54
63
?.replace('{subject.rkey}', rest.join('/') || null)
···
56
65
?.replace('{source_record.collection}', sCollection)
57
66
?.replace('{source_record.rkey}', sRest.join('/') || null);
58
67
}
59
-
link = directLink ?? link;
68
+
link = resolvedLink ?? directLink ?? link;
60
69
61
70
const contents = (
62
71
<>
+67
lexicons/atproto.js
+67
lexicons/atproto.js
···
1
+
import { Client, CredentialManager, ok, simpleFetchHandler } from '@atcute/client';
2
+
import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver } from '@atcute/identity-resolver';
3
+
4
+
// cleanup needed
5
+
6
+
const docResolver = new CompositeDidDocumentResolver({
7
+
methods: {
8
+
plc: new PlcDidDocumentResolver(),
9
+
web: new WebDidDocumentResolver(),
10
+
},
11
+
});
12
+
13
+
async function resolve_did(did) {
14
+
return await docResolver.resolve(did);
15
+
}
16
+
17
+
function pds({ service }) {
18
+
if (!service) {
19
+
throw new Error('missing service from identity doc');
20
+
}
21
+
const { serviceEndpoint } = service[0];
22
+
if (!serviceEndpoint) {
23
+
throw new Error('missing serviceEndpoint from identity service array');
24
+
}
25
+
return serviceEndpoint;
26
+
}
27
+
28
+
29
+
async function get_pds_record(endpoint, did, collection, rkey) {
30
+
const handler = simpleFetchHandler({ service: endpoint });
31
+
const rpc = new Client({ handler });
32
+
const { ok, data } = await rpc.get('com.atproto.repo.getRecord', {
33
+
params: { repo: did, collection, rkey },
34
+
});
35
+
if (!ok) throw new Error('fetching pds record failed');
36
+
return data;
37
+
}
38
+
39
+
function parse_at_uri(uri) {
40
+
let collection, rkey;
41
+
if (!uri.startsWith('at://')) {
42
+
throw new Error('invalid at-uri: did not start with "at://"');
43
+
}
44
+
let remaining = uri.slice('at://'.length); // remove the at:// prefix
45
+
remaining = remaining.split('#')[0]; // hash is valid in at-uri but we don't handle them
46
+
remaining = remaining.split('?')[0]; // query is valid in at-uri but we don't handle it
47
+
const segments = remaining.split('/');
48
+
if (segments.length === 0) {
49
+
throw new Error('invalid at-uri: could not find did after "at://"');
50
+
}
51
+
const did = segments[0];
52
+
if (segments.length > 1) {
53
+
collection = segments[1];
54
+
}
55
+
if (segments.length > 2) {
56
+
rkey = segments.slice(2).join('/'); // hmm are slashes actually valid in rkey?
57
+
}
58
+
return { did, collection, rkey };
59
+
}
60
+
61
+
export async function getAtUri(atUri) {
62
+
const { did, collection, rkey } = parse_at_uri(atUri);
63
+
const doc = await resolve_did(did);
64
+
const endpoint = pds(doc);
65
+
const { value } = await get_pds_record(endpoint, did, collection, rkey);
66
+
return value;
67
+
}
+73
-2
lexicons/bits.js
+73
-2
lexicons/bits.js
···
1
1
import psl from 'psl';
2
+
import { JSONPath } from 'jsonpath-plus';
2
3
import defs from './defs.js';
4
+
import { getAtUri } from './atproto.js';
3
5
4
6
export function getBits(source) {
5
7
const [nsid, ...rp] = source.split(':');
6
8
const segments = nsid.split('.');
7
9
const group = segments.slice(0, segments.length - 1).join('.') ?? null;
8
-
const unreversed = segments.toReversed().join('.');
9
-
const app = psl.parse(unreversed)?.domain ?? null;
10
+
segments.reverse();
11
+
const app = psl.parse(segments.join('.'))?.domain ?? null;
10
12
return { app, group };
11
13
}
14
+
15
+
function getAppDefs(source) {
16
+
const { app } = getBits(source);
17
+
const appPrefix = source.slice(0, app.length);
18
+
const appSource = source.slice(app.length + 1);
19
+
return [appSource, defs[appPrefix]];
20
+
}
21
+
22
+
export async function getContext(source, source_record, subject) {
23
+
}
24
+
25
+
const uriBits = async uri => {
26
+
const bits = uri.slice('at://'.length).split('/');
27
+
// TODO: identifier might be a handle
28
+
// TODO: rest might contain stuff after the rkey
29
+
const [did, nsid, rkey] = [bits[0], bits[1], bits.slice(2)];
30
+
return [did, nsid, rkey.join('/') || null];
31
+
};
32
+
33
+
export async function getLink(source, source_record, subject) {
34
+
// TODO: pass in preferred client
35
+
const [appSource, appDefs] = getAppDefs(source);
36
+
const appLinks = appDefs?.clients?.[0]?.direct_links;
37
+
const linkType = subject.startsWith('did:') ? 'did' : 'at_uri';
38
+
const linkTemplate = appLinks?.[`${linkType}:${appSource}`];
39
+
if (!linkTemplate) return null;
40
+
41
+
let link = linkTemplate;
42
+
43
+
// 1. sync subs
44
+
const [sourceDid, sourceNsid, sourceRkey] = await uriBits(source_record);
45
+
link = link
46
+
.replaceAll('{source_record.did}', sourceDid)
47
+
.replaceAll('{source_record.collection}', sourceNsid)
48
+
.replaceAll('{source_record.rkey}', sourceRkey);
49
+
if (linkTemplate === 'did') {
50
+
link = link.replaceAll('{subject.did}', subject);
51
+
} else {
52
+
const [subjectDid, subjectNsid, subjectRkey] = await uriBits(subject);
53
+
link = link
54
+
.replaceAll('{subject.did}', subjectDid)
55
+
.replaceAll('{subject.collection}', subjectNsid)
56
+
.replaceAll('{subject.rkey}', subjectRkey);
57
+
}
58
+
59
+
// 2. async lookups
60
+
61
+
// do we need to fetch anything from the link subject record?
62
+
if (linkType === 'at_uri') {
63
+
const subjectMatches = [...link.matchAll(/(\{@subject:(?<path>[^\}]+)\})/g)];
64
+
if (subjectMatches.length > 0) {
65
+
const subjectRecord = await getAtUri(subject);
66
+
67
+
// do the actual replacements
68
+
for (const match of subjectMatches) {
69
+
// TODO: JSONPath won't actually cut it once we get $type in
70
+
const sub = JSONPath({
71
+
path: `$.${match.groups.path}`,
72
+
json: subjectRecord,
73
+
})[0];
74
+
75
+
link = link.replaceAll(match[0], sub);
76
+
}
77
+
}
78
+
}
79
+
80
+
// 2.b TODO: source record lookups if needed
81
+
return link;
82
+
}
+3
lexicons/defs.js
+3
lexicons/defs.js
+1
-1
lexicons/index.js
+1
-1
lexicons/index.js
+121
lexicons/package-lock.json
+121
lexicons/package-lock.json
···
8
8
"name": "lexicons",
9
9
"version": "0.0.1",
10
10
"dependencies": {
11
+
"@atcute/client": "^4.0.3",
12
+
"@atcute/identity-resolver": "^1.1.3",
13
+
"jsonpath-plus": "^10.3.0",
11
14
"psl": "^1.15.0"
15
+
}
16
+
},
17
+
"node_modules/@atcute/client": {
18
+
"version": "4.0.3",
19
+
"resolved": "https://registry.npmjs.org/@atcute/client/-/client-4.0.3.tgz",
20
+
"integrity": "sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==",
21
+
"license": "MIT",
22
+
"dependencies": {
23
+
"@atcute/identity": "^1.0.2",
24
+
"@atcute/lexicons": "^1.0.3"
25
+
}
26
+
},
27
+
"node_modules/@atcute/identity": {
28
+
"version": "1.0.3",
29
+
"resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-1.0.3.tgz",
30
+
"integrity": "sha512-mNMxbKHFGys03A8JXKk0KfMBzdd0vrYMzZZWjpw1nYTs0+ea6bo5S1hwqVUZxHdo1gFHSe/t63jxQIF4yL9aKw==",
31
+
"license": "0BSD",
32
+
"dependencies": {
33
+
"@atcute/lexicons": "^1.0.4",
34
+
"@badrap/valita": "^0.4.5"
35
+
}
36
+
},
37
+
"node_modules/@atcute/identity-resolver": {
38
+
"version": "1.1.3",
39
+
"resolved": "https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-1.1.3.tgz",
40
+
"integrity": "sha512-KZgGgg99CWaV7Df3+h3X/WMrDzTPQVfsaoIVbTNLx2B56BvCL2EmaxPSVw/7BFUJMZHlVU4rtoEB4lyvNyMswA==",
41
+
"license": "MIT",
42
+
"dependencies": {
43
+
"@atcute/lexicons": "^1.0.4",
44
+
"@atcute/util-fetch": "^1.0.1",
45
+
"@badrap/valita": "^0.4.4"
46
+
},
47
+
"peerDependencies": {
48
+
"@atcute/identity": "^1.0.0"
49
+
}
50
+
},
51
+
"node_modules/@atcute/lexicons": {
52
+
"version": "1.1.0",
53
+
"resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.1.0.tgz",
54
+
"integrity": "sha512-LFqwnria78xLYb62Ri/+WwQpUTgZp2DuyolNGIIOV1dpiKhFFFh//nscHMA6IExFLQRqWDs3tTjy7zv0h3sf1Q==",
55
+
"license": "0BSD",
56
+
"dependencies": {
57
+
"esm-env": "^1.2.2"
58
+
}
59
+
},
60
+
"node_modules/@atcute/util-fetch": {
61
+
"version": "1.0.1",
62
+
"resolved": "https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-1.0.1.tgz",
63
+
"integrity": "sha512-Clc0E/5ufyGBVfYBUwWNlHONlZCoblSr4Ho50l1LhmRPGB1Wu/AQ9Sz+rsBg7fdaW/auve8ulmwhRhnX2cGRow==",
64
+
"license": "MIT",
65
+
"dependencies": {
66
+
"@badrap/valita": "^0.4.2"
67
+
}
68
+
},
69
+
"node_modules/@badrap/valita": {
70
+
"version": "0.4.5",
71
+
"resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.4.5.tgz",
72
+
"integrity": "sha512-4QwGbuhh/JesHRQj79mO/l37PvJj4l/tlAu7+S1n4h47qwaNpZ0WDvIwUGLYUsdi9uQ5UPpiG9wb1Wm3XUFBUQ==",
73
+
"license": "MIT",
74
+
"engines": {
75
+
"node": ">= 18"
76
+
}
77
+
},
78
+
"node_modules/@jsep-plugin/assignment": {
79
+
"version": "1.3.0",
80
+
"resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz",
81
+
"integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==",
82
+
"license": "MIT",
83
+
"engines": {
84
+
"node": ">= 10.16.0"
85
+
},
86
+
"peerDependencies": {
87
+
"jsep": "^0.4.0||^1.0.0"
88
+
}
89
+
},
90
+
"node_modules/@jsep-plugin/regex": {
91
+
"version": "1.0.4",
92
+
"resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz",
93
+
"integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==",
94
+
"license": "MIT",
95
+
"engines": {
96
+
"node": ">= 10.16.0"
97
+
},
98
+
"peerDependencies": {
99
+
"jsep": "^0.4.0||^1.0.0"
100
+
}
101
+
},
102
+
"node_modules/esm-env": {
103
+
"version": "1.2.2",
104
+
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
105
+
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
106
+
"license": "MIT"
107
+
},
108
+
"node_modules/jsep": {
109
+
"version": "1.4.0",
110
+
"resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz",
111
+
"integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==",
112
+
"license": "MIT",
113
+
"engines": {
114
+
"node": ">= 10.16.0"
115
+
}
116
+
},
117
+
"node_modules/jsonpath-plus": {
118
+
"version": "10.3.0",
119
+
"resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz",
120
+
"integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==",
121
+
"license": "MIT",
122
+
"dependencies": {
123
+
"@jsep-plugin/assignment": "^1.3.0",
124
+
"@jsep-plugin/regex": "^1.0.4",
125
+
"jsep": "^1.4.0"
126
+
},
127
+
"bin": {
128
+
"jsonpath": "bin/jsonpath-cli.js",
129
+
"jsonpath-plus": "bin/jsonpath-cli.js"
130
+
},
131
+
"engines": {
132
+
"node": ">=18.0.0"
12
133
}
13
134
},
14
135
"node_modules/psl": {