+15
.tangled/workflows/test.yml
+15
.tangled/workflows/test.yml
+1
-1
alicia/README.md
+1
-1
alicia/README.md
+7
alicia/identity/birdie_snapshots/parse_empty_handle.accepted
+7
alicia/identity/birdie_snapshots/parse_empty_handle.accepted
+7
alicia/identity/birdie_snapshots/parse_invalid_handle.accepted
+7
alicia/identity/birdie_snapshots/parse_invalid_handle.accepted
+7
alicia/identity/birdie_snapshots/parse_valid_handle.accepted
+7
alicia/identity/birdie_snapshots/parse_valid_handle.accepted
+3
alicia/identity/gleam.toml
+3
alicia/identity/gleam.toml
+25
alicia/identity/manifest.toml
+25
alicia/identity/manifest.toml
···
2
2
# You typically do not need to edit this file
3
3
4
4
packages = [
5
+
{ name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
6
+
{ name = "birdie", version = "1.5.3", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "envoy", "filepath", "glance", "gleam_community_ansi", "gleam_stdlib", "global_value", "justin", "rank", "simplifile", "term_size", "tom", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "64E7D9CC9E84272DA07061628E1B8F31F34FCD2008BCED47AB8FD58457CA63E2" },
7
+
{ name = "edit_distance", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "7DC465C34695F9E57D79FC65670C53C992CE342BF29E0AA41FF44F61AF62FC56" },
8
+
{ name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" },
9
+
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
10
+
{ name = "glam", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "237C2CE218A2A0A5D46D625F8EF5B78F964BC91018B78D692B17E1AB84295229" },
11
+
{ name = "glance", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "49E0ED4793BB3F56C3E5ED00528D70CAE21D263F70A735604124B95C5F62E2DB" },
12
+
{ name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" },
13
+
{ name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" },
14
+
{ name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
15
+
{ name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
5
16
{ name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" },
17
+
{ name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" },
6
18
{ name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
19
+
{ name = "glexer", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "splitter"], otp_app = "glexer", source = "hex", outer_checksum = "40A1FB0919FA080AD6C5809B4C7DBA545841CAAC8168FACDFA0B0667C22475CC" },
20
+
{ name = "global_value", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "global_value", source = "hex", outer_checksum = "23F74C91A7B819C43ABCCBF49DAD5BB8799D81F2A3736BA9A534BD47F309FF4F" },
21
+
{ name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" },
22
+
{ name = "pprint", version = "1.0.6", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "4E9B34AE03B2E81D60F230B9BAF1792BE1AC37AFB5564B8DEBEE56BAEC866B7D" },
23
+
{ name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" },
24
+
{ name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" },
25
+
{ name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" },
26
+
{ name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
27
+
{ name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" },
28
+
{ name = "trie_again", version = "1.1.4", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "E3BD66B4E126EF567EA8C4944EAB216413392ADF6C16C36047AF79EE5EF13466" },
7
29
]
8
30
9
31
[requirements]
32
+
birdie = { version = ">= 1.5.3 and < 2.0.0" }
33
+
gleam_regexp = { version = ">= 1.1.1 and < 2.0.0" }
10
34
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
11
35
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
36
+
pprint = { version = ">= 1.0.6 and < 2.0.0" }
+62
alicia/identity/src/alicia/identity/did.gleam
+62
alicia/identity/src/alicia/identity/did.gleam
···
1
+
import gleam/dynamic/decode
2
+
3
+
pub type Did =
4
+
String
5
+
6
+
pub type Document {
7
+
Document(
8
+
did: Did,
9
+
also_known_as: List(String),
10
+
verification_method: List(DocumentVerificationMethod),
11
+
service: List(DocumentService),
12
+
)
13
+
}
14
+
15
+
pub fn document_decoder() -> decode.Decoder(Document) {
16
+
use did <- decode.field("did", decode.string)
17
+
use also_known_as <- decode.field("alsoKnownAs", decode.list(decode.string))
18
+
use verification_method <- decode.field(
19
+
"verificationMethod",
20
+
decode.list(document_verification_method_decoder()),
21
+
)
22
+
use service <- decode.field(
23
+
"service",
24
+
decode.list(document_service_decoder()),
25
+
)
26
+
decode.success(Document(did:, also_known_as:, verification_method:, service:))
27
+
}
28
+
29
+
pub type DocumentVerificationMethod {
30
+
DocumentVerificationMethod(
31
+
id: String,
32
+
type_: String,
33
+
controller: String,
34
+
public_key_multibase: String,
35
+
)
36
+
}
37
+
38
+
pub fn document_verification_method_decoder() -> decode.Decoder(
39
+
DocumentVerificationMethod,
40
+
) {
41
+
use id <- decode.field("id", decode.string)
42
+
use type_ <- decode.field("type", decode.string)
43
+
use controller <- decode.field("controller", decode.string)
44
+
use public_key_multibase <- decode.field("publicKeyMultibase", decode.string)
45
+
decode.success(DocumentVerificationMethod(
46
+
id:,
47
+
type_:,
48
+
controller:,
49
+
public_key_multibase:,
50
+
))
51
+
}
52
+
53
+
pub type DocumentService {
54
+
DocumentService(id: String, type_: String, service_endpoint: String)
55
+
}
56
+
57
+
pub fn document_service_decoder() -> decode.Decoder(DocumentService) {
58
+
use id <- decode.field("id", decode.string)
59
+
use type_ <- decode.field("type", decode.string)
60
+
use service_endpoint <- decode.field("serviceEndpoint", decode.string)
61
+
decode.success(DocumentService(id:, type_:, service_endpoint:))
62
+
}
+203
alicia/identity/src/alicia/identity.gleam
+203
alicia/identity/src/alicia/identity.gleam
···
1
+
import alicia/identity/did
2
+
import gleam/bool
3
+
import gleam/dict
4
+
import gleam/list
5
+
import gleam/regexp
6
+
import gleam/result
7
+
import gleam/string
8
+
import gleam/uri
9
+
10
+
pub const invalid_handle = "handle.invalid"
11
+
12
+
pub type IdentityError {
13
+
NoServicesFound
14
+
ServiceNotFound(String)
15
+
UrlParseFailure
16
+
RegexCompileFailure
17
+
HandleEmpty
18
+
HandleTooLong
19
+
HandleRegexFailure(String)
20
+
HandleNotDeclared
21
+
}
22
+
23
+
pub type Handle =
24
+
String
25
+
26
+
pub type Identity {
27
+
Identity(
28
+
did: did.Did,
29
+
handle: Handle,
30
+
also_known_as: List(String),
31
+
services: dict.Dict(String, ServiceEndpoint),
32
+
keys: dict.Dict(String, VerificationMethod),
33
+
)
34
+
}
35
+
36
+
pub type ServiceEndpoint {
37
+
ServiceEndpoint(type_: String, url: String)
38
+
}
39
+
40
+
pub type VerificationMethod {
41
+
VerificationMethod(type_: String, public_key_multibase: String)
42
+
}
43
+
44
+
pub fn parse_identity(doc doc: did.Document) -> Identity {
45
+
let keys =
46
+
list.fold(doc.verification_method, dict.new(), fn(keys, vm) {
47
+
{
48
+
use #(_, right) <- result.try(string.split_once(vm.id, "#"))
49
+
// ignore keys not controlled by this DID
50
+
use <- bool.guard(when: vm.controller != doc.did, return: Error(Nil))
51
+
// don't overwrite existing keys with this ID fragment
52
+
use <- bool.guard(when: dict.has_key(keys, right), return: Error(Nil))
53
+
Ok(dict.insert(
54
+
keys,
55
+
right,
56
+
VerificationMethod(
57
+
type_: vm.type_,
58
+
public_key_multibase: vm.public_key_multibase,
59
+
),
60
+
))
61
+
}
62
+
|> result.unwrap(keys)
63
+
})
64
+
65
+
let services =
66
+
list.fold(doc.service, dict.new(), fn(services, svc) {
67
+
{
68
+
use #(_, right) <- result.try(string.split_once(svc.id, "#"))
69
+
// don't overwrite existing services with this ID fragment
70
+
use <- bool.guard(
71
+
when: dict.has_key(services, right),
72
+
return: Error(Nil),
73
+
)
74
+
Ok(dict.insert(
75
+
services,
76
+
right,
77
+
ServiceEndpoint(type_: svc.type_, url: svc.service_endpoint),
78
+
))
79
+
}
80
+
|> result.unwrap(services)
81
+
})
82
+
Identity(
83
+
did: doc.did,
84
+
handle: invalid_handle,
85
+
also_known_as: doc.also_known_as,
86
+
services:,
87
+
keys:,
88
+
)
89
+
}
90
+
91
+
pub fn service_endpoint(
92
+
identity identity: Identity,
93
+
id id: String,
94
+
) -> Result(String, IdentityError) {
95
+
use <- bool.guard(
96
+
when: dict.is_empty(identity.services),
97
+
return: Error(NoServicesFound),
98
+
)
99
+
use endpoint <- result.try(
100
+
dict.get(identity.services, id)
101
+
|> result.replace_error(ServiceNotFound(id)),
102
+
)
103
+
use _ <- result.try(
104
+
uri.parse(endpoint.url)
105
+
|> result.replace_error(UrlParseFailure),
106
+
)
107
+
// TODO: Possibly return Uri here instead of just using it to validate the
108
+
// url? - Depends on usecase in future.
109
+
Ok(endpoint.url)
110
+
}
111
+
112
+
pub fn pds_endpoint(
113
+
identity identity: Identity,
114
+
) -> Result(String, IdentityError) {
115
+
service_endpoint(identity:, id: "atproto_pds")
116
+
}
117
+
118
+
pub fn handle_regex() -> Result(regexp.Regexp, IdentityError) {
119
+
regexp.from_string(
120
+
"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$",
121
+
)
122
+
|> result.replace_error(RegexCompileFailure)
123
+
}
124
+
125
+
pub fn parse_handle(handle handle: String) -> Result(Handle, IdentityError) {
126
+
use <- bool.guard(when: string.is_empty(handle), return: Error(HandleEmpty))
127
+
use <- bool.guard(
128
+
when: string.length(handle) > 253,
129
+
return: Error(HandleTooLong),
130
+
)
131
+
use regex <- result.try(handle_regex())
132
+
use <- bool.guard(
133
+
when: !regexp.check(regex, handle),
134
+
return: Error(HandleRegexFailure(handle)),
135
+
)
136
+
Ok(handle)
137
+
}
138
+
139
+
pub fn normalise_handle(handle handle: Handle) -> Handle {
140
+
string.lowercase(handle)
141
+
}
142
+
143
+
pub fn declared_handle(
144
+
identity identity: Identity,
145
+
) -> Result(Handle, IdentityError) {
146
+
let valid_akas =
147
+
list.map(identity.also_known_as, fn(handle) {
148
+
case handle {
149
+
"at://" <> rest -> Ok(rest)
150
+
_ -> Error(HandleNotDeclared)
151
+
}
152
+
})
153
+
use handle <- result.try(
154
+
valid_akas
155
+
|> result.values
156
+
|> list.first
157
+
|> result.replace_error(HandleNotDeclared),
158
+
)
159
+
use handle <- result.try(parse_handle(handle))
160
+
Ok(normalise_handle(handle))
161
+
}
162
+
163
+
pub fn public_key(identity identity: Identity, id id: String) -> String {
164
+
todo as "unimplemented"
165
+
}
166
+
167
+
pub fn repo_public_key(identity identity: Identity) -> String {
168
+
public_key(identity:, id: "atproto")
169
+
}
170
+
171
+
pub fn did_document(identity identity: Identity) -> did.Document {
172
+
let verification_method =
173
+
dict.fold(identity.keys, [], fn(acc, handle, vm) {
174
+
[
175
+
did.DocumentVerificationMethod(
176
+
id: identity.did <> "#" <> handle,
177
+
type_: vm.type_,
178
+
controller: identity.did,
179
+
public_key_multibase: vm.public_key_multibase,
180
+
),
181
+
..acc
182
+
]
183
+
})
184
+
185
+
let service =
186
+
dict.fold(identity.services, [], fn(acc, handle, svc) {
187
+
[
188
+
did.DocumentService(
189
+
id: "#" <> handle,
190
+
type_: svc.type_,
191
+
service_endpoint: svc.url,
192
+
),
193
+
..acc
194
+
]
195
+
})
196
+
197
+
did.Document(
198
+
did: identity.did,
199
+
also_known_as: identity.also_known_as,
200
+
verification_method:,
201
+
service:,
202
+
)
203
+
}
+18
-5
alicia/identity/test/alicia_identity_test.gleam
+18
-5
alicia/identity/test/alicia_identity_test.gleam
···
1
+
import alicia/identity
2
+
import birdie
1
3
import gleeunit
4
+
import pprint
2
5
3
6
pub fn main() -> Nil {
4
7
gleeunit.main()
5
8
}
6
9
7
-
// gleeunit test functions end in `_test`
8
-
pub fn hello_world_test() {
9
-
let name = "Joe"
10
-
let greeting = "Hello, " <> name <> "!"
10
+
pub fn handle_parse_test() {
11
+
identity.parse_handle("lesbian.skin")
12
+
|> pprint.format
13
+
|> birdie.snap(title: "Parse valid handle")
14
+
}
15
+
16
+
pub fn empty_handle_parse_test() {
17
+
identity.parse_handle("")
18
+
|> pprint.format
19
+
|> birdie.snap(title: "Parse empty handle")
20
+
}
11
21
12
-
assert greeting == "Hello, Joe!"
22
+
pub fn invalid_handle_parse_test() {
23
+
identity.parse_handle("lesbian#skin")
24
+
|> pprint.format
25
+
|> birdie.snap(title: "Parse invalid handle")
13
26
}
+17
-4
flake.nix
+17
-4
flake.nix
···
16
16
default = pkgs.mkShell {
17
17
packages = with pkgs; [
18
18
gleam
19
-
erlang_28
20
-
beam28Packages.rebar3
19
+
beamMinimal28Packages.erlang
20
+
beamMinimal28Packages.rebar3
21
21
jq
22
22
];
23
23
};
···
28
28
cd "$rootDir/${service}"
29
29
'';
30
30
runtimeInputs = with pkgs; [
31
+
jujutsu
31
32
gleam
32
-
erlang_28
33
-
beam28Packages.rebar3
33
+
beamMinimal28Packages.erlang
34
+
beamMinimal28Packages.rebar3
34
35
];
35
36
in {
36
37
# TODO: make default app run both frontend and backend
···
67
68
${pkgs.gleam}/bin/gleam run -m alicia/lexgen -- --dir=${./lexicons}
68
69
'';
69
70
})}/bin/run-lexgen";
71
+
};
72
+
test = {
73
+
type = "app";
74
+
program = "${(pkgs.writeShellApplication {
75
+
inherit runtimeInputs;
76
+
name = "run-tests";
77
+
text = ''
78
+
${cdInto "alicia/identity"}
79
+
${pkgs.gleam}/bin/gleam test
80
+
${cdInto ""}
81
+
'';
82
+
})}/bin/run-tests";
70
83
};
71
84
});
72
85
};
+21
lexicons/system/dnd5e/character.json
+21
lexicons/system/dnd5e/character.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.sheetr.system.dnd5e.character",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"key": "tid",
8
+
"description": "A character for D&D 5th edition.",
9
+
"record": {
10
+
"type": "object",
11
+
"required": [],
12
+
"properties": {
13
+
"description": {
14
+
"type": "string",
15
+
"format": "at-uri"
16
+
}
17
+
}
18
+
}
19
+
}
20
+
}
21
+
}
-21
lexicons/system/dnd5e/system.json
-21
lexicons/system/dnd5e/system.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "app.sheetr.system.dnd5e",
4
-
"defs": {
5
-
"main": {
6
-
"type": "record",
7
-
"key": "tid",
8
-
"description": "A system for Sheetr to support D&D 5th edition.",
9
-
"record": {
10
-
"type": "object",
11
-
"required": [],
12
-
"properties": {
13
-
"description": {
14
-
"type": "string",
15
-
"format": "at-uri"
16
-
}
17
-
}
18
-
}
19
-
}
20
-
}
21
-
}