add part 2 draft tutorial
This commit is contained in:
parent
90fcd376f0
commit
be4ba104be
554
Cargo.lock
generated
554
Cargo.lock
generated
@ -46,6 +46,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
@ -317,6 +326,15 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.10.0"
|
||||
@ -359,6 +377,40 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29c39203181991a7dd4343b8005bd804e7a9a37afb8ac070e43771e8c820bbde"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz-build",
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz-build"
|
||||
version = "0.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f509c3a87b33437b05e2458750a0700e5bdd6956176773e6c7d6dd15a283a0c"
|
||||
dependencies = [
|
||||
"parse-zoneinfo",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.3.0"
|
||||
@ -395,6 +447,12 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.2"
|
||||
@ -443,6 +501,12 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deunicode"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
|
||||
|
||||
[[package]]
|
||||
name = "devise"
|
||||
version = "0.3.1"
|
||||
@ -552,12 +616,59 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fsevent-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fuchsia-zircon-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon-sys"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.21"
|
||||
@ -740,6 +851,30 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"fnv",
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globwalk"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.2.4"
|
||||
@ -863,6 +998,12 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.20"
|
||||
@ -887,6 +1028,37 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef5528d9c2817db4e10cc78f8d4c8228906e5854f389ff6b076cee3572a09d35"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"globset",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex",
|
||||
"same-file",
|
||||
"thread_local",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.1"
|
||||
@ -904,6 +1076,26 @@ version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
@ -913,6 +1105,15 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iovec"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.3"
|
||||
@ -928,6 +1129,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
||||
dependencies = [
|
||||
"winapi 0.2.8",
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kuska-handshake"
|
||||
version = "0.2.0"
|
||||
@ -989,6 +1200,12 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.129"
|
||||
@ -1063,6 +1280,25 @@ version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.6.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"fuchsia-zircon",
|
||||
"fuchsia-zircon-sys",
|
||||
"iovec",
|
||||
"kernel32-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"miow",
|
||||
"net2",
|
||||
"slab",
|
||||
"winapi 0.2.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.4"
|
||||
@ -1075,6 +1311,30 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio-extras"
|
||||
version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
|
||||
dependencies = [
|
||||
"lazycell",
|
||||
"log",
|
||||
"mio 0.6.23",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miow"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
|
||||
dependencies = [
|
||||
"kernel32-sys",
|
||||
"net2",
|
||||
"winapi 0.2.8",
|
||||
"ws2_32-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multer"
|
||||
version = "2.0.3"
|
||||
@ -1095,6 +1355,63 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "net2"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"libc",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "normpath"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04aaf5e9cb0fbf883cc0423159eacdf96a9878022084b35c462c428cab73bcaf"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "4.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"filetime",
|
||||
"fsevent",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"libc",
|
||||
"mio 0.6.23",
|
||||
"mio-extras",
|
||||
"walkdir",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.13.1"
|
||||
@ -1155,6 +1472,15 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-zoneinfo"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "part_1_sbot_rocket"
|
||||
version = "0.1.0"
|
||||
@ -1163,6 +1489,16 @@ dependencies = [
|
||||
"rocket",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "part_2_subscribe_form"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"golgi",
|
||||
"log",
|
||||
"rocket",
|
||||
"rocket_dyn_templates",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pear"
|
||||
version = "0.2.3"
|
||||
@ -1192,6 +1528,89 @@ version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69486e2b8c2d2aeb9762db7b4e00b0331156393555cff467f4163ff06821eef8"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b13570633aff33c6d22ce47dd566b10a3b9122c2fe9d8e7501895905be532b91"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3c567e5702efdc79fb18859ea74c3eb36e14c43da7b8c1f098a4ed6514ec7a0"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eb32be5ee3bbdafa8c7a18b0a8a8d962b66cfa2ceee4037f49267a50ee821fe"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"pest",
|
||||
"sha-1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
"uncased",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
@ -1431,6 +1850,19 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rocket_dyn_templates"
|
||||
version = "0.1.0-rc.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab13df598440527c200f46fb944dc55d8d67a1818b617eb5a3981dcd8b63fd2"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"normpath",
|
||||
"notify",
|
||||
"rocket",
|
||||
"tera",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rocket_http"
|
||||
version = "0.5.0-rc.2"
|
||||
@ -1523,6 +1955,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha-1"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.2"
|
||||
@ -1562,6 +2005,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.7"
|
||||
@ -1571,6 +2020,15 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slug"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
|
||||
dependencies = [
|
||||
"deunicode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.9.0"
|
||||
@ -1642,6 +2100,28 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tera"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d4685e72cb35f0eb74319c8fe2d3b61e93da5609841cde2cb87fcc3bea56d20"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"globwalk",
|
||||
"humansize",
|
||||
"lazy_static",
|
||||
"percent-encoding",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"rand",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slug",
|
||||
"unic-segment",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.32"
|
||||
@ -1699,7 +2179,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"memchr",
|
||||
"mio",
|
||||
"mio 0.8.4",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
@ -1843,6 +2323,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c"
|
||||
|
||||
[[package]]
|
||||
name = "uncased"
|
||||
version = "0.9.7"
|
||||
@ -1853,6 +2339,56 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
|
||||
dependencies = [
|
||||
"unic-char-range",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-range"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
|
||||
|
||||
[[package]]
|
||||
name = "unic-common"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
|
||||
|
||||
[[package]]
|
||||
name = "unic-segment"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
|
||||
dependencies = [
|
||||
"unic-ucd-segment",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-ucd-segment"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
|
||||
dependencies = [
|
||||
"unic-char-property",
|
||||
"unic-char-range",
|
||||
"unic-ucd-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-ucd-version"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
|
||||
dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.3"
|
||||
@ -2031,6 +2567,12 @@ dependencies = [
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-build"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
@ -2138,6 +2680,16 @@ version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
||||
|
||||
[[package]]
|
||||
name = "ws2_32-sys"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
|
||||
dependencies = [
|
||||
"winapi 0.2.8",
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
|
@ -1,5 +1,6 @@
|
||||
[workspace]
|
||||
|
||||
members = [
|
||||
"part_1_sbot_rocket"
|
||||
"part_1_sbot_rocket",
|
||||
"part_2_subscribe_form"
|
||||
]
|
||||
|
@ -233,7 +233,7 @@ async fn rocket() -> _ {
|
||||
|
||||
### Conclusion
|
||||
|
||||
That's all for the first part of this tutorial series. We installed and configured go-sbot, wrote a simple web server and made our first RPC call to the sbot. Not bad for 29 lines of code! In the next installment we'll setup a key-value database to store application data and learn how to follow a Scuttlebutt peer.
|
||||
That's all for the first part of this tutorial series. We installed and configured go-sbot, wrote a simple web server and made our first RPC call to the sbot. Not bad for 29 lines of code! In the next installment we'll setup the basic scaffolding for subscribing to Scuttlebutt peers.
|
||||
|
||||
## Funding
|
||||
|
||||
|
1
part_2_subscribe_form/.gitignore
vendored
Normal file
1
part_2_subscribe_form/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
10
part_2_subscribe_form/Cargo.toml
Normal file
10
part_2_subscribe_form/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "part_2_subscribe_form"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
|
||||
log = "0.4"
|
||||
rocket = "0.5.0-rc.1"
|
||||
rocket_dyn_templates = { version = "0.1.0-rc.1", features = ["tera"] }
|
423
part_2_subscribe_form/README.md
Normal file
423
part_2_subscribe_form/README.md
Normal file
@ -0,0 +1,423 @@
|
||||
# lykin tutorial
|
||||
|
||||
## Part 2: Subscription Form and Key Validation
|
||||
|
||||
### Introduction
|
||||
|
||||
In the first part of the tutorial series we created a basic web server and wrote our first Scuttlebutt-related code. This tutorial installment will add an HTML form and route handler(s) to allow peer subscriptions through the web interface. We will learn how to validate public keys submitted via the form and how to check whether or not we already follow the peer represented by a submitted key. These additions will pave the way for following and unfollowing peers.
|
||||
|
||||
There's a lot of ground to cover today. Let's dive into it.
|
||||
|
||||
### Outline
|
||||
|
||||
Here's what we'll tackle in this second part of the series:
|
||||
|
||||
- Split code into modules
|
||||
- Add peer subscription form and routes
|
||||
- Validate a public key
|
||||
- Add flash messages
|
||||
- Check if we are following a peer
|
||||
|
||||
### Libraries
|
||||
|
||||
The following libraries are introduced in this part:
|
||||
|
||||
- [`log`](https://crates.io/crates/log)
|
||||
- [`rocket_dyn_templates`](https://crates.io/crates/rocket_dyn_templates)
|
||||
|
||||
### Split Code into Modules
|
||||
|
||||
A simple task to begin with: let's create an `sbot` module and a `routes` module and reorganise our code from the first part of the tutorial.
|
||||
|
||||
`src/routes.rs`
|
||||
|
||||
```rust
|
||||
use rocket::get;
|
||||
|
||||
use crate::sbot;
|
||||
|
||||
#[get("/")]
|
||||
pub async fn home() -> String {
|
||||
match sbot::whoami().await {
|
||||
Ok(id) => id,
|
||||
Err(e) => format!("whoami call failed: {}", e),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`src/sbot.rs`
|
||||
|
||||
```rust
|
||||
use std::env;
|
||||
|
||||
use golgi::{sbot::Keystore, Sbot};
|
||||
|
||||
pub async fn init_sbot() -> Result<Sbot, String> {
|
||||
let go_sbot_port = env::var("GO_SBOT_PORT").unwrap_or_else(|_| "8021".to_string());
|
||||
|
||||
let keystore = Keystore::GoSbot;
|
||||
let ip_port = Some(format!("127.0.0.1:{}", go_sbot_port));
|
||||
let net_id = None;
|
||||
|
||||
Sbot::init(keystore, ip_port, net_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn whoami() -> Result<String, String> {
|
||||
let mut sbot = init_sbot().await?;
|
||||
sbot.whoami().await.map_err(|e| e.to_string())
|
||||
}
|
||||
```
|
||||
|
||||
`src/main.rs`
|
||||
|
||||
```rust
|
||||
mod routes;
|
||||
mod sbot;
|
||||
|
||||
use rocket::{launch, routes};
|
||||
|
||||
use crate::routes::*;
|
||||
|
||||
#[launch]
|
||||
async fn rocket() -> _ {
|
||||
rocket::build().mount("/", routes![home])
|
||||
}
|
||||
```
|
||||
|
||||
### Add Peer Subscription Form and Routes
|
||||
|
||||
Now that we've taken care of some housekeeping, we can begin adding new functionality. We need a way to accept a public key; this will allow us to subscribe and unsubscribe to the posts of selected peer. We'll use the Tera templating engine to create HTML templates for our application. Tera is inspired by the Jinja2 template language and is supported by Rocket.
|
||||
|
||||
The Tera functionality we require is bundled in the `rocket_dyn_templates` crate. We can add that to our manifest:
|
||||
|
||||
`Cargo.toml`
|
||||
|
||||
`rocket_dyn_templates = { version = "0.1.0-rc.1", features = ["tera"] }`
|
||||
|
||||
We also need to modify the Rocket launch code in `src/main.rs` to attach a template fairing:
|
||||
|
||||
```rust
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
#[launch]
|
||||
async fn rocket() -> _ {
|
||||
rocket::build()
|
||||
.attach(Template::fairing())
|
||||
.mount("/", routes![home, subscribe_form, unsubscribe_form])
|
||||
}
|
||||
```
|
||||
|
||||
Let's create a base template and add a form for submitting a Scuttlebutt public key. First we need to make a `templates` directory in the root of our lykin project:
|
||||
|
||||
`mkdir templates`
|
||||
|
||||
Open a template file for editing (notice the `.tera` suffix):
|
||||
|
||||
`templates/base.html.tera`
|
||||
|
||||
For now we'll write some HTML boilerplate code and a form to accept a public key. We'll use the same form for subscription and unsubscription events. Also notice the `{{ whoami }}` syntax which allows us to render a variable from the template context (defined in the route handler):
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>lykin</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<h1>lykin</h1>
|
||||
<p>{{ whoami }}</p>
|
||||
<form action="/subscribe" method="post">
|
||||
<label for="public_key">Public Key</label>
|
||||
<input type="text" id="public_key" name="public_key" maxlength=53>
|
||||
<input type="submit" value="Subscribe">
|
||||
<input type="submit" value="Unsubscribe" formaction="/unsubscribe">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Our `home` request handler needs to be updated to serve this HTML template:
|
||||
|
||||
`src/routes.rs`
|
||||
|
||||
```rust
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
|
||||
#[get("/")]
|
||||
pub async fn home() -> Template {
|
||||
let whoami = match sbot::whoami().await {
|
||||
Ok(id) => id,
|
||||
Err(e) => format!("whoami call failed: {}", e),
|
||||
};
|
||||
|
||||
Template::render("base", context! { whoami: whoami })
|
||||
}
|
||||
```
|
||||
|
||||
With the form in place, we can write our POST request handlers in our Rocket web server. These request handlers will be responsible for processing the submitted public key and triggering the follow / unfollow calls to the sbot. For now we'll simply use the `log` library to confirm that the handler(s) have been called before redirecting to the home route.
|
||||
|
||||
First add the `log` dependency to `Cargo.toml`:
|
||||
|
||||
`log = "0.4"`
|
||||
|
||||
Then add the subscription route handlers to the existing code in `src/routes.rs`:
|
||||
|
||||
```rust
|
||||
use log::info;
|
||||
use rocket::{form::Form, get, post, response::Redirect, uri, FromFor}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct PeerForm {
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
|
||||
#[post("/subscribe", data = "<peer>")]
|
||||
pub async fn subscribe_form(peer: Form<PeerForm>) -> Redirect {
|
||||
info!("Subscribing to peer {}", &peer.public_key);
|
||||
|
||||
Redirect::to(uri!(home))
|
||||
}
|
||||
|
||||
#[post("/unsubscribe", data = "<peer>")]
|
||||
pub async fn unsubscribe_form(peer: Form<PeerForm>) -> Redirect {
|
||||
info!("Unsubscribing to peer {}", &peer.public_key);
|
||||
|
||||
Redirect::to(uri!(home))
|
||||
}
|
||||
```
|
||||
|
||||
Run the project with the appropriate log level and ensure that everything is working correctly. You can test this by pasting a public key into the form input and clicking the Subscribe and Unsubscribe buttons.
|
||||
|
||||
`RUST_LOG=lykin=info cargo run`
|
||||
|
||||
### Validate a Public Key
|
||||
|
||||
We can now write some code to validate the input from our subscription form and ensure the data represents a valid Ed25519 key. We'll create a utilities module to house this function:
|
||||
|
||||
`src/utils.rs`
|
||||
|
||||
```rust
|
||||
pub fn validate_public_key(public_key: &str) -> Result<(), String> {
|
||||
// Ensure the ID starts with the correct sigil link.
|
||||
if !public_key.starts_with('@') {
|
||||
return Err("expected '@' sigil as first character".to_string());
|
||||
}
|
||||
|
||||
// Find the dot index denoting the start of the algorithm definition tag.
|
||||
let dot_index = match public_key.rfind('.') {
|
||||
Some(index) => index,
|
||||
None => return Err("no dot index was found".to_string()),
|
||||
};
|
||||
|
||||
// Check the hashing algorithm (must end with ".ed25519").
|
||||
if !&public_key.ends_with(".ed25519") {
|
||||
return Err("hashing algorithm must be ed25519".to_string());
|
||||
}
|
||||
|
||||
// Obtain the base64 portion (substring) of the public key.
|
||||
let base64_str = &public_key[1..dot_index];
|
||||
|
||||
// Ensure the length of the base64 encoded ed25519 public key is correct.
|
||||
if base64_str.len() != 44 {
|
||||
return Err("base64 data length is incorrect".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Now the validation function can be called from our subscribe / unsubscribe route handlers, allowing us to ensure the provided public key is valid before using it to make further RPC calls to the sbot:
|
||||
|
||||
`src/router.rs`
|
||||
|
||||
```rust
|
||||
use crate::utils;
|
||||
|
||||
#[post("/subscribe", data = "<peer>")]
|
||||
pub async fn subscribe_form(peer: Form<PeerForm>) -> Redirect {
|
||||
info!("Subscribing to peer {}", &peer.public_key);
|
||||
if let Err(e) = utils::validate_public_key(&peer.public_key) {
|
||||
warn!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
}
|
||||
|
||||
Redirect::to(uri!(home))
|
||||
}
|
||||
|
||||
#[post("/unsubscribe", data = "<peer>")]
|
||||
pub async fn unsubscribe_form(peer: Form<PeerForm>) -> Redirect {
|
||||
info!("Unsubscribing to peer {}", &peer.public_key);
|
||||
if let Err(e) = utils::validate_public_key(&peer.public_key) {
|
||||
warn!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
}
|
||||
|
||||
Redirect::to(uri!(home))
|
||||
}
|
||||
```
|
||||
|
||||
### Add Flash Messages
|
||||
|
||||
Our log messages are helpful to us during development and production runs but the user of our applications is missing out on valuable information. Currently, they will have no idea whether or not the public keys they submit for subscription are valid or not. Let's add flash message support so we have a channel for reporting back to the user via the UI.
|
||||
|
||||
Rocket makes this addition very simple, having built-in support for flash message cookies in both the response and request handlers. We will have to update our `src/routes.rs` file as follows:
|
||||
|
||||
```rust
|
||||
use rocket::{request::FlashMessage, response::Flash};
|
||||
|
||||
#[get("/")]
|
||||
// Note the addition of the `flash` parameter
|
||||
pub async fn home(flash: Option<FlashMessage<'_>>) -> Template {
|
||||
// ...
|
||||
|
||||
// The `flash` parameter value is added to the template context data
|
||||
Template::render("base", context! { whoami: whoami, flash: flash })
|
||||
}
|
||||
|
||||
#[post("/subscribe", data = "<peer>")]
|
||||
// We return a `Result` type instead of a simple `Redirect`
|
||||
pub async fn subscribe_form(peer: Form<PeerForm>) -> Result<Redirect, Flash<Redirect>> {
|
||||
info!("Subscribing to peer {}", &peer.public_key);
|
||||
if let Err(e) = utils::validate_public_key(&peer.public_key) {
|
||||
let validation_err_msg = format!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
warn!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), validation_err_msg));
|
||||
}
|
||||
|
||||
Ok(Redirect::to(uri!(home)))
|
||||
}
|
||||
|
||||
#[post("/unsubscribe", data = "<peer>")]
|
||||
pub async fn unsubscribe_form(peer: Form<PeerForm>) -> Result<Redirect, Flash<Redirect>> {
|
||||
info!("Unsubscribing to peer {}", &peer.public_key);
|
||||
if let Err(e) = utils::validate_public_key(&peer.public_key) {
|
||||
let validation_err_msg = format!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
warn!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), validation_err_msg));
|
||||
}
|
||||
|
||||
Ok(Redirect::to(uri!(home)))
|
||||
}
|
||||
```
|
||||
|
||||
From the code changes we've made above we can see that a successful key validation will simply result in a redirect to the home page, while an error during key validation will result in a redirect with the addition of a flash message cookie. Now we need to update our HTML template to show any error flash messages which might be set.
|
||||
|
||||
`templates/base.html.tera`
|
||||
|
||||
Add the following code below the `</form>` tag:
|
||||
|
||||
```html
|
||||
{% if flash and flash.kind == "error" %}
|
||||
<p style="color: red;">[ {{ flash.message }} ]</p>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
Now, if a submitted public key is invalid, a red error message will be displayed below the form - informing the application user of the error.
|
||||
|
||||
### Check Peer Follow Status
|
||||
|
||||
OK, that's a lot of web application shenanigans but I know you're really here for the Scuttlebutt goodness. Let's close-out this installment by writing a function to check whether or not the peer represented by our local go-sbot instance follows another peer; in simpler words: do we follow a peer account or not?
|
||||
|
||||
In order to do this using the `golgi` RPC library, we have to construct a `RelationshipQuery` `struct` and call the `friends_is_following()` method.
|
||||
|
||||
`src/sbot.rs`
|
||||
|
||||
```rust
|
||||
pub async fn is_following(public_key_a: &str, public_key_b: &str) -> Result<String, String> {
|
||||
let mut sbot = init_sbot().await?;
|
||||
|
||||
let query = RelationshipQuery {
|
||||
source: public_key_a.to_string(),
|
||||
dest: public_key_b.to_string(),
|
||||
};
|
||||
|
||||
sbot.friends_is_following(query)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
```
|
||||
|
||||
When calling `is_following()`, we are asking: "does the peer represented by `public_key_a` follow the peer represented by `public_key_b`?" The returned value may be `Ok("true")`, `Ok("false")` or an error. Let's add these queries to our subscribe and unsubscribe route handlers:
|
||||
|
||||
`src/routes.rs`
|
||||
|
||||
```rust
|
||||
#[post("/subscribe", data = "<peer>")]
|
||||
pub async fn subscribe_form(peer: Form<PeerForm>) -> Result<Redirect, Flash<Redirect>> {
|
||||
if let Err(e) = utils::validate_public_key(&peer.public_key) {
|
||||
let validation_err_msg = format!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
warn!("{}", validation_err_msg);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), validation_err_msg));
|
||||
} else {
|
||||
info!("Public key {} is valid", &peer.public_key);
|
||||
// Retrieve the value of the local public key by calling `whoami`
|
||||
if let Ok(whoami) = sbot::whoami().await {
|
||||
// Do we follow the peer represented by the submitted public key?
|
||||
match sbot::is_following(&whoami, &peer.public_key).await {
|
||||
Ok(status) if status.as_str() == "false" => {
|
||||
info!("Not currently following peer {}", &peer.public_key);
|
||||
// This is where we will initiate a follow in the next
|
||||
// installment of the tutorial series
|
||||
}
|
||||
Ok(status) if status.as_str() == "true" => {
|
||||
info!(
|
||||
"Already following peer {}. No further action taken",
|
||||
&peer.public_key
|
||||
)
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
warn!("Received an error during `whoami` RPC call. Please ensure the go-sbot is running and try again")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Redirect::to(uri!(home)))
|
||||
}
|
||||
|
||||
#[post("/unsubscribe", data = "<peer>")]
|
||||
pub async fn unsubscribe_form(peer: Form<PeerForm>) -> Result<Redirect, Flash<Redirect>> {
|
||||
if let Err(e) = utils::validate_public_key(&peer.public_key) {
|
||||
let validation_err_msg = format!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
warn!("{}", validation_err_msg);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), validation_err_msg));
|
||||
} else {
|
||||
info!("Public key {} is valid", &peer.public_key);
|
||||
if let Ok(whoami) = sbot::whoami().await {
|
||||
match sbot::is_following(&whoami, &peer.public_key).await {
|
||||
Ok(status) if status.as_str() == "true" => {
|
||||
info!("Currently following peer {}", &peer.public_key);
|
||||
}
|
||||
Ok(status) if status.as_str() == "false" => {
|
||||
info!(
|
||||
"Not currently following peer {}. No further action taken",
|
||||
&peer.public_key
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
warn!("Received an error during `whoami` RPC call. Please ensure the go-sbot is running and try again")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Redirect::to(uri!(home)))
|
||||
}
|
||||
```
|
||||
|
||||
The code above is quite verbose due to the fact that we are matching on multiple possibilities. We could just as easily ignore the "already following" case in the subscription handler and the "not following" case in the unsubscription handler. The real star of the show is the sbot method: `sbot::is_following(peer_a, peer_b)`.
|
||||
|
||||
### Conclusion
|
||||
|
||||
Today we did a lot of work to make our project a more complete web application. We iimproved the organisation of our code base by splitting it into modules, added an HTML form and handlers to enable peer subscription events, learned how to validate public keys and query follow status, and added flash message support to be able to report errors via the UI.
|
||||
|
||||
If you're confused by any of the code samples above, remember that you can see the complete code for this installment in the git repo.
|
||||
|
||||
In the next installment we'll add a key-value store and learn how to follow and unfollow Scuttlebutt peers.
|
||||
|
||||
## Funding
|
||||
|
||||
This work has been funded by a Scuttlebutt Community Grant.
|
7
part_2_subscribe_form/notes
Normal file
7
part_2_subscribe_form/notes
Normal file
@ -0,0 +1,7 @@
|
||||
[ part 2 ]
|
||||
|
||||
- start by splitting sbot and routes into modules
|
||||
- add html form to subscribe to a peer (tera template)
|
||||
- add route for subscription form submission
|
||||
- validate public key
|
||||
- add flash message
|
15
part_2_subscribe_form/src/main.rs
Normal file
15
part_2_subscribe_form/src/main.rs
Normal file
@ -0,0 +1,15 @@
|
||||
mod routes;
|
||||
mod sbot;
|
||||
mod utils;
|
||||
|
||||
use rocket::{launch, routes};
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use crate::routes::*;
|
||||
|
||||
#[launch]
|
||||
async fn rocket() -> _ {
|
||||
rocket::build()
|
||||
.attach(Template::fairing())
|
||||
.mount("/", routes![home, subscribe_form, unsubscribe_form])
|
||||
}
|
84
part_2_subscribe_form/src/routes.rs
Normal file
84
part_2_subscribe_form/src/routes.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use log::{info, warn};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
uri, FromForm,
|
||||
};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
|
||||
use crate::{sbot, utils};
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct PeerForm {
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn home(flash: Option<FlashMessage<'_>>) -> Template {
|
||||
let whoami = match sbot::whoami().await {
|
||||
Ok(id) => id,
|
||||
Err(e) => format!("Error making `whoami` RPC call: {}. Please ensure the local go-sbot is running and refresh.", e),
|
||||
};
|
||||
|
||||
Template::render("base", context! { whoami: whoami, flash: flash })
|
||||
}
|
||||
|
||||
#[post("/subscribe", data = "<peer>")]
|
||||
pub async fn subscribe_form(peer: Form<PeerForm>) -> Result<Redirect, Flash<Redirect>> {
|
||||
if let Err(e) = utils::validate_public_key(&peer.public_key) {
|
||||
let validation_err_msg = format!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
warn!("{}", validation_err_msg);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), validation_err_msg));
|
||||
} else {
|
||||
info!("Public key {} is valid", &peer.public_key);
|
||||
if let Ok(whoami) = sbot::whoami().await {
|
||||
match sbot::is_following(&whoami, &peer.public_key).await {
|
||||
Ok(status) if status.as_str() == "false" => {
|
||||
info!("Not currently following peer {}", &peer.public_key);
|
||||
}
|
||||
Ok(status) if status.as_str() == "true" => {
|
||||
info!(
|
||||
"Already following peer {}. No further action taken",
|
||||
&peer.public_key
|
||||
)
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
warn!("Received an error during `whoami` RPC call. Please ensure the go-sbot is running and try again")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Redirect::to(uri!(home)))
|
||||
}
|
||||
|
||||
#[post("/unsubscribe", data = "<peer>")]
|
||||
pub async fn unsubscribe_form(peer: Form<PeerForm>) -> Result<Redirect, Flash<Redirect>> {
|
||||
if let Err(e) = utils::validate_public_key(&peer.public_key) {
|
||||
let validation_err_msg = format!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
warn!("{}", validation_err_msg);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), validation_err_msg));
|
||||
} else {
|
||||
info!("Public key {} is valid", &peer.public_key);
|
||||
if let Ok(whoami) = sbot::whoami().await {
|
||||
match sbot::is_following(&whoami, &peer.public_key).await {
|
||||
Ok(status) if status.as_str() == "true" => {
|
||||
info!("Currently following peer {}", &peer.public_key);
|
||||
}
|
||||
Ok(status) if status.as_str() == "false" => {
|
||||
info!(
|
||||
"Not currently following peer {}. No further action taken",
|
||||
&peer.public_key
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
warn!("Received an error during `whoami` RPC call. Please ensure the go-sbot is running and try again")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Redirect::to(uri!(home)))
|
||||
}
|
34
part_2_subscribe_form/src/sbot.rs
Normal file
34
part_2_subscribe_form/src/sbot.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use std::env;
|
||||
|
||||
use golgi::{api::friends::RelationshipQuery, sbot::Keystore, Sbot};
|
||||
|
||||
pub async fn init_sbot() -> Result<Sbot, String> {
|
||||
let go_sbot_port = env::var("GO_SBOT_PORT").unwrap_or_else(|_| "8021".to_string());
|
||||
|
||||
let keystore = Keystore::GoSbot;
|
||||
let ip_port = Some(format!("127.0.0.1:{}", go_sbot_port));
|
||||
let net_id = None;
|
||||
|
||||
Sbot::init(keystore, ip_port, net_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn whoami() -> Result<String, String> {
|
||||
let mut sbot = init_sbot().await?;
|
||||
|
||||
sbot.whoami().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn is_following(public_key_a: &str, public_key_b: &str) -> Result<String, String> {
|
||||
let mut sbot = init_sbot().await?;
|
||||
|
||||
let query = RelationshipQuery {
|
||||
source: public_key_a.to_string(),
|
||||
dest: public_key_b.to_string(),
|
||||
};
|
||||
|
||||
sbot.friends_is_following(query)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
32
part_2_subscribe_form/src/utils.rs
Normal file
32
part_2_subscribe_form/src/utils.rs
Normal file
@ -0,0 +1,32 @@
|
||||
//! Public key validation.
|
||||
|
||||
/// Ensure that the given public key is a valid ed25519 key.
|
||||
///
|
||||
/// Return an error string if the key is invalid.
|
||||
pub fn validate_public_key(public_key: &str) -> Result<(), String> {
|
||||
// Ensure the ID starts with the correct sigil link.
|
||||
if !public_key.starts_with('@') {
|
||||
return Err("expected '@' sigil as first character".to_string());
|
||||
}
|
||||
|
||||
// Find the dot index denoting the start of the algorithm definition tag.
|
||||
let dot_index = match public_key.rfind('.') {
|
||||
Some(index) => index,
|
||||
None => return Err("no dot index was found".to_string()),
|
||||
};
|
||||
|
||||
// Check the hashing algorithm (must end with ".ed25519").
|
||||
if !&public_key.ends_with(".ed25519") {
|
||||
return Err("hashing algorithm must be ed25519".to_string());
|
||||
}
|
||||
|
||||
// Obtain the base64 portion (substring) of the public key.
|
||||
let base64_str = &public_key[1..dot_index];
|
||||
|
||||
// Ensure the length of the base64 encoded ed25519 public key is correct.
|
||||
if base64_str.len() != 44 {
|
||||
return Err("base64 data length is incorrect".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
21
part_2_subscribe_form/templates/base.html.tera
Normal file
21
part_2_subscribe_form/templates/base.html.tera
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>lykin</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<a href="/"><h1>lykin</h1></a>
|
||||
<p>{{ whoami }}</p>
|
||||
<form action="/subscribe" method="post">
|
||||
<label for="public_key">Public Key</label>
|
||||
<input type="text" id="public_key" name="public_key" maxlength=53>
|
||||
<input type="submit" value="Subscribe">
|
||||
<input type="submit" value="Unsubscribe" formaction="/unsubscribe">
|
||||
</form>
|
||||
{% if flash and flash.kind == "error" %}
|
||||
<p style="color: red;">[ {{ flash.message }} ]</p>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user