Image sharing backed by ATProto
atproto images gleam

auth + blob uploading

Signed-off-by: Naomi Roberts <mia@naomieow.xyz>

lesbian.skin 6c4c54f3 9229cd76

verified
+826 -3
+5
.gitignore
··· 2 2 *.ez 3 3 /build 4 4 erl_crash.dump 5 + node_modules/ 6 + 7 + #Added automatically by Lustre Dev Tools 8 + /.lustre 9 + /dist
+77
bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "configVersion": 1, 4 + "workspaces": { 5 + "": { 6 + "dependencies": { 7 + "@atproto/api": "^0.18.4", 8 + "@atproto/oauth-client-browser": "^0.3.36", 9 + }, 10 + }, 11 + }, 12 + "packages": { 13 + "@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.4", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.3", "zod": "^3.23.8" } }, "sha512-sbXxBnAJWsKv/FEGG6a/WLz7zQYUr1vA2TXvNnPwwJQJCjPwEJMOh1vM22wBr185Phy7D2GD88PcRokn7eUVyw=="], 14 + 15 + "@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="], 16 + 17 + "@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.3", "zod": "^3.23.8" } }, "sha512-wsNopfzfgO3uPvfnFDgNeXgDufXxSXhjBjp2WEiSzEiLrMy0Jodnqggw4OzD9MJKf0a4Iu2/ydd537qdy91LrQ=="], 18 + 19 + "@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.4", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.4", "@atproto-labs/handle-resolver": "0.3.4" } }, "sha512-HNUEFQIo2ws6iATxmgHd5D5rAsWYupgxZucgwolVHPiMjE1SY+EmxEsfbEN1wDEzM8/u9AKUg/jrxxPEwsgbew=="], 20 + 21 + "@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="], 22 + 23 + "@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="], 24 + 25 + "@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 26 + 27 + "@atproto/api": ["@atproto/api@0.18.4", "", { "dependencies": { "@atproto/common-web": "^0.4.6", "@atproto/lexicon": "^0.5.2", "@atproto/syntax": "^0.4.2", "@atproto/xrpc": "^0.7.6", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-+kSxto/GRFXRFFlGwfERrwEKnC6OqTgK34BUToer/Fv08q4WMR+GYPRabbWlnDoJWu3owcQfeYdcblQ88vi16g=="], 28 + 29 + "@atproto/common-web": ["@atproto/common-web@0.4.6", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "@atproto/lex-json": "0.0.2", "zod": "^3.23.8" } }, "sha512-+2mG/1oBcB/ZmYIU1ltrFMIiuy9aByKAkb2Fos/0eTdczcLBaH17k0KoxMGvhfsujN2r62XlanOAMzysa7lv1g=="], 30 + 31 + "@atproto/did": ["@atproto/did@0.2.3", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-VI8JJkSizvM2cHYJa37WlbzeCm5tWpojyc1/Zy8q8OOjyoy6X4S4BEfoP941oJcpxpMTObamibQIXQDo7tnIjg=="], 32 + 33 + "@atproto/jwk": ["@atproto/jwk@0.6.0", "", { "dependencies": { "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw=="], 34 + 35 + "@atproto/jwk-jose": ["@atproto/jwk-jose@0.1.11", "", { "dependencies": { "@atproto/jwk": "0.6.0", "jose": "^5.2.0" } }, "sha512-i4Fnr2sTBYmMmHXl7NJh8GrCH+tDQEVWrcDMDnV5DjJfkgT17wIqvojIw9SNbSL4Uf0OtfEv6AgG0A+mgh8b5Q=="], 36 + 37 + "@atproto/jwk-webcrypto": ["@atproto/jwk-webcrypto@0.2.0", "", { "dependencies": { "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "zod": "^3.23.8" } }, "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg=="], 38 + 39 + "@atproto/lex-data": ["@atproto/lex-data@0.0.2", "", { "dependencies": { "@atproto/syntax": "0.4.2", "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-euV2rDGi+coH8qvZOU+ieUOEbwPwff9ca6IiXIqjZJ76AvlIpj7vtAyIRCxHUW2BoU6h9yqyJgn9MKD2a7oIwg=="], 40 + 41 + "@atproto/lex-json": ["@atproto/lex-json@0.0.2", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "tslib": "^2.8.1" } }, "sha512-Pd72lO+l2rhOTutnf11omh9ZkoB/elbzE3HSmn2wuZlyH1mRhTYvoH8BOGokWQwbZkCE8LL3nOqMT3gHCD2l7g=="], 42 + 43 + "@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="], 44 + 45 + "@atproto/oauth-client": ["@atproto/oauth-client@0.5.10", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.4", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.4", "@atproto-labs/identity-resolver": "0.3.4", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.3", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.5.2", "@atproto/xrpc": "0.7.6", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-2mdJFyYbaOw3e/1KMBOQ2/J9p+MfWW8kE6FKdExWrJ7JPJpTJw2ZF2EmdGHCVeXw386dQgXbLkr+w4vbgSqfMQ=="], 46 + 47 + "@atproto/oauth-client-browser": ["@atproto/oauth-client-browser@0.3.36", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.4", "@atproto-labs/handle-resolver": "0.3.4", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.2.3", "@atproto/jwk": "0.6.0", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.10", "@atproto/oauth-types": "0.5.2", "core-js": "^3" } }, "sha512-tilBmqi/6Pel+6JHsse8GlN41x68XlDzxwAovgOUhf5UVFc7NBXP4QQDwLzBOFUfXiwmkb3o9JMtJt/PO6DiVw=="], 48 + 49 + "@atproto/oauth-types": ["@atproto/oauth-types@0.5.2", "", { "dependencies": { "@atproto/did": "0.2.3", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-9DCDvtvCanTwAaU5UakYDO0hzcOITS3RutK5zfLytE5Y9unj0REmTDdN8Xd8YCfUJl7T/9pYpf04Uyq7bFTASg=="], 50 + 51 + "@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 52 + 53 + "@atproto/xrpc": ["@atproto/xrpc@0.7.6", "", { "dependencies": { "@atproto/lexicon": "^0.5.2", "zod": "^3.23.8" } }, "sha512-RvCf4j0JnKYWuz3QzsYCntJi3VuiAAybQsMIUw2wLWcHhchO9F7UaBZINLL2z0qc/cYWPv5NSwcVydMseoCZLA=="], 54 + 55 + "await-lock": ["await-lock@2.2.2", "", {}, "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="], 56 + 57 + "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], 58 + 59 + "iso-datestring-validator": ["iso-datestring-validator@2.2.2", "", {}, "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="], 60 + 61 + "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], 62 + 63 + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], 64 + 65 + "multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 66 + 67 + "tlds": ["tlds@1.261.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA=="], 68 + 69 + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 70 + 71 + "uint8arrays": ["uint8arrays@3.0.0", "", { "dependencies": { "multiformats": "^9.4.2" } }, "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA=="], 72 + 73 + "unicode-segmenter": ["unicode-segmenter@0.14.1", "", {}, "sha512-yHedxlEpUyD+u1UE8qAuCMXVdMLn7yUdlmd8WN7FGmO1ICnpE7LJfnmuXBB+T0zkie3qHsy8fSucqceI/MylOg=="], 74 + 75 + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 76 + } 77 + }
+3 -1
flake.nix
··· 18 18 gleam 19 19 erlang_28 20 20 beam28Packages.rebar3 21 + bun 21 22 ]; 22 23 }; 23 24 }); ··· 26 27 gleam 27 28 erlang_28 28 29 beam28Packages.rebar3 30 + bun 29 31 ]; 30 32 in { 31 33 default = { ··· 34 36 inherit runtimeInputs; 35 37 name = "app"; 36 38 text = '' 37 - ${pkgs.gleam}/bin/gleam run 39 + ${pkgs.gleam}/bin/gleam run -m lustre/dev start 38 40 ''; 39 41 })}/bin/app"; 40 42 };
+11
gleam.toml
··· 1 1 name = "plonk" 2 2 version = "1.0.0" 3 + target = "javascript" 3 4 4 5 # Fill out these fields if you intend to generate HTML documentation or publish 5 6 # your project to the Hex package manager. ··· 14 15 15 16 [dependencies] 16 17 gleam_stdlib = ">= 0.44.0 and < 2.0.0" 18 + lustre = ">= 5.4.0 and < 6.0.0" 19 + lustre_dev_tools = ">= 2.3.1 and < 3.0.0" 20 + formal = ">= 3.0.0 and < 4.0.0" 21 + gleam_javascript = ">= 1.0.0 and < 2.0.0" 22 + gleam_http = ">= 4.3.0 and < 5.0.0" 17 23 18 24 [dev-dependencies] 19 25 gleeunit = ">= 1.0.0 and < 2.0.0" 26 + 27 + [tools.lustre.html] 28 + scripts = [ 29 + { type = "importmap", content = "{ \"imports\": { \"@atproto/oauth-client-browser\": \"https://esm.sh/@atproto/oauth-client-browser@0.3.36\", \"@atproto/api\": \"https://esm.sh/@atproto/api@0.18.4\" } }" } 30 + ]
+43
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 = "booklet", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "booklet", source = "hex", outer_checksum = "08E0FDB78DC4D8A5D3C80295B021505C7D2A2E7B6C6D5EAB7286C36F4A53C851" }, 7 + { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, 8 + { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, 9 + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, 10 + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 11 + { name = "formal", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "formal", source = "hex", outer_checksum = "686D0C4C9CB36397DBAC2EC8C6ED9BD0F81B2DF2E88F75A7DB75F56768DDD8FC" }, 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_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 15 + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 16 + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 17 + { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, 18 + { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, 19 + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 20 + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, 21 + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, 5 22 { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" }, 23 + { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, 24 + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 6 25 { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 26 + { name = "glint", version = "1.2.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "2214C7CEFDE457CEE62140C3D4899B964E05236DA74E4243DFADF4AF29C382BB" }, 27 + { name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" }, 28 + { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, 29 + { name = "group_registry", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "group_registry", source = "hex", outer_checksum = "BC798A53D6F2406DB94E27CB45C57052CB56B32ACF7CC16EA20F6BAEC7E36B90" }, 30 + { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, 31 + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 32 + { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 33 + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 34 + { name = "lustre", version = "5.4.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "40E097BABCE65FB7C460C073078611F7F5802EB07E1A9BFB5C229F71B60F8E50" }, 35 + { name = "lustre_dev_tools", version = "2.3.1", build_tools = ["gleam"], requirements = ["argv", "booklet", "filepath", "gleam_community_ansi", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_regexp", "gleam_stdlib", "glint", "group_registry", "justin", "lustre", "mist", "polly", "simplifile", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "2C8C646FF45087C31C2DE8088C2F6DB26E8CEE52B3A883F47F6B2C4F5A16C9C6" }, 36 + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, 37 + { name = "mist", version = "5.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7C4BE717A81305323C47C8A591E6B9BA4AC7F56354BF70B4D3DF08CC01192668" }, 38 + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, 39 + { name = "polly", version = "2.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib", "simplifile"], otp_app = "polly", source = "hex", outer_checksum = "1BA4D0ACE9BCF52AEA6AD9DE020FD8220CCA399A379E50A1775FC5C1204FCF56" }, 40 + { name = "simplifile", version = "2.3.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "957E0E5B75927659F1D2A1B7B75D7B9BA96FAA8D0C53EA71C4AD9CD0C6B848F6" }, 41 + { name = "snag", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "274F41D6C3ECF99F7686FDCE54183333E41D2C1CA5A3A673F9A8B2C7A4401077" }, 42 + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, 43 + { name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" }, 44 + { name = "wisp", version = "2.1.0", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "362BDDD11BF48EB38CDE51A73BC7D1B89581B395CA998E3F23F11EC026151C54" }, 7 45 ] 8 46 9 47 [requirements] 48 + formal = { version = ">= 3.0.0 and < 4.0.0" } 49 + gleam_http = { version = ">= 4.3.0 and < 5.0.0" } 50 + gleam_javascript = { version = ">= 1.0.0 and < 2.0.0" } 10 51 gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 11 52 gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 53 + lustre = { version = ">= 5.4.0 and < 6.0.0" } 54 + lustre_dev_tools = { version = ">= 2.3.1 and < 3.0.0" }
+6
package.json
··· 1 + { 2 + "dependencies": { 3 + "@atproto/api": "^0.18.4", 4 + "@atproto/oauth-client-browser": "^0.3.36" 5 + } 6 + }
+391 -2
src/plonk.gleam
··· 1 - import gleam/io 1 + import formal/form 2 + import gleam/dynamic/decode 3 + import gleam/javascript/promise 4 + import gleam/list 5 + import gleam/option.{None, Some} 6 + import gleam/result 7 + import lustre 8 + import lustre/attribute as attr 9 + import lustre/effect 10 + import lustre/element 11 + import lustre/element/html 12 + import lustre/event 13 + import plonk/atp 14 + import plonk/file 15 + 16 + type Msg { 17 + OAuthClientCreated(atp.BrowserOAuthClient) 18 + OAuthClientInitialised(atp.OACInit) 19 + OAuthLogin(atp.OAuthSession) 20 + OAuthGetSessionResponse(Result(atp.SessionResponse, String)) 21 + UserSubmittedLoginForm(result: Result(Login, form.Form(Login))) 22 + UserClickedLogout 23 + UserSubmittedImageUploadForm(formdata: List(#(String, FormDataType))) 24 + FileBlobUploadAttempted(result: Result(atp.BlobOutputSchema, Nil)) 25 + ImageRecordCreationAttempted( 26 + result: Result(atp.CreateRecordOutputSchema, String), 27 + ) 28 + } 29 + 30 + type Model { 31 + Model( 32 + client_id: String, 33 + oac: option.Option(atp.BrowserOAuthClient), 34 + agent: option.Option(atp.Agent), 35 + session: option.Option(atp.OAuthSession), 36 + show_spinner: Bool, 37 + show_login: Bool, 38 + login_busy: Bool, 39 + login_form: form.Form(Login), 40 + handle: option.Option(String), 41 + files: List(file.File), 42 + ) 43 + } 2 44 3 45 pub fn main() -> Nil { 4 - io.println("Hello from plonk!") 46 + let app = lustre.application(init, update, view) 47 + let assert Ok(_) = lustre.start(app, "#app", Nil) 48 + Nil 49 + } 50 + 51 + fn init(_flags: Nil) -> #(Model, effect.Effect(Msg)) { 52 + let client_id = atp.build_client_id() 53 + #( 54 + Model( 55 + client_id:, 56 + oac: None, 57 + agent: None, 58 + session: None, 59 + show_spinner: True, 60 + show_login: False, 61 + login_busy: False, 62 + login_form: login_form(), 63 + handle: None, 64 + files: [], 65 + ), 66 + effect.from(fn(dispatch) { 67 + atp.get_oauth_client(client_id:) 68 + |> promise.map(OAuthClientCreated) 69 + |> promise.tap(dispatch) 70 + Nil 71 + }), 72 + ) 73 + } 74 + 75 + fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) { 76 + case msg { 77 + OAuthClientCreated(oac) -> { 78 + #( 79 + Model(..model, oac: Some(oac)), 80 + effect.from(fn(dispatch) { 81 + atp.init_oauth_client(oac) 82 + |> promise.map(OAuthClientInitialised) 83 + |> promise.tap(dispatch) 84 + Nil 85 + }), 86 + ) 87 + } 88 + OAuthClientInitialised(oac_init) -> 89 + case atp.get_session(oac_init:) { 90 + Ok(session) -> { 91 + let agent = atp.get_agent(session) 92 + #( 93 + Model(..model, agent: Some(agent), show_login: False), 94 + effect.from(fn(dispatch) { 95 + atp.get_session_response(agent) 96 + |> promise.map(OAuthGetSessionResponse) 97 + |> promise.tap(dispatch) 98 + Nil 99 + }), 100 + ) 101 + } 102 + Error(_) -> #( 103 + Model(..model, show_spinner: False, show_login: True), 104 + effect.none(), 105 + ) 106 + } 107 + UserSubmittedLoginForm(result: Error(form)) -> #( 108 + Model(..model, login_form: form), 109 + effect.none(), 110 + ) 111 + UserSubmittedLoginForm(result: Ok(login)) -> 112 + case model.oac { 113 + Some(oac) -> 114 + case 115 + echo atp.do_login(oac, case login.username { 116 + "@" <> rest -> rest 117 + _ -> login.username 118 + }) 119 + { 120 + Ok(promise) -> #( 121 + Model(..model, login_busy: True), 122 + effect.from(fn(dispatch) { 123 + promise 124 + |> promise.map(OAuthLogin) 125 + |> promise.tap(dispatch) 126 + Nil 127 + }), 128 + ) 129 + Error(_) -> #(model, effect.none()) 130 + } 131 + None -> #(model, effect.none()) 132 + } 133 + 134 + OAuthLogin(session) -> #( 135 + Model(..model, session: Some(session)), 136 + effect.none(), 137 + ) 138 + OAuthGetSessionResponse(sr) -> 139 + case sr { 140 + Error(_) -> #(model, effect.none()) 141 + Ok(sr) -> #( 142 + Model( 143 + ..model, 144 + show_spinner: False, 145 + handle: Some(atp.get_handle(atp.get_sr_data(sr))), 146 + ), 147 + effect.none(), 148 + ) 149 + } 150 + UserClickedLogout -> #( 151 + model, 152 + effect.from(fn(_) { 153 + // message can only be sent if oac and agent exist 154 + let assert Some(oac) = model.oac 155 + let assert Some(agent) = model.agent 156 + atp.revoke_oauth_client(oac, agent) 157 + Nil 158 + }), 159 + ) 160 + UserSubmittedImageUploadForm(formdata:) -> { 161 + let files = 162 + list.map(formdata, fn(a) { 163 + case a { 164 + #(_, FormDataFile(file)) -> Ok(file) 165 + #(_, FormDataString(_)) -> Error(Nil) 166 + } 167 + }) 168 + |> result.values 169 + 170 + // if user is submitting files(s) we can assume agent exists 171 + let assert Some(agent) = model.agent 172 + 173 + #( 174 + Model(..model, files:), 175 + effect.batch( 176 + list.map(files, fn(file) { 177 + effect.from(fn(dispatch) { 178 + atp.upload_file_blob(agent, file, Nil) 179 + |> promise.map(FileBlobUploadAttempted) 180 + |> promise.tap(dispatch) 181 + Nil 182 + }) 183 + }), 184 + ), 185 + ) 186 + } 187 + FileBlobUploadAttempted(result: Ok(output)) -> { 188 + // if user is submitting files(s) we can assume agent exists 189 + let assert Some(agent) = model.agent 190 + echo "yippee" 191 + echo output 192 + let ref = atp.get_blob_ref(output) 193 + #( 194 + model, 195 + effect.from(fn(dispatch) { 196 + atp.create_image_record(agent, ref, "") 197 + |> promise.map(ImageRecordCreationAttempted) 198 + |> promise.tap(dispatch) 199 + Nil 200 + }), 201 + ) 202 + } 203 + FileBlobUploadAttempted(result: Error(_)) -> { 204 + echo "fuck" 205 + #(model, effect.none()) 206 + } 207 + ImageRecordCreationAttempted(result: Error(_)) -> { 208 + echo "fuck x2" 209 + #(model, effect.none()) 210 + } 211 + ImageRecordCreationAttempted(result: Ok(_)) -> { 212 + echo "todo: redirect to page" 213 + #(model, effect.none()) 214 + } 215 + } 216 + } 217 + 218 + fn view(model: Model) -> element.Element(Msg) { 219 + html.main([attr.class("container")], [ 220 + html.section([attr.class("content")], [ 221 + header(option.is_some(model.handle)), 222 + case model.show_spinner { 223 + True -> loading_spinner() 224 + False -> element.none() 225 + }, 226 + case model.show_login { 227 + True -> login_container(model.login_form, model.login_busy) 228 + False -> element.none() 229 + }, 230 + case model.handle { 231 + None -> element.none() 232 + Some(_) -> image_upload_container() 233 + }, 234 + ]), 235 + ]) 236 + } 237 + 238 + fn header(show_logout: Bool) -> element.Element(Msg) { 239 + html.header([], [ 240 + html.hgroup([], [html.h1([], [html.text("OAuth Browser Example")])]), 241 + html.nav([], [ 242 + html.ul([], [ 243 + case show_logout { 244 + True -> 245 + html.li([attr.id("logout-nav"), event.on_click(UserClickedLogout)], [ 246 + html.a([attr.href("#")], [html.text("Logout")]), 247 + ]) 248 + False -> element.none() 249 + }, 250 + html.li([], [ 251 + html.a([attr.href("https://github.com/bluesky-social/cookbook")], [ 252 + html.text("[Based Off]"), 253 + ]), 254 + ]), 255 + ]), 256 + ]), 257 + ]) 258 + } 259 + 260 + fn loading_spinner() -> element.Element(Msg) { 261 + html.article([attr.id("loading-spinner")], [ 262 + html.h3([attr.aria_busy(True)], [html.text("Loading Session..")]), 263 + html.article( 264 + [ 265 + attr.id("loading-error"), 266 + attr.styles([#("display", "none"), #("background-color", "#861D13")]), 267 + ], 268 + [], 269 + ), 270 + ]) 271 + } 272 + 273 + type Login { 274 + Login(username: String) 275 + } 276 + 277 + fn login_form() -> form.Form(Login) { 278 + form.new({ 279 + use username <- form.field("username", { 280 + form.parse_string 281 + |> form.check_not_empty 282 + }) 283 + Login(username:) 284 + |> form.success 285 + }) 286 + } 287 + 288 + fn login_container( 289 + form: form.Form(Login), 290 + login_busy: Bool, 291 + ) -> element.Element(Msg) { 292 + let submitted = fn(fields) { 293 + form 294 + |> form.add_values(fields) 295 + |> form.run 296 + |> UserSubmittedLoginForm 297 + } 298 + 299 + html.article([attr.id("login-container")], [ 300 + html.h3([], [html.text("Login with ATProto")]), 301 + html.form([attr.id("login-form"), event.on_submit(submitted)], [ 302 + html.p([], [html.text("Enter handle to continue")]), 303 + html.fieldset([attr.role("group")], [ 304 + html.input([ 305 + attr.name("username"), 306 + attr.id("username"), 307 + attr.placeholder("@alice.example.com"), 308 + attr.style("font-family", "monospace"), 309 + attr.required(True), 310 + ]), 311 + html.button([attr.id("login-button"), attr.aria_busy(login_busy)], [ 312 + html.text("Login"), 313 + ]), 314 + ]), 315 + ]), 316 + html.p([], [ 317 + html.text( 318 + "If you're a Bluesky user, you already have an ATProto account.", 319 + ), 320 + ]), 321 + html.button( 322 + [ 323 + attr.id("bsky-button"), 324 + attr.class("secondary"), 325 + attr.style("width", "100%"), 326 + ], 327 + [html.text("Create account with BlueSky")], 328 + ), 329 + html.p([], []), 330 + html.a([attr.href("https://atproto.com/guides/self-hosting")], [ 331 + html.button([attr.class("secondary outline")], [ 332 + html.text("Other Options"), 333 + ]), 334 + ]), 335 + html.p([attr.id("login-form-error"), attr.style("color", "#E37474")], []), 336 + ]) 337 + } 338 + 339 + pub type FormDataType { 340 + FormDataString(String) 341 + FormDataFile(file.File) 342 + } 343 + 344 + fn on_submit_with_files( 345 + msg: fn(List(#(String, FormDataType))) -> msg, 346 + ) -> attr.Attribute(msg) { 347 + event.on("submit", { 348 + use formdata <- decode.subfield(["detail", "formData"], formdata_decoder()) 349 + formdata 350 + |> msg 351 + |> decode.success() 352 + }) 353 + |> event.prevent_default 354 + } 355 + 356 + fn formdata_decoder() -> decode.Decoder(List(#(String, FormDataType))) { 357 + let k_v_decoder = { 358 + use key <- decode.field(0, decode.string) 359 + use value <- decode.field( 360 + 1, 361 + decode.one_of(decode.map(decode.string, FormDataString), [ 362 + decode.map( 363 + decode.new_primitive_decoder("File", file.dynamic_file), 364 + FormDataFile, 365 + ), 366 + ]), 367 + ) 368 + decode.success(#(key, value)) 369 + } 370 + 371 + decode.list(k_v_decoder) 372 + } 373 + 374 + fn image_upload_container() -> element.Element(Msg) { 375 + html.article([attr.id("image-upload-container")], [ 376 + html.h2([], [html.text("Upload Image")]), 377 + html.form( 378 + [ 379 + attr.id("image-upload-form"), 380 + on_submit_with_files(UserSubmittedImageUploadForm), 381 + ], 382 + [ 383 + html.input([ 384 + attr.type_("file"), 385 + attr.id("file-upload"), 386 + attr.name("image_file"), 387 + attr.accept(["image/*"]), 388 + attr.required(True), 389 + ]), 390 + html.button([attr.id("image-upload-button")], [html.text("Upload")]), 391 + ], 392 + ), 393 + ]) 5 394 }
+178
src/plonk/atp.ffi.mjs
··· 1 + import { BrowserOAuthClient } from "@atproto/oauth-client-browser"; 2 + import { Agent, BlobRef, ComAtprotoRepoUploadBlob, RichText } from "@atproto/api"; 3 + import { Result, Result$Ok, Result$Error } from "../gleam.mjs"; 4 + 5 + export function inspect(a) { 6 + console.log(a); 7 + return a; 8 + } 9 + 10 + export function buildClientID() { 11 + const isLocal = ["localhost", "127.0.0.1"].includes(window.location.hostname); 12 + if (isLocal) { 13 + // see https://atproto.com/specs/oauth#localhost-client-development 14 + return `http://localhost?${new URLSearchParams({ 15 + scope: "atproto repo:top.plonk.image?action=create&action=delete&action=update blob:*/*", 16 + redirect_uri: Object.assign(new URL(window.location.origin), { hostname: "127.0.0.1" }).href, 17 + })}`; 18 + } 19 + return `https://${window.location.host}/oauth-client-metadata.json`; 20 + } 21 + 22 + /** 23 + * 24 + * @param {String} clientId 25 + * @returns {Promise<BrowserOAuthClient>} oac 26 + */ 27 + export async function getOAC(clientId) { 28 + return await BrowserOAuthClient.load({ 29 + clientId, 30 + handleResolver: "https://bsky.social", 31 + }); 32 + } 33 + 34 + /** 35 + * 36 + * @param {BrowserOAuthClient} oac 37 + * @returns {Promise<{session: OAuthSession; state?: never;} | {session: OAuthSession; state: string | null;} | undefined>} 38 + */ 39 + export async function initOAC(oac) { 40 + return await oac.init(); 41 + } 42 + 43 + /** 44 + * 45 + * @param {{session: OAuthSession; state?: never;} | {session: OAuthSession; state: string | null;} | undefined} oac 46 + * @returns {Result<OAuthSession, String>} session 47 + */ 48 + export function getSession(oac_init) { 49 + if (oac_init) { 50 + const { session, state } = oac_init; 51 + if (state != null) { 52 + console.log(`${session.sub} was successfully authenticated (state: ${state})`); 53 + } else { 54 + console.log(`${session.sub} was restored (last active session)`); 55 + } 56 + return Result$Ok(session); 57 + } else { 58 + return Result$Error("no existing session"); 59 + } 60 + } 61 + 62 + /** 63 + * 64 + * @param {SessionManager} session 65 + * @returns {Agent} agent 66 + */ 67 + export function getAgent(session) { 68 + return new Agent(session); 69 + } 70 + 71 + /** 72 + * 73 + * @param {Agent} agent 74 + * @returns {Promise<Response>} res 75 + */ 76 + export async function newSession(agent) { 77 + return await agent.com.atproto.server.getSession(); 78 + } 79 + 80 + /** 81 + * 82 + * @param {String} identifier 83 + * @returns {Result<Promise<OAuthSession>, String>} res 84 + */ 85 + export async function doLogin(oac, identifier) { 86 + console.log(oac); 87 + try { 88 + return Result$Ok( 89 + await oac.signIn(identifier, { 90 + state: "some value needed later", 91 + signal: new AbortController().signal, 92 + }), 93 + ); 94 + } catch (err) { 95 + return Result$Error(`${err}`); 96 + } 97 + } 98 + 99 + /** 100 + * 101 + * @param {Agent} agent 102 + * @returns {Result<Response, String>} 103 + */ 104 + export async function getSessionResponse(agent) { 105 + const res = await agent.com.atproto.server.getSession(); 106 + if (!res.success) { 107 + return Result$Error(JSON.stringify(res)); 108 + } 109 + return Result$Ok(res); 110 + } 111 + 112 + export function getSessionResponseData(sr) { 113 + return sr.data; 114 + } 115 + 116 + export function getHandle(srData) { 117 + return srData.handle; 118 + } 119 + 120 + export function revokeOAC(oac, agent) { 121 + oac.revoke(agent.did); 122 + window.location.reload(); 123 + } 124 + 125 + /** 126 + * 127 + * @param {Agent} agent 128 + * @param {BlobRef} blobRef 129 + * @param {String} title 130 + * @returns 131 + */ 132 + export async function createImageRecord(agent, blobRef, title) { 133 + let res; 134 + try { 135 + res = await agent.com.atproto.repo.createRecord({ 136 + repo: agent.did, 137 + collection: "top.plonk.image", 138 + record: { 139 + $type: "top.plonk.image", 140 + title: title, 141 + content: blobRef, 142 + createdAt: new Date().toISOString(), 143 + }, 144 + }); 145 + if (!res.success) { 146 + return Result$Error(JSON.stringify(res)); 147 + } 148 + return Result$Ok(res.data); 149 + } catch (err) { 150 + return Result$Error(`${err}`); 151 + } 152 + } 153 + 154 + /** 155 + * 156 + * @param {Agent} agent 157 + * @param {ComAtprotoRepoUploadBlob.InputSchema} blob 158 + * @param {ComAtprotoRepoUploadBlob.CallOptions?} options 159 + * @returns {Promise<Result<ComAtprotoRepoUploadBlob.OutputSchema, null>>} 160 + */ 161 + export async function uploadBlob(agent, blob, options) { 162 + const res = await agent.uploadBlob(blob, options); 163 + 164 + if (res.success) { 165 + return Result$Ok(res.data); 166 + } else { 167 + return Result$Error(null); 168 + } 169 + } 170 + 171 + /** 172 + * 173 + * @param {Com.ComAtprotoRepoUploadBlob.OutputSchema} outputSchema 174 + * @returns {BlobRef} 175 + */ 176 + export function blobRef(outputSchema) { 177 + return outputSchema.blob; 178 + }
+84
src/plonk/atp.gleam
··· 1 + import gleam/http/response 2 + import gleam/javascript/promise 3 + import plonk/file 4 + 5 + pub type BrowserOAuthClient 6 + 7 + pub type Agent 8 + 9 + pub type OAuthSession 10 + 11 + pub type OACInit 12 + 13 + pub type SessionResponse 14 + 15 + pub type SessionOutputSchema 16 + 17 + pub type RichText 18 + 19 + pub type RecordResult 20 + 21 + @external(javascript, "./atp.ffi.mjs", "buildClientID") 22 + pub fn build_client_id() -> String 23 + 24 + @external(javascript, "./atp.ffi.mjs", "getOAC") 25 + pub fn get_oauth_client( 26 + client_id _: String, 27 + ) -> promise.Promise(BrowserOAuthClient) 28 + 29 + @external(javascript, "./atp.ffi.mjs", "initOAC") 30 + pub fn init_oauth_client(oac _: BrowserOAuthClient) -> promise.Promise(OACInit) 31 + 32 + @external(javascript, "./atp.ffi.mjs", "getSession") 33 + pub fn get_session(oac_init _: OACInit) -> Result(OAuthSession, String) 34 + 35 + @external(javascript, "./atp.ffi.mjs", "getAgent") 36 + pub fn get_agent(session _: OAuthSession) -> Agent 37 + 38 + @external(javascript, "./atp.ffi.mjs", "newSession") 39 + pub fn new_session( 40 + agent _: Agent, 41 + ) -> promise.Promise(response.Response(OAuthSession)) 42 + 43 + @external(javascript, "./atp.ffi.mjs", "doLogin") 44 + pub fn do_login( 45 + oac _: BrowserOAuthClient, 46 + identifier _: String, 47 + ) -> Result(promise.Promise(OAuthSession), String) 48 + 49 + @external(javascript, "./atp.ffi.mjs", "getSessionResponse") 50 + pub fn get_session_response( 51 + agent _: Agent, 52 + ) -> promise.Promise(Result(SessionResponse, String)) 53 + 54 + @external(javascript, "./atp.ffi.mjs", "getSessionResponseData") 55 + pub fn get_sr_data(session_response _: SessionResponse) -> SessionOutputSchema 56 + 57 + @external(javascript, "./atp.ffi.mjs", "getHandle") 58 + pub fn get_handle(output_schema _: SessionOutputSchema) -> String 59 + 60 + @external(javascript, "./atp.ffi.mjs", "revokeOAC") 61 + pub fn revoke_oauth_client(oac _: BrowserOAuthClient, agent _: Agent) -> Nil 62 + 63 + pub type BlobOutputSchema 64 + 65 + @external(javascript, "./atp.ffi.mjs", "uploadBlob") 66 + pub fn upload_file_blob( 67 + agent _: Agent, 68 + blob _: file.File, 69 + opts _: Nil, 70 + ) -> promise.Promise(Result(BlobOutputSchema, Nil)) 71 + 72 + pub type BlobRef 73 + 74 + @external(javascript, "./atp.ffi.mjs", "blobRef") 75 + pub fn get_blob_ref(output_schema _: BlobOutputSchema) -> BlobRef 76 + 77 + pub type CreateRecordOutputSchema 78 + 79 + @external(javascript, "./atp.ffi.mjs", "createImageRecord") 80 + pub fn create_image_record( 81 + agent _: Agent, 82 + blob_ref _: BlobRef, 83 + title _: String, 84 + ) -> promise.Promise(Result(CreateRecordOutputSchema, String))
+19
src/plonk/file.ffi.mjs
··· 1 + import { Result, Result$Ok, Result$Error } from "../gleam.mjs"; 2 + 3 + export function inspect(file) { 4 + console.log("inspecting"); 5 + console.log(file); 6 + return file; 7 + } 8 + 9 + /** 10 + * 11 + * @returns {Result<File, File>} result 12 + */ 13 + export function file_decoder(data) { 14 + if (data instanceof File) { 15 + return Result$Ok(data); 16 + } else { 17 + return Result$Error(new File([], "")); 18 + } 19 + }
+9
src/plonk/file.gleam
··· 1 + import gleam/dynamic 2 + 3 + pub type File 4 + 5 + @external(javascript, "./file.ffi.mjs", "file_decoder") 6 + pub fn dynamic_file(from _data: dynamic.Dynamic) -> Result(File, File) 7 + 8 + @external(javascript, "./file.ffi.mjs", "inspect") 9 + pub fn inspect(file: File) -> File