diff --git a/.github/ISSUE_TEMPLATE/kernel-or-runtime-bug-report.md b/.github/ISSUE_TEMPLATE/kernel-or-runtime-bug-report.md index 081d5def7..e913f8502 100644 --- a/.github/ISSUE_TEMPLATE/kernel-or-runtime-bug-report.md +++ b/.github/ISSUE_TEMPLATE/kernel-or-runtime-bug-report.md @@ -30,7 +30,7 @@ If applicable, add screenshots to help explain your problem. **System (please supply the following information, if relevant):** - OS: [e.g. macOS, linux64, FreeBSD] - Vere and Urbit OS versions - - Your ship's `%base` hash (use `.^(@uv %cz /=base=)` to check) + - Your ship's `%base` hash (use `+trouble` to check) **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/os1-bug-report.md b/.github/ISSUE_TEMPLATE/os1-bug-report.md index 3f12e2523..974acf045 100644 --- a/.github/ISSUE_TEMPLATE/os1-bug-report.md +++ b/.github/ISSUE_TEMPLATE/os1-bug-report.md @@ -27,13 +27,13 @@ If applicable, add screenshots to help explain your problem. If possible, please **Desktop (please complete the following information):** - OS: [e.g. MacOS 10.15.3] - Browser [e.g. chrome, safari] - - Base hash of your urbit ship. Run ` .^(@uv %cz /=base=)` in Dojo to see this. + - Base hash of your urbit ship. Run `+trouble` in Dojo to see this. **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - - Base hash of your urbit ship. Run ` .^(@uv %cz /=base=)` in Dojo to see this. + - Base hash of your urbit ship. Run `+trouble` in Dojo to see this. **Additional context** Add any other context about the problem here. diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 3087d8f29..755378649 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -175,14 +175,15 @@ the pill to have the new files/hash. For most things, it is sufficient to run However, if you've made a change to Landscape's JS, then you will need to build a "glob" and upload it to bootstrap.urbit.org. To do this, run `npm install; npm run build:prod` in `pkg/interface`, and add the resulting -`pkg/arvo/app/landscape/index.js` to a fakezod at that path (or just create a +`pkg/arvo/app/landscape/index.[hash].js` to a fakezod at that path (or just create a new fakezod with `urbit -F zod -B bin/solid.pill -A pkg/arvo`). Run `:glob|make`, and this will output a file in `fakezod/.urb/put/glob-0vXXX.glob`. Upload this file to bootstrap.urbit.org, and modify `+hash` at the top of -`pkg/arvo/app/glob.hoon` to match the hash in the filename. Do not commit the -produced `index.js` and make sure it doesn't end up in your pills (they should -be less than 10MB each). +`pkg/arvo/app/glob.hoon` to match the hash in the filename of the `.glob` file. +Amend `pkg/arvo/app/landscape/index.html` to import the hashed JS bundle, instead +of the unversioned index.js. Do not commit the produced `index.js` and +make sure it doesn't end up in your pills (they should be less than 10MB each). ### Tag the resulting commit diff --git a/bin/solid.pill b/bin/solid.pill index 0ca93db1f..02e11e82a 100644 --- a/bin/solid.pill +++ b/bin/solid.pill @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecf3f8593815742e409008421f318b664124e672b1eecd131e4a1e49864a1c2a -size 6175676 +oid sha256:6cd7246753c12c7acb757e1a6ee54c177806c20a137ad8fb4300c000ac146a0f +size 6260139 diff --git a/nix/nixcrpkgs/pkgs/libsigsegv/builder.sh b/nix/nixcrpkgs/pkgs/libsigsegv/builder.sh index 39d7ddc01..f5e7072de 100644 --- a/nix/nixcrpkgs/pkgs/libsigsegv/builder.sh +++ b/nix/nixcrpkgs/pkgs/libsigsegv/builder.sh @@ -3,7 +3,7 @@ source $setup tar -xf $src cd libsigsegv-$version -patch -p1 << 'HEREDOC' +patch -p1 << 'PATCH_I386' --- a/src/fault-linux-i386.h 2020-06-25 23:46:02.099235491 +0000 +++ b/src/fault-linux-i386.h 2020-06-25 23:45:48.679156892 +0000 @@ -18,6 +18,7 @@ @@ -14,12 +14,32 @@ patch -p1 << 'HEREDOC' #if defined __x86_64__ /* 64 bit registers */ -HEREDOC +PATCH_I386 +patch -p1 << 'PATCH_ARM' +--- a/src/fault-linux-arm.h ++++ b/src/fault-linux-arm.h +@@ -17,6 +17,7 @@ + + #include "fault-posix-ucontext.h" + ++#define HAVE_STACKVMA 0 + #if defined(__aarch64__) || defined(__ARM_64BIT_STATE) || defined(__ARM_PCS_AAPCS64) /* 64-bit */ + + /* See glibc/sysdeps/unix/sysv/linux/aarch64/sys/ucontext.h. + +PATCH_ARM cd .. mkdir build cd build +# Hack +if [ $host = aarch64-linux-musleabi ] +then + sed -i 's/^CFG_FAULT=$/CFG_FAULT=fault-linux-arm.h/' \ + ../libsigsegv-$version/configure +fi + ../libsigsegv-$version/configure \ --host=$host \ --prefix=$out \ diff --git a/nix/pkgs/urbit/libsigsegv_fix.patch b/nix/pkgs/urbit/libsigsegv_fix.patch index a3b630860..3f69cef52 100644 --- a/nix/pkgs/urbit/libsigsegv_fix.patch +++ b/nix/pkgs/urbit/libsigsegv_fix.patch @@ -7,4 +7,14 @@ +#define HAVE_STACKVMA 0 #if defined __x86_64__ /* 64 bit registers */ + +--- a/src/fault-linux-arm.h ++++ b/src/fault-linux-arm.h +@@ -17,6 +17,7 @@ + #include "fault-posix-ucontext.h" + ++#define HAVE_STACKVMA 0 + #if defined(__aarch64__) || defined(__ARM_64BIT_STATE) || defined(__ARM_PCS_AAPCS64) /* 64-bit */ + + /* See glibc/sysdeps/unix/sysv/linux/aarch64/sys/ucontext.h. diff --git a/pkg/arvo/app/chat-cli.hoon b/pkg/arvo/app/chat-cli.hoon index 1f298cf3d..6ff7667a5 100644 --- a/pkg/arvo/app/chat-cli.hoon +++ b/pkg/arvo/app/chat-cli.hoon @@ -72,7 +72,7 @@ +$ glyph char ++ glyphs "!@#$%^&()-=_+[]\{}'\\:\",.<>?" :: -+$ nu-security ?(%channel %village %village-with-group) ++$ nu-security ?(%channel %village) :: +$ command $% [%target (set target)] :: set messaging target @@ -81,7 +81,7 @@ :: :: :: create chat - [%create nu-security path (unit glyph) (unit ?)] + [%create nu-security path (unit resource) (unit glyph) (unit ?)] [%delete path] :: delete chat [%invite [? path] (set ship)] :: allow [%banish [? path] (set ship)] :: disallow @@ -293,8 +293,6 @@ :: ++ target-to-path |= target - %+ weld - ?:(in-group ~ /~) [(scot %p ship) path] :: +path-to-target: deduces a target from a mailbox path :: @@ -464,6 +462,7 @@ security ;~ plug path + (punt ;~(pfix ace group)) (punt ;~(pfix ace glyph)) (punt ;~(pfix ace (fuss 'y' 'n'))) == @@ -535,16 +534,15 @@ :: ;~(pfix ace ;~(plug i.opt $(opt t.opt))) :: -- :: + ++ group ;~((glue net) ship sym) ++ tag |*(a=@tas (cold a (jest a))) ::TODO into stdlib ++ ship ;~(pfix sig fed:ag) ++ path ;~(pfix net ;~(plug urs:ab (easy ~))) ::NOTE short only, tmp :: +mang: un/managed indicator prefix :: - ++ mang - ;~ pose - (cold %| (jest '~/')) - (cold %& (easy ~)) - == + :: deprecated, as sig prefix is no longer used + :: + ++ mang (cold %& (easy ~)) :: +tarl: local target, as /path :: ++ tarl (stag our-self path) @@ -585,7 +583,7 @@ :: +security: security mode :: ++ security - (perk %channel %village-with-group %village ~) + (perk %channel %village ~) :: :: +glyph: shorthand character :: @@ -741,15 +739,21 @@ :: +create: new local mailbox :: ++ create - |= [security=nu-security =path gyf=(unit char) allow-history=(unit ?)] + |= $: security=nu-security + =path + ugroup=(unit resource) + gyf=(unit char) + allow-history=(unit ?) + == ^- (quip card _state) - =/ with-group=? ?=(%village-with-group security) + =/ with-group=? ?=(^ ugroup) =/ =target [with-group our-self path] =/ real-path=^path (target-to-path target) + =/ group-path=^path ?~(ugroup ship+real-path (en-path:resource u.ugroup)) =/ =policy ?- security - %channel *open:policy - ?(%village %village-with-group) *invite:policy + %channel *open:policy + %village *invite:policy == ?^ (scry-for (unit mailbox:store) %chat-store [%mailbox real-path]) =- [[- ~] state] @@ -767,7 +771,7 @@ (rsh 3 1 (spat path)) '' real-path :: chat - real-path :: group + group-path :: group policy ~ (fall allow-history %.y) diff --git a/pkg/arvo/app/chat-hook.hoon b/pkg/arvo/app/chat-hook.hoon index cdc102c9f..fc8d546ec 100644 --- a/pkg/arvo/app/chat-hook.hoon +++ b/pkg/arvo/app/chat-hook.hoon @@ -18,17 +18,19 @@ state-1 state-2 state-3 + state-4 + state-5 + state-6 + state-7 == :: -+$ state-3 - $: %3 - state-base - == ++$ state-7 [%7 state-base] ++$ state-6 [%6 state-base] ++$ state-5 [%5 state-base] ++$ state-4 [%4 state-base] ++$ state-3 [%3 state-base] ++$ state-2 [%2 state-base] :: -+$ state-2 - $: %2 - state-base - == +$ state-1 $: %1 loaded-cards=* @@ -52,7 +54,7 @@ $% [%chat-update update:store] == -- -=| state-3 +=| state-7 =* state - :: %- agent:dbug @@ -81,8 +83,20 @@ =/ old !<(versioned-state old-vase) =| cards=(list card) |- - ?: ?=(%3 -.old) + ?: ?=(%7 -.old) [cards this(state old)] + ?: ?=(%6 -.old) + =. cards + %+ weld cards + ^- (list card) + [%pass /s %agent [our.bol %chat-hook] %poke %noun !>(%fix-out-of-sync)]~ + $(-.old %7) + ?: ?=(?(%3 %4 %5) -.old) + =. cards + %+ weld cards + ^- (list card) + [%pass /pokeme %agent [our.bol %chat-hook] %poke %noun !>(%fix-dm)]~ + $(-.old %6) ?: ?=(%2 -.old) =. cards %+ weld cards @@ -319,9 +333,9 @@ ^- (quip card _this) =^ cards state ?+ mark (on-poke:def mark vase) - %json (poke-json:cc !<(json vase)) - %chat-action (poke-chat-action:cc !<(action:store vase)) - %noun [~ state] + %json (poke-json:cc !<(json vase)) + %chat-action (poke-chat-action:cc !<(action:store vase)) + %noun (poke-noun:cc !<(?(%fix-dm %fix-out-of-sync) vase)) :: %chat-hook-action (poke-chat-hook-action:cc !<(action:hook vase)) @@ -383,6 +397,81 @@ |_ bol=bowl:gall ++ grp ~(. grpl bol) :: +++ poke-noun + |= a=?(%fix-dm %fix-out-of-sync) + ^- (quip card _state) + |^ + :_ state + ?- a + %fix-dm (fix-dm %fix-dm) + %fix-out-of-sync (fix-out-of-sync %fix-out-of-sync) + == + :: + ++ fix-out-of-sync + |= b=%fix-out-of-sync + ^- (list card) + %- zing + %+ turn ~(tap by synced) + |= [=path host=ship] + ^- (list card) + ?: =(host our.bol) ~ + ?> ?=([@ @ ~] path) + =/ =ship (slav %p i.path) + :~ =- [%pass / %agent [our.bol %chat-hook] %poke %chat-hook-action -] + !> ^- action:hook + [%remove path] + :: + =- [%pass / %agent [our.bol %chat-hook] %poke %chat-hook-action -] + !> ^- action:hook + [%add-synced ship path %.y] + == + :: + ++ fix-dm + |= b=%fix-dm + ^- (list card) + %- zing + %+ turn + ~(tap by synced) + |= [=path host=ship] + ^- (list card) + ?> ?=([@ @ *] path) + =/ =ship (slav %p i.path) + ?: =(ship our.bol) + :: local dm, no need to do cleanup + ~ + ?: ?=(^ (groups-of-chat path)) + :: correctly initialized, no need to do cleanup + :: + ~ + ?. =((end 3 4 i.t.path) 'dm--') + ~ + :- =- [%pass /fixdm %agent [our.bol %chat-view] %poke %chat-view-action -] + !> ^- action:view + [%delete path] + =/ new-dm /(scot %p our.bol)/(crip (weld "dm--" (trip (scot %p ship)))) + =/ mailbox=(unit mailbox:store) (chat-scry path) + ?~ mailbox + ~ + :~ =- [%pass /fixdm %agent [our.bol %chat-view] %poke %chat-view-action -] + !> ^- action:view + :* %create + %- crip + (zing [(trip (scot %p our.bol)) " <-> " (trip (scot %p ship)) ~]) + '' + new-dm + ship+new-dm + [%invite (silt ~[ship])] + (silt ~[ship]) + %.y + %.n + == + :: + =- [%pass /fixdm %agent [our.bol %chat-store] %poke %chat-action -] + !> ^- action:store + [%messages new-dm envelopes.u.mailbox] + == + -- +:: ++ poke-json |= jon=json ^- (quip card _state) diff --git a/pkg/arvo/app/contact-view.hoon b/pkg/arvo/app/contact-view.hoon index 49f4ba517..80bd55a45 100644 --- a/pkg/arvo/app/contact-view.hoon +++ b/pkg/arvo/app/contact-view.hoon @@ -268,7 +268,7 @@ %group-store %group-push-hook =/ =cage - :- %group-action + :- %group-update !> ^- action:group-store [%change-policy rid %invite %add-invites (sy ship ~)] [%pass / %agent [entity.rid app] %poke cage] diff --git a/pkg/arvo/app/file-server.hoon b/pkg/arvo/app/file-server.hoon index 6732fd30a..89f4da8ff 100644 --- a/pkg/arvo/app/file-server.hoon +++ b/pkg/arvo/app/file-server.hoon @@ -7,17 +7,20 @@ $% [%clay =path] [%glob =glob:glob] == -+$ state-1 - $: %1 - =configuration:srv ++$ state-base + $: =configuration:srv =serving == ++$ state-2 + $: %2 + state-base + == -- :: %+ verb | %- agent:dbug :: -=| state-1 +=| state-2 =* state - ^- agent:gall |_ =bowl:gall @@ -60,12 +63,18 @@ ^- [content ?] [[%clay clay-path] public] == - ?> ?=(%1 -.old-state) + =? old-state ?=(%1 -.old-state) + %= old-state + - %2 + serving (~(del by serving.old-state) /'~landscape'/js/index) + == + ?> ?=(%2 -.old-state) [~ this(state old-state)] :: +$ versioned-state - $% state-1 - state-0 + $% state-0 + state-1 + state-2 == :: +$ serving-0 (map url-base=path [=clay=path public=?]) @@ -74,6 +83,10 @@ =configuration:srv =serving-0 == + +$ state-1 + $: %1 + state-base + == -- :: ++ on-poke @@ -169,7 +182,7 @@ ?~ content [not-found:gen %.n] ?- -.content.u.content %clay - =/ scry-path + =/ scry-path=path :* (scot %p our.bowl) q.byk.bowl (scot %da now.bowl) @@ -179,10 +192,16 @@ =/ file (as-octs:mimes:html .^(@ %cx scry-path)) :_ public.u.content ?+ ext.req-line not-found:gen - [~ %html] (html-response:gen file) [~ %js] (js-response:gen file) [~ %css] (css-response:gen file) [~ %png] (png-response:gen file) + :: + [~ %html] + %. file + %* . html-response:gen + cache + !=(/app/landscape/index/html (slag 3 scry-path)) + == == :: %glob @@ -269,7 +288,7 @@ == :: ++ on-leave on-leave:def -++ on-peek +++ on-peek |= =path ^- (unit (unit cage)) |^ @@ -287,10 +306,9 @@ *@uv =/ parent (scot %p ship.u.ota) =+ .^(=cass:clay %cs /[parent]/[desk.u.ota]/1/late/foo) - %^ end 3 3 + %^ end 0 25 .^(@uv %cz /[parent]/[desk.u.ota]/(scot %ud ud.cass)) -- - ++ on-agent on-agent:def ++ on-fail on-fail:def -- diff --git a/pkg/arvo/app/glob.hoon b/pkg/arvo/app/glob.hoon index 953a99bbd..f19d51515 100644 --- a/pkg/arvo/app/glob.hoon +++ b/pkg/arvo/app/glob.hoon @@ -1,7 +1,7 @@ /- glob /+ default-agent, verb, dbug |% -++ hash 0v5.knd3c.vvtvt.h0gg0.8qcau.8iii4 +++ hash 0v2.pbthv.gd1q2.h2ura.5esrn.d361c +$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))] +$ all-states $% state-0 @@ -41,7 +41,7 @@ -- =| state=state-0 =. hash.state hash -=/ serve-path=path /'~landscape'/js/index +=/ serve-path=path /'~landscape'/js/bundle ^- agent:gall %+ verb | %- agent:dbug @@ -58,7 +58,6 @@ ++ on-load |= old-state=vase ^- (quip card _this) - ~& > %initting =+ !<(old=all-states old-state) ?> ?=(%0 -.old) ?~ glob.old @@ -83,9 +82,19 @@ :_ this =/ home=path /(scot %p our.bowl)/home/(scot %da now.bowl) =+ .^(=tube:clay %cc (weld home /js/mime)) - =+ .^(js=@t %cx (weld home /app/landscape/js/index/js)) + =+ .^(arch %cy (weld home /app/landscape/js/bundle)) + =/ bundle=path + %- need + ^- (unit path) + %- ~(rep by dir) + |= [[file=@t ~] out=(unit path)] + ?^ out out + ?. =((end 3 5 file) 'index') + ~ + `/[file]/js + =+ .^(js=@t %cx :(weld home /app/landscape/js/bundle bundle)) =+ !<(=mime (tube !>(js))) - =/ =glob:glob (~(put by *glob:glob) /js mime) + =/ =glob:glob (~(put by *glob:glob) bundle mime) =/ =path /(cat 3 'glob-' (scot %uv (sham glob)))/glob [%pass /make %agent [our.bowl %hood] %poke %drum-put !>([path (jam glob)])]~ :: diff --git a/pkg/arvo/app/graph-store.hoon b/pkg/arvo/app/graph-store.hoon new file mode 100644 index 000000000..ab9a6b971 --- /dev/null +++ b/pkg/arvo/app/graph-store.hoon @@ -0,0 +1,572 @@ +/+ store=graph-store, sigs=signatures, res=resource, default-agent, dbug +~% %graph-store-top ..is ~ +|% ++$ card card:agent:gall ++$ versioned-state + $% state-0 + == +:: ++$ state-0 [%0 network:store] +++ orm orm:store +++ orm-log orm-log:store +-- +:: +=| state-0 +=* state - +:: +%- agent:dbug +^- agent:gall +~% %graph-store-agent ..card ~ +|_ =bowl:gall ++* this . + def ~(. (default-agent this %|) bowl) +:: +++ on-init [~ this] +++ on-save !>(state) +++ on-load + |= old=vase + ^- (quip card _this) + [~ this(state !<(state-0 old))] +:: +++ on-watch + ~/ %graph-store-watch + |= =path + ^- (quip card _this) + |^ + ?> (team:title our.bowl src.bowl) + =/ cards=(list card) + ?+ path (on-watch:def path) + [%updates ~] ~ + [%keys ~] (give [%keys ~(key by graphs)]) + [%tags ~] (give [%tags ~(key by tag-queries)]) + == + [cards this] + :: + ++ give + |= =update-0:store + ^- (list card) + [%give %fact ~ [%graph-update !>([%0 now.bowl update-0])]]~ + -- +:: +++ on-poke + ~/ %graph-store-poke + |= [=mark =vase] + ^- (quip card _this) + |^ + ?> (team:title our.bowl src.bowl) + =^ cards state + ?+ mark (on-poke:def mark vase) + %graph-update (graph-update !<(update:store vase)) + == + [cards this] + :: + ++ graph-update + |= =update:store + ^- (quip card _state) + |^ + ?> ?=(%0 -.update) + ?- -.q.update + %add-graph (add-graph +.q.update) + %remove-graph (remove-graph +.q.update) + %add-nodes (add-nodes p.update +.q.update) + %remove-nodes (remove-nodes p.update +.q.update) + %add-signatures (add-signatures p.update +.q.update) + %remove-signatures (remove-signatures p.update +.q.update) + %add-tag (add-tag +.q.update) + %remove-tag (remove-tag +.q.update) + %archive-graph (archive-graph +.q.update) + %unarchive-graph (unarchive-graph +.q.update) + %run-updates (run-updates +.q.update) + %keys ~|('cannot send %keys as poke' !!) + %tags ~|('cannot send %tags as poke' !!) + %tag-queries ~|('cannot send %tag-queries as poke' !!) + == + :: + ++ add-graph + |= [=resource:store =graph:store mark=(unit mark:store)] + ^- (quip card _state) + ?< (~(has by archive) resource) + ?< (~(has by graphs) resource) + ?> (validate-graph graph mark) + :_ %_ state + graphs (~(put by graphs) resource [graph mark]) + update-logs (~(put by update-logs) resource (gas:orm-log ~ ~)) + validators + ?~ mark validators + (~(put in validators) u.mark) + == + %- zing + :~ (give [/updates /keys ~] [%add-graph resource graph mark]) + ?~ mark ~ + ?: (~(has in validators) u.mark) ~ + =/ wire (weld /graph (en-path:res resource)) + =/ =rave:clay [%sing %b [%da now.bowl] /[u.mark]] + [%pass wire %arvo %c %warp our.bowl [%home `rave]]~ + == + :: + ++ remove-graph + |= =resource:store + ^- (quip card _state) + ?< (~(has by archive) resource) + ?> (~(has by graphs) resource) + :- (give [/updates /keys ~] [%remove-graph resource]) + %_ state + graphs (~(del by graphs) resource) + update-logs (~(del by update-logs) resource) + == + :: + ++ add-nodes + |= $: =time + =resource:store + nodes=(map index:store node:store) + == + ^- (quip card _state) + |^ + =/ [=graph:store mark=(unit mark:store)] + (~(got by graphs) resource) + =/ =update-log:store (~(got by update-logs) resource) + =. update-log + (put:orm-log update-log time [%0 time [%add-nodes resource nodes]]) + :: + :- (give [/updates]~ [%add-nodes resource nodes]) + %_ state + update-logs (~(put by update-logs) resource update-log) + graphs + %+ ~(put by graphs) + resource + :_ mark + (add-node-list resource graph mark (sort-nodes nodes)) + == + :: + ++ sort-nodes + |= nodes=(map index:store node:store) + ^- (list [index:store node:store]) + %+ sort ~(tap by nodes) + |= [p=[=index:store *] q=[=index:store *]] + ^- ? + (lth (lent index.p) (lent index.q)) + :: + ++ add-node-list + |= $: =resource:store + =graph:store + mark=(unit mark:store) + node-list=(list [index:store node:store]) + == + ^- graph:store + ?~ node-list graph + =* index -.i.node-list + =* node +.i.node-list + %_ $ + node-list t.node-list + graph (add-node-at-index graph index node mark) + == + :: + ++ add-node-at-index + =| parent-hash=(unit hash:store) + |= $: =graph:store + =index:store + =node:store + mark=(unit mark:store) + == + ^- graph:store + ?< ?=(~ index) + ~| "validation of node failed using mark {}" + ?> (validate-graph (gas:orm ~ [i.index node]~) mark) + =* atom i.index + %^ put:orm + graph + atom + :: add child + :: + ?~ t.index + =* p post.node + =/ =validated-portion:store + [parent-hash author.p time-sent.p contents.p] + =/ =hash:store `@ux`(sham validated-portion) + ?~ hash.p node(signatures.post *signatures:store) + ~| "signatures do not match the calculated hash" + ?> (are-signatures-valid:sigs signatures.p hash now.bowl) + ~| "hash of post does not match calculated hash" + ?> =(hash u.hash.p) + node + :: recurse children + :: + =/ parent=node:store + ~| "index does not exist to add a node to!" + (need (get:orm graph atom)) + %_ parent + children + ^- internal-graph:store + :- %graph + %_ $ + index t.index + parent-hash hash.post.parent + graph + ?: ?=(%graph -.children.parent) + p.children.parent + (gas:orm ~ ~) + == + == + -- + :: + ++ remove-nodes + |= [=time =resource:store indices=(set index:store)] + ^- (quip card _state) + |^ + =/ [=graph:store mark=(unit mark:store)] + (~(got by graphs) resource) + =/ =update-log:store (~(got by update-logs) resource) + =. update-log + (put:orm-log update-log time [%0 time [%remove-nodes resource indices]]) + :: + :- (give [/updates]~ [%remove-nodes resource indices]) + %_ state + update-logs (~(put by update-logs) resource update-log) + graphs + %+ ~(put by graphs) + resource + [(remove-indices resource graph ~(tap in indices)) mark] + == + :: + ++ remove-indices + |= [=resource:store =graph:store indices=(list index:store)] + ^- graph:store + ?~ indices graph + %_ $ + indices t.indices + graph (remove-index graph i.indices) + == + :: + ++ remove-index + |= [=graph:store =index:store] + ^- graph:store + ?~ index graph + =* atom i.index + :: last index in list + :: + ?~ t.index + +:`[* graph:store]`(del:orm graph atom) + =/ =node:store + ~| "parent index does not exist to remove a node from!" + (need (get:orm graph atom)) + ~| "child index does not exist to remove a node from!" + ?> ?=(%graph -.children.node) + %^ put:orm + graph + atom + node(p.children $(graph p.children.node, index t.index)) + -- + :: + ++ add-signatures + |= [=time =uid:store =signatures:store] + ^- (quip card _state) + |^ + =* resource resource.uid + =/ [=graph:store mark=(unit mark:store)] + (~(got by graphs) resource) + =/ =update-log:store (~(got by update-logs) resource) + =. update-log + (put:orm-log update-log time [%0 time [%add-signatures uid signatures]]) + :: + :- (give [/updates]~ [%add-signatures uid signatures]) + %_ state + update-logs (~(put by update-logs) resource update-log) + graphs + %+ ~(put by graphs) resource + [(add-at-index graph index.uid signatures) mark] + == + :: + ++ add-at-index + |= [=graph:store =index:store =signatures:store] + ^- graph:store + ?~ index graph + =* atom i.index + =/ =node:store + ~| "node does not exist to add signatures to!" + (need (get:orm graph atom)) + :: last index in list + :: + %^ put:orm + graph + atom + ?~ t.index + ~| "cannot add signatures to a node missing a hash" + ?> ?=(^ hash.post.node) + ~| "signatures did not match public keys!" + ?> (are-signatures-valid:sigs signatures u.hash.post.node now.bowl) + node(signatures.post (~(uni in signatures) signatures.post.node)) + ~| "child graph does not exist to add signatures to!" + ?> ?=(%graph -.children.node) + node(p.children $(graph p.children.node, index t.index)) + -- + :: + ++ remove-signatures + |= [=time =uid:store =signatures:store] + ^- (quip card _state) + |^ + =* resource resource.uid + =/ [=graph:store mark=(unit mark:store)] + (~(got by graphs) resource) + =/ =update-log:store (~(got by update-logs) resource) + =. update-log + %^ put:orm-log update-log + time + [%0 time [%remove-signatures uid signatures]] + :: + :- (give [/updates]~ [%remove-signatures uid signatures]) + %_ state + update-logs (~(put by update-logs) resource update-log) + graphs + %+ ~(put by graphs) resource + [(remove-at-index graph index.uid signatures) mark] + == + :: + ++ remove-at-index + |= [=graph:store =index:store =signatures:store] + ^- graph:store + ?~ index graph + =* atom i.index + =/ =node:store + ~| "node does not exist to add signatures to!" + (need (get:orm graph atom)) + :: last index in list + :: + %^ put:orm + graph + atom + ?~ t.index + node(signatures.post (~(dif in signatures) signatures.post.node)) + ~| "child graph does not exist to add signatures to!" + ?> ?=(%graph -.children.node) + node(p.children $(graph p.children.node, index t.index)) + -- + :: + ++ add-tag + |= [=term =resource:store] + ^- (quip card _state) + ?> (~(has by graphs) resource) + :- (give [/updates /tags ~] [%add-tag term resource]) + %_ state + tag-queries (~(put ju tag-queries) term resource) + == + :: + ++ remove-tag + |= [=term =resource:store] + ^- (quip card _state) + ?> (~(has by graphs) resource) + :- (give [/updates /tags ~] [%remove-tag term resource]) + %_ state + tag-queries (~(del ju tag-queries) term resource) + == + :: + ++ archive-graph + |= =resource:store + ^- (quip card _state) + ?< (~(has by archive) resource) + ?> (~(has by graphs) resource) + :- (give [/updates /keys /tags ~] [%archive-graph resource]) + %_ state + archive (~(put by archive) resource (~(got by graphs) resource)) + graphs (~(del by graphs) resource) + update-logs (~(del by update-logs) resource) + tag-queries + %- ~(run by tag-queries) + |= =resources:store + (~(del in resources) resource) + == + :: + ++ unarchive-graph + |= =resource:store + ^- (quip card _state) + ?> (~(has by archive) resource) + ?< (~(has by graphs) resource) + :- (give [/updates /keys ~] [%unarchive-graph resource]) + %_ state + archive (~(del by archive) resource) + graphs (~(put by graphs) resource (~(got by archive) resource)) + update-logs (~(put by update-logs) resource (gas:orm-log ~ ~)) + == + :: + ++ run-updates + |= [=resource:store =update-log:store] + ^- (quip card _state) + ?< (~(has by archive) resource) + ?> (~(has by graphs) resource) + :_ state + %+ turn (tap:orm-log update-log) + |= [=time update=logged-update:store] + ^- card + ?> ?=(%0 -.update) + :* %pass + /run-updates/(scot %da time) + %agent + [our.bowl %graph-store] + %poke + :- %graph-update + !> + ^- update:store + ?- -.q.update + %add-nodes update(resource.q resource) + %remove-nodes update(resource.q resource) + %add-signatures update(resource.uid.q resource) + %remove-signatures update(resource.uid.q resource) + == + == + :: + ++ validate-graph + |= [=graph:store mark=(unit mark:store)] + ^- ? + ?~ mark %.y + ?~ graph %.y + =/ =dais:clay + .^ =dais:clay + %cb + /(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)/[u.mark] + == + %+ roll (tap:orm graph) + |= [[=atom =node:store] out=?] + ?& out + =(%& -:(mule |.((vale:dais [atom post.node])))) + ?- -.children.node + %empty %.y + %graph ^$(graph p.children.node) + == + == + :: + ++ give + |= [paths=(list path) update=update-0:store] + ^- (list card) + [%give %fact paths [%graph-update !>([%0 now.bowl update])]]~ + -- + -- +:: +++ on-peek + ~/ %graph-store-peek + |= =path + ^- (unit (unit cage)) + |^ + ?> (team:title our.bowl src.bowl) + ?+ path (on-peek:def path) + [%x %keys ~] ``noun+!>(~(key by graphs)) + [%x %tags ~] ``noun+!>(~(key by tag-queries)) + [%x %tag-queries ~] ``noun+!>(tag-queries) + [%x %graph @ @ ~] + =/ =ship (slav %p i.t.t.path) + =/ =term i.t.t.t.path + =/ result=(unit marked-graph:store) + (~(get by graphs) [ship term]) + ?~ result [~ ~] + ``noun+!>(u.result) + :: + [%x %graph-subset @ @ @ @ ~] + =/ =ship (slav %p i.t.t.path) + =/ =term i.t.t.t.path + =/ start=(unit atom) (rush i.t.t.t.t.path dem:ag) + =/ end=(unit atom) (rush i.t.t.t.t.t.path dem:ag) + =/ graph=(unit marked-graph:store) + (~(get by graphs) [ship term]) + ?~ graph [~ ~] + ``noun+!>(`graph:store`(subset:orm p.u.graph start end)) + :: + [%x %node @ @ @ *] + =/ =ship (slav %p i.t.t.path) + =/ =term i.t.t.t.path + =/ =index:store + (turn t.t.t.t.path |=(=cord (slav %ud cord))) + =/ node=(unit node:store) (get-node ship term index) + ?~ node [~ ~] + ``noun+!>(u.node) + :: + [%x %post @ @ @ *] + =/ =ship (slav %p i.t.t.path) + =/ =term i.t.t.t.path + =/ =index:store + (turn t.t.t.t.path |=(=cord (slav %ud cord))) + =/ node=(unit node:store) (get-node ship term index) + ?~ node [~ ~] + ``noun+!>(post.u.node) + :: + [%x %node-children @ @ @ *] + =/ =ship (slav %p i.t.t.path) + =/ =term i.t.t.t.path + =/ =index:store + (turn t.t.t.t.path |=(=cord (slav %ud cord))) + =/ node=(unit node:store) (get-node ship term index) + ?~ node [~ ~] + ?- -.children.u.node + %empty [~ ~] + %graph ``noun+!>(p.children.u.node) + == + :: + [%x %node-children-subset @ @ @ @ @ *] + =/ =ship (slav %p i.t.t.path) + =/ =term i.t.t.t.path + =/ start=(unit atom) (rush i.t.t.t.t.path dem:ag) + =/ end=(unit atom) (rush i.t.t.t.t.t.path dem:ag) + =/ =index:store + (turn t.t.t.t.t.t.path |=(=cord (slav %ud cord))) + =/ node=(unit node:store) (get-node ship term index) + ?~ node [~ ~] + ?- -.children.u.node + %empty [~ ~] + %graph ``noun+!>(`graph:store`(subset:orm p.children.u.node start end)) + == + :: + [%x %update-log @ @ ~] + =/ =ship (slav %p i.t.t.path) + =/ =term i.t.t.t.path + =/ update-log=(unit update-log:store) (~(get by update-logs) [ship term]) + ?~ update-log [~ ~] + ``noun+!>(u.update-log) + :: + [%x %peek-update-log @ @ ~] + =/ =ship (slav %p i.t.t.path) + =/ =term i.t.t.t.path + =/ update-log=(unit update-log:store) (~(get by update-logs) [ship term]) + ?~ update-log [~ ~] + =/ result=(unit [time update:store]) + (peek:orm-log:store u.update-log) + ?~ result [~ ~] + ``noun+!>([~ -.u.result]) + == + :: + ++ get-node + |= [=ship =term =index:store] + ^- (unit node:store) + =/ parent-graph=(unit marked-graph:store) + (~(get by graphs) [ship term]) + ?~ parent-graph ~ + =/ node=(unit node:store) ~ + =/ =graph:store p.u.parent-graph + |- + ?~ index + node + ?~ t.index + (get:orm graph i.index) + =. node (get:orm graph i.index) + ?~ node ~ + ?- -.children.u.node + %empty ~ + %graph $(graph p.children.u.node, index t.index) + == + -- +:: +++ on-arvo + |= [=wire =sign-arvo] + ^- (quip card _this) + ?+ -.sign-arvo (on-arvo:def wire sign-arvo) + %c + :_ this + ?> ?=([%graph @ *] wire) + =/ =resource:store (de-path:res t.wire) + =/ gra=(unit marked-graph:store) (~(get by graphs) resource) + ?~ gra ~ + ?~ q.u.gra ~ + =/ =rave:clay [%next %b [%da now.bowl] /[u.q.u.gra]] + [%pass wire %arvo %c %warp our.bowl [%home `rave]]~ + == +:: +++ on-agent on-agent:def +++ on-leave on-leave:def +++ on-fail on-fail:def +-- diff --git a/pkg/arvo/app/hood.hoon b/pkg/arvo/app/hood.hoon index e83b30b15..1bc70ed80 100644 --- a/pkg/arvo/app/hood.hoon +++ b/pkg/arvo/app/hood.hoon @@ -2,22 +2,16 @@ /+ drum=hood-drum, helm=hood-helm, kiln=hood-kiln |% +$ state - $: %8 - drum=state:drum - helm=state:helm - kiln=state:kiln - == -+$ state-7 - $: %7 + $: %9 drum=state:drum helm=state:helm kiln=state:kiln == +$ any-state $% state - state-7 [ver=?(%1 %2 %3 %4 %5 %6) lac=(map @tas fin-any-state)] [%7 drum=state:drum helm=state:helm kiln=state:kiln] + [%8 drum=state:drum helm=state:helm kiln=state:kiln] == +$ any-state-tuple $: drum=any-state:drum diff --git a/pkg/arvo/app/landscape/img/groups.png b/pkg/arvo/app/landscape/img/groups.png new file mode 100644 index 000000000..fa14f36da Binary files /dev/null and b/pkg/arvo/app/landscape/img/groups.png differ diff --git a/pkg/arvo/app/landscape/img/icon-home.png b/pkg/arvo/app/landscape/img/icon-home.png index 04b1e7b87..9eb1d0289 100644 Binary files a/pkg/arvo/app/landscape/img/icon-home.png and b/pkg/arvo/app/landscape/img/icon-home.png differ diff --git a/pkg/arvo/app/landscape/index.html b/pkg/arvo/app/landscape/index.html index 844b9df50..93841346b 100644 --- a/pkg/arvo/app/landscape/index.html +++ b/pkg/arvo/app/landscape/index.html @@ -4,7 +4,7 @@ OS1 + content="width=device-width, initial-scale=1, shrink-to-fit=no,maximum-scale=1"/> @@ -23,7 +23,7 @@
- + diff --git a/pkg/arvo/app/metadata-store.hoon b/pkg/arvo/app/metadata-store.hoon index bf052800d..a0e88e2e6 100644 --- a/pkg/arvo/app/metadata-store.hoon +++ b/pkg/arvo/app/metadata-store.hoon @@ -85,15 +85,16 @@ old [%2 +.old] :: cards - %+ turn + %+ murn ~(tap in ~(key by group-indices.old)) |= =group-path - ^- card - =/ rid=resource - (de-path:resource group-path) - ?: =(our.bowl entity.rid) - (poke-md-hook %add-owned group-path) - (poke-md-hook %add-synced entity.rid group-path) + ^- (unit card) + =/ rid=(unit resource) + (de-path-soft:resource group-path) + ?~ rid ~ + ?: =(our.bowl entity.u.rid) + `(poke-md-hook %add-owned group-path) + `(poke-md-hook %add-synced entity.u.rid group-path) == =/ new-state=state-one %* . *state-one @@ -254,6 +255,11 @@ =/ =group-path (stab (slav %t i.t.t.path)) =/ =md-resource [`@tas`i.t.t.t.path (stab (slav %t i.t.t.t.t.path))] ``noun+!>((~(get by associations) [group-path md-resource])) + :: + [%x %resource @ *] + =/ app=@tas i.t.t.path + =/ app-path=^path t.t.t.path + ``noun+!>((~(get by resource-indices) app app-path)) == :: ++ on-agent on-agent:def diff --git a/pkg/arvo/app/publish.hoon b/pkg/arvo/app/publish.hoon index d4c5b470c..2ed386584 100644 --- a/pkg/arvo/app/publish.hoon +++ b/pkg/arvo/app/publish.hoon @@ -54,6 +54,7 @@ [%3 state-three] [%4 state-three] [%5 state-three] + [%6 state-three] == :: +$ metadata-delta @@ -69,7 +70,7 @@ == -- :: -=| [%5 state-three] +=| [%6 state-three] =* state - %- agent:dbug %+ verb | @@ -86,7 +87,6 @@ :_ this :~ [%pass /view-bind %arvo %e %connect [~ /'publish-view'] %publish] [%pass /read/paths %arvo %c %warp our.bol q.byk.bol `rav] - [%pass /permissions %agent [our.bol %permission-store] %watch /updates] (invite-poke:main [%create /publish]) :* %pass /invites %agent [our.bol %invite-store] %watch /invitatory/publish @@ -218,6 +218,26 @@ == :: %5 + %= $ + -.p.old-state %6 + cards + %+ weld cards + %+ roll ~(tap by books.p.old-state) + |= [[[who=@p book=@tas] nb=notebook] out=(list card)] + ^- (list card) + ?. =(who our.bol) + out + =/ rid (de-path:resource writers.nb) + =/ grp=(unit group) (scry-group:grup:main rid) + ?~ grp out + ?: hidden.u.grp + out + =/ =tag [%publish (cat 3 'writers-' book)] + :_ out + (group-proxy-poke entity.rid %add-tag rid tag members.u.grp) + == + :: + %6 [cards this(state p.old-state)] == ++ convert-notebook-3-4 @@ -995,6 +1015,22 @@ [~ state] :_ state %- zing + :- ^- (list card) + %+ roll ~(tap by books) + |= [[[who=@p book=@tas] nb=notebook] out=(list card)] + ^- (list card) + ?. =(who our.bol) + out + ?. =(writers.nb path) + out + =/ rid (de-path:resource writers.nb) + =/ grp=(unit group) (scry-group:grup rid) + ?~ grp out + ?: hidden.u.grp + out + =/ =tag [%publish (cat 3 'writers-' book)] + :_ out + (group-proxy-poke entity.rid %add-tag rid tag members.u.grp) %+ turn ~(tap in ships) |= who=@p ?. (allowed who %read u.book) @@ -1226,12 +1262,19 @@ ^- [(list card) write=path read=path] ?> ?=(^ group-path.group) =/ scry-path - ;:(welp /(scot %p our.bol)/group-store/(scot %da now.bol) [%groups group-path.group] /noun) - =/ grp .^((unit ^group) %gx scry-path) + ;: welp + /(scot %p our.bol)/group-store/(scot %da now.bol) + [%groups group-path.group] + /noun + == + =/ rid=resource (de-path:resource group-path.group) + =/ grp=(unit ^group) (scry-group:grup rid) ?: use-preexisting.group ?~ grp !! ?. (is-managed group-path.group) !! - `[group-path.group group-path.group] + =/ =tag [%publish (cat 3 'writers-' book)] + :_ [group-path.group group-path.group] + [(group-proxy-poke entity.rid %add-tag rid tag members.u.grp)]~ :: =/ =policy *open:policy @@ -1684,10 +1727,9 @@ ?> ?=(^ subscribers.u.book) =/ cards=(list card) ~[(delete-dir pax)] - =/ rid=resource (de-path:resource writers.u.book) - =? cards (is-managed:grup rid) + =? cards !(is-managed:grup rid) [(group-poke %remove-group rid ~) cards] [cards state] :: %del-note: @@ -1789,8 +1831,14 @@ :: %subscribe ?> (team:title our.bol src.bol) + ?: =(our.bol who.act) + [~ state] =/ join-wire=wire /join-group/[(scot %p who.act)]/[book.act] + =/ meta=(unit (set path)) + (metadata-resource-scry %publish /(scot %p who.act)/[book.act]) + ?^ meta + (subscribe-notebook who.act book.act) =/ rid=resource [who.act book.act] =/ =cage @@ -1811,12 +1859,16 @@ (de-path:resource writers.book) =/ =group (need (scry-group:grup rid)) - :_ state(books (~(del by books) who.act book.act)) - :~ `card`[%pass wir %agent [who.act %publish] %leave ~] - `card`[%give %fact [/primary]~ %publish-primary-delta !>(del)] - (group-proxy-poke who.act %remove-members rid (sy our.bol ~)) - (group-poke %remove-group rid ~) - == + =/ cards=(list card) + :~ [%pass wir %agent [who.act %publish] %leave ~] + [%give %fact [/primary]~ %publish-primary-delta !>(del)] + == + =? cards hidden.group + %+ weld cards + :~ (group-proxy-poke who.act %remove-members rid (sy our.bol ~)) + (group-poke %remove-group rid ~) + == + [cards state(books (~(del by books) who.act book.act))] :: %read :: %read @@ -1952,6 +2004,19 @@ /noun == :: +++ metadata-resource-scry + |= [app=@tas app-path=path] + ^- (unit (set path)) + ?. .^(? %gu (scot %p our.bol) %metadata-store (scot %da now.bol) ~) ~ + .^ (unit (set path)) + %gx + ;: weld + /(scot %p our.bol)/metadata-store/(scot %da now.bol)/resource/[app] + app-path + /noun + == + == +:: ++ emit-metadata |= del=metadata-delta ^- (list card) @@ -2044,9 +2109,11 @@ (emit-updates-and-state host.del book.del data.del del sty) =/ rid=resource (de-path:resource writers.data.del) + =? cards !=(our.bol entity.rid) + :_ cards + (group-pull-hook-poke [%add host.del rid]) :_ state - :* (group-pull-hook-poke [%add host.del rid]) - (metadata-hook-poke [%add-synced host.del writers.data.del]) + :* (metadata-hook-poke [%add-synced host.del writers.data.del]) cards == :: diff --git a/pkg/arvo/gen/graph-store/add-graph.hoon b/pkg/arvo/gen/graph-store/add-graph.hoon new file mode 100644 index 000000000..a95468928 --- /dev/null +++ b/pkg/arvo/gen/graph-store/add-graph.hoon @@ -0,0 +1,10 @@ +:: graph-store|add-graph: add new graph +:: +/+ *graph-store +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[=resource mark=(unit mark) ~] ~] + == +:- %graph-update +^- update +[%0 now [%add-graph resource (gas:orm ~ ~) mark]] diff --git a/pkg/arvo/gen/graph-store/add-post.hoon b/pkg/arvo/gen/graph-store/add-post.hoon new file mode 100644 index 000000000..643e073c0 --- /dev/null +++ b/pkg/arvo/gen/graph-store/add-post.hoon @@ -0,0 +1,20 @@ +:: graph-store|add-post: add post to a graph +:: +/- *graph-store +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[[our=ship name=term] contents=(list content) ~] ~] + == +=/ =post *post +=: author.post our + index.post [now]~ + time-sent.post now + contents.post contents +== +:: +:- %graph-update +^- update +:+ %0 now +:+ %add-nodes [our name] +%- ~(gas by *(map index node)) +~[[[now]~ [post [%empty ~]]]] diff --git a/pkg/arvo/gen/graph-store/add-signatures.hoon b/pkg/arvo/gen/graph-store/add-signatures.hoon new file mode 100644 index 000000000..9a536d6d5 --- /dev/null +++ b/pkg/arvo/gen/graph-store/add-signatures.hoon @@ -0,0 +1,10 @@ +:: graph-store|add-signatures: add signatures to a node at a particular uid +:: +/- *graph-store +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[[=resource =index] =signatures ~] ~] + == +:- %graph-update +^- update +[%0 now [%add-signatures [resource index] signatures]] diff --git a/pkg/arvo/gen/graph-store/add-tag.hoon b/pkg/arvo/gen/graph-store/add-tag.hoon new file mode 100644 index 000000000..8c83f1c4e --- /dev/null +++ b/pkg/arvo/gen/graph-store/add-tag.hoon @@ -0,0 +1,10 @@ +:: graph-store|add-tag: tag a particular graph +:: +/- *graph-store +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[=term =resource ~] ~] + == +:- %graph-update +^- update +[%0 now [%add-tag term resource]] diff --git a/pkg/arvo/gen/graph-store/archive-graph.hoon b/pkg/arvo/gen/graph-store/archive-graph.hoon new file mode 100644 index 000000000..a40a7a533 --- /dev/null +++ b/pkg/arvo/gen/graph-store/archive-graph.hoon @@ -0,0 +1,10 @@ +:: graph-store|archive-graph: archive graph +:: +/- *graph-store +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[=resource ~] ~] + == +:- %graph-update +^- update +[%0 now [%archive-graph resource]] diff --git a/pkg/arvo/gen/graph-store/remove-graph.hoon b/pkg/arvo/gen/graph-store/remove-graph.hoon new file mode 100644 index 000000000..e06305981 --- /dev/null +++ b/pkg/arvo/gen/graph-store/remove-graph.hoon @@ -0,0 +1,10 @@ +:: graph-store|remove-graph: remove graph +:: +/- *graph-store +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[=resource ~] ~] + == +:- %graph-update +^- update +[%0 now [%remove-graph resource]] diff --git a/pkg/arvo/gen/graph-store/remove-nodes.hoon b/pkg/arvo/gen/graph-store/remove-nodes.hoon new file mode 100644 index 000000000..8c5e13603 --- /dev/null +++ b/pkg/arvo/gen/graph-store/remove-nodes.hoon @@ -0,0 +1,10 @@ +:: graph-store|remove-nodes: remove nodes from a graph at indices +:: +/- *graph-store +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[=resource indices=(set index) ~] ~] + == +:- %graph-update +^- update +[%0 now [%remove-nodes resource indices]] diff --git a/pkg/arvo/gen/graph-store/remove-signatures.hoon b/pkg/arvo/gen/graph-store/remove-signatures.hoon new file mode 100644 index 000000000..b9bd658fa --- /dev/null +++ b/pkg/arvo/gen/graph-store/remove-signatures.hoon @@ -0,0 +1,11 @@ +:: graph-store|remove-signatures: remove signatures from a node at a +:: particular uid +:: +/- *graph-store +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[[=resource =index] =signatures ~] ~] + == +:- %graph-update +^- update +[%0 now [%remove-signatures [resource index] signatures]] diff --git a/pkg/arvo/gen/graph-store/remove-tag.hoon b/pkg/arvo/gen/graph-store/remove-tag.hoon new file mode 100644 index 000000000..722d4af5c --- /dev/null +++ b/pkg/arvo/gen/graph-store/remove-tag.hoon @@ -0,0 +1,10 @@ +:: graph-store|remove-tag: remove a tag from a particular graph +:: +/- *graph-store +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[=term =resource ~] ~] + == +:- %graph-update +^- update +[%0 now [%remove-tag term resource]] diff --git a/pkg/arvo/gen/graph-store/unarchive-graph.hoon b/pkg/arvo/gen/graph-store/unarchive-graph.hoon new file mode 100644 index 000000000..1d684de6a --- /dev/null +++ b/pkg/arvo/gen/graph-store/unarchive-graph.hoon @@ -0,0 +1,10 @@ +:: graph-store|unarchive-graph: unarchive graph +:: +/- *graph-store +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[=resource ~] ~] + == +:- %graph-update +^- update +[%0 now [%unarchive-graph resource]] diff --git a/pkg/arvo/gen/hood/moon-cycle-keys.hoon b/pkg/arvo/gen/hood/moon-cycle-keys.hoon index e9be8a32f..38ca3d364 100644 --- a/pkg/arvo/gen/hood/moon-cycle-keys.hoon +++ b/pkg/arvo/gen/hood/moon-cycle-keys.hoon @@ -36,7 +36,7 @@ public-key =/ cub (pit:nu:crub:crypto 512 (shaz (jam mon life eny))) =/ =seed:able:jael - [mon 1 sec:ex:cub ~] + [mon life sec:ex:cub ~] %- %- slog :~ leaf+"moon: {(scow %p mon)}" leaf+(scow %uw (jam seed)) diff --git a/pkg/arvo/gen/hood/ota.hoon b/pkg/arvo/gen/hood/ota.hoon index c91f5b9f3..fcccb68a8 100644 --- a/pkg/arvo/gen/hood/ota.hoon +++ b/pkg/arvo/gen/hood/ota.hoon @@ -8,7 +8,11 @@ :: :- %say |= $: [now=@da eny=@uvJ bec=beak] - [arg=?(~ [her=@p sud=@tas ~]) ~] + [arg=?(~ [%disable ~] [her=@p sud=@tas ~]) ~] == +?~ arg + :- %kiln-ota-info ~ :- %kiln-ota -?~(arg ~ `[her sud]:arg) +?: ?=([%disable ~] arg) + ~ +`[her sud]:arg diff --git a/pkg/arvo/gen/timers.hoon b/pkg/arvo/gen/timers.hoon index b016fd41c..d448fd6ac 100644 --- a/pkg/arvo/gen/timers.hoon +++ b/pkg/arvo/gen/timers.hoon @@ -5,5 +5,5 @@ [%tang >timers< ~] .^ (list [date=@da =duct]) %bx - (en-beam:format [p.bec %$ r.bec] /debug/timers) + (en-beam:format [p.bec %$ r.bec] /timers/debug) == diff --git a/pkg/arvo/lib/graph-store.hoon b/pkg/arvo/lib/graph-store.hoon new file mode 100644 index 000000000..bf099c89f --- /dev/null +++ b/pkg/arvo/lib/graph-store.hoon @@ -0,0 +1,411 @@ +/- sur=graph-store, pos=post +/+ res=resource +=< [sur .] +=< [pos .] +=, sur +=, pos +|% +:: NOTE: move these functions to zuse +++ nu :: parse number as hex + |= jon/json + ?> ?=({$s *} jon) + (rash p.jon hex) +:: +++ re :: recursive reparsers + |* {gar/* sef/_|.(fist:dejs-soft:format)} + |= jon/json + ^- (unit _gar) + =- ~! gar ~! (need -) - + ((sef) jon) +:: +++ dank :: tank + ^- $-(json (unit tank)) + =, ^? dejs-soft:format + %+ re *tank |. ~+ + %- of :~ + leaf+sa + palm+(ot style+(ot mid+sa cap+sa open+sa close+sa ~) lines+(ar dank) ~) + rose+(ot style+(ot mid+sa open+sa close+sa ~) lines+(ar dank) ~) + == +:: +++ orm ((ordered-map atom node) gth) +++ orm-log ((ordered-map time logged-update) gth) +:: +++ enjs + =, enjs:format + |% + ++ update + |= upd=^update + ^- json + ?> ?=(%0 -.upd) + |^ (frond %graph-update (pairs ~[(encode q.upd)])) + :: + ++ encode + |= upd=update-0 + ^- [cord json] + ?- -.upd + %add-graph + :- %add-graph + %- pairs + :~ [%resource (enjs:res resource.upd)] + [%graph (graph graph.upd)] + [%mark ?~(mark.upd ~ s+u.mark.upd)] + == + :: + %remove-graph + [%remove-graph (enjs:res resource.upd)] + :: + %add-nodes + :- %add-nodes + %- pairs + :~ [%resource (enjs:res resource.upd)] + [%nodes (nodes nodes.upd)] + == + :: + %remove-nodes + :- %remove-nodes + %- pairs + :~ [%resource (enjs:res resource.upd)] + [%indices (indices indices.upd)] + == + :: + %add-signatures + :- %add-signatures + %- pairs + :~ [%uid (uid uid.upd)] + [%signatures (signatures signatures.upd)] + == + :: + %remove-signatures + :- %remove-signatures + %- pairs + :~ [%uid (uid uid.upd)] + [%signatures (signatures signatures.upd)] + == + :: + %add-tag + :- %add-tag + %- pairs + :~ [%term s+term.upd] + [%resource (enjs:res resource.upd)] + == + :: + %remove-tag + :- %remove-tag + %- pairs + :~ [%term s+term.upd] + [%resource (enjs:res resource.upd)] + == + :: + %archive-graph + [%archive-graph (enjs:res resource.upd)] + :: + %unarchive-graph + [%unarchive-graph (enjs:res resource.upd)] + :: + %keys + [%keys [%a (turn ~(tap in resources.upd) enjs:res)]] + :: + %tags + [%tags [%a (turn ~(tap in tags.upd) |=(=term s+term))]] + :: + %run-updates + [%run-updates ~] + :: + %tag-queries + :- %tag-queries + %- pairs + %+ turn ~(tap by tag-queries.upd) + |= [=term =resources] + ^- [cord json] + [term [%a (turn ~(tap in resources) enjs:res)]] + == + :: + ++ graph + |= g=^graph + ^- json + :- %a + %+ turn (tap:orm g) + |= [a=atom n=^node] + ^- json + :- %a + :~ (index [a]~) + (node n) + == + :: + ++ index + |= i=^index + ^- json + =/ j=^tape "" + |- + ?~ i [%s (crip j)] + =/ k=json (numb i.i) + ?> ?=(%n -.k) + %_ $ + i t.i + j (weld j (weld "/" (trip +.k))) + == + :: + ++ node + |= n=^node + ^- json + %- pairs + :~ [%post (post post.n)] + :- %children + ?- -.children.n + %empty ~ + %graph (graph +.children.n) + == + == + :: + ++ post + |= p=^post + ^- json + %- pairs + :~ [%author (ship author.p)] + [%index (index index.p)] + [%time-sent (time time-sent.p)] + [%contents [%a (turn contents.p content)]] + [%hash ?~(hash.p ~ s+(scot %ux u.hash.p))] + [%signatures (signatures signatures.p)] + == + :: + ++ content + |= c=^content + ^- json + ?- -.c + %text (frond %text s+text.c) + %url (frond %url s+url.c) + %reference (frond %reference (uid uid.c)) + %code + %+ frond %code + %- pairs + :- [%expression s+expression.c] + :_ ~ + :- %output + :: virtualize output rendering, +tank:enjs:format might crash + :: + =/ result=(each (list json) tang) + (mule |.((turn output.c tank))) + ?- -.result + %& a+p.result + %| a+[a+[%s '[[output rendering error]]']~]~ + == + == + :: + ++ nodes + |= m=(map ^index ^node) + ^- json + :- %a + %+ turn ~(tap by m) + |= [n=^index o=^node] + ^- json + :- %a + :~ (index n) + (node o) + == + :: + ++ indices + |= i=(set ^index) + ^- json + [%a (turn ~(tap in i) index)] + :: + ++ uid + |= u=^uid + ^- json + %- pairs + :~ [%resource (enjs:res resource.u)] + [%index (index index.u)] + == + :: + ++ signatures + |= s=^signatures + ^- json + [%a (turn ~(tap in s) signature)] + :: + ++ signature + |= s=^signature + ^- json + %- pairs + :~ [%signature s+(scot %ux p.s)] + [%ship (ship q.s)] + [%life (numb r.s)] + == + -- + -- +:: +++ dejs + =, dejs:format + |% + ++ update + |= jon=json + ^- ^update + :- %0 + :- *time + ^- update-0 + =< (decode jon) + |% + ++ decode + %- of + :~ [%add-graph add-graph] + [%remove-graph remove-graph] + [%add-nodes add-nodes] + [%remove-nodes remove-nodes] + [%add-signatures add-signatures] + [%remove-signatures remove-signatures] + [%add-tag add-tag] + [%remove-tag remove-tag] + [%archive-graph archive-graph] + [%unarchive-graph unarchive-graph] + [%keys keys] + [%tags tags] + [%tag-queries tag-queries] + [%run-updates run-updates] + == + :: + ++ add-graph + %- ot + :~ [%resource dejs:res] + [%graph graph] + [%mark (mu so)] + == + :: + ++ graph + |= a=json + ^- ^graph + =/ or-mp ((ordered-map atom ^node) gth) + %+ gas:or-mp ~ + %+ turn ~(tap by ((om node) a)) + |* [b=cord c=*] + ^- [atom ^node] + => .(+< [b c]=+<) + [(rash b dem) c] + :: + ++ remove-graph (ot [%resource dejs:res]~) + ++ archive-graph (ot [%resource dejs:res]~) + ++ unarchive-graph (ot [%resource dejs:res]~) + :: + ++ add-nodes + %- ot + :~ [%resource dejs:res] + [%nodes nodes] + == + :: + ++ nodes (op ;~(pfix net (more net dem)) node) + :: + ++ node + %- ot + :~ [%post post] + :: TODO: support adding nodes with children by supporting the + :: graph key + [%children (of [%empty ul]~)] + == + :: + ++ post + %- ot + :~ [%author (su ;~(pfix sig fed:ag))] + [%index index] + [%time-sent di] + [%contents (ar content)] + [%hash (mu nu)] + [%signatures (as signature)] + == + :: + ++ content + %- of + :~ [%text so] + [%url so] + [%reference uid] + [%code eval] + == + :: + ++ eval + |= a=^json + ^- [cord (list tank)] + =, ^? dejs-soft:format + =+ exp=((ot expression+so ~) a) + %- need + ?~ exp [~ '' ~] + :+ ~ u.exp + :: NOTE: when sending, if output is an empty list, + :: graph-store will evaluate + (fall ((ot output+(ar dank) ~) a) ~) + :: + ++ remove-nodes + %- ot + :~ [%resource dejs:res] + [%indices (as index)] + == + :: + ++ add-signatures + %- ot + :~ [%uid uid] + [%signatures (as signature)] + == + :: + ++ remove-signatures + %- ot + :~ [%uid uid] + [%signatures (as signature)] + == + :: + ++ signature + %- ot + :~ [%hash nu] + [%ship (su ;~(pfix sig fed:ag))] + [%life ni] + == + :: + ++ uid + %- ot + :~ [%resource dejs:res] + [%index index] + == + :: + ++ index (su ;~(pfix net (more net dem))) + :: + ++ add-tag + %- ot + :~ [%term so] + [%resource dejs:res] + == + :: + ++ remove-tag + %- ot + :~ [%term so] + [%resource dejs:res] + == + :: + ++ keys + |= =json + *resources + :: + ++ tags + |= =json + *(set term) + :: + ++ tag-queries + |= =json + *^tag-queries + :: + ++ run-updates + |= a=json + ^- [resource update-log] + [*resource *update-log] + -- + -- +:: +++ create + |_ [our=ship now=time] + ++ post + |= [=index contents=(list content)] + ^- ^post + :* our + index + now + contents + ~ + *signatures + == + -- +-- diff --git a/pkg/arvo/lib/graph.hoon b/pkg/arvo/lib/graph.hoon new file mode 100644 index 000000000..395c0554f --- /dev/null +++ b/pkg/arvo/lib/graph.hoon @@ -0,0 +1,24 @@ +/- *resource +/+ store=graph-store +|_ =bowl:gall +++ scry-for + |* [=mold =path] + .^ mold + %gx + (scot %p our.bowl) + %graph-store + (scot %da now.bowl) + (snoc `^path`path %noun) + == +:: +++ get-graph + |= res=resource + ^- marked-graph:store + %+ scry-for marked-graph:store + /graph/(scot %p entity.res)/[name.res] +:: +++ peek-log + |= res=resource + ^- (unit time) + (scry-for (unit time) /peek-update-log/(scot %p entity.res)/[name.res]) +-- diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index 0afec46ce..e7b91cfbe 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -104,6 +104,7 @@ %s3-store %file-server %glob + %graph-store == :: ++ deft-fish :: default connects @@ -206,7 +207,7 @@ == :: ++ on-load - |= [hood-version=?(%1 %2 %3 %4 %5 %6 %7 %8) old=any-state] + |= [hood-version=?(%1 %2 %3 %4 %5 %6 %7 %8 %9) old=any-state] =< se-abet =< se-view =. sat old =. dev (~(gut by bin) ost *source) @@ -233,6 +234,8 @@ =? ..on-load (lte hood-version %8) => (se-born | %home %group-push-hook) (se-born | %home %group-pull-hook) + =? ..on-load (lte hood-version %9) + (se-born | %home %graph-store) ..on-load :: ++ reap-phat :: ack connect diff --git a/pkg/arvo/lib/hood/kiln.hoon b/pkg/arvo/lib/hood/kiln.hoon index 07f04de83..21273f827 100644 --- a/pkg/arvo/lib/hood/kiln.hoon +++ b/pkg/arvo/lib/hood/kiln.hoon @@ -208,7 +208,7 @@ :: ++ get-germ |= =desk - =+ .^(=cass:clay %cw /(scot %p our)/home/(scot %da now)) + =+ .^(=cass:clay %cw /(scot %p our)/[desk]/(scot %da now)) ?- ud.cass %0 %init %1 %that @@ -341,13 +341,22 @@ abet:(spam (render "already syncing" [sud her syd]:hos) ~) abet:abet:start-sync:(auto hos) :: +++ ota-info + ?~ ota + "OTAs disabled" + "OTAs enabled from {} on {}" +:: +++ poke-ota-info + |= * + =< abet %- spam + :~ [%leaf ota-info] + [%leaf "use |ota %disable or |ota ~sponsor %kids to reset it"] + == +:: ++ poke-syncs :: print sync config |= ~ =< abet %- spam - :- :- %leaf - ?~ ota - "OTAs disabled" - "OTAs from {} on {}" + :- [%leaf ota-info] ?: =(0 ~(wyt by syn)) [%leaf "no other syncs configured"]~ %+ turn ~(tap in ~(key by syn)) @@ -416,6 +425,7 @@ %kiln-merge =;(f (f !<(_+<.f vase)) poke-merge) %kiln-mount =;(f (f !<(_+<.f vase)) poke-mount) %kiln-ota =;(f (f !<(_+<.f vase)) poke:update) + %kiln-ota-info =;(f (f !<(_+<.f vase)) poke-ota-info) %kiln-permission =;(f (f !<(_+<.f vase)) poke-permission) %kiln-rm =;(f (f !<(_+<.f vase)) poke-rm) %kiln-schedule =;(f (f !<(_+<.f vase)) poke-schedule) diff --git a/pkg/arvo/lib/push-hook.hoon b/pkg/arvo/lib/push-hook.hoon index 4958085a2..776aa0f59 100644 --- a/pkg/arvo/lib/push-hook.hoon +++ b/pkg/arvo/lib/push-hook.hoon @@ -59,7 +59,6 @@ |~ [term tang] *[(list card) _^|(..on-init)] :: +resource-for-update: get affected resource from an update - ++ resource-for-update |~ vase *(unit resource) diff --git a/pkg/arvo/lib/server.hoon b/pkg/arvo/lib/server.hoon index 4a0b7ded0..d51fa12c8 100644 --- a/pkg/arvo/lib/server.hoon +++ b/pkg/arvo/lib/server.hoon @@ -80,9 +80,11 @@ ++ max-1-wk ['cache-control' 'max-age=604800'] :: ++ html-response + =| cache=? |= =octs ^- simple-payload:http - [[200 [['content-type' 'text/html'] max-1-wk ~]] `octs] + :_ `octs + [200 [['content-type' 'text/html'] ?:(cache [max-1-wk ~] ~)]] :: ++ js-response |= =octs diff --git a/pkg/arvo/lib/signatures.hoon b/pkg/arvo/lib/signatures.hoon new file mode 100644 index 000000000..12b58a644 --- /dev/null +++ b/pkg/arvo/lib/signatures.hoon @@ -0,0 +1,43 @@ +/- post +^? +=< [post .] +=, post +|% +++ sign + |= [our=ship now=time =hash] + ^- signature + =/ =life .^(life %j /=life/(scot %da now)/(scot %p our)) + =/ =ring .^(ring %j /=vein/(scot %da now)/(scot %ud life)) + :+ `@ux`(sign:as:(nol:nu:crub:crypto ring) hash) + our + life +:: +++ is-signature-valid + |= [=signature =hash now=time] + ^- ? + =/ deed=(unit [a=life b=pass c=(unit @ux)]) + .^ (unit [life pass (unit @ux)]) + %j + /=deed/(scot %da now)/(scot %p q.signature)/(scot %ud p.signature) + == + :: we do not have a public key from ship + :: + ?~ deed %.y + :: we do not have a public key from ship at this life + :: + ?. =(a.u.deed r.signature) %.y + :: verify signature from ship at life + :: + =(`hash (tear:as:crub:crypto b.u.deed p.signature)) +:: +++ are-signatures-valid + |= [=signatures =hash now=time] + ^- ? + =/ signature-list ~(tap in signatures) + |- + ?~ signature-list + %.y + ?: (is-signature-valid i.signature-list hash now) + $(signature-list t.signature-list) + %.n +-- diff --git a/pkg/arvo/mar/graph/update.hoon b/pkg/arvo/mar/graph/update.hoon new file mode 100644 index 000000000..d5f0f4abe --- /dev/null +++ b/pkg/arvo/mar/graph/update.hoon @@ -0,0 +1,13 @@ +/+ *graph-store +|_ upd=update +++ grow + |% + ++ json (update:enjs upd) + -- +:: +++ grab + |% + ++ noun update + ++ json update:dejs + -- +-- diff --git a/pkg/arvo/mar/graph/validator/chat.hoon b/pkg/arvo/mar/graph/validator/chat.hoon new file mode 100644 index 000000000..c49373514 --- /dev/null +++ b/pkg/arvo/mar/graph/validator/chat.hoon @@ -0,0 +1,17 @@ +/- *post +|_ i=indexed-post +++ grow + |% + ++ noun i + -- +++ grab + |% + ++ noun + |= p=* + =/ ip ;;(indexed-post p) + ?> ?=([@ ~] index.p.ip) + ip + -- +:: +++ grad %noun +-- diff --git a/pkg/arvo/sur/graph-store.hoon b/pkg/arvo/sur/graph-store.hoon new file mode 100644 index 000000000..edcfb4135 --- /dev/null +++ b/pkg/arvo/sur/graph-store.hoon @@ -0,0 +1,61 @@ +/- *post +|% ++$ graph ((mop atom node) gth) ++$ marked-graph [p=graph q=(unit mark)] +:: ++$ node [=post children=internal-graph] ++$ graphs (map resource marked-graph) +:: ++$ tag-queries (jug term resource) +:: ++$ update-log ((mop time logged-update) gth) ++$ update-logs (map resource update-log) +:: ++$ internal-graph + $~ [%empty ~] + $% [%graph p=graph] + [%empty ~] + == +:: ++$ network + $: =graphs + =tag-queries + =update-logs + archive=graphs + validators=(set mark) + == +:: ++$ update + $% [%0 p=time q=update-0] + == +:: ++$ logged-update + $% [%0 p=time q=logged-update-0] + == +:: ++$ logged-update-0 + $% [%add-nodes =resource nodes=(map index node)] + [%remove-nodes =resource indices=(set index)] + [%add-signatures =uid =signatures] + [%remove-signatures =uid =signatures] + == +:: ++$ update-0 + $% logged-update-0 + [%add-graph =resource =graph mark=(unit mark)] + [%remove-graph =resource] + :: + [%add-tag =term =resource] + [%remove-tag =term =resource] + :: + [%archive-graph =resource] + [%unarchive-graph =resource] + [%run-updates =resource =update-log] + :: + :: NOTE: cannot be sent as pokes + :: + [%keys =resources] + [%tags tags=(set term)] + [%tag-queries =tag-queries] + == +-- diff --git a/pkg/arvo/sur/post.hoon b/pkg/arvo/sur/post.hoon new file mode 100644 index 000000000..c5c354615 --- /dev/null +++ b/pkg/arvo/sur/post.hoon @@ -0,0 +1,37 @@ +/- *resource +|% ++$ index (list atom) ++$ uid [=resource =index] +:: +:: +sham (half sha-256) hash of +validated-portion ++$ hash @ux +:: ++$ signature [p=@ux q=ship r=life] ++$ signatures (set signature) ++$ post + $: author=ship + =index + time-sent=time + contents=(list content) + hash=(unit hash) + =signatures + == +:: ++$ indexed-post [a=atom p=post] +:: ++$ validated-portion + $: parent-hash=(unit hash) + author=ship + time-sent=time + contents=(list content) + == +:: ++$ content + $% [%text text=cord] + [%url url=cord] + [%code expression=cord output=(list tank)] + [%reference =uid] + :: TODO: maybe use a cask? + ::[%cage =cage] + == +-- diff --git a/pkg/arvo/sur/pull-hook.hoon b/pkg/arvo/sur/pull-hook.hoon index 81655a181..1c66648c1 100644 --- a/pkg/arvo/sur/pull-hook.hoon +++ b/pkg/arvo/sur/pull-hook.hoon @@ -8,5 +8,4 @@ +$ update $% [%tracking tracking=(map resource ship)] == -:: -- diff --git a/pkg/arvo/sys/hoon.hoon b/pkg/arvo/sys/hoon.hoon index 824b3a2cd..369977f8a 100644 --- a/pkg/arvo/sys/hoon.hoon +++ b/pkg/arvo/sys/hoon.hoon @@ -7762,11 +7762,13 @@ ++ teal |= mod/spec ^- spec + ?: ?=(%& -.tik) mod [%over [%& 3]~ mod] :: ++ tele |= syn/skin ^- skin + ?: ?=(%& -.tik) syn [%over [%& 3]~ syn] :: ++ gray diff --git a/pkg/arvo/sys/vane/ames.hoon b/pkg/arvo/sys/vane/ames.hoon index cbf7485b8..74b3c64b0 100644 --- a/pkg/arvo/sys/vane/ames.hoon +++ b/pkg/arvo/sys/vane/ames.hoon @@ -1121,17 +1121,32 @@ ?> =(rcvr-life.shut-packet our-life.channel) :: non-galaxy: update route with heard lane or forwarded lane :: - =? route.peer-state - ?: =(%czar (clan:title her.channel)) - %.n - =/ is-old-direct=? ?=([~ %& *] route.peer-state) - =/ is-new-direct=? ?=(~ origin.packet) - :: old direct takes precedence over new indirect - :: - |(is-new-direct !is-old-direct) + =? route.peer-state !=(%czar (clan:title her.channel)) + :: if new packet is direct, use that. otherwise, if the new new + :: and old lanes are indirect, use the new one. if the new lane + :: is indirect but the old lane is direct, then if the lanes are + :: identical, don't mark it indirect; if they're not identical, + :: use the new lane and mark it indirect. :: - ?~ origin.packet + :: if you mark lane as indirect because you got an indirect + :: packet even though you already had a direct identical lane, + :: then delayed forwarded packets will come later and reset to + :: indirect, so you're unlikely to get a stable direct route + :: (unless the forwarder goes offline for a while). + :: + :: conversely, if you don't accept indirect routes with different + :: lanes, then if your lane is stale and they're trying to talk + :: to you, your acks will go to the stale lane, and you'll never + :: time it out unless you reach out to them. this manifests as + :: needing to |hi or dotpost to get a response when the other + :: ship has changed lanes. + :: + ?: ?=(~ origin.packet) `[direct=%.y lane] + ?: ?=([~ %& *] route.peer-state) + ?: =(lane.u.route.peer-state u.origin.packet) + route.peer-state + `[direct=%.n u.origin.packet] `[direct=%.n u.origin.packet] :: perform peer-specific handling of packet :: diff --git a/pkg/arvo/sys/vane/clay.hoon b/pkg/arvo/sys/vane/clay.hoon index e60c47b00..14d23c8d4 100644 --- a/pkg/arvo/sys/vane/clay.hoon +++ b/pkg/arvo/sys/vane/clay.hoon @@ -109,6 +109,10 @@ mut/(list (trel path lobe cage)) :: mutations == :: :: +:: Over-the-wire backfill request +:: ++$ fill [=desk =lobe] +:: :: Ford cache :: +$ ford-cache @@ -214,18 +218,29 @@ :: requests, and a possible nako if we've received data from the other ship and :: are in the process of validating it. :: -++ rind :: request manager - $: nix/@ud :: request index - bom/(map @ud {p/duct q/rave}) :: outstanding - fod/(map duct @ud) :: current requests - haw/(map mood (unit cage)) :: simple cache - == :: ++$ rind :: request manager + $: nix=@ud :: request index + bom=(map @ud update-state) :: outstanding + fod=(map duct @ud) :: current requests + haw=(map mood (unit cage)) :: simple cache + == :: +:: +:: Active downloads +:: ++$ update-state + $: =duct + =rave + have=(map lobe blob) + need=(list lobe) + nako=(qeu (unit nako)) + busy=_| + == :: :: Result of a subscription :: ++ sub-result $% [%blab =mood data=(each cage lobe)] - [%bleb ins=@ud range=(unit (pair aeon aeon))] + [%bleb ver=@ud ins=@ud range=(unit (pair aeon aeon))] [%balk cage=(unit (each cage lobe)) =mood] [%blas moods=(set mood)] [%blub ~] @@ -246,7 +261,7 @@ :: Generally used when we store a request in our state somewhere. :: ++ cach (unit (unit (each cage lobe))) :: cached result -+$ wove [for=(unit ship) =rove] :: stored source + req ++$ wove [for=(unit [=ship ver=@ud]) =rove] :: stored source + req ++ rove :: stored request $% [%sing =mood] :: single request [%next =mood aeon=(unit aeon) =cach] :: next version of one @@ -1134,13 +1149,13 @@ :: Give next step in a subscription. :: ++ bleb - |= {hen/duct ins/@ud hip/(unit (pair aeon aeon))} + |= [hen=duct ver=@ud ins=@ud hip=(unit (pair aeon aeon))] ^+ +> %^ blab hen [%w [%ud ins] ~] :- %& ?~ hip [%null [%atom %n ~] ~] - [%nako !>((make-nako:ze u.hip))] + [%nako !>((make-nako:ze ver u.hip))] :: :: Tell subscriber that subscription is done. :: @@ -1183,7 +1198,7 @@ =/ =desk p.riff =/ =wire /warp-index/(scot %p ship)/(scot %tas desk)/(scot %ud index) =/ =path [%question desk (scot %ud index) ~] - (emit duct %pass wire %a %plea ship %c path riff) + (emit duct %pass wire %a %plea ship %c path [[%1 ~] riff]) :: :: Create a request that cannot be filled immediately. :: @@ -1210,7 +1225,7 @@ (send-over-ames hen her inx syd `rave) %= +>+.$ nix.u.ref +(nix.u.ref) - bom.u.ref (~(put by bom.u.ref) inx [hen rave]) + bom.u.ref (~(put by bom.u.ref) inx [hen rave ~ ~ ~ |]) fod.u.ref (~(put by fod.u.ref) hen inx) == :: @@ -2003,6 +2018,7 @@ :: bob's. :: ?: ?=(%init germ) + ?> ?=(~ bob-yaki) &+`[conflicts=~ new=|+ali-yaki lat=~] :: =/ bob-yaki (need bob-yaki) @@ -2589,7 +2605,7 @@ :: and then waiting if the subscription range extends into the future. :: ++ start-request - |= [for=(unit ship) rav=rave] + |= [for=(unit [ship @ud]) rav=rave] ^+ ..start-request =^ [new-sub=(unit rove) sub-results=(list sub-result)] fod.dom (try-fill-sub for (rave-to-rove rav)) @@ -2612,9 +2628,9 @@ ?> ?=(^ ref) =+ ruv=(~(get by bom.u.ref) inx) ?~ ruv +>.$ - =/ rav=rave q.u.ruv + =/ rav=rave rave.u.ruv ?: ?=(%many -.rav) - (take-foreign-update inx rut) + abet:(apex:(foreign-update inx) rut) ?~ rut :: nothing here, so cache that :: @@ -2689,36 +2705,138 @@ !>(;;(@uvI q.page)) -- :: - :: A full foreign update. Validate and apply to our local cache of - :: their state. + :: Respond to backfill request :: - ++ take-foreign-update - |= [inx=@ud rut=(unit rand)] - ^+ ..take-foreign-update + :: Maybe should verify the requester is allowed to access this blob? + :: + ++ give-backfill + |= =lobe + ^+ ..give-backfill + (emit hen %give %boon (~(got by lat.ran) lobe)) + :: + :: Ingest foreign update, requesting missing blobs if necessary + :: + ++ foreign-update + |= inx=@ud ?> ?=(^ ref) - =/ ruv (~(get by bom.u.ref) inx) - ?~ ruv - ~& [%clay-foreign-update-lost her syd inx] - ..take-foreign-update - =. hen p.u.ruv - =/ =rave q.u.ruv - ?> ?=(%many -.rave) - |^ - ?~ rut - done - =. lim ?.(?=(%da -.to.moat.rave) lim p.to.moat.rave) - ?> ?=(%nako p.r.u.rut) - =/ nako ;;(nako q.r.u.rut) - =. ..take-foreign-update - =< ?>(?=(^ ref) .) - (apply-foreign-update nako) - done + =/ [sat=update-state lost=?] + =/ ruv (~(get by bom.u.ref) inx) + ?~ ruv + ~& [%clay-foreign-update-lost her syd inx] + [*update-state &] + [u.ruv |] + =/ done=? | + =. hen duct.sat + |% + ++ abet + ^+ ..foreign-update + ?: lost + ..foreign-update + ?: done + =: bom.u.ref (~(del by bom.u.ref) inx) + fod.u.ref (~(del by fod.u.ref) hen) + == + =<(?>(?=(^ ref) .) wake) + =. bom.u.ref (~(put by bom.u.ref) inx sat) + ..foreign-update :: - ++ done - =: bom.u.ref (~(del by bom.u.ref) inx) - bom.u.ref (~(del by bom.u.ref) hen) - == - =<(?>(?=(^ ref) .) wake) + ++ apex + |= rut=(unit rand) + ^+ ..abet + ?: lost ..abet + ?~ rut + =. nako.sat (~(put to nako.sat) ~) + work + ?> ?=(%nako p.r.u.rut) + =/ nako ;;(nako q.r.u.rut) + =/ missing (missing-blobs nako) + =. need.sat `(list lobe)`(welp need.sat ~(tap in missing)) + =. nako.sat (~(put to nako.sat) ~ nako) + work + :: + ++ missing-blobs + |= =nako + ^- (set lobe) + =/ yakis ~(tap in lar.nako) + |- ^- (set lobe) + =* yaki-loop $ + ?~ yakis + ~ + =/ lobes=(list [=path =lobe]) ~(tap by q.i.yakis) + |- ^- (set lobe) + =* blob-loop $ + ?~ lobes + yaki-loop(yakis t.yakis) + ?: (~(has by lat.ran) lobe.i.lobes) + blob-loop(lobes t.lobes) + (~(put in blob-loop(lobes t.lobes)) lobe.i.lobes) + :: + :: Receive backfill response + :: + ++ take-backfill + |= =blob + ^+ ..abet + ?: lost ..abet + =? need.sat + ?& ?=(%delta -.blob) + !(~(has by lat.ran) q.q.blob) + !(~(has by have.sat) q.q.blob) + == + [q.q.blob need.sat] + :: We can't put a blob in lat.ran if its parent isn't already + :: there. Unions are in reverse order so we don't overwrite + :: existing blobs. + :: + =. ..abet + ?: &(?=(%delta -.blob) !(~(has by lat.ran) q.q.blob)) + ..abet(have.sat (~(uni by (malt [p.blob `^blob`blob] ~)) have.sat)) + ..abet(lat.ran (~(uni by (malt [p.blob blob] ~)) lat.ran)) + work(busy.sat |) + :: + :: Fetch next blob + :: + ++ work + ^+ ..abet + ?: busy.sat + ..abet + |- ^+ ..abet + ?: =(~ need.sat) + :: NB: if you change to release nakos as we get enough blobs + :: for them instead of all at the end, you *must* store the + :: `lim` that should be applied after the nako is complete and + :: not use the one in the rave, since that will apply to the + :: end of subscription. + :: + =. lat.ran (~(uni by have.sat) lat.ran) + |- ^+ ..abet + ?: =(~ nako.sat) + ..abet + =^ next=(unit nako) nako.sat ~(get to nako.sat) + ?~ next + ..abet(done &) + =. ..abet (apply-foreign-update u.next) + =. ..foreign-update =<(?>(?=(^ ref) .) wake) + $ + ?> ?=(^ need.sat) + :: This is what removes an item from `need`. This happens every + :: time we take a backfill response, but it could happen more than + :: once if we somehow got this data in the meantime (maybe from + :: another desk updating concurrently, or a previous update on this + :: same desk). + :: + ?: ?| (~(has by lat.ran) i.need.sat) + (~(has by have.sat) i.need.sat) + == + $(need.sat t.need.sat) + :: Otherwise, fetch the next blob + :: + =/ =fill [syd i.need.sat] + =/ =wire /back-index/(scot %p her)/[syd]/(scot %ud inx) + =/ =path [%backfill syd (scot %ud inx) ~] + =. ..foreign-update + =< ?>(?=(^ ref) .) + (emit hen %pass wire %a %plea her %c path fill) + ..abet(busy.sat &) :: :: When we get a %w foreign update, store this in our state. :: @@ -2728,7 +2846,7 @@ :: ++ apply-foreign-update |= =nako - ^+ ..take-foreign-update + ^+ ..abet :: hit: updated commit-hashes by @ud case :: nut: new commit-hash/commit pairs :: hut: updated commits by hash @@ -2765,12 +2883,19 @@ $(aeon +(aeon)) :: produce updated state :: + =/ =rave rave:(~(got by bom.u.ref) inx) + ?> ?=(%many -.rave) =: let.dom (max let.nako let.dom) hit.dom hit hut.ran hut lat.ran lat + :: Is this correct? Seeems like it should only go to `to` if + :: we've gotten all the way to the end. Leaving this + :: behavior unchanged for now, but I believe it's wrong. + :: + lim ?.(?=(%da -.to.moat.rave) lim p.to.moat.rave) == - ..take-foreign-update + ..abet -- :: :: fire function if request is in future @@ -2862,8 +2987,9 @@ :: Try to fill a subscription :: ++ try-fill-sub - |= [for=(unit ship) rov=rove] + |= [far=(unit [=ship ver=@ud]) rov=rove] ^- [[new-sub=(unit rove) (list sub-result)] ford-cache] + =/ for=(unit ship) ?~(far ~ `ship.u.far) ?- -.rov %sing =/ cache-value=(unit (unit cage)) @@ -3075,6 +3201,7 @@ :: [`rov ~] =/ to-aeon (case-to-aeon to.moat.rov) + =/ ver ?~(far %1 ver.u.far) ?~ to-aeon :: we're in the middle of the range, so produce what we can, :: but don't end the subscription @@ -3092,7 +3219,7 @@ ~ :: else changes, so produce them :: - [%bleb let.dom ?:(track.rov ~ `[u.from-aeon let.dom])]~ + [%bleb ver let.dom ?:(track.rov ~ `[u.from-aeon let.dom])]~ :: we're past the end of the range, so end subscription :: :- ~ @@ -3103,7 +3230,7 @@ =/ bleb=(list sub-result) ?: =(lobes.rov new-lobes) ~ - [%bleb +(u.from-aeon) ?:(track.rov ~ `[u.from-aeon u.to-aeon])]~ + [%bleb ver +(u.from-aeon) ?:(track.rov ~ `[u.from-aeon u.to-aeon])]~ :: end subscription :: =/ blub=(list sub-result) @@ -3111,17 +3238,6 @@ (weld bleb blub) == :: - ++ drop-me - ^+ . - ~| %clay-drop-me-not-implemented - !! - :: ?~ mer - :: . - :: %- emit(mer ~) ^- move :* - :: hen.u.mer %give %mere %| %user-interrupt - :: >sor.u.mer< >our< >cas.u.mer< >gem.u.mer< ~ - :: == - :: :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: :: This core has no additional state, and the distinction exists purely for @@ -3209,7 +3325,7 @@ :: Creates a nako of all the changes between a and b. :: ++ make-nako - |= {a/aeon b/aeon} + |= [ver=@ud a=aeon b=aeon] ^- nako :+ ?> (lte b let.dom) |- @@ -3219,7 +3335,7 @@ b ?: =(0 b) [~ ~] - (data-twixt-takos (~(get by hit.dom) a) (aeon-to-tako b)) + (data-twixt-takos =(0 ver) (~(get by hit.dom) a) (aeon-to-tako b)) :: :: Traverse parentage and find all ancestor hashes :: @@ -3245,16 +3361,21 @@ :: ones we found before `a`. Then convert the takos to yakis and also get :: all the data in all the yakis. :: + :: What happens if you run an %init merge on a desk that already + :: had a commit? + :: ++ data-twixt-takos - |= {a/(unit tako) b/tako} - ^- {(set yaki) (set plop)} + |= [plops=? a=(unit tako) b=tako] + ^- [(set yaki) (set plop)] =+ old=?~(a ~ (reachable-takos u.a)) - =/ yal/(set tako) + =/ yal=(set tako) %- silt %+ skip ~(tap in (reachable-takos b)) - |=(tak/tako (~(has in old) tak)) + |=(tak=tako (~(has in old) tak)) :- (silt (turn ~(tap in yal) tako-to-yaki)) + ?. plops + ~ (silt (turn ~(tap in (new-lobes (new-lobes ~ old) yal)) lobe-to-blob)) :: :: Get all the lobes that are referenced in `a` except those that are @@ -3728,7 +3849,7 @@ :: :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: =| :: instrument state - $: ver=%3 :: vane version + $: ver=%4 :: vane version ruf=raft :: revision tree == :: |= [our=ship now=@da eny=@uvJ ski=sley] :: current invocation @@ -3939,7 +4060,14 @@ =^ for req ?: ?=(%warp -.req) [~ req] - :- ?:(=(our who.req) ~ `who.req) + :: ?: =(our who.req) + :: [~ [%warp wer.req rif.req]] + =^ ver rif.req + ?@ -.rif.req + [%0 rif.req] + [-<.rif.req +.rif.req] + ?> ?=(@ -.rif.req) + :- ?:(=(our who.req) ~ `[who.req ver]) [%warp wer.req rif.req] :: ?> ?=(%warp -.req) @@ -3957,8 +4085,14 @@ =* pax path.plea.req =* res payload.plea.req :: - ?> ?=({%question *} pax) - =+ ryf=;;(riff res) + ?: ?=([%backfill *] pax) + =+ ;;(=fill res) + =^ mos ruf + =/ den ((de our now ski hen ruf) our desk.fill) + abet:(give-backfill:den +.fill) + [[[hen %give %done ~] mos] ..^$] + ?> ?=([%question *] pax) + =+ ryf=;;(riff-any res) :_ ..^$ :~ [hen %give %done ~] =/ =wire @@ -3971,11 +4105,58 @@ !: |^ |= old=any-state - ~! [old=old new=*state-3] + ~! [old=old new=*state-4] =? old ?=(%2 -.old) (load-2-to-3 old) - ?> ?=(%3 -.old) + =? old ?=(%3 -.old) (load-3-to-4 old) + ?> ?=(%4 -.old) ..^^$(ruf +.old) :: + ++ load-3-to-4 + |= =state-3 + ^- state-4 + |^ + =- state-3(- %4, hoy hoy.-, rom (room-3-to-4 rom.state-3)) + ^- hoy=(map ship rung) + %- ~(run by hoy.state-3) + |= =rung-3 + ^- rung + %- ~(run by rus.rung-3) + |= =rede-3 + ^- rede + =- rede-3(ref ref.-, qyx (cult-3-to-4 qyx.rede-3)) + ^- ref=(unit rind) + ?~ ref.rede-3 + ~ + =- `u.ref.rede-3(bom bom.-) + ^- bom=(map @ud update-state) + %- ~(run by bom.u.ref.rede-3) + |= [=duct =rave] + ^- update-state + [duct rave ~ ~ ~ |] + :: + ++ room-3-to-4 + |= =room-3 + ^- room + =- room-3(dos dos.-) + ^- dos=(map desk dojo) + %- ~(run by dos.room-3) + |= =dojo-3 + ^- dojo + dojo-3(qyx (cult-3-to-4 qyx.dojo-3)) + :: + ++ cult-3-to-4 + |= =cult-3 + ^- cult + %- malt + %+ turn ~(tap by cult-3) + |= [=wove-3 ducts=(set duct)] + ^- [wove (set duct)] + :_ ducts :_ rove.wove-3 + ?~ for.wove-3 + ~ + `[u.for.wove-3 %0] + -- + :: ++ load-2-to-3 |= =state-2 ^- state-3 @@ -4005,11 +4186,11 @@ :- %ford-fusion [leaf+"queued merge canceled due to upgrade to ford fusion" ~] `[duct %slip %b %drip !>([%mere %| err])] - ^- rom=room + ^- rom=room-3 :- hun.rom.state-2 %- ~(urn by dos.rom.state-2) |= [=desk =dojo-2] - ^- dojo + ^- dojo-3 =- dojo-2(dom -) ^- dome =/ fer=(unit reef-cache) @@ -4019,23 +4200,22 @@ (~(got by hut.ran.state-2) (~(got by hit.dom.dojo-2) let.dom.dojo-2)) `(build-reef desk q.yaki) [ank let hit lab mim fod=*ford-cache fer=fer]:[dom.dojo-2 .] - ^- hoy=(map ship rung) + ^- hoy=(map ship rung-3) %- ~(run by hoy.state-2) |= =rung-2 - ^- rung + ^- rung-3 %- ~(run by rus.rung-2) |= =rede-2 - ^- rede + ^- rede-3 =- rede-2(ref ref.-, dom dom.-) :- ^- dom=dome [ank let hit lab mim fod=*ford-cache fer=~]:[dom.rede-2 .] - ^- ref=(unit rind) + ^- ref=(unit rind-3) ?~ ref.rede-2 ~ - :: TODO: somehow call +wake later to notify subscribers :- ~ - ^- rind - =/ rin=rind [nix bom fod haw]:u.ref.rede-2 + ^- rind-3 + =/ rin=rind-3 [nix bom fod haw]:u.ref.rede-2 =. rin =/ pur=(list [inx=@ud =rand *]) ~(tap by pur.u.ref.rede-2) |- ^+ rin @@ -4138,8 +4318,46 @@ -- -- :: - +$ any-state $%(state-3 state-2) - +$ state-3 [%3 raft] + +$ any-state $%(state-4 state-3 state-2) + +$ state-4 [%4 raft] + +$ state-3 + $: %3 + rom=room-3 + hoy=(map ship rung-3) + ran=rang + mon=(map term beam) + hez=(unit duct) + cez=(map @ta crew) + pud=(unit [=desk =yoki]) + pun=(list move) + == + +$ rung-3 rus=(map desk rede-3) + +$ rede-3 + $: lim/@da + ref/(unit rind-3) + qyx/cult-3 + dom/dome + per/regs + pew/regs + == + +$ rind-3 + $: nix/@ud + bom/(map @ud {p/duct q/rave}) + fod/(map duct @ud) + haw/(map mood (unit cage)) + == + +$ room-3 + $: hun/duct + dos/(map desk dojo-3) + == + ++ dojo-3 + $: qyx/cult-3 + dom/dome + per/regs + pew/regs + == + +$ cult-3 (jug wove-3 duct) + +$ wove-3 [for=(unit ship) =rove] +$ state-2 $: %2 rom=room-2 :: domestic @@ -4156,7 +4374,7 @@ dos/(map desk dojo-2) :: native desk == :: +$ dojo-2 - $: qyx/cult :: subscribers + $: qyx/cult-3 :: subscribers dom/dome-2 :: desk state per/regs :: read perms per path pew/regs :: write perms per path @@ -4172,7 +4390,7 @@ +$ rede-2 $: lim/@da :: complete to ref/(unit rind-2) :: outgoing requests - qyx/cult :: subscribers + qyx/cult-3 :: subscribers dom/dome-2 :: revision state per/regs :: read perms per path pew/regs :: write perms per path @@ -4303,6 +4521,35 @@ [mos ..^$] == :: + ?: ?=([%back-index @ @ @ ~] tea) + ?+ +<.q.hin ~| %clay-backfill-index-strange !! + %done + ?~ error.q.hin + [~ ..^$] + :: TODO better error handling + :: + ~& %clay-take-backfill-index-error^our^tea^tag.u.error.q.hin + %- (slog tang.u.error.q.hin) + [~ ..^$] + :: + %lost + ~| %clay-take-backfill-lost^our + :: TODO better error handling + !! + :: + %boon + =+ ;; =blob payload.q.hin + :: + =/ her=ship (slav %p i.t.tea) + =/ =desk (slav %tas i.t.t.tea) + =/ index=@ud (slav %ud i.t.t.t.tea) + :: + =^ mos ruf + =/ den ((de our now ski hen ruf) her desk) + abet:abet:(take-backfill:(foreign-update:den index) blob) + [mos ..^$] + == + :: ?: ?=([%sinks ~] tea) ?> ?=(%public-keys +<.q.hin) ?. ?=(%breach -.public-keys-result.q.hin) @@ -4396,7 +4643,9 @@ :+ desk %| :~ ankh+&+ank.dom.dojo mime+&+mim.dom.dojo - ford+&+fod.dom.dojo + ford-vases+&+vases.fod.dom.dojo + ford-marks+&+marks.fod.dom.dojo + ford-casts+&+casts.fod.dom.dojo == :~ domestic+|+domestic foreign+&+hoy.ruf diff --git a/pkg/arvo/sys/zuse.hoon b/pkg/arvo/sys/zuse.hoon index e1b3998a0..74a2f7990 100644 --- a/pkg/arvo/sys/zuse.hoon +++ b/pkg/arvo/sys/zuse.hoon @@ -861,7 +861,7 @@ $>(%trim vane-task) :: trim state $>(%vega vane-task) :: report upgrade {$warp wer/ship rif/riff} :: internal file req - {$werp who/ship wer/ship rif/riff} :: external file req + {$werp who/ship wer/ship rif/riff-any} :: external file req $>(%plea vane-task) :: ames request == :: -- ::able @@ -967,7 +967,10 @@ who/(pair (set ship) (map @ta crew)) :: == :: ++ regs (map path rule) :: rules for paths - ++ riff {p/desk q/(unit rave)} :: request+desist + +$ riff [p=desk q=(unit rave)] :: request+desist + +$ riff-any + $^ [[%1 ~] riff] + riff ++ rite :: new permissions $% {$r red/(unit rule)} :: for read {$w wit/(unit rule)} :: for write diff --git a/pkg/arvo/ted/ph/breach-multiple.hoon b/pkg/arvo/ted/ph/breach-multiple.hoon index 4c76bc8a6..d486f9d43 100644 --- a/pkg/arvo/ted/ph/breach-multiple.hoon +++ b/pkg/arvo/ted/ph/breach-multiple.hoon @@ -11,14 +11,14 @@ ;< ~ bind:m (spawn az ~marbud) ;< ~ bind:m (real-ship az ~bud) ;< ~ bind:m (real-ship az ~marbud) -;< file=@t bind:m (touch-file ~bud %base %foo) +;< file=@t bind:m (touch-file ~bud %kids %foo) ;< ~ bind:m (check-file-touched ~marbud %home file) ;< ~ bind:m (breach-and-hear az ~bud ~marbud) ;< ~ bind:m (real-ship az ~bud) ;< ~ bind:m (breach-and-hear az ~marbud ~bud) ;< ~ bind:m (real-ship az ~marbud) -;< file=@t bind:m (touch-file ~bud %base %bar) -;< file=@t bind:m (touch-file ~bud %base %baz) +;< file=@t bind:m (touch-file ~bud %kids %bar) +;< file=@t bind:m (touch-file ~bud %kids %baz) ;< ~ bind:m (check-file-touched ~marbud %home file) ;< ~ bind:m end-azimuth (pure:m *vase) diff --git a/pkg/arvo/ted/ph/breach-sudden.hoon b/pkg/arvo/ted/ph/breach-sudden.hoon index ae6af62dd..5491c84c4 100644 --- a/pkg/arvo/ted/ph/breach-sudden.hoon +++ b/pkg/arvo/ted/ph/breach-sudden.hoon @@ -13,13 +13,13 @@ ;< ~ bind:m (spawn az ~marbud) ;< ~ bind:m (real-ship az ~bud) ;< ~ bind:m (real-ship az ~marbud) -;< file=@t bind:m (touch-file ~bud %base %foo) +;< file=@t bind:m (touch-file ~bud %kids %foo) ;< ~ bind:m (check-file-touched ~marbud %home file) ;< ~ bind:m (breach az ~bud) ;< ~ bind:m (real-ship az ~bud) -;< ~ bind:m (dojo ~bud "|merge %base ~marbud %kids, =gem %this") -;< file=@t bind:m (touch-file ~bud %base %bar) -;< file=@t bind:m (touch-file ~bud %base %baz) +;< ~ bind:m (dojo ~bud "|merge %home ~marbud %kids, =gem %this") +;< file=@t bind:m (touch-file ~bud %kids %bar) +;< file=@t bind:m (touch-file ~bud %kids %baz) ;< ~ bind:m (check-file-touched ~marbud %home file) ;< ~ bind:m end-azimuth (pure:m *vase) diff --git a/pkg/arvo/ted/ph/breach-sync.hoon b/pkg/arvo/ted/ph/breach-sync.hoon index 833ececd1..a603c779f 100644 --- a/pkg/arvo/ted/ph/breach-sync.hoon +++ b/pkg/arvo/ted/ph/breach-sync.hoon @@ -13,6 +13,10 @@ ;< ~ bind:m (real-ship az ~marbud) ;< file=@t bind:m (touch-file ~bud %kids %foo) ;< ~ bind:m (check-file-touched ~marbud %home file) +:: Merge so that when we unify history with the %this merge later, we +:: don't get a spurious conflict in %home +:: +;< ~ bind:m (dojo ~marbud "|merge %kids our %home") ;< ~ bind:m (breach-and-hear az ~bud ~marbud) ;< ~ bind:m (real-ship az ~bud) ;< ~ bind:m (dojo ~bud "|merge %kids ~marbud %kids, =gem %this") diff --git a/pkg/arvo/ted/ph/child-sync.hoon b/pkg/arvo/ted/ph/child-sync.hoon index 291ce598e..381a67064 100644 --- a/pkg/arvo/ted/ph/child-sync.hoon +++ b/pkg/arvo/ted/ph/child-sync.hoon @@ -7,7 +7,8 @@ ;< ~ bind:m start-simple ;< ~ bind:m (raw-ship ~bud ~) ;< ~ bind:m (raw-ship ~marbud ~) -;< file=@t bind:m (touch-file ~bud %base %foo) +;< file=@t bind:m (touch-file ~bud %home %foo) +;< ~ bind:m (dojo ~bud "|merge %kids our %home") ;< ~ bind:m (check-file-touched ~marbud %home file) ;< ~ bind:m end-simple (pure:m *vase) diff --git a/pkg/arvo/ted/ph/child-update.hoon b/pkg/arvo/ted/ph/child-update.hoon index fc0878ad2..d505e74ea 100644 --- a/pkg/arvo/ted/ph/child-update.hoon +++ b/pkg/arvo/ted/ph/child-update.hoon @@ -29,15 +29,15 @@ %^ cat 3 (get-val /mar/js/hoon) ' ~& > new-val=new-val .' =/ js-contents - %^ cat 3 (get-val /app/publish/js/index/js) + %^ cat 3 (get-val /app/landscape/js/channel/js) 'extra' =/ files :~ [/sys/zuse/hoon zuse-contents] [/mar/js/hoon mar-contents] - [/app/publish/js/index/js js-contents] + [/app/landscape/js/channel/js js-contents] == ;< ~ bind:m (send-events (insert-files:util her desk files)) - (pure:m /app/publish/js/index/js js-contents) + (pure:m /app/landscape/js/channel/js js-contents) :: ++ aqua-path |= =path diff --git a/pkg/hs/natpmp-static/LICENSE b/pkg/hs/natpmp-static/LICENSE new file mode 100644 index 000000000..7fff2c26a --- /dev/null +++ b/pkg/hs/natpmp-static/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2007-2011, Thomas BERNARD +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + diff --git a/pkg/hs/natpmp-static/README.txt b/pkg/hs/natpmp-static/README.txt new file mode 100644 index 000000000..87e6e25e3 --- /dev/null +++ b/pkg/hs/natpmp-static/README.txt @@ -0,0 +1,5 @@ +This is a vendored copy of libnatpmp-20150609, along with haskell bindings to +the library. Only the C code which was needed for these bindings was copied out +of the distribution. + +Original code: http://miniupnp.free.fr/libnatpmp.html diff --git a/pkg/hs/natpmp-static/Setup.hs b/pkg/hs/natpmp-static/Setup.hs new file mode 100644 index 000000000..9a994af67 --- /dev/null +++ b/pkg/hs/natpmp-static/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/pkg/hs/natpmp-static/cbits/binding.c b/pkg/hs/natpmp-static/cbits/binding.c new file mode 100644 index 000000000..50f3d4f09 --- /dev/null +++ b/pkg/hs/natpmp-static/cbits/binding.c @@ -0,0 +1,77 @@ +/* $Id: natpmpc.c,v 1.13 2012/08/21 17:23:38 nanard Exp $ */ +/* libnatpmp +Copyright (c) 2007-2011, Thomas BERNARD +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include +#include + +#include +#include +#include "natpmp.h" + +// Additional binding code in C to make this more convenient to call from +// Haskell. libnatpmp expects that code which uses it to select() on an +// internal socket, which we don't want to expose to the Haskell bindings user. +// +// This is mostly an adaptation of the code in the demo natpmpc.c to use the +// select() loop. +int readNatResponseSynchronously(natpmp_t* natpmp, natpmpresp_t * response) +{ + fd_set fds; + struct timeval timeout; + int r; + int sav_errno; + + do { + FD_ZERO(&fds); + FD_SET(natpmp->s, &fds); + getnatpmprequesttimeout(natpmp, &timeout); + r = select(FD_SETSIZE, &fds, NULL, NULL, &timeout); + sav_errno = errno; + if(r<0) { + /* fprintf(stderr, "select(): errno=%d '%s'\n", */ + /* sav_errno, strerror(sav_errno)); */ + return 1; + } + r = readnatpmpresponseorretry(natpmp, response); + sav_errno = errno; + /* printf("readnatpmpresponseorretry returned %d (%s)\n", */ + /* r, r==0?"OK":(r==NATPMP_TRYAGAIN?"TRY AGAIN":"FAILED")); */ +/* if(r<0 && r!=NATPMP_TRYAGAIN) { */ +/* #ifdef ENABLE_STRNATPMPERR */ +/* fprintf(stderr, "readnatpmpresponseorretry() failed : %s\n", */ +/* strnatpmperr(r)); */ +/* #endif */ +/* fprintf(stderr, " errno=%d '%s'\n", */ +/* sav_errno, strerror(sav_errno)); */ +/* } */ + } while(r==NATPMP_TRYAGAIN); + + return r; +} diff --git a/pkg/hs/natpmp-static/cbits/binding.h b/pkg/hs/natpmp-static/cbits/binding.h new file mode 100644 index 000000000..f781c8a20 --- /dev/null +++ b/pkg/hs/natpmp-static/cbits/binding.h @@ -0,0 +1,8 @@ +#ifndef __NATPMP_BINDING_H__ +#define __NATPMP_BINDING_H__ + +#include "natpmp.h" + +int readNatResponseSynchronously(natpmp_t* natpmp, natpmpresp_t * response); + +#endif diff --git a/pkg/hs/natpmp-static/cbits/getgateway.c b/pkg/hs/natpmp-static/cbits/getgateway.c new file mode 100644 index 000000000..dfb9f3e21 --- /dev/null +++ b/pkg/hs/natpmp-static/cbits/getgateway.c @@ -0,0 +1,573 @@ +/* $Id: getgateway.c,v 1.25 2014/04/22 10:28:57 nanard Exp $ */ +/* libnatpmp + +Copyright (c) 2007-2014, Thomas BERNARD +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ +#include +#include +#ifndef WIN32 +#include +#endif +#if !defined(_MSC_VER) +#include +#endif +/* There is no portable method to get the default route gateway. + * So below are four (or five ?) differents functions implementing this. + * Parsing /proc/net/route is for linux. + * sysctl is the way to access such informations on BSD systems. + * Many systems should provide route information through raw PF_ROUTE + * sockets. + * In MS Windows, default gateway is found by looking into the registry + * or by using GetBestRoute(). */ +#ifdef __linux__ +#define USE_PROC_NET_ROUTE +#undef USE_SOCKET_ROUTE +#undef USE_SYSCTL_NET_ROUTE +#endif + +#if defined(BSD) || defined(__FreeBSD_kernel__) +#undef USE_PROC_NET_ROUTE +#define USE_SOCKET_ROUTE +#undef USE_SYSCTL_NET_ROUTE +#endif + +#ifdef __APPLE__ +#undef USE_PROC_NET_ROUTE +#undef USE_SOCKET_ROUTE +#define USE_SYSCTL_NET_ROUTE +#endif + +#if (defined(sun) && defined(__SVR4)) +#undef USE_PROC_NET_ROUTE +#define USE_SOCKET_ROUTE +#undef USE_SYSCTL_NET_ROUTE +#endif + +#ifdef WIN32 +#undef USE_PROC_NET_ROUTE +#undef USE_SOCKET_ROUTE +#undef USE_SYSCTL_NET_ROUTE +//#define USE_WIN32_CODE +#define USE_WIN32_CODE_2 +#endif + +#ifdef __CYGWIN__ +#undef USE_PROC_NET_ROUTE +#undef USE_SOCKET_ROUTE +#undef USE_SYSCTL_NET_ROUTE +#define USE_WIN32_CODE +#include +#include +#include +#include +#endif + +#ifdef __HAIKU__ +#include +#include +#include +#include +#define USE_HAIKU_CODE +#endif + +#ifdef USE_SYSCTL_NET_ROUTE +#include +#include +#include +#include +#endif +#ifdef USE_SOCKET_ROUTE +#include +#include +#include +#include +#include +#endif + +#ifdef USE_WIN32_CODE +#include +#include +#define MAX_KEY_LENGTH 255 +#define MAX_VALUE_LENGTH 16383 +#endif + +#ifdef USE_WIN32_CODE_2 +#include +#include +#endif + +#include "getgateway.h" + +#ifndef WIN32 +#define SUCCESS (0) +#define FAILED (-1) +#endif + +#ifdef USE_PROC_NET_ROUTE +/* + parse /proc/net/route which is as follow : + +Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT +wlan0 0001A8C0 00000000 0001 0 0 0 00FFFFFF 0 0 0 +eth0 0000FEA9 00000000 0001 0 0 0 0000FFFF 0 0 0 +wlan0 00000000 0101A8C0 0003 0 0 0 00000000 0 0 0 +eth0 00000000 00000000 0001 0 0 1000 00000000 0 0 0 + + One header line, and then one line by route by route table entry. +*/ +int getdefaultgateway(in_addr_t * addr) +{ + unsigned long d, g; + char buf[256]; + int line = 0; + FILE * f; + char * p; + f = fopen("/proc/net/route", "r"); + if(!f) + return FAILED; + while(fgets(buf, sizeof(buf), f)) { + if(line > 0) { /* skip the first line */ + p = buf; + /* skip the interface name */ + while(*p && !isspace(*p)) + p++; + while(*p && isspace(*p)) + p++; + if(sscanf(p, "%lx%lx", &d, &g)==2) { + if(d == 0 && g != 0) { /* default */ + *addr = g; + fclose(f); + return SUCCESS; + } + } + } + line++; + } + /* default route not found ! */ + if(f) + fclose(f); + return FAILED; +} +#endif /* #ifdef USE_PROC_NET_ROUTE */ + + +#ifdef USE_SYSCTL_NET_ROUTE + +#define ROUNDUP(a) \ + ((a) > 0 ? (1 + (((a) - 1) | (sizeof(long) - 1))) : sizeof(long)) + +int getdefaultgateway(in_addr_t * addr) +{ +#if 0 + /* net.route.0.inet.dump.0.0 ? */ + int mib[] = {CTL_NET, PF_ROUTE, 0, AF_INET, + NET_RT_DUMP, 0, 0/*tableid*/}; +#endif + /* net.route.0.inet.flags.gateway */ + int mib[] = {CTL_NET, PF_ROUTE, 0, AF_INET, + NET_RT_FLAGS, RTF_GATEWAY}; + size_t l; + char * buf, * p; + struct rt_msghdr * rt; + struct sockaddr * sa; + struct sockaddr * sa_tab[RTAX_MAX]; + int i; + int r = FAILED; + if(sysctl(mib, sizeof(mib)/sizeof(int), 0, &l, 0, 0) < 0) { + return FAILED; + } + if(l>0) { + buf = malloc(l); + if(sysctl(mib, sizeof(mib)/sizeof(int), buf, &l, 0, 0) < 0) { + free(buf); + return FAILED; + } + for(p=buf; prtm_msglen) { + rt = (struct rt_msghdr *)p; + sa = (struct sockaddr *)(rt + 1); + for(i=0; irtm_addrs & (1 << i)) { + sa_tab[i] = sa; + sa = (struct sockaddr *)((char *)sa + ROUNDUP(sa->sa_len)); + } else { + sa_tab[i] = NULL; + } + } + if( ((rt->rtm_addrs & (RTA_DST|RTA_GATEWAY)) == (RTA_DST|RTA_GATEWAY)) + && sa_tab[RTAX_DST]->sa_family == AF_INET + && sa_tab[RTAX_GATEWAY]->sa_family == AF_INET) { + if(((struct sockaddr_in *)sa_tab[RTAX_DST])->sin_addr.s_addr == 0) { + *addr = ((struct sockaddr_in *)(sa_tab[RTAX_GATEWAY]))->sin_addr.s_addr; + r = SUCCESS; + } + } + } + free(buf); + } + return r; +} +#endif /* #ifdef USE_SYSCTL_NET_ROUTE */ + + +#ifdef USE_SOCKET_ROUTE +/* Thanks to Darren Kenny for this code */ +#define NEXTADDR(w, u) \ + if (rtm_addrs & (w)) {\ + l = sizeof(struct sockaddr); memmove(cp, &(u), l); cp += l;\ + } + +#define rtm m_rtmsg.m_rtm + +struct { + struct rt_msghdr m_rtm; + char m_space[512]; +} m_rtmsg; + +int getdefaultgateway(in_addr_t *addr) +{ + int s, seq, l, rtm_addrs, i; + pid_t pid; + struct sockaddr so_dst, so_mask; + char *cp = m_rtmsg.m_space; + struct sockaddr *gate = NULL, *sa; + struct rt_msghdr *msg_hdr; + + pid = getpid(); + seq = 0; + rtm_addrs = RTA_DST | RTA_NETMASK; + + memset(&so_dst, 0, sizeof(so_dst)); + memset(&so_mask, 0, sizeof(so_mask)); + memset(&rtm, 0, sizeof(struct rt_msghdr)); + + rtm.rtm_type = RTM_GET; + rtm.rtm_flags = RTF_UP | RTF_GATEWAY; + rtm.rtm_version = RTM_VERSION; + rtm.rtm_seq = ++seq; + rtm.rtm_addrs = rtm_addrs; + + so_dst.sa_family = AF_INET; + so_mask.sa_family = AF_INET; + + NEXTADDR(RTA_DST, so_dst); + NEXTADDR(RTA_NETMASK, so_mask); + + rtm.rtm_msglen = l = cp - (char *)&m_rtmsg; + + s = socket(PF_ROUTE, SOCK_RAW, 0); + + if (write(s, (char *)&m_rtmsg, l) < 0) { + close(s); + return FAILED; + } + + do { + l = read(s, (char *)&m_rtmsg, sizeof(m_rtmsg)); + } while (l > 0 && (rtm.rtm_seq != seq || rtm.rtm_pid != pid)); + + close(s); + + msg_hdr = &rtm; + + cp = ((char *)(msg_hdr + 1)); + if (msg_hdr->rtm_addrs) { + for (i = 1; i; i <<= 1) + if (i & msg_hdr->rtm_addrs) { + sa = (struct sockaddr *)cp; + if (i == RTA_GATEWAY ) + gate = sa; + + cp += sizeof(struct sockaddr); + } + } else { + return FAILED; + } + + + if (gate != NULL ) { + *addr = ((struct sockaddr_in *)gate)->sin_addr.s_addr; + return SUCCESS; + } else { + return FAILED; + } +} +#endif /* #ifdef USE_SOCKET_ROUTE */ + +#ifdef USE_WIN32_CODE +LIBSPEC int getdefaultgateway(in_addr_t * addr) +{ + HKEY networkCardsKey; + HKEY networkCardKey; + HKEY interfacesKey; + HKEY interfaceKey; + DWORD i = 0; + DWORD numSubKeys = 0; + TCHAR keyName[MAX_KEY_LENGTH]; + DWORD keyNameLength = MAX_KEY_LENGTH; + TCHAR keyValue[MAX_VALUE_LENGTH]; + DWORD keyValueLength = MAX_VALUE_LENGTH; + DWORD keyValueType = REG_SZ; + TCHAR gatewayValue[MAX_VALUE_LENGTH]; + DWORD gatewayValueLength = MAX_VALUE_LENGTH; + DWORD gatewayValueType = REG_MULTI_SZ; + int done = 0; + + //const char * networkCardsPath = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkCards"; + //const char * interfacesPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces"; +#ifdef UNICODE + LPCTSTR networkCardsPath = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkCards"; + LPCTSTR interfacesPath = L"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces"; +#define STR_SERVICENAME L"ServiceName" +#define STR_DHCPDEFAULTGATEWAY L"DhcpDefaultGateway" +#define STR_DEFAULTGATEWAY L"DefaultGateway" +#else + LPCTSTR networkCardsPath = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkCards"; + LPCTSTR interfacesPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces"; +#define STR_SERVICENAME "ServiceName" +#define STR_DHCPDEFAULTGATEWAY "DhcpDefaultGateway" +#define STR_DEFAULTGATEWAY "DefaultGateway" +#endif + // The windows registry lists its primary network devices in the following location: + // HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkCards + // + // Each network device has its own subfolder, named with an index, with various properties: + // -NetworkCards + // -5 + // -Description = Broadcom 802.11n Network Adapter + // -ServiceName = {E35A72F8-5065-4097-8DFE-C7790774EE4D} + // -8 + // -Description = Marvell Yukon 88E8058 PCI-E Gigabit Ethernet Controller + // -ServiceName = {86226414-5545-4335-A9D1-5BD7120119AD} + // + // The above service name is the name of a subfolder within: + // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces + // + // There may be more subfolders in this interfaces path than listed in the network cards path above: + // -Interfaces + // -{3a539854-6a70-11db-887c-806e6f6e6963} + // -DhcpIPAddress = 0.0.0.0 + // -[more] + // -{E35A72F8-5065-4097-8DFE-C7790774EE4D} + // -DhcpIPAddress = 10.0.1.4 + // -DhcpDefaultGateway = 10.0.1.1 + // -[more] + // -{86226414-5545-4335-A9D1-5BD7120119AD} + // -DhcpIpAddress = 10.0.1.5 + // -DhcpDefaultGateay = 10.0.1.1 + // -[more] + // + // In order to extract this information, we enumerate each network card, and extract the ServiceName value. + // This is then used to open the interface subfolder, and attempt to extract a DhcpDefaultGateway value. + // Once one is found, we're done. + // + // It may be possible to simply enumerate the interface folders until we find one with a DhcpDefaultGateway value. + // However, the technique used is the technique most cited on the web, and we assume it to be more correct. + + if(ERROR_SUCCESS != RegOpenKeyEx(HKEY_LOCAL_MACHINE, // Open registry key or predifined key + networkCardsPath, // Name of registry subkey to open + 0, // Reserved - must be zero + KEY_READ, // Mask - desired access rights + &networkCardsKey)) // Pointer to output key + { + // Unable to open network cards keys + return -1; + } + + if(ERROR_SUCCESS != RegOpenKeyEx(HKEY_LOCAL_MACHINE, // Open registry key or predefined key + interfacesPath, // Name of registry subkey to open + 0, // Reserved - must be zero + KEY_READ, // Mask - desired access rights + &interfacesKey)) // Pointer to output key + { + // Unable to open interfaces key + RegCloseKey(networkCardsKey); + return -1; + } + + // Figure out how many subfolders are within the NetworkCards folder + RegQueryInfoKey(networkCardsKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + + //printf( "Number of subkeys: %u\n", (unsigned int)numSubKeys); + + // Enumrate through each subfolder within the NetworkCards folder + for(i = 0; i < numSubKeys && !done; i++) + { + keyNameLength = MAX_KEY_LENGTH; + if(ERROR_SUCCESS == RegEnumKeyEx(networkCardsKey, // Open registry key + i, // Index of subkey to retrieve + keyName, // Buffer that receives the name of the subkey + &keyNameLength, // Variable that receives the size of the above buffer + NULL, // Reserved - must be NULL + NULL, // Buffer that receives the class string + NULL, // Variable that receives the size of the above buffer + NULL)) // Variable that receives the last write time of subkey + { + if(RegOpenKeyEx(networkCardsKey, keyName, 0, KEY_READ, &networkCardKey) == ERROR_SUCCESS) + { + keyValueLength = MAX_VALUE_LENGTH; + if(ERROR_SUCCESS == RegQueryValueEx(networkCardKey, // Open registry key + STR_SERVICENAME, // Name of key to query + NULL, // Reserved - must be NULL + &keyValueType, // Receives value type + (LPBYTE)keyValue, // Receives value + &keyValueLength)) // Receives value length in bytes + { +// printf("keyValue: %s\n", keyValue); + if(RegOpenKeyEx(interfacesKey, keyValue, 0, KEY_READ, &interfaceKey) == ERROR_SUCCESS) + { + gatewayValueLength = MAX_VALUE_LENGTH; + if(ERROR_SUCCESS == RegQueryValueEx(interfaceKey, // Open registry key + STR_DHCPDEFAULTGATEWAY, // Name of key to query + NULL, // Reserved - must be NULL + &gatewayValueType, // Receives value type + (LPBYTE)gatewayValue, // Receives value + &gatewayValueLength)) // Receives value length in bytes + { + // Check to make sure it's a string + if((gatewayValueType == REG_MULTI_SZ || gatewayValueType == REG_SZ) && (gatewayValueLength > 1)) + { + //printf("gatewayValue: %s\n", gatewayValue); + done = 1; + } + } + else if(ERROR_SUCCESS == RegQueryValueEx(interfaceKey, // Open registry key + STR_DEFAULTGATEWAY, // Name of key to query + NULL, // Reserved - must be NULL + &gatewayValueType, // Receives value type + (LPBYTE)gatewayValue,// Receives value + &gatewayValueLength)) // Receives value length in bytes + { + // Check to make sure it's a string + if((gatewayValueType == REG_MULTI_SZ || gatewayValueType == REG_SZ) && (gatewayValueLength > 1)) + { + //printf("gatewayValue: %s\n", gatewayValue); + done = 1; + } + } + RegCloseKey(interfaceKey); + } + } + RegCloseKey(networkCardKey); + } + } + } + + RegCloseKey(interfacesKey); + RegCloseKey(networkCardsKey); + + if(done) + { +#if UNICODE + char tmp[32]; + for(i = 0; i < 32; i++) { + tmp[i] = (char)gatewayValue[i]; + if(!tmp[i]) + break; + } + tmp[31] = '\0'; + *addr = inet_addr(tmp); +#else + *addr = inet_addr(gatewayValue); +#endif + return 0; + } + + return -1; +} +#endif /* #ifdef USE_WIN32_CODE */ + +#ifdef USE_WIN32_CODE_2 +int getdefaultgateway(in_addr_t *addr) +{ + MIB_IPFORWARDROW ip_forward; + memset(&ip_forward, 0, sizeof(ip_forward)); + if(GetBestRoute(inet_addr("0.0.0.0"), 0, &ip_forward) != NO_ERROR) + return -1; + *addr = ip_forward.dwForwardNextHop; + return 0; +} +#endif /* #ifdef USE_WIN32_CODE_2 */ + +#ifdef USE_HAIKU_CODE +int getdefaultgateway(in_addr_t *addr) +{ + int fd, ret = -1; + struct ifconf config; + void *buffer = NULL; + struct ifreq *interface; + + if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { + return -1; + } + if (ioctl(fd, SIOCGRTSIZE, &config, sizeof(config)) != 0) { + goto fail; + } + if (config.ifc_value < 1) { + goto fail; /* No routes */ + } + if ((buffer = malloc(config.ifc_value)) == NULL) { + goto fail; + } + config.ifc_len = config.ifc_value; + config.ifc_buf = buffer; + if (ioctl(fd, SIOCGRTTABLE, &config, sizeof(config)) != 0) { + goto fail; + } + for (interface = buffer; + (uint8_t *)interface < (uint8_t *)buffer + config.ifc_len; ) { + struct route_entry route = interface->ifr_route; + int intfSize; + if (route.flags & (RTF_GATEWAY | RTF_DEFAULT)) { + *addr = ((struct sockaddr_in *)route.gateway)->sin_addr.s_addr; + ret = 0; + break; + } + intfSize = sizeof(route) + IF_NAMESIZE; + if (route.destination != NULL) { + intfSize += route.destination->sa_len; + } + if (route.mask != NULL) { + intfSize += route.mask->sa_len; + } + if (route.gateway != NULL) { + intfSize += route.gateway->sa_len; + } + interface = (struct ifreq *)((uint8_t *)interface + intfSize); + } +fail: + free(buffer); + close(fd); + return ret; +} +#endif /* #ifdef USE_HAIKU_CODE */ + +#if !defined(USE_PROC_NET_ROUTE) && !defined(USE_SOCKET_ROUTE) && !defined(USE_SYSCTL_NET_ROUTE) && !defined(USE_WIN32_CODE) && !defined(USE_WIN32_CODE_2) && !defined(USE_HAIKU_CODE) +int getdefaultgateway(in_addr_t * addr) +{ + return -1; +} +#endif diff --git a/pkg/hs/natpmp-static/cbits/getgateway.h b/pkg/hs/natpmp-static/cbits/getgateway.h new file mode 100644 index 000000000..5d3df7312 --- /dev/null +++ b/pkg/hs/natpmp-static/cbits/getgateway.h @@ -0,0 +1,49 @@ +/* $Id: getgateway.h,v 1.8 2014/04/22 09:15:40 nanard Exp $ */ +/* libnatpmp +Copyright (c) 2007-2014, Thomas BERNARD +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ +#ifndef __GETGATEWAY_H__ +#define __GETGATEWAY_H__ + +#ifdef WIN32 +#if !defined(_MSC_VER) || _MSC_VER >= 1600 +#include +#else +typedef unsigned long uint32_t; +typedef unsigned short uint16_t; +#endif +#define in_addr_t uint32_t +#endif +/* #include "declspec.h" */ + +/* getdefaultgateway() : + * return value : + * 0 : success + * -1 : failure */ +/* LIBSPEC */int getdefaultgateway(in_addr_t * addr); + +#endif diff --git a/pkg/hs/natpmp-static/cbits/natpmp.c b/pkg/hs/natpmp-static/cbits/natpmp.c new file mode 100644 index 000000000..289c36445 --- /dev/null +++ b/pkg/hs/natpmp-static/cbits/natpmp.c @@ -0,0 +1,387 @@ +/* $Id: natpmp.c,v 1.20 2015/05/27 12:43:15 nanard Exp $ */ +/* libnatpmp +Copyright (c) 2007-2015, Thomas BERNARD +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ +#ifdef __linux__ +#define _BSD_SOURCE 1 +#endif +#include +#include +#if !defined(_MSC_VER) +#include +#endif +#ifdef WIN32 +#include +#include +#include +#include +#define EWOULDBLOCK WSAEWOULDBLOCK +#define ECONNREFUSED WSAECONNREFUSED +#include "wingettimeofday.h" +#define gettimeofday natpmp_gettimeofday +#else +#include +#include +#include +#include +#include +#define closesocket close +#endif +#include "natpmp.h" +#include "getgateway.h" +#include + +LIBSPEC int initnatpmp(natpmp_t * p, int forcegw, in_addr_t forcedgw) +{ +#ifdef WIN32 + u_long ioctlArg = 1; +#else + int flags; +#endif + struct sockaddr_in addr; + if(!p) + return NATPMP_ERR_INVALIDARGS; + memset(p, 0, sizeof(natpmp_t)); + p->s = socket(PF_INET, SOCK_DGRAM, 0); + if(p->s < 0) + return NATPMP_ERR_SOCKETERROR; +#ifdef WIN32 + if(ioctlsocket(p->s, FIONBIO, &ioctlArg) == SOCKET_ERROR) + return NATPMP_ERR_FCNTLERROR; +#else + if((flags = fcntl(p->s, F_GETFL, 0)) < 0) + return NATPMP_ERR_FCNTLERROR; + if(fcntl(p->s, F_SETFL, flags | O_NONBLOCK) < 0) + return NATPMP_ERR_FCNTLERROR; +#endif + + if(forcegw) { + p->gateway = forcedgw; + } else { + if(getdefaultgateway(&(p->gateway)) < 0) + return NATPMP_ERR_CANNOTGETGATEWAY; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(NATPMP_PORT); + addr.sin_addr.s_addr = p->gateway; + if(connect(p->s, (struct sockaddr *)&addr, sizeof(addr)) < 0) + return NATPMP_ERR_CONNECTERR; + return 0; +} + +LIBSPEC int closenatpmp(natpmp_t * p) +{ + if(!p) + return NATPMP_ERR_INVALIDARGS; + if(closesocket(p->s) < 0) + return NATPMP_ERR_CLOSEERR; + return 0; +} + +int sendpendingrequest(natpmp_t * p) +{ + int r; +/* struct sockaddr_in addr;*/ + if(!p) + return NATPMP_ERR_INVALIDARGS; +/* memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(NATPMP_PORT); + addr.sin_addr.s_addr = p->gateway; + r = (int)sendto(p->s, p->pending_request, p->pending_request_len, 0, + (struct sockaddr *)&addr, sizeof(addr));*/ + r = (int)send(p->s, (const char *)p->pending_request, p->pending_request_len, 0); + return (r<0) ? NATPMP_ERR_SENDERR : r; +} + +int sendnatpmprequest(natpmp_t * p) +{ + int n; + if(!p) + return NATPMP_ERR_INVALIDARGS; + /* TODO : check if no request is already pending */ + p->has_pending_request = 1; + p->try_number = 1; + n = sendpendingrequest(p); + gettimeofday(&p->retry_time, NULL); // check errors ! + p->retry_time.tv_usec += 250000; /* add 250ms */ + if(p->retry_time.tv_usec >= 1000000) { + p->retry_time.tv_usec -= 1000000; + p->retry_time.tv_sec++; + } + return n; +} + +LIBSPEC int getnatpmprequesttimeout(natpmp_t * p, struct timeval * timeout) +{ + struct timeval now; + if(!p || !timeout) + return NATPMP_ERR_INVALIDARGS; + if(!p->has_pending_request) + return NATPMP_ERR_NOPENDINGREQ; + if(gettimeofday(&now, NULL) < 0) + return NATPMP_ERR_GETTIMEOFDAYERR; + timeout->tv_sec = p->retry_time.tv_sec - now.tv_sec; + timeout->tv_usec = p->retry_time.tv_usec - now.tv_usec; + if(timeout->tv_usec < 0) { + timeout->tv_usec += 1000000; + timeout->tv_sec--; + } + return 0; +} + +LIBSPEC int sendpublicaddressrequest(natpmp_t * p) +{ + if(!p) + return NATPMP_ERR_INVALIDARGS; + //static const unsigned char request[] = { 0, 0 }; + p->pending_request[0] = 0; + p->pending_request[1] = 0; + p->pending_request_len = 2; + // TODO: return 0 instead of sizeof(request) ?? + return sendnatpmprequest(p); +} + +LIBSPEC int sendnewportmappingrequest(natpmp_t * p, int protocol, + uint16_t privateport, uint16_t publicport, + uint32_t lifetime) +{ + if(!p || (protocol!=NATPMP_PROTOCOL_TCP && protocol!=NATPMP_PROTOCOL_UDP)) + return NATPMP_ERR_INVALIDARGS; + p->pending_request[0] = 0; + p->pending_request[1] = protocol; + p->pending_request[2] = 0; + p->pending_request[3] = 0; + /* break strict-aliasing rules : + *((uint16_t *)(p->pending_request + 4)) = htons(privateport); */ + p->pending_request[4] = (privateport >> 8) & 0xff; + p->pending_request[5] = privateport & 0xff; + /* break stric-aliasing rules : + *((uint16_t *)(p->pending_request + 6)) = htons(publicport); */ + p->pending_request[6] = (publicport >> 8) & 0xff; + p->pending_request[7] = publicport & 0xff; + /* break stric-aliasing rules : + *((uint32_t *)(p->pending_request + 8)) = htonl(lifetime); */ + p->pending_request[8] = (lifetime >> 24) & 0xff; + p->pending_request[9] = (lifetime >> 16) & 0xff; + p->pending_request[10] = (lifetime >> 8) & 0xff; + p->pending_request[11] = lifetime & 0xff; + p->pending_request_len = 12; + return sendnatpmprequest(p); +} + +LIBSPEC int readnatpmpresponse(natpmp_t * p, natpmpresp_t * response) +{ + unsigned char buf[16]; + struct sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + int n; + if(!p) + return NATPMP_ERR_INVALIDARGS; + n = recvfrom(p->s, (char *)buf, sizeof(buf), 0, + (struct sockaddr *)&addr, &addrlen); + if(n<0) +#ifdef WIN32 + switch(WSAGetLastError()) { +#else + switch(errno) { +#endif + /*case EAGAIN:*/ + case EWOULDBLOCK: + n = NATPMP_TRYAGAIN; + break; + case ECONNREFUSED: + n = NATPMP_ERR_NOGATEWAYSUPPORT; + break; + default: + n = NATPMP_ERR_RECVFROM; + } + /* check that addr is correct (= gateway) */ + else if(addr.sin_addr.s_addr != p->gateway) + n = NATPMP_ERR_WRONGPACKETSOURCE; + else { + response->resultcode = ntohs(*((uint16_t *)(buf + 2))); + response->epoch = ntohl(*((uint32_t *)(buf + 4))); + if(buf[0] != 0) + n = NATPMP_ERR_UNSUPPORTEDVERSION; + else if(buf[1] < 128 || buf[1] > 130) + n = NATPMP_ERR_UNSUPPORTEDOPCODE; + else if(response->resultcode != 0) { + switch(response->resultcode) { + case 1: + n = NATPMP_ERR_UNSUPPORTEDVERSION; + break; + case 2: + n = NATPMP_ERR_NOTAUTHORIZED; + break; + case 3: + n = NATPMP_ERR_NETWORKFAILURE; + break; + case 4: + n = NATPMP_ERR_OUTOFRESOURCES; + break; + case 5: + n = NATPMP_ERR_UNSUPPORTEDOPCODE; + break; + default: + n = NATPMP_ERR_UNDEFINEDERROR; + } + } else { + response->type = buf[1] & 0x7f; + if(buf[1] == 128) + //response->publicaddress.addr = *((uint32_t *)(buf + 8)); + response->pnu.publicaddress.addr.s_addr = *((uint32_t *)(buf + 8)); + else { + response->pnu.newportmapping.privateport = ntohs(*((uint16_t *)(buf + 8))); + response->pnu.newportmapping.mappedpublicport = ntohs(*((uint16_t *)(buf + 10))); + response->pnu.newportmapping.lifetime = ntohl(*((uint32_t *)(buf + 12))); + } + n = 0; + } + } + return n; +} + +int readnatpmpresponseorretry(natpmp_t * p, natpmpresp_t * response) +{ + int n; + if(!p || !response) + return NATPMP_ERR_INVALIDARGS; + if(!p->has_pending_request) + return NATPMP_ERR_NOPENDINGREQ; + n = readnatpmpresponse(p, response); + if(n<0) { + if(n==NATPMP_TRYAGAIN) { + struct timeval now; + gettimeofday(&now, NULL); // check errors ! + if(timercmp(&now, &p->retry_time, >=)) { + int delay, r; + // NOTE: This used to be 9, and was changed for the haskell + // bindings to be 5. + if(p->try_number >= 5) { + return NATPMP_ERR_NOGATEWAYSUPPORT; + } + /*printf("retry! %d\n", p->try_number);*/ + + // NOTE: Changed how delays are calculated. Waiting up to four + // minutes for a packet that might never get a response is not + // a good user experience. Instead, retry up to 2 seconds. + // + // delay = 250 * (1<try_number); // ms + delay = 250 * p->try_number; // ms + /*for(i=0; itry_number; i++) + delay += delay;*/ + p->retry_time.tv_sec += (delay / 1000); + p->retry_time.tv_usec += (delay % 1000) * 1000; + if(p->retry_time.tv_usec >= 1000000) { + p->retry_time.tv_usec -= 1000000; + p->retry_time.tv_sec++; + } + p->try_number++; + r = sendpendingrequest(p); + if(r<0) + return r; + } + } + } else { + p->has_pending_request = 0; + } + return n; +} + +#ifdef ENABLE_STRNATPMPERR +LIBSPEC const char * strnatpmperr(int r) +{ + const char * s; + switch(r) { + case NATPMP_ERR_INVALIDARGS: + s = "invalid arguments"; + break; + case NATPMP_ERR_SOCKETERROR: + s = "socket() failed"; + break; + case NATPMP_ERR_CANNOTGETGATEWAY: + s = "cannot get default gateway ip address"; + break; + case NATPMP_ERR_CLOSEERR: +#ifdef WIN32 + s = "closesocket() failed"; +#else + s = "close() failed"; +#endif + break; + case NATPMP_ERR_RECVFROM: + s = "recvfrom() failed"; + break; + case NATPMP_ERR_NOPENDINGREQ: + s = "no pending request"; + break; + case NATPMP_ERR_NOGATEWAYSUPPORT: + s = "the gateway does not support nat-pmp"; + break; + case NATPMP_ERR_CONNECTERR: + s = "connect() failed"; + break; + case NATPMP_ERR_WRONGPACKETSOURCE: + s = "packet not received from the default gateway"; + break; + case NATPMP_ERR_SENDERR: + s = "send() failed"; + break; + case NATPMP_ERR_FCNTLERROR: + s = "fcntl() failed"; + break; + case NATPMP_ERR_GETTIMEOFDAYERR: + s = "gettimeofday() failed"; + break; + case NATPMP_ERR_UNSUPPORTEDVERSION: + s = "unsupported nat-pmp version error from server"; + break; + case NATPMP_ERR_UNSUPPORTEDOPCODE: + s = "unsupported nat-pmp opcode error from server"; + break; + case NATPMP_ERR_UNDEFINEDERROR: + s = "undefined nat-pmp server error"; + break; + case NATPMP_ERR_NOTAUTHORIZED: + s = "not authorized"; + break; + case NATPMP_ERR_NETWORKFAILURE: + s = "network failure"; + break; + case NATPMP_ERR_OUTOFRESOURCES: + s = "nat-pmp server out of resources"; + break; + default: + s = "Unknown libnatpmp error"; + } + return s; +} +#endif + diff --git a/pkg/hs/natpmp-static/cbits/natpmp.h b/pkg/hs/natpmp-static/cbits/natpmp.h new file mode 100644 index 000000000..3f721b7b9 --- /dev/null +++ b/pkg/hs/natpmp-static/cbits/natpmp.h @@ -0,0 +1,221 @@ +/* $Id: natpmp.h,v 1.20 2014/04/22 09:15:40 nanard Exp $ */ +/* libnatpmp +Copyright (c) 2007-2014, Thomas BERNARD +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ +#ifndef __NATPMP_H__ +#define __NATPMP_H__ + +/* NAT-PMP Port as defined by the NAT-PMP draft */ +#define NATPMP_PORT (5351) + +#define ENABLE_STRNATPMPERR + +#include +#if !defined(_MSC_VER) +#include +#endif /* !defined(_MSC_VER) */ + +#ifdef WIN32 +#include +#if !defined(_MSC_VER) || _MSC_VER >= 1600 +#include +#else /* !defined(_MSC_VER) || _MSC_VER >= 1600 */ +typedef unsigned long uint32_t; +typedef unsigned short uint16_t; +#endif /* !defined(_MSC_VER) || _MSC_VER >= 1600 */ +#define in_addr_t uint32_t +#include "declspec.h" +#else /* WIN32 */ +#define LIBSPEC +#include +#endif /* WIN32 */ + +/* causes problem when installing. Maybe should it be inlined ? */ +/* #include "declspec.h" */ + +typedef struct { + int s; /* socket */ + in_addr_t gateway; /* default gateway (IPv4) */ + int has_pending_request; + unsigned char pending_request[12]; + int pending_request_len; + int try_number; + struct timeval retry_time; +} natpmp_t; + +typedef struct { + uint16_t type; /* NATPMP_RESPTYPE_* */ + uint16_t resultcode; /* NAT-PMP response code */ + uint32_t epoch; /* Seconds since start of epoch */ + union { + struct { + //in_addr_t addr; + struct in_addr addr; + } publicaddress; + struct { + uint16_t privateport; + uint16_t mappedpublicport; + uint32_t lifetime; + } newportmapping; + } pnu; +} natpmpresp_t; + +/* possible values for type field of natpmpresp_t */ +#define NATPMP_RESPTYPE_PUBLICADDRESS (0) +#define NATPMP_RESPTYPE_UDPPORTMAPPING (1) +#define NATPMP_RESPTYPE_TCPPORTMAPPING (2) + +/* Values to pass to sendnewportmappingrequest() */ +#define NATPMP_PROTOCOL_UDP (1) +#define NATPMP_PROTOCOL_TCP (2) + +/* return values */ +/* NATPMP_ERR_INVALIDARGS : invalid arguments passed to the function */ +#define NATPMP_ERR_INVALIDARGS (-1) +/* NATPMP_ERR_SOCKETERROR : socket() failed. check errno for details */ +#define NATPMP_ERR_SOCKETERROR (-2) +/* NATPMP_ERR_CANNOTGETGATEWAY : can't get default gateway IP */ +#define NATPMP_ERR_CANNOTGETGATEWAY (-3) +/* NATPMP_ERR_CLOSEERR : close() failed. check errno for details */ +#define NATPMP_ERR_CLOSEERR (-4) +/* NATPMP_ERR_RECVFROM : recvfrom() failed. check errno for details */ +#define NATPMP_ERR_RECVFROM (-5) +/* NATPMP_ERR_NOPENDINGREQ : readnatpmpresponseorretry() called while + * no NAT-PMP request was pending */ +#define NATPMP_ERR_NOPENDINGREQ (-6) +/* NATPMP_ERR_NOGATEWAYSUPPORT : the gateway does not support NAT-PMP */ +#define NATPMP_ERR_NOGATEWAYSUPPORT (-7) +/* NATPMP_ERR_CONNECTERR : connect() failed. check errno for details */ +#define NATPMP_ERR_CONNECTERR (-8) +/* NATPMP_ERR_WRONGPACKETSOURCE : packet not received from the network gateway */ +#define NATPMP_ERR_WRONGPACKETSOURCE (-9) +/* NATPMP_ERR_SENDERR : send() failed. check errno for details */ +#define NATPMP_ERR_SENDERR (-10) +/* NATPMP_ERR_FCNTLERROR : fcntl() failed. check errno for details */ +#define NATPMP_ERR_FCNTLERROR (-11) +/* NATPMP_ERR_GETTIMEOFDAYERR : gettimeofday() failed. check errno for details */ +#define NATPMP_ERR_GETTIMEOFDAYERR (-12) + +/* */ +#define NATPMP_ERR_UNSUPPORTEDVERSION (-14) +#define NATPMP_ERR_UNSUPPORTEDOPCODE (-15) + +/* Errors from the server : */ +#define NATPMP_ERR_UNDEFINEDERROR (-49) +#define NATPMP_ERR_NOTAUTHORIZED (-51) +#define NATPMP_ERR_NETWORKFAILURE (-52) +#define NATPMP_ERR_OUTOFRESOURCES (-53) + +/* NATPMP_TRYAGAIN : no data available for the moment. try again later */ +#define NATPMP_TRYAGAIN (-100) + +#ifdef __cplusplus +extern "C" { +#endif + +/* initnatpmp() + * initialize a natpmp_t object + * With forcegw=1 the gateway is not detected automaticaly. + * Return values : + * 0 = OK + * NATPMP_ERR_INVALIDARGS + * NATPMP_ERR_SOCKETERROR + * NATPMP_ERR_FCNTLERROR + * NATPMP_ERR_CANNOTGETGATEWAY + * NATPMP_ERR_CONNECTERR */ +LIBSPEC int initnatpmp(natpmp_t * p, int forcegw, in_addr_t forcedgw); + +/* closenatpmp() + * close resources associated with a natpmp_t object + * Return values : + * 0 = OK + * NATPMP_ERR_INVALIDARGS + * NATPMP_ERR_CLOSEERR */ +LIBSPEC int closenatpmp(natpmp_t * p); + +/* sendpublicaddressrequest() + * send a public address NAT-PMP request to the network gateway + * Return values : + * 2 = OK (size of the request) + * NATPMP_ERR_INVALIDARGS + * NATPMP_ERR_SENDERR */ +LIBSPEC int sendpublicaddressrequest(natpmp_t * p); + +/* sendnewportmappingrequest() + * send a new port mapping NAT-PMP request to the network gateway + * Arguments : + * protocol is either NATPMP_PROTOCOL_TCP or NATPMP_PROTOCOL_UDP, + * lifetime is in seconds. + * To remove a port mapping, set lifetime to zero. + * To remove all port mappings to the host, set lifetime and both ports + * to zero. + * Return values : + * 12 = OK (size of the request) + * NATPMP_ERR_INVALIDARGS + * NATPMP_ERR_SENDERR */ +LIBSPEC int sendnewportmappingrequest(natpmp_t * p, int protocol, + uint16_t privateport, uint16_t publicport, + uint32_t lifetime); + +/* getnatpmprequesttimeout() + * fills the timeval structure with the timeout duration of the + * currently pending NAT-PMP request. + * Return values : + * 0 = OK + * NATPMP_ERR_INVALIDARGS + * NATPMP_ERR_GETTIMEOFDAYERR + * NATPMP_ERR_NOPENDINGREQ */ +LIBSPEC int getnatpmprequesttimeout(natpmp_t * p, struct timeval * timeout); + +/* readnatpmpresponseorretry() + * fills the natpmpresp_t structure if possible + * Return values : + * 0 = OK + * NATPMP_TRYAGAIN + * NATPMP_ERR_INVALIDARGS + * NATPMP_ERR_NOPENDINGREQ + * NATPMP_ERR_NOGATEWAYSUPPORT + * NATPMP_ERR_RECVFROM + * NATPMP_ERR_WRONGPACKETSOURCE + * NATPMP_ERR_UNSUPPORTEDVERSION + * NATPMP_ERR_UNSUPPORTEDOPCODE + * NATPMP_ERR_NOTAUTHORIZED + * NATPMP_ERR_NETWORKFAILURE + * NATPMP_ERR_OUTOFRESOURCES + * NATPMP_ERR_UNSUPPORTEDOPCODE + * NATPMP_ERR_UNDEFINEDERROR */ +LIBSPEC int readnatpmpresponseorretry(natpmp_t * p, natpmpresp_t * response); + +#ifdef ENABLE_STRNATPMPERR +LIBSPEC const char * strnatpmperr(int t); +#endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/pkg/hs/natpmp-static/hsrc_lib/Network/NatPmp.hsc b/pkg/hs/natpmp-static/hsrc_lib/Network/NatPmp.hsc new file mode 100644 index 000000000..f410625d6 --- /dev/null +++ b/pkg/hs/natpmp-static/hsrc_lib/Network/NatPmp.hsc @@ -0,0 +1,266 @@ +{-# LANGUAGE ForeignFunctionInterface, DeriveDataTypeable #-} + +-- | This module is a thin wrapper above libnatpmp.h and getgateway.h. + +module Network.NatPmp (Error(..), + NatPmpResponse(..), + ProtocolType(..), + NatPmpHandle, + Port, + LifetimeSeconds, + initNatPmp, + closeNatPmp, + getDefaultGateway, + getPublicAddress, + setPortMapping) where + +#include + +#include +#include +#include + +import Prelude +import Foreign +import Foreign.C +import Network.Socket + +import Control.Monad.IO.Unlift (MonadIO(..)) + +-- Opaque type for the internals of nat pmp +data NatPmpStruct +type NatPmpHandle = Ptr NatPmpStruct + +type Port = Word16 +type LifetimeSeconds = Word32 + +-- The response type, in its internal form. This struct is a C tagged union +-- with additional data, but we need to read and write from its C form. +data NatPmpResponse + = NatPmpResponsePublicAddress HostAddress + | NatPmpResponseUdpPortMapping Port Port LifetimeSeconds + | NatPmpResponseTcpPortMapping Port Port LifetimeSeconds + deriving (Show) + +instance Storable NatPmpResponse where + sizeOf _ = #{size natpmpresp_t} + alignment _ = alignment (undefined :: CString) + + peek p = do + t <- uintToEnum <$> (#{peek natpmpresp_t, type} p) + case t of + RTPublicAddress -> + NatPmpResponsePublicAddress <$> + (#{peek natpmpresp_t, pnu.publicaddress.addr} p) + RTUdpPortMapping -> + NatPmpResponseUdpPortMapping + <$> (#{peek natpmpresp_t, pnu.newportmapping.privateport} p) + <*> (#{peek natpmpresp_t, pnu.newportmapping.mappedpublicport} p) + <*> (#{peek natpmpresp_t, pnu.newportmapping.lifetime} p) + RTTcpPortMapping -> + NatPmpResponseTcpPortMapping + <$> (#{peek natpmpresp_t, pnu.newportmapping.privateport} p) + <*> (#{peek natpmpresp_t, pnu.newportmapping.mappedpublicport} p) + <*> (#{peek natpmpresp_t, pnu.newportmapping.lifetime} p) + + poke _ _ = error "Responses are an output data structure; poke makes no sense" + +type NatPmpResponseHandle = Ptr NatPmpResponse + +foreign import ccall unsafe "getgateway.h getdefaultgateway" _get_default_gateway :: Ptr CUInt -> IO CInt + +foreign import ccall unsafe "natpmp.h initnatpmp" _init_nat_pmp :: NatPmpHandle -> CInt -> CInt -> IO CInt +foreign import ccall unsafe "natpmp.h closenatpmp" _close_nat_pmp :: NatPmpHandle -> IO CInt +foreign import ccall unsafe "natpmp.h sendpublicaddressrequest" sendPublicAddressRequest :: NatPmpHandle -> IO CInt +foreign import ccall unsafe "natpmp.h sendnewportmappingrequest" sendNewPortMappingRequest :: NatPmpHandle -> CInt -> CUShort -> CUShort -> CUInt -> IO CInt + +foreign import ccall unsafe "binding.h readNatResponseSynchronously" readNatResponseSynchronously :: NatPmpHandle -> NatPmpResponseHandle -> IO CInt + +-- Give the type system some help +_peekCUInt :: Ptr CUInt -> IO CUInt +_peekCUInt = peek + +uintToEnum :: Enum e => CUInt -> e +uintToEnum = toEnum . fromIntegral + +intToEnum :: Enum e => CInt -> e +intToEnum = toEnum . fromIntegral + + +-- Fetches the default gateway as an ipv4 address +getDefaultGateway :: IO (Maybe HostAddress) +getDefaultGateway = + alloca $ \(pReturnAddr :: Ptr CUInt) -> do + _get_default_gateway pReturnAddr >>= \case + 0 -> (Just . fromIntegral) <$> _peekCUInt pReturnAddr + _ -> pure Nothing + + +data RespType + = RTPublicAddress + | RTUdpPortMapping + | RTTcpPortMapping + deriving (Eq, Show) + +instance Enum RespType where + fromEnum RTPublicAddress = 0 + fromEnum RTUdpPortMapping = 1 + fromEnum RTTcpPortMapping = 2 + + toEnum 0 = RTPublicAddress + toEnum 1 = RTUdpPortMapping + toEnum 2 = RTTcpPortMapping + toEnum unmatched = error ("RespType.toEnum: Cannot match " ++ show unmatched) + + +data ProtocolType + = PTUdp + | PTTcp + deriving (Eq, Show) + +instance Enum ProtocolType where + fromEnum PTUdp = 1 + fromEnum PTTcp = 2 + + toEnum 1 = PTUdp + toEnum 2 = PTTcp + toEnum x = error ("ProtocolType.toEnum: Cannot match " ++ show x) + + +data Error + = ErrInvalidArgs + | ErrSocketError + | ErrCannotGetGateway + | ErrCloseErr + | ErrRecvFrom + | ErrNoPendingReq + | ErrNoGatewaySupport + | ErrConnectErr + | ErrWrongPacketSource + | ErrSendErr + | ErrFcntlError + | ErrGetTimeOfDayError + -- + | ErrUnsuportedVersion + | ErrUnsupportedOpcode + -- + | ErrUndefinedError + | ErrNotAuthorized + | ErrNetworkFailure + | ErrOutOfResources + -- + | ErrTryAgain + | ErrHaskellBindings + deriving (Eq, Show) + +instance Enum Error where + fromEnum ErrInvalidArgs = -1 + fromEnum ErrSocketError = -2 + fromEnum ErrCannotGetGateway = -3 + fromEnum ErrCloseErr = -4 + fromEnum ErrRecvFrom = -5 + fromEnum ErrNoPendingReq = -6 + fromEnum ErrNoGatewaySupport = -7 + fromEnum ErrConnectErr = -8 + fromEnum ErrWrongPacketSource = -9 + fromEnum ErrSendErr = -10 + fromEnum ErrFcntlError = -11 + fromEnum ErrGetTimeOfDayError = -12 + -- + fromEnum ErrUnsuportedVersion = -14 + fromEnum ErrUnsupportedOpcode = -15 + -- + fromEnum ErrUndefinedError = -49 + fromEnum ErrNotAuthorized = -51 + fromEnum ErrNetworkFailure = -52 + fromEnum ErrOutOfResources = -53 + -- + fromEnum ErrTryAgain = -100 + fromEnum ErrHaskellBindings = -200 + + toEnum (-1) = ErrInvalidArgs + toEnum (-2) = ErrSocketError + toEnum (-3) = ErrCannotGetGateway + toEnum (-4) = ErrCloseErr + toEnum (-5) = ErrRecvFrom + toEnum (-6) = ErrNoPendingReq + toEnum (-7) = ErrNoGatewaySupport + toEnum (-8) = ErrConnectErr + toEnum (-9) = ErrWrongPacketSource + toEnum (-10) = ErrSendErr + toEnum (-11) = ErrFcntlError + toEnum (-12) = ErrGetTimeOfDayError + -- + toEnum (-14) = ErrUnsuportedVersion + toEnum (-15) = ErrUnsupportedOpcode + -- + toEnum (-49) = ErrUndefinedError + toEnum (-51) = ErrNotAuthorized + toEnum (-52) = ErrNetworkFailure + toEnum (-53) = ErrOutOfResources + -- + toEnum (-100) = ErrTryAgain + toEnum (-200) = ErrHaskellBindings + toEnum unmatched = error ("Error.toEnum: Cannot match " ++ show unmatched) + + +initNatPmp :: MonadIO m => m (Either Error NatPmpHandle) +initNatPmp = liftIO do + natpmp <- mallocBytes #{size natpmp_t} + ret <- _init_nat_pmp natpmp 0 0 + case ret of + 0 -> pure $ Right natpmp + _ -> do + free natpmp + pure $ Left $ intToEnum ret + + +closeNatPmp :: MonadIO m => NatPmpHandle -> m (Either Error ()) +closeNatPmp handle = liftIO do + ret <- _close_nat_pmp handle + free handle + case ret of + 0 -> pure $ Right () + _ -> pure $ Left $ intToEnum ret + + +-- | Public interface for getting the public IPv4 address +getPublicAddress :: MonadIO m => NatPmpHandle -> m (Either Error HostAddress) +getPublicAddress natpmp = liftIO do + sendRetcode <- sendPublicAddressRequest natpmp + case sendRetcode of + 2 -> alloca $ \(pResponse :: NatPmpResponseHandle) -> do + respRetcode <- readNatResponseSynchronously natpmp pResponse + case respRetcode of + 0 -> peek pResponse >>= \case + NatPmpResponsePublicAddress addr -> pure $ Right addr + _ -> pure $ Left ErrHaskellBindings + _ -> pure $ Left $ intToEnum respRetcode + _ -> pure $ Left $ intToEnum sendRetcode + +-- | Requests that the router maps the privatePort on our local computer in our +-- private network to publicPort on the public internet. +setPortMapping :: MonadIO m + => NatPmpHandle + -> ProtocolType + -> Port + -> Port + -> LifetimeSeconds + -> m (Either Error ()) +setPortMapping natpmp protocol privatePort publicPort lifetime = liftIO do + let protocolNum = fromEnum protocol + sendResp <- + sendNewPortMappingRequest natpmp + (fromIntegral protocolNum) (CUShort privatePort) (CUShort publicPort) + (CUInt lifetime) + + case sendResp of + 12 -> alloca $ \(pResponse :: NatPmpResponseHandle) -> do + respRetcode <- readNatResponseSynchronously natpmp pResponse + case respRetcode of + 0 -> peek pResponse >>= \case + NatPmpResponseUdpPortMapping _ _ _ -> pure $ Right () + NatPmpResponseTcpPortMapping _ _ _ -> pure $ Right () + _ -> pure $ Left ErrHaskellBindings + _ -> pure $ Left $ intToEnum respRetcode + x -> pure $ Left $ intToEnum x diff --git a/pkg/hs/natpmp-static/natpmp-static.cabal b/pkg/hs/natpmp-static/natpmp-static.cabal new file mode 100644 index 000000000..9ee268ab7 --- /dev/null +++ b/pkg/hs/natpmp-static/natpmp-static.cabal @@ -0,0 +1,89 @@ +cabal-version: >=1.10 +-- Initial package description 'natpmp-static.cabal' generated by 'cabal +-- init'. For further documentation, see +-- http://haskell.org/cabal/users-guide/ + +name: natpmp-static +version: 0.1.0.0 +synopsis: Haskell bindings to libnatpmp +description: + libnatpmp is a C library to communicate with routers and request + that they port forward traffic from the outside internet to your + program. + . + natpmp-static has Haskell bindings to libnatpmp to allow Haskell + programs to punch NAT holes in routers, containing a vendored copy + of the libnatpmp code so that we build Urbit's "almost static" + builds which we distribute. + . + See for upstream source. + +-- bug-reports: +license: BSD3 +license-file: LICENSE +author: Elliot Glaysher +maintainer: elliot@tlon.io +copyright: (c) 2020 Tlon. +stability: experimental +build-type: Simple + +library + hs-Source-Dirs: hsrc_lib + default-language: Haskell2010 + build-depends: base + , network + , unliftio-core + build-tools: hsc2hs + + Include-dirs: cbits + Includes: natpmp.h getgateway.h + C-Sources: cbits/natpmp.c cbits/getgateway.c cbits/binding.c + cc-options: -Wall -Os -g -fPIC + ghc-options: -Wall -fprof-auto -fPIC + + exposed-modules: Network.NatPmp + -- other-modules: + -- other-extensions: + + default-extensions: ApplicativeDo + , BangPatterns + , BlockArguments + , DataKinds + , DefaultSignatures + , DeriveAnyClass + , DeriveDataTypeable + , DeriveFoldable + , DeriveGeneric + , DeriveTraversable + , DerivingStrategies + , EmptyCase + , EmptyDataDecls + , FlexibleContexts + , FlexibleInstances + , FunctionalDependencies + , GADTs + , GeneralizedNewtypeDeriving + , LambdaCase + , MagicHash + , MultiParamTypeClasses + , NamedFieldPuns + , NoImplicitPrelude + , NumericUnderscores + , OverloadedStrings + , PartialTypeSignatures + , PatternSynonyms + , QuasiQuotes + , Rank2Types + , RankNTypes + , RecordWildCards + , ScopedTypeVariables + , StandaloneDeriving + , TemplateHaskell + , TupleSections + , TypeApplications + , TypeFamilies + , TypeOperators + , UnboxedTuples + , UnicodeSyntax + , ViewPatterns + diff --git a/pkg/hs/stack.yaml b/pkg/hs/stack.yaml index 74bad3fe5..88e2364f1 100644 --- a/pkg/hs/stack.yaml +++ b/pkg/hs/stack.yaml @@ -2,6 +2,7 @@ resolver: lts-14.21 packages: - lmdb-static + - natpmp-static - proto - racquire - terminal-progress-bar @@ -17,8 +18,8 @@ extra-deps: - flat-0.3.4@sha256:002a0e0ae656ea8cc02a772d0bcb6ea7dbd7f2e79070959cc748ad1e7138eb38 - base58-bytestring-0.1.0@sha256:a1da72ee89d5450bac1c792d9fcbe95ed7154ab7246f2172b57bd4fd9b5eab79 - lock-file-0.7.0.0@sha256:3ad84b5e454145e1d928063b56abb96db24a99a21b493989520e58fa0ab37b00 - - urbit-hob-0.3.1@sha256:afbdc7ad071eefc6ca85f5b598b6c62ed49079d15d1840dac27438a3b3150303 - para-1.1@sha256:a90eebb063ad70271e6e2a7f00a93e8e8f8b77273f100f39852fbf8301926f81 + - urbit-hob-0.3.3@sha256:ff8dae3844881cd979fee96bcb3ab3d5ea95f7c3ad2302dbc4b2dc417ff6595b # This allows building on NixOS. nix: diff --git a/pkg/hs/urbit-king/TODO.md b/pkg/hs/urbit-king/TODO.md index f1a292b85..d76f84530 100644 --- a/pkg/hs/urbit-king/TODO.md +++ b/pkg/hs/urbit-king/TODO.md @@ -63,12 +63,6 @@ Polish: changed too quickly. -# Finding the Serf Executable - -- [ ] Right now, `urbit-worker` is found by looking it up in the PATH. This - is wrong, but what is right? - - # Take Advantage of New IPC Features - [ ] Hook up `scry` to drivers. diff --git a/pkg/hs/urbit-king/lib/Urbit/Arvo/Effect.hs b/pkg/hs/urbit-king/lib/Urbit/Arvo/Effect.hs index 606539907..d77b25051 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Arvo/Effect.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Arvo/Effect.hs @@ -112,6 +112,7 @@ data Blit = Bel () | Clr () | Hop Word64 + | Klr Stub | Lin [Char] | Mor () | Sag Path Noun @@ -119,12 +120,84 @@ data Blit | Url Cord deriving (Eq, Ord) +data Deco + = DecoBl + | DecoBr + | DecoUn + | DecoNull + deriving (Eq, Ord, Show) + +data Tint + = TintR + | TintG + | TintB + | TintC + | TintM + | TintY + | TintK + | TintW + | TintNull + deriving (Eq, Ord, Show) + +data Stye = Stye + { deco :: (HoonSet Deco) + , back :: Tint + , fore :: Tint + } + deriving (Eq, Ord, Show) + +newtype Stub = Stub [(Stye, [Char])] + deriving (Eq, Ord, Show) + +instance ToNoun Deco where + toNoun = \case + DecoBl -> toNoun $ Cord "bl" + DecoBr -> toNoun $ Cord "br" + DecoUn -> toNoun $ Cord "un" + DecoNull -> Atom 0 + +instance FromNoun Deco where + parseNoun = named "Deco" . \case + Atom 0 -> pure DecoNull + n -> parseNoun @Cord n <&> unCord >>= \case + "bl" -> pure DecoBl + "br" -> pure DecoBr + "un" -> pure DecoUn + t -> fail ("invalid: " <> unpack t) + +instance ToNoun Tint where + toNoun = \case + TintR -> toNoun $ Cord "r" + TintG -> toNoun $ Cord "g" + TintB -> toNoun $ Cord "b" + TintC -> toNoun $ Cord "c" + TintM -> toNoun $ Cord "m" + TintY -> toNoun $ Cord "y" + TintK -> toNoun $ Cord "k" + TintW -> toNoun $ Cord "w" + TintNull -> Atom 0 + +instance FromNoun Tint where + parseNoun = named "Tint" . \case + Atom 0 -> pure TintNull + n -> parseNoun @Cord n <&> unCord >>= \case + "r" -> pure TintR + "g" -> pure TintG + "b" -> pure TintB + "c" -> pure TintC + "m" -> pure TintM + "y" -> pure TintY + "k" -> pure TintK + "w" -> pure TintW + t -> fail ("invalid: " <> unpack t) + -- Manual instance to not save the noun/atom in Sag/Sav, because these can be -- megabytes and makes king hang. instance Show Blit where show (Bel ()) = "Bel ()" show (Clr ()) = "Clr ()" show (Hop x) = "Hop " ++ (show x) + show (Klr s) = "Klr " ++ (show s) show (Lin c) = "Lin " ++ (show c) show (Mor ()) = "Mor ()" show (Sag path _) = "Sag " ++ (show path) @@ -144,6 +217,8 @@ data TermEf | TermEfMass Path Noun -- Irrelevant deriving (Eq, Ord, Show) +deriveNoun ''Stye +deriveNoun ''Stub deriveNoun ''Blit deriveNoun ''TermEf diff --git a/pkg/hs/urbit-king/lib/Urbit/King/App.hs b/pkg/hs/urbit-king/lib/Urbit/King/App.hs index bd8b6b1a5..107708d16 100644 --- a/pkg/hs/urbit-king/lib/Urbit/King/App.hs +++ b/pkg/hs/urbit-king/lib/Urbit/King/App.hs @@ -9,6 +9,8 @@ module Urbit.King.App , kingEnvKillSignal , killKingActionL , onKillKingSigL + , HostEnv + , runHostEnv , PierEnv , runPierEnv , killPierActionL @@ -17,6 +19,8 @@ module Urbit.King.App , HasKingId(..) , HasProcId(..) , HasKingEnv(..) + , HasMultiEyreApi(..) + , HasHostEnv(..) , HasPierEnv(..) , module Urbit.King.Config ) @@ -25,11 +29,16 @@ where import Urbit.King.Config import Urbit.Prelude -import System.Directory (createDirectoryIfMissing, getHomeDirectory) +import System.Directory ( createDirectoryIfMissing + , getXdgDirectory + , XdgDirectory(XdgCache) + ) import System.Posix.Internals (c_getpid) import System.Posix.Types (CPid(..)) import System.Random (randomIO) import Urbit.King.App.Class (HasStderrLogFunc(..)) +import Urbit.Vere.Eyre.Multi (MultiEyreApi) +import Urbit.Vere.Ports (PortControlApi, HasPortControlApi(..)) -- KingEnv --------------------------------------------------------------------- @@ -70,39 +79,50 @@ instance HasProcId KingEnv where instance HasKingId KingEnv where kingIdL = kingEnvKingId - -- Running KingEnvs ------------------------------------------------------------ -runKingEnvStderr :: Bool -> RIO KingEnv a -> IO a -runKingEnvStderr verb inner = do +runKingEnvStderr :: Bool -> LogLevel -> RIO KingEnv a -> IO a +runKingEnvStderr verb lvl inner = do logOptions <- - logOptionsHandle stderr verb <&> setLogUseTime True <&> setLogUseLoc False - + logOptionsHandle stderr verb + <&> setLogUseTime True + <&> setLogUseLoc False + <&> setLogMinLevel lvl withLogFunc logOptions $ \logFunc -> runKingEnv logFunc logFunc inner -runKingEnvLogFile :: Bool -> RIO KingEnv a -> IO a -runKingEnvLogFile verb inner = withLogFileHandle $ \h -> do - logOptions <- - logOptionsHandle h verb <&> setLogUseTime True <&> setLogUseLoc False - stderrLogOptions <- - logOptionsHandle stderr verb <&> setLogUseTime False <&> setLogUseLoc False +runKingEnvLogFile :: Bool -> LogLevel -> Maybe FilePath -> RIO KingEnv a -> IO a +runKingEnvLogFile verb lvl fileM inner = do + logFile <- case fileM of + Just f -> pure f + Nothing -> defaultLogFile + withLogFileHandle logFile $ \h -> do + logOptions <- + logOptionsHandle h verb + <&> setLogUseTime True + <&> setLogUseLoc False + <&> setLogMinLevel lvl + stderrLogOptions <- + logOptionsHandle stderr verb + <&> setLogUseTime False + <&> setLogUseLoc False + <&> setLogMinLevel lvl + withLogFunc stderrLogOptions $ \stderrLogFunc -> withLogFunc logOptions + $ \logFunc -> runKingEnv logFunc stderrLogFunc inner - withLogFunc stderrLogOptions $ \stderrLogFunc -> withLogFunc logOptions - $ \logFunc -> runKingEnv logFunc stderrLogFunc inner - -withLogFileHandle :: (Handle -> IO a) -> IO a -withLogFileHandle act = do - home <- getHomeDirectory - let logDir = home ".urbit" - createDirectoryIfMissing True logDir - withFile (logDir "king.log") AppendMode $ \handle -> do +withLogFileHandle :: FilePath -> (Handle -> IO a) -> IO a +withLogFileHandle f act = + withFile f AppendMode $ \handle -> do hSetBuffering handle LineBuffering act handle +defaultLogFile :: IO FilePath +defaultLogFile = do + logDir <- getXdgDirectory XdgCache "urbit" + createDirectoryIfMissing True logDir + pure (logDir "king.log") + runKingEnvNoLog :: RIO KingEnv a -> IO a -runKingEnvNoLog act = withFile "/dev/null" AppendMode $ \handle -> do - logOptions <- logOptionsHandle handle True - withLogFunc logOptions $ \logFunc -> runKingEnv logFunc logFunc act +runKingEnvNoLog act = runKingEnv mempty mempty act runKingEnv :: LogFunc -> LogFunc -> RIO KingEnv a -> IO a runKingEnv logFunc stderr action = do @@ -121,14 +141,69 @@ killKingActionL :: HasKingEnv e => Getter e (STM ()) killKingActionL = kingEnvL . kingEnvKillSignal . to (\kil -> void (tryPutTMVar kil ())) +-- HostEnv ------------------------------------------------------------------ + +-- The host environment is everything in King, eyre configuration shared +-- across ships, and nat punching data. + +class HasMultiEyreApi a where + multiEyreApiL :: Lens' a MultiEyreApi + +class (HasKingEnv a, HasMultiEyreApi a, HasPortControlApi a) => + HasHostEnv a where + hostEnvL :: Lens' a HostEnv + +data HostEnv = HostEnv + { _hostEnvKingEnv :: !KingEnv + , _hostEnvMultiEyreApi :: !MultiEyreApi + , _hostEnvPortControlApi :: !PortControlApi + } + +makeLenses ''HostEnv + +instance HasKingEnv HostEnv where + kingEnvL = hostEnvKingEnv + +instance HasLogFunc HostEnv where + logFuncL = kingEnvL . logFuncL + +instance HasStderrLogFunc HostEnv where + stderrLogFuncL = kingEnvL . stderrLogFuncL + +instance HasProcId HostEnv where + procIdL = kingEnvL . procIdL + +instance HasKingId HostEnv where + kingIdL = kingEnvL . kingEnvKingId + +instance HasMultiEyreApi HostEnv where + multiEyreApiL = hostEnvMultiEyreApi + +instance HasPortControlApi HostEnv where + portControlApiL = hostEnvPortControlApi + +-- Running Running Envs -------------------------------------------------------- + +runHostEnv :: MultiEyreApi -> PortControlApi -> RIO HostEnv a + -> RIO KingEnv a +runHostEnv multi ports action = do + king <- ask + + let hostEnv = HostEnv { _hostEnvKingEnv = king + , _hostEnvMultiEyreApi = multi + , _hostEnvPortControlApi = ports + } + + io (runRIO hostEnv action) -- PierEnv --------------------------------------------------------------------- -class (HasKingEnv a, HasPierConfig a, HasNetworkConfig a) => HasPierEnv a where +class (HasKingEnv a, HasHostEnv a, HasPierConfig a, HasNetworkConfig a) => + HasPierEnv a where pierEnvL :: Lens' a PierEnv data PierEnv = PierEnv - { _pierEnvKingEnv :: !KingEnv + { _pierEnvHostEnv :: !HostEnv , _pierEnvPierConfig :: !PierConfig , _pierEnvNetworkConfig :: !NetworkConfig , _pierEnvKillSignal :: !(TMVar ()) @@ -137,7 +212,16 @@ data PierEnv = PierEnv makeLenses ''PierEnv instance HasKingEnv PierEnv where - kingEnvL = pierEnvKingEnv + kingEnvL = pierEnvHostEnv . kingEnvL + +instance HasHostEnv PierEnv where + hostEnvL = pierEnvHostEnv + +instance HasMultiEyreApi PierEnv where + multiEyreApiL = pierEnvHostEnv . multiEyreApiL + +instance HasPortControlApi PierEnv where + portControlApiL = pierEnvHostEnv . portControlApiL instance HasPierEnv PierEnv where pierEnvL = id @@ -180,11 +264,11 @@ killPierActionL = -- Running Pier Envs ----------------------------------------------------------- runPierEnv - :: PierConfig -> NetworkConfig -> TMVar () -> RIO PierEnv a -> RIO KingEnv a + :: PierConfig -> NetworkConfig -> TMVar () -> RIO PierEnv a -> RIO HostEnv a runPierEnv pierConfig networkConfig vKill action = do - app <- ask + host <- ask - let pierEnv = PierEnv { _pierEnvKingEnv = app + let pierEnv = PierEnv { _pierEnvHostEnv = host , _pierEnvPierConfig = pierConfig , _pierEnvNetworkConfig = networkConfig , _pierEnvKillSignal = vKill diff --git a/pkg/hs/urbit-king/lib/Urbit/King/CLI.hs b/pkg/hs/urbit-king/lib/Urbit/King/CLI.hs index b5edbcd2f..5657ce8c8 100644 --- a/pkg/hs/urbit-king/lib/Urbit/King/CLI.hs +++ b/pkg/hs/urbit-king/lib/Urbit/King/CLI.hs @@ -6,21 +6,24 @@ -} module Urbit.King.CLI where -import ClassyPrelude +import ClassyPrelude hiding (log) import Options.Applicative import Options.Applicative.Help.Pretty import Data.Word (Word16) +import RIO (LogLevel(..)) import System.Environment (getProgName) -------------------------------------------------------------------------------- -data KingOpts = KingOpts - { koSharedHttpPort :: Maybe Word16 - , koSharedHttpsPort :: Maybe Word16 +data Host = Host + { hSharedHttpPort :: Maybe Word16 + , hSharedHttpsPort :: Maybe Word16 + , hUseNatPmp :: Nat } deriving (Show) +-- | Options for each running pier. data Opts = Opts { oQuiet :: Bool , oHashless :: Bool @@ -44,6 +47,19 @@ data Opts = Opts } deriving (Show) +-- | Options for the logging subsystem. +data Log = Log + { lTarget :: Maybe (LogTarget FilePath) + , lLevel :: LogLevel + } + deriving (Show) + +data LogTarget a + = LogOff + | LogStderr + | LogFile a + deriving (Show) + data BootType = BootComet | BootFake Text @@ -55,6 +71,12 @@ data PillSource | PillSourceURL String deriving (Show) +data Nat + = NatAlways + | NatWhenPrivateNetwork + | NatNever + deriving (Show) + data New = New { nPillSource :: PillSource , nPierPath :: Maybe FilePath -- Derived from ship name if not specified. @@ -102,8 +124,8 @@ data Bug deriving (Show) data Cmd - = CmdNew New Opts - | CmdRun KingOpts [(Run, Opts, Bool)] + = CmdNew New Opts + | CmdRun Host [(Run, Opts, Bool)] | CmdBug Bug | CmdCon FilePath deriving (Show) @@ -135,7 +157,7 @@ footNote exe = string $ intercalate "\n" -------------------------------------------------------------------------------- -parseArgs :: IO Cmd +parseArgs :: IO (Cmd, Log) parseArgs = do nm <- getProgName @@ -293,7 +315,7 @@ opts = do oVerbose <- switch $ short 'v' <> long "verbose" - <> help "Verbose" + <> help "Puts the serf and king into verbose mode" <> hidden oExit <- switch $ short 'x' @@ -332,22 +354,69 @@ opts = do oFullReplay <- switch $ long "full-log-replay" - <> help "Ignores the snapshot and recomputes state from log" + <> help "Ignores snapshot and recomputes state from event log" <> hidden pure (Opts{..}) -newShip :: Parser Cmd -newShip = CmdNew <$> new <*> opts +log :: Parser Log +log = do + lTarget <- + optional + $ ( flag' LogStderr + $ long "log-to-stderr" + <> long "stderr" + <> help "Display logs on stderr" + <> hidden + ) + <|> ( fmap LogFile . strOption + $ long "log-to" + <> metavar "LOG_FILE" + <> help "Append logs to the given file" + <> hidden + ) + <|> ( flag' LogOff + $ long "no-logging" + <> help "Disable logging entirely" + <> hidden + ) + + lLevel <- + ( flag' LevelDebug + $ long "log-debug" + <> help "Log errors, warnings, info, and debug messages" + <> hidden + ) + <|> ( flag' LevelInfo + $ long "log-info" + <> help "Log errors, warnings, and info" + <> hidden + ) + <|> ( flag' LevelWarn + $ long "log-warn" + <> help "Log errors and warnings (default)" + <> hidden + ) + <|> ( flag' LevelError + $ long "log-error" + <> help "Log errors only" + <> hidden + ) + <|> pure LevelWarn + + pure (Log{..}) + +newShip :: Parser (Cmd, Log) +newShip = (,) <$> (CmdNew <$> new <*> opts) <*> log runOneShip :: Parser (Run, Opts, Bool) runOneShip = (,,) <$> fmap Run pierPath <*> opts <*> df where df = switch (short 'd' <> long "daemon" <> help "Daemon mode" <> hidden) -kingOpts :: Parser KingOpts -kingOpts = do - koSharedHttpPort <- +host :: Parser Host +host = do + hSharedHttpPort <- optional $ option auto $ metavar "PORT" @@ -355,7 +424,7 @@ kingOpts = do <> help "HTTP port" <> hidden - koSharedHttpsPort <- + hSharedHttpsPort <- optional $ option auto $ metavar "PORT" @@ -363,10 +432,29 @@ kingOpts = do <> help "HTTPS port" <> hidden - pure (KingOpts{..}) + hUseNatPmp <- + ( flag' NatAlways + $ long "port-forwarding" + <> help "Always try to search for a router to forward ames ports" + <> hidden + ) <|> + ( flag' NatNever + $ long "no-port-forwarding" + <> help "Disable trying to ask the router to forward ames ports" + <> hidden + ) <|> + ( flag' NatWhenPrivateNetwork + $ long "port-forwarding-when-internal" + <> help ("Try asking the router to forward when ip is 192.168.0.0/16, " <> + "172.16.0.0/12 or 10.0.0.0/8 (default).") + <> hidden + ) <|> + (pure $ NatWhenPrivateNetwork) -runShip :: Parser Cmd -runShip = CmdRun <$> kingOpts <*> some runOneShip + pure (Host{..}) + +runShip :: Parser (Cmd, Log) +runShip = (,) <$> (CmdRun <$> host <*> some runOneShip) <*> log valPill :: Parser Bug valPill = do @@ -410,8 +498,8 @@ browseEvs = EventBrowser <$> pierPath checkDawn :: Parser Bug checkDawn = CheckDawn <$> keyfilePath -bugCmd :: Parser Cmd -bugCmd = fmap CmdBug +bugCmd :: Parser (Cmd, Log) +bugCmd = (flip (,) <$> log <*>) $ fmap CmdBug $ subparser $ command "validate-pill" ( info (valPill <**> helper) @@ -446,15 +534,15 @@ bugCmd = fmap CmdBug $ progDesc "Shows the list of stars accepting comets" ) -conCmd :: Parser Cmd -conCmd = CmdCon <$> pierPath +conCmd :: Parser (Cmd, Log) +conCmd = (,) <$> (CmdCon <$> pierPath) <*> log allFx :: Parser Bug allFx = do bPierPath <- strArgument (metavar "PIER" <> help "Path to pier") pure CollectAllFX{..} -cmd :: Parser Cmd +cmd :: Parser (Cmd, Log) cmd = subparser $ command "new" ( info (newShip <**> helper) $ progDesc "Boot a new ship." diff --git a/pkg/hs/urbit-king/lib/Urbit/King/Config.hs b/pkg/hs/urbit-king/lib/Urbit/King/Config.hs index 7cb9ceb2c..4ccece736 100644 --- a/pkg/hs/urbit-king/lib/Urbit/King/Config.hs +++ b/pkg/hs/urbit-king/lib/Urbit/King/Config.hs @@ -14,7 +14,7 @@ import qualified Urbit.Vere.Serf as Serf data PierConfig = PierConfig { _pcPierPath :: FilePath , _pcDryRun :: Bool - , _pcSerfExe :: Text + , _pcSerfExe :: Maybe Text , _pcSerfFlags :: [Serf.Flag] } deriving (Show) diff --git a/pkg/hs/urbit-king/lib/Urbit/King/Main.hs b/pkg/hs/urbit-king/lib/Urbit/King/Main.hs index 2fb280d2d..66092ffb6 100644 --- a/pkg/hs/urbit-king/lib/Urbit/King/Main.hs +++ b/pkg/hs/urbit-king/lib/Urbit/King/Main.hs @@ -82,7 +82,8 @@ import Urbit.Arvo import Urbit.King.Config import Urbit.Vere.Dawn import Urbit.Vere.Pier -import Urbit.Vere.Eyre.Multi (multiEyre, MultiEyreApi, MultiEyreConf(..)) +import Urbit.Vere.Ports +import Urbit.Vere.Eyre.Multi (multiEyre, MultiEyreConf(..)) import Urbit.Vere.Pier.Types import Urbit.Vere.Serf import Urbit.King.App @@ -91,6 +92,7 @@ import Control.Concurrent (myThreadId) import Control.Exception (AsyncException(UserInterrupt)) import Control.Lens ((&)) import System.Process (system) +import System.IO (hPutStrLn) import Text.Show.Pretty (pPrint) import Urbit.Noun.Conversions (cordToUW) import Urbit.Noun.Time (Wen) @@ -144,7 +146,7 @@ toPierConfig pierPath o@(CLI.Opts{..}) = PierConfig { .. } where _pcPierPath = pierPath _pcDryRun = oDryRun || isJust oDryFrom - _pcSerfExe = fromMaybe "urbit-worker" oSerfExe + _pcSerfExe = oSerfExe _pcSerfFlags = toSerfFlags o toNetworkConfig :: CLI.Opts -> NetworkConfig @@ -184,18 +186,17 @@ tryBootFromPill -> Bool -> Ship -> LegacyBootEvent - -> MultiEyreApi -> RIO PierEnv () -tryBootFromPill oExit pill lite ship boot multi = do +tryBootFromPill oExit pill lite ship boot = do mStart <- newEmptyMVar vSlog <- logSlogs - runOrExitImmediately vSlog (bootedPier vSlog) oExit mStart multi + runOrExitImmediately vSlog (bootedPier vSlog) oExit mStart where bootedPier vSlog = do view pierPathL >>= lockFile - rio $ logDebug "Starting boot" + rio $ logInfo "Starting boot" sls <- Pier.booted vSlog pill lite ship boot - rio $ logDebug "Completed boot" + rio $ logInfo "Completed boot" pure sls runOrExitImmediately @@ -203,38 +204,36 @@ runOrExitImmediately -> RAcquire PierEnv (Serf, Log.EventLog) -> Bool -> MVar () - -> MultiEyreApi -> RIO PierEnv () -runOrExitImmediately vSlog getPier oExit mStart multi = do +runOrExitImmediately vSlog getPier oExit mStart = do rwith getPier (if oExit then shutdownImmediately else runPier) where shutdownImmediately :: (Serf, Log.EventLog) -> RIO PierEnv () shutdownImmediately (serf, log) = do - logDebug "Sending shutdown signal" + logInfo "Sending shutdown signal" Serf.stop serf - logDebug "Shutdown!" + logInfo "Shutdown!" runPier :: (Serf, Log.EventLog) -> RIO PierEnv () runPier serfLog = do - runRAcquire (Pier.pier serfLog vSlog mStart multi) + runRAcquire (Pier.pier serfLog vSlog mStart) tryPlayShip :: Bool -> Bool -> Maybe Word64 -> MVar () - -> MultiEyreApi -> RIO PierEnv () -tryPlayShip exitImmediately fullReplay playFrom mStart multi = do +tryPlayShip exitImmediately fullReplay playFrom mStart = do when fullReplay wipeSnapshot vSlog <- logSlogs - runOrExitImmediately vSlog (resumeShip vSlog) exitImmediately mStart multi + runOrExitImmediately vSlog (resumeShip vSlog) exitImmediately mStart where wipeSnapshot = do shipPath <- view pierPathL - logDebug "wipeSnapshot" - logDebug $ display $ pack @Text ("Wiping " <> north shipPath) - logDebug $ display $ pack @Text ("Wiping " <> south shipPath) + logInfo "wipeSnapshot" + logInfo $ display $ pack @Text ("Wiping " <> north shipPath) + logInfo $ display $ pack @Text ("Wiping " <> south shipPath) removeFileIfExists (north shipPath) removeFileIfExists (south shipPath) @@ -244,9 +243,9 @@ tryPlayShip exitImmediately fullReplay playFrom mStart multi = do resumeShip :: TVar (Text -> IO ()) -> RAcquire PierEnv (Serf, Log.EventLog) resumeShip vSlog = do view pierPathL >>= lockFile - rio $ logDebug "RESUMING SHIP" + rio $ logInfo "RESUMING SHIP" sls <- Pier.resumed vSlog playFrom - rio $ logDebug "SHIP RESUMED" + rio $ logInfo "SHIP RESUMED" pure sls runRAcquire :: (MonadUnliftIO (m e), MonadIO (m e), MonadReader e (m e)) @@ -261,7 +260,7 @@ checkEvs pierPath first last = do rwith (Log.existing logPath) $ \log -> do let ident = Log.identity log let pbSty = PB.defStyle { PB.stylePostfix = PB.exact } - logDebug (displayShow ident) + logInfo (displayShow ident) last <- atomically $ Log.lastEv log <&> \lastReal -> min last lastReal @@ -286,7 +285,7 @@ checkEvs pierPath first last = do showEvents pb eId cycle = await >>= \case Nothing -> do lift $ PB.killProgressBar pb - lift $ logDebug "Everything checks out." + lift $ logInfo "Everything checks out." Just bs -> do lift $ PB.incProgress pb 1 lift $ do @@ -315,10 +314,10 @@ collectAllFx = error "TODO" -} collectAllFx :: FilePath -> RIO KingEnv () collectAllFx top = do - logDebug $ display $ pack @Text top + logInfo $ display $ pack @Text top vSlog <- logSlogs rwith (collectedFX vSlog) $ \() -> - logDebug "Done collecting effects!" + logInfo "Done collecting effects!" where tmpDir :: FilePath tmpDir = top ".tmpdir" @@ -339,10 +338,10 @@ collectAllFx top = do replayPartEvs :: FilePath -> Word64 -> RIO KingEnv () replayPartEvs top last = do - logDebug $ display $ pack @Text top + logInfo $ display $ pack @Text top fetchSnapshot rwith replayedEvs $ \() -> - logDebug "Done replaying events!" + logInfo "Done replaying events!" where fetchSnapshot :: RIO KingEnv () fetchSnapshot = do @@ -385,57 +384,57 @@ replayPartEvs top last = do -} testPill :: HasLogFunc e => FilePath -> Bool -> Bool -> RIO e () testPill pax showPil showSeq = do - logDebug "Reading pill file." + logInfo "Reading pill file." pillBytes <- readFile pax - logDebug "Cueing pill file." + logInfo "Cueing pill file." pillNoun <- io $ cueBS pillBytes & either throwIO pure - logDebug "Parsing pill file." + logInfo "Parsing pill file." pill <- fromNounErr pillNoun & either (throwIO . uncurry ParseErr) pure - logDebug "Using pill to generate boot sequence." + logInfo "Using pill to generate boot sequence." bootSeq <- genBootSeq (Ship 0) pill False (Fake (Ship 0)) - logDebug "Validate jam/cue and toNoun/fromNoun on pill value" + logInfo "Validate jam/cue and toNoun/fromNoun on pill value" reJam <- validateNounVal pill - logDebug "Checking if round-trip matches input file:" + logInfo "Checking if round-trip matches input file:" unless (reJam == pillBytes) $ do - logDebug " Our jam does not match the file...\n" - logDebug " This is surprising, but it is probably okay." + logInfo " Our jam does not match the file...\n" + logInfo " This is surprising, but it is probably okay." when showPil $ do - logDebug "\n\n== Pill ==\n" + logInfo "\n\n== Pill ==\n" io $ pPrint pill when showSeq $ do - logDebug "\n\n== Boot Sequence ==\n" + logInfo "\n\n== Boot Sequence ==\n" io $ pPrint bootSeq validateNounVal :: (HasLogFunc e, Eq a, ToNoun a, FromNoun a) => a -> RIO e ByteString validateNounVal inpVal = do - logDebug " jam" + logInfo " jam" inpByt <- evaluate $ jamBS $ toNoun inpVal - logDebug " cue" + logInfo " cue" outNon <- cueBS inpByt & either throwIO pure - logDebug " fromNoun" + logInfo " fromNoun" outVal <- fromNounErr outNon & either (throwIO . uncurry ParseErr) pure - logDebug " toNoun" + logInfo " toNoun" outNon <- evaluate (toNoun outVal) - logDebug " jam" + logInfo " jam" outByt <- evaluate $ jamBS outNon - logDebug "Checking if: x == cue (jam x)" + logInfo "Checking if: x == cue (jam x)" unless (inpVal == outVal) $ error "Value fails test: x == cue (jam x)" - logDebug "Checking if: jam x == jam (cue (jam x))" + logInfo "Checking if: jam x == jam (cue (jam x))" unless (inpByt == outByt) $ error "Value fails test: jam x == jam (cue (jam x))" @@ -444,14 +443,14 @@ validateNounVal inpVal = do -------------------------------------------------------------------------------- -pillFrom :: CLI.PillSource -> RIO KingEnv Pill +pillFrom :: CLI.PillSource -> RIO HostEnv Pill pillFrom = \case CLI.PillSourceFile pillPath -> do - logDebug $ display $ "boot: reading pill from " ++ (pack pillPath :: Text) + logInfo $ display $ "boot: reading pill from " ++ (pack pillPath :: Text) io (loadFile pillPath >>= either throwIO pure) CLI.PillSourceURL url -> do - logDebug $ display $ "boot: retrieving pill from " ++ (pack url :: Text) + logInfo $ display $ "boot: retrieving pill from " ++ (pack url :: Text) -- Get the jamfile with the list of stars accepting comets right now. manager <- io $ C.newManager tlsManagerSettings request <- io $ C.parseRequest url @@ -475,7 +474,12 @@ newShip CLI.New{..} opts = do -} multi <- multiEyre (MultiEyreConf Nothing Nothing True) - case nBootType of + -- TODO: We hit the same problem as above: we need a host env to boot a ship + -- because it may autostart the ship, so build an inactive port configuration. + let ports = buildInactivePorts + + -- here we are with a king env, and we now need a multi env. + runHostEnv multi ports $ case nBootType of CLI.BootComet -> do pill <- pillFrom nPillSource putStrLn "boot: retrieving list of stars currently accepting comets" @@ -486,12 +490,12 @@ newShip CLI.New{..} opts = do eny <- io $ Sys.randomIO let seed = mineComet (Set.fromList starList) eny putStrLn ("boot: found comet " ++ renderShip (sShip seed)) - bootFromSeed multi pill seed + bootFromSeed pill seed CLI.BootFake name -> do pill <- pillFrom nPillSource ship <- shipFrom name - runTryBootFromPill multi pill name ship (Fake ship) + runTryBootFromPill pill name ship (Fake ship) CLI.BootFromKeyfile keyFile -> do text <- readFileUtf8 keyFile @@ -506,10 +510,10 @@ newShip CLI.New{..} opts = do pill <- pillFrom nPillSource - bootFromSeed multi pill seed + bootFromSeed pill seed where - shipFrom :: Text -> RIO KingEnv Ship + shipFrom :: Text -> RIO HostEnv Ship shipFrom name = case Ob.parsePatp name of Left x -> error "Invalid ship name" Right p -> pure $ Ship $ fromIntegral $ Ob.fromPatp p @@ -519,7 +523,7 @@ newShip CLI.New{..} opts = do Just x -> x Nothing -> "./" <> unpack name - nameFromShip :: Ship -> RIO KingEnv Text + nameFromShip :: HasKingEnv e => Ship -> RIO e Text nameFromShip s = name where nameWithSig = Ob.renderPatp $ Ob.patp $ fromIntegral s @@ -527,8 +531,8 @@ newShip CLI.New{..} opts = do Nothing -> error "Urbit.ob didn't produce string with ~" Just x -> pure x - bootFromSeed :: MultiEyreApi -> Pill -> Seed -> RIO KingEnv () - bootFromSeed multi pill seed = do + bootFromSeed :: Pill -> Seed -> RIO HostEnv () + bootFromSeed pill seed = do ethReturn <- dawnVent seed case ethReturn of @@ -536,19 +540,23 @@ newShip CLI.New{..} opts = do Right dawn -> do let ship = sShip $ dSeed dawn name <- nameFromShip ship - runTryBootFromPill multi pill name ship (Dawn dawn) + runTryBootFromPill pill name ship (Dawn dawn) -- Now that we have all the information for running an application with a -- PierConfig, do so. - runTryBootFromPill multi pill name ship bootEvent = do - vKill <- view kingEnvKillSignal + runTryBootFromPill :: Pill + -> Text + -> Ship + -> LegacyBootEvent + -> RIO HostEnv () + runTryBootFromPill pill name ship bootEvent = do + vKill <- view (kingEnvL . kingEnvKillSignal) let pierConfig = toPierConfig (pierPath name) opts let networkConfig = toNetworkConfig opts runPierEnv pierConfig networkConfig vKill $ - tryBootFromPill True pill nLite ship bootEvent multi ------- tryBootFromPill (CLI.oExit opts) pill nLite flags ship bootEvent + tryBootFromPill True pill nLite ship bootEvent -runShipEnv :: CLI.Run -> CLI.Opts -> TMVar () -> RIO PierEnv a -> RIO KingEnv a +runShipEnv :: CLI.Run -> CLI.Opts -> TMVar () -> RIO PierEnv a -> RIO HostEnv a runShipEnv (CLI.Run pierPath) opts vKill act = do runPierEnv pierConfig netConfig vKill act where @@ -556,8 +564,8 @@ runShipEnv (CLI.Run pierPath) opts vKill act = do netConfig = toNetworkConfig opts runShip - :: CLI.Run -> CLI.Opts -> Bool -> MultiEyreApi -> RIO PierEnv () -runShip (CLI.Run pierPath) opts daemon multi = do + :: CLI.Run -> CLI.Opts -> Bool -> RIO PierEnv () +runShip (CLI.Run pierPath) opts daemon = do mStart <- newEmptyMVar if daemon then runPier mStart @@ -580,9 +588,17 @@ runShip (CLI.Run pierPath) opts daemon multi = do (CLI.oFullReplay opts) (CLI.oDryFrom opts) mStart - multi +buildPortHandler :: HasLogFunc e => CLI.Nat -> RIO e PortControlApi +buildPortHandler CLI.NatNever = pure buildInactivePorts +-- TODO: Figure out what to do about logging here. The "port: " messages are +-- the sort of thing that should be put on the muxed terminal log, but we don't +-- have that at this layer. +buildPortHandler CLI.NatAlways = buildNatPorts (io . hPutStrLn stderr . unpack) +buildPortHandler CLI.NatWhenPrivateNetwork = + buildNatPortsWhenPrivate (io . hPutStrLn stderr . unpack) + startBrowser :: HasLogFunc e => FilePath -> RIO e () startBrowser pierPath = runRAcquire $ do -- lockFile pierPath @@ -621,12 +637,12 @@ checkComet = do main :: IO () main = do - args <- CLI.parseArgs + (args, log) <- CLI.parseArgs hSetBuffering stdout NoBuffering setupSignalHandlers - runKingEnv args $ case args of + runKingEnv args log $ case args of CLI.CmdRun ko ships -> runShips ko ships CLI.CmdNew n o -> newShip n o CLI.CmdBug (CLI.CollectAllFX pax ) -> collectAllFx pax @@ -640,11 +656,14 @@ main = do CLI.CmdCon pier -> connTerm pier where - runKingEnv args = - let verb = verboseLogging args - in if willRunTerminal args - then runKingEnvLogFile verb - else runKingEnvStderr verb + runKingEnv args log = + let + verb = verboseLogging args + CLI.Log {..} = log + in case logTarget lTarget args of + CLI.LogFile f -> runKingEnvLogFile verb lLevel f + CLI.LogStderr -> runKingEnvStderr verb lLevel + CLI.LogOff -> runKingEnvNoLog setupSignalHandlers = do mainTid <- myThreadId @@ -657,12 +676,23 @@ main = do CLI.CmdRun ko ships -> any CLI.oVerbose (ships <&> \(_, o, _) -> o) _ -> False - willRunTerminal :: CLI.Cmd -> Bool - willRunTerminal = \case - CLI.CmdCon _ -> True - CLI.CmdRun ko [(_,_,daemon)] -> not daemon - CLI.CmdRun ko _ -> False - _ -> False + -- If the user hasn't specified where to log, what we do depends on what + -- command she has issued. Notably, the LogFile Nothing outcome means that + -- runKingEnvLogFile should run an IO action to get the official app data + -- directory and open a canonically named log file there. + logTarget :: Maybe (CLI.LogTarget FilePath) + -> CLI.Cmd + -> CLI.LogTarget (Maybe FilePath) + logTarget = \case + Just (CLI.LogFile f) -> const $ CLI.LogFile (Just f) + Just CLI.LogStderr -> const $ CLI.LogStderr + Just CLI.LogOff -> const $ CLI.LogOff + Nothing -> \case + CLI.CmdCon _ -> CLI.LogFile Nothing + CLI.CmdRun ko [(_,_,daemon)] | daemon -> CLI.LogStderr + | otherwise -> CLI.LogFile Nothing + CLI.CmdRun ko _ -> CLI.LogStderr + _ -> CLI.LogStderr {- @@ -674,15 +704,15 @@ main = do TODO Use logging system instead of printing. -} runShipRestarting - :: CLI.Run -> CLI.Opts -> MultiEyreApi -> RIO KingEnv () -runShipRestarting r o multi = do + :: CLI.Run -> CLI.Opts -> RIO HostEnv () +runShipRestarting r o = do let pier = pack (CLI.rPierPath r) - loop = runShipRestarting r o multi + loop = runShipRestarting r o onKill <- view onKillKingSigL vKillPier <- newEmptyTMVarIO - tid <- asyncBound $ runShipEnv r o vKillPier $ runShip r o True multi + tid <- asyncBound $ runShipEnv r o vKillPier $ runShip r o True let onShipExit = Left <$> waitCatchSTM tid onKillRequ = Right <$> onKill @@ -699,7 +729,7 @@ runShipRestarting r o multi = do logTrace $ display (pier <> " shutdown requested") race_ (wait tid) $ do threadDelay 5_000_000 - logDebug $ display (pier <> " not down after 5s, killing with fire.") + logInfo $ display (pier <> " not down after 5s, killing with fire.") cancel tid logTrace $ display ("Ship terminated: " <> pier) @@ -707,10 +737,11 @@ runShipRestarting r o multi = do TODO This is messy and shared a lot of logic with `runShipRestarting`. -} runShipNoRestart - :: CLI.Run -> CLI.Opts -> Bool -> MultiEyreApi -> RIO KingEnv () -runShipNoRestart r o d multi = do - vKill <- view kingEnvKillSignal -- killing ship same as killing king - tid <- asyncBound (runShipEnv r o vKill $ runShip r o d multi) + :: CLI.Run -> CLI.Opts -> Bool -> RIO HostEnv () +runShipNoRestart r o d = do + -- killing ship same as killing king + vKill <- view (kingEnvL . kingEnvKillSignal) + tid <- asyncBound (runShipEnv r o vKill $ runShip r o d) onKill <- view onKillKingSigL let pier = pack (CLI.rPierPath r) @@ -731,40 +762,32 @@ runShipNoRestart r o d multi = do cancel tid logTrace $ display (pier <> " terminated.") -runShips :: CLI.KingOpts -> [(CLI.Run, CLI.Opts, Bool)] -> RIO KingEnv () -runShips CLI.KingOpts {..} ships = do +runShips :: CLI.Host -> [(CLI.Run, CLI.Opts, Bool)] -> RIO KingEnv () +runShips CLI.Host {..} ships = do let meConf = MultiEyreConf - { mecHttpPort = fromIntegral <$> koSharedHttpPort - , mecHttpsPort = fromIntegral <$> koSharedHttpsPort + { mecHttpPort = fromIntegral <$> hSharedHttpPort + , mecHttpsPort = fromIntegral <$> hSharedHttpsPort , mecLocalhostOnly = False -- TODO Localhost-only needs to be -- a king-wide option. } - - {- - TODO Need to rework RIO environment to fix this. Should have a - bunch of nested contexts: - - - King has started. King has Id. Logging available. - - In running environment. MultiEyre and global config available. - - In pier environment: pier path and config available. - - In running ship environment: serf state, event queue available. - -} multi <- multiEyre meConf - go multi ships + ports <- buildPortHandler hUseNatPmp + + runHostEnv multi ports (go ships) where - go :: MultiEyreApi -> [(CLI.Run, CLI.Opts, Bool)] -> RIO KingEnv () - go me = \case + go :: [(CLI.Run, CLI.Opts, Bool)] -> RIO HostEnv () + go = \case [] -> pure () - [rod] -> runSingleShip rod me - ships -> runMultipleShips (ships <&> \(r, o, _) -> (r, o)) me + [rod] -> runSingleShip rod + ships -> runMultipleShips (ships <&> \(r, o, _) -> (r, o)) -- TODO Duplicated logic. -runSingleShip :: (CLI.Run, CLI.Opts, Bool) -> MultiEyreApi -> RIO KingEnv () -runSingleShip (r, o, d) multi = do - shipThread <- async (runShipNoRestart r o d multi) +runSingleShip :: (CLI.Run, CLI.Opts, Bool) -> RIO HostEnv () +runSingleShip (r, o, d) = do + shipThread <- async (runShipNoRestart r o d) {- Wait for the ship to go down. @@ -784,10 +807,10 @@ runSingleShip (r, o, d) multi = do pure () -runMultipleShips :: [(CLI.Run, CLI.Opts)] -> MultiEyreApi -> RIO KingEnv () -runMultipleShips ships multi = do +runMultipleShips :: [(CLI.Run, CLI.Opts)] -> RIO HostEnv () +runMultipleShips ships = do shipThreads <- for ships $ \(r, o) -> do - async (runShipRestarting r o multi) + async (runShipRestarting r o) {- Since `spin` never returns, this will run until the main diff --git a/pkg/hs/urbit-king/lib/Urbit/Prelude.hs b/pkg/hs/urbit-king/lib/Urbit/Prelude.hs index a19d44cc5..eee5cef16 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Prelude.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Prelude.hs @@ -34,10 +34,9 @@ import Text.Show.Pretty (pPrint, ppShow) import RIO (RIO, runRIO) import RIO (Utf8Builder, display, displayShow) import RIO (threadDelay) - -import RIO (HasLogFunc, LogFunc, logDebug, logError, logFuncL, logInfo, - logOptionsHandle, logOther, logWarn, mkLogFunc, setLogUseLoc, - setLogUseTime, withLogFunc) +import RIO (HasLogFunc, LogFunc, LogLevel(..), logDebug, logError, logFuncL, + logInfo, logOptionsHandle, logOther, logWarn, mkLogFunc, + setLogMinLevel, setLogUseLoc, setLogUseTime, withLogFunc) io :: MonadIO m => IO a -> m a io = liftIO diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Ames.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Ames.hs index 146389810..5065cb4ac 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Ames.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Ames.hs @@ -10,6 +10,7 @@ import Network.Socket hiding (recvFrom, sendTo) import Urbit.Arvo hiding (Fake) import Urbit.King.Config import Urbit.Vere.Pier.Types +import Urbit.Vere.Ports import Urbit.King.App (HasKingId(..), HasPierEnv(..)) import Urbit.Vere.Ames.DNS (NetworkMode(..), ResolvServ(..)) @@ -105,7 +106,10 @@ udpPort isFake who = do mPort <- view (networkConfigL . ncAmesPort) pure $ maybe (listenPort mode who) fromIntegral mPort -udpServ :: (HasLogFunc e, HasNetworkConfig e) => Bool -> Ship -> RIO e UdpServ +udpServ :: (HasLogFunc e, HasNetworkConfig e, HasPortControlApi e) + => Bool + -> Ship + -> RIO e UdpServ udpServ isFake who = do mode <- netMode isFake port <- udpPort isFake who @@ -170,7 +174,7 @@ ames' who isFake stderr = do -} ames :: forall e - . (HasLogFunc e, HasNetworkConfig e, HasKingId e) + . (HasLogFunc e, HasNetworkConfig e, HasPortControlApi e, HasKingId e) => e -> Ship -> Bool @@ -229,7 +233,7 @@ ames env who isFake enqueueEv stderr = (initialEvents, runAmes) NewtEfSend (_id, ()) dest (MkBytes bs) -> do atomically (readTVar aTurfs) >>= \case - Nothing -> pure () + Nothing -> stderr "ames: send before turfs" >> pure () Just turfs -> sendPacket drv mode dest bs sendPacket :: AmesDrv -> NetworkMode -> AmesDest -> ByteString -> RIO e () diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Ames/DNS.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Ames/DNS.hs index f16f6a55a..25eccdc4b 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Ames/DNS.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Ames/DNS.hs @@ -131,11 +131,11 @@ doResolv gal (prevWen, prevIP) turfs stderr = do io (resolv gal turfs) >>= \case Nothing -> do stderr $ "ames: czar at " ++ galStr ++ ": not found" - logDebug $ displayShow ("(ames) Failed to lookup IP for ", gal) + logInfo $ displayShow ("(ames) Failed to lookup IP for ", gal) pure (prevIP, tim) Just (turf, host, port, addr) -> do when (Just addr /= prevIP) (printCzar addr) - logDebug $ displayShow ("(ames) Looked up ", host, port, turf, addr) + logInfo $ displayShow ("(ames) Looked up ", host, port, turf, addr) pure (Just addr, tim) where galStr = renderGalaxy gal @@ -155,7 +155,7 @@ resolvWorker resolvWorker gal vTurfs vLast waitMsg send stderr = async (forever go) where logDrop = - logDebug $ displayShow ("(ames) Dropping packet; no ip for galaxy ", gal) + logInfo $ displayShow ("(ames) Dropping packet; no ip for galaxy ", gal) go :: RIO e () go = do diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Ames/UDP.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Ames/UDP.hs index 47a9b24fb..75014dd0d 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Ames/UDP.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Ames/UDP.hs @@ -33,6 +33,7 @@ module Urbit.Vere.Ames.UDP where import Urbit.Prelude +import Urbit.Vere.Ports import Network.Socket hiding (recvFrom, sendTo) @@ -79,14 +80,14 @@ forceBind :: HasLogFunc e => PortNumber -> HostAddress -> RIO e Socket forceBind por hos = go where go = do - logDebug (display ("AMES: UDP: Opening socket on port " <> tshow por)) + logInfo (display ("AMES: UDP: Opening socket on port " <> tshow por)) io (doBind por hos) >>= \case Right sk -> do - logDebug (display ("AMES: UDP: Opened socket on port " <> tshow por)) + logInfo (display ("AMES: UDP: Opened socket on port " <> tshow por)) pure sk Left err -> do - logDebug (display ("AMES: UDP: " <> tshow err)) - logDebug ("AMES: UDP: Failed to open UDP socket. Waiting") + logInfo (display ("AMES: UDP: " <> tshow err)) + logInfo ("AMES: UDP: Failed to open UDP socket. Waiting") threadDelay 250_000 go @@ -137,7 +138,7 @@ recvPacket sok = do -} fakeUdpServ :: HasLogFunc e => RIO e UdpServ fakeUdpServ = do - logDebug $ displayShow ("AMES", "UDP", "\"Starting\" fake UDP server.") + logInfo $ displayShow ("AMES", "UDP", "\"Starting\" fake UDP server.") pure UdpServ { .. } where usSend = \_ _ -> pure () @@ -151,9 +152,13 @@ fakeUdpServ = do Real UDP server. See module-level docs. -} realUdpServ - :: forall e . HasLogFunc e => PortNumber -> HostAddress -> RIO e UdpServ + :: forall e + . (HasLogFunc e, HasPortControlApi e) + => PortNumber + -> HostAddress + -> RIO e UdpServ realUdpServ por hos = do - logDebug $ displayShow ("AMES", "UDP", "Starting real UDP server.") + logInfo $ displayShow ("AMES", "UDP", "Starting real UDP server.") env <- ask @@ -173,7 +178,7 @@ realUdpServ por hos = do -} let signalBrokenSocket :: Socket -> RIO e () signalBrokenSocket sock = do - logDebug $ displayShow ("AMES", "UDP" + logInfo $ displayShow ("AMES", "UDP" , "Socket broken. Requesting new socket" ) atomically $ do @@ -197,11 +202,21 @@ realUdpServ por hos = do logWarn "AMES: UDP: Dropping outbound packet because queue is full." tOpen <- async $ forever $ do - sk <- forceBind por hos - atomically (writeTVar vSock (Just sk)) - broken <- atomically (takeTMVar vFail) - logWarn "AMES: UDP: Closing broken socket." - io (close broken) + sk <- forceBind por hos + sn <- io $ getSocketName sk + + let waitForRelease = do + atomically (writeTVar vSock (Just sk)) + broken <- atomically (takeTMVar vFail) + logWarn "AMES: UDP: Closing broken socket." + io (close broken) + + case sn of + (SockAddrInet boundPort _) -> + -- When we're on IPv4, maybe port forward at the NAT. + rwith (requestPortAccess $ fromIntegral boundPort) $ + \() -> waitForRelease + _ -> waitForRelease tSend <- async $ forever $ join $ atomically $ do (adr, byt) <- readTBQueue qSend @@ -227,11 +242,11 @@ realUdpServ por hos = do enqueueRecvPacket p a b let shutdown = do - logDebug "AMES: UDP: Shutting down. (killing threads)" + logInfo "AMES: UDP: Shutting down. (killing threads)" cancel tOpen cancel tSend cancel tRecv - logDebug "AMES: UDP: Shutting down. (closing socket)" + logInfo "AMES: UDP: Shutting down. (closing socket)" io $ join $ atomically $ do res <- readTVar vSock <&> maybe (pure ()) close writeTVar vSock Nothing diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Clay.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Clay.hs index 6b8272266..ae626ed08 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Clay.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Clay.hs @@ -163,7 +163,7 @@ clay env plan = handleEffect :: ClayDrv -> SyncEf -> IO () handleEffect cd = runRIO env . \case SyncEfHill _ mountPoints -> do - logDebug $ displayShow ("(clay) known mount points:", mountPoints) + logInfo $ displayShow ("(clay) known mount points:", mountPoints) pierPath <- view pierPathL mountPairs <- flip mapM mountPoints $ \desk -> do ss <- takeFilesystemSnapshot (pierPath (deskToPath desk)) @@ -171,14 +171,14 @@ clay env plan = atomically $ writeTVar (cdMountPoints cd) (M.fromList mountPairs) SyncEfDirk p desk -> do - logDebug $ displayShow ("(clay) dirk:", p, desk) + logInfo $ displayShow ("(clay) dirk:", p, desk) m <- atomically $ readTVar (cdMountPoints cd) let snapshot = M.findWithDefault M.empty desk m pierPath <- view pierPathL let dir = pierPath deskToPath desk actions <- buildActionListFromDifferences dir snapshot - logDebug $ displayShow ("(clay) dirk actions: ", actions) + logInfo $ displayShow ("(clay) dirk actions: ", actions) let !intoList = map (actionsToInto dir) actions @@ -196,7 +196,7 @@ clay env plan = (applyActionsToMountPoints desk actions) SyncEfErgo p desk actions -> do - logDebug $ displayShow ("(clay) ergo:", p, desk, actions) + logInfo $ displayShow ("(clay) ergo:", p, desk, actions) m <- atomically $ readTVar (cdMountPoints cd) let mountPoint = M.findWithDefault M.empty desk m @@ -211,7 +211,7 @@ clay env plan = (applyActionsToMountPoints desk hashedActions) SyncEfOgre p desk -> do - logDebug $ displayShow ("(clay) ogre:", p, desk) + logInfo $ displayShow ("(clay) ogre:", p, desk) pierPath <- view pierPathL removeDirectoryRecursive $ pierPath deskToPath desk atomically $ modifyTVar (cdMountPoints cd) (M.delete desk) @@ -229,13 +229,13 @@ clay env plan = performAction :: (Map FilePath Int) -> (FilePath, Maybe (Mime, Int)) -> RIO e () performAction m (fp, Nothing) = do - logDebug $ displayShow ("(clay) deleting file ", fp) + logInfo $ displayShow ("(clay) deleting file ", fp) removeFile fp performAction m (fp, Just ((Mime _ (File (Octs bs)), hash))) - | skip = logDebug $ + | skip = logInfo $ displayShow ("(clay) skipping unchanged file update " , fp) | otherwise = do - logDebug $ displayShow ("(clay) updating file " , fp) + logInfo $ displayShow ("(clay) updating file " , fp) createDirectoryIfMissing True $ takeDirectory fp writeFile fp bs where diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Dawn.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Dawn.hs index ae79088da..7c3ffc7c6 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Dawn.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Dawn.hs @@ -2,54 +2,60 @@ Use etherium to access PKI information. -} -module Urbit.Vere.Dawn where +module Urbit.Vere.Dawn ( dawnVent + , dawnCometList + , renderShip + , mineComet + -- Used only in testing + , mix + , shas + , shaf + , cometFingerprintBS + , cometFingerprint + ) where import Urbit.Arvo.Common import Urbit.Arvo.Event hiding (Address) -import Urbit.Prelude hiding (Call, rights, to) +import Urbit.Prelude hiding (Call, rights, to, (.=)) +import Data.Bifunctor (bimap) import Data.Bits (xor) import Data.List (nub) import Data.Text (splitOn) -import Network.Ethereum.Account -import Network.Ethereum.Api.Eth -import Network.Ethereum.Api.Provider -import Network.Ethereum.Api.Types hiding (blockNumber) -import Network.Ethereum.Web3 -import Network.HTTP.Client.TLS +import Data.Aeson +import Data.HexString +import Numeric (showHex) import qualified Crypto.Hash.SHA256 as SHA256 import qualified Crypto.Hash.SHA512 as SHA512 import qualified Crypto.Sign.Ed25519 as Ed import qualified Data.Binary as B -import qualified Data.ByteArray as BA import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as C -import qualified Network.Ethereum.Ens as Ens +import qualified Data.ByteString.Lazy as L import qualified Network.HTTP.Client as C -import qualified Urbit.Azimuth as AZ import qualified Urbit.Ob as Ob +import qualified Network.HTTP.Client.TLS as TLS +import qualified Network.HTTP.Types as HT + -- During boot, use the infura provider -provider = HttpProvider - "https://mainnet.infura.io/v3/196a7f37c7d54211b4a07904ec73ad87" +provider :: String +provider = "http://eth-mainnet.urbit.org:8545" + +-- The address of the azimuth contract as a string. +azimuthAddr :: Text +azimuthAddr = "0x223c067f8cf28ae173ee5cafea60ca44c335fecb" -- Conversion Utilities -------------------------------------------------------- --- Takes the web3's bytes representation and changes the endianness. -bytes32ToBS :: BytesN 32 -> ByteString -bytes32ToBS = reverse . BA.pack . BA.unpack +passFromBS :: ByteString -> ByteString -> ByteString -> Pass +passFromBS enc aut sut + | bytesAtom sut /= 1 = Pass (Ed.PublicKey mempty) (Ed.PublicKey mempty) + | otherwise = Pass (Ed.PublicKey aut) (Ed.PublicKey enc) -toBloq :: Quantity -> Bloq -toBloq = fromIntegral . unQuantity - -passFromEth :: BytesN 32 -> BytesN 32 -> UIntN 32 -> Pass -passFromEth enc aut sut | sut /= 1 = - Pass (Ed.PublicKey mempty) (Ed.PublicKey mempty) -passFromEth enc aut sut = - Pass (decode aut) (decode enc) - where - decode = Ed.PublicKey . bytes32ToBS +bsToBool :: ByteString -> Bool +bsToBool bs = bytesAtom bs == 1 clanFromShip :: Ship -> Ob.Class clanFromShip = Ob.clan . Ob.patp . fromIntegral @@ -60,6 +66,13 @@ shipSein = Ship . fromIntegral . Ob.fromPatp . Ob.sein . Ob.patp . fromIntegral renderShip :: Ship -> Text renderShip = Ob.renderPatp . Ob.patp . fromIntegral +hexStrToAtom :: Text -> Atom +hexStrToAtom = + bytesAtom . reverse . toBytes . hexString . removePrefix . encodeUtf8 + +onLeft :: (a -> b) -> Either a c -> Either b c +onLeft fun = bimap fun id + -- Data Validation ------------------------------------------------------------- -- Derive public key structure from the key derivation seed structure @@ -73,85 +86,261 @@ ringToPass Ring{..} = Pass{..} Nothing -> error "Invalid seed passed to createKeypairFromSeed" Just x -> x +-- JSONRPC Functions ----------------------------------------------------------- + +-- The big problem here is that we can't really use the generated web3 wrappers +-- around the azimuth contracts, especially for the galaxy table request. They +-- make multiple rpc invocations per galaxy request (which aren't even +-- batched!), while Vere built a single batched rpc call to fetch the entire +-- galaxy table. +-- +-- The included Network.JsonRpc.TinyClient that Network.Web3 embeds can't do +-- batches, so calling that directly is out. +-- +-- Network.JSONRPC appears to not like something about the JSON that Infura +-- returns; it's just hanging? Also no documentation. +-- +-- So, like with Vere, we roll our own. + +dawnSendHTTP :: String -> L.ByteString -> RIO e (Either Int L.ByteString) +dawnSendHTTP endpoint requestData = liftIO do + manager <- C.newManager TLS.tlsManagerSettings + + initialRequest <- C.parseRequest endpoint + let request = initialRequest + { C.method = "POST" + , C.requestBody = C.RequestBodyLBS $ requestData + , C.requestHeaders = [("Accept", "application/json"), + ("Content-Type", "application/json"), + ("Charsets", "utf-8")] + } + + response <- C.httpLbs request manager + + -- Return body if 200. + let code = HT.statusCode $ C.responseStatus response + case code of + 200 -> pure $ Right $ C.responseBody response + _ -> pure $ Left code + +class RequestMethod m where + getRequestMethod :: m -> Text + +data RawResponse = RawResponse + { rrId :: Int + , rrResult :: Text + } + deriving (Show) + +instance FromJSON RawResponse where + parseJSON = withObject "Response" $ \v -> do + rrId <- v .: "id" + rrResult <- v .: "result" + pure RawResponse{..} + + +-- Given a list of methods and parameters, return a list of decoded responses. +dawnPostRequests :: forall req e resp + . (ToJSON req, RequestMethod req) + => String + -> (req -> Text -> resp) + -> [req] + -> RIO e [resp] +dawnPostRequests endpoint responseBuilder requests = do + -- Encode our input requests + let requestPayload = + encode $ Array $ fromList $ fmap toFullRequest $ zip [0..] requests + + -- Send to the server + responses <- dawnSendHTTP endpoint requestPayload >>= \case + Left err -> error $ "error fetching " <> provider <> ": HTTP " <> (show err) + Right x -> pure x + + -- Get a list of the result texts in the order of the submitted requests + rawSorted <- case decode responses of + Nothing -> error $ "couldn't decode json" + Just x -> pure $ map rrResult $ sortOn rrId x + + -- Build the final result structure by calling the passed in builder with the + -- request (some outputs need data from the request structure, eitherwise, + -- we'd lean on FromJSON). + let results = map (uncurry responseBuilder) (zip requests rawSorted) + pure results + + where + toFullRequest :: (Int, req) -> Value + toFullRequest (rid, req) = object [ "jsonrpc" .= ("2.0" :: Text) + , "method" .= getRequestMethod req + , "params" .= req + , "id" .= rid + ] + +-- Azimuth JSON Requests ------------------------------------------------------- + +-- Not a full implementation of the Ethereum ABI, but just the ability to call +-- a method by encoded id (like 0x63fa9a87 for `points(uint32)`), and a single +-- UIntN 32 parameter. +encodeCall :: Text -> Int -> Text +encodeCall method idx = method <> leadingZeroes <> renderedNumber + where + renderedNumber = pack $ showHex idx "" + leadingZeroes = replicate (64 - length renderedNumber) '0' + +data BlockRequest = BlockRequest + deriving (Show, Eq) + +instance RequestMethod BlockRequest where + getRequestMethod BlockRequest = "eth_blockNumber" + +instance ToJSON BlockRequest where + toJSON BlockRequest = Array $ fromList [] + +-- No need to parse, it's already in the format we'll pass as an argument to +-- eth calls which take a block number. +parseBlockRequest :: BlockRequest -> Text -> TextBlockNum +parseBlockRequest _ txt = txt + +type TextBlockNum = Text + +data PointRequest = PointRequest + { grqHexBlockNum :: TextBlockNum + , grqPointId :: Int + } deriving (Show, Eq) + +instance RequestMethod PointRequest where + getRequestMethod PointRequest{..} = "eth_call" + +instance ToJSON PointRequest where + -- 0x63fa9a87 is the points(uint32) call. + toJSON PointRequest{..} = + Array $ fromList [object [ "to" .= azimuthAddr + , "data" .= encodeCall "0x63fa9a87" grqPointId], + String grqHexBlockNum + ] + +parseAndChunkResultToBS :: Text -> [ByteString] +parseAndChunkResultToBS result = + map reverse $ + chunkBytestring 32 $ + toBytes $ + hexString $ + removePrefix $ + encodeUtf8 result + +-- The incoming result is a text bytestring. We need to take that text, and +-- spit out the parsed data. +-- +-- We're sort of lucky here. After removing the front "0x", we can just chop +-- the incoming text string into 10 different 64 character chunks and then +-- parse them as numbers. +parseEthPoint :: PointRequest -> Text -> EthPoint +parseEthPoint PointRequest{..} result = EthPoint{..} + where + [rawEncryptionKey, + rawAuthenticationKey, + rawHasSponsor, + rawActive, + rawEscapeRequested, + rawSponsor, + rawEscapeTo, + rawCryptoSuite, + rawKeyRevision, + rawContinuityNum] = parseAndChunkResultToBS result + + escapeState = if bsToBool rawEscapeRequested + then Just $ Ship $ fromIntegral $ bytesAtom rawEscapeTo + else Nothing + + -- Vere doesn't set ownership information, neither did the old Dawn.hs + -- implementation. + epOwn = (0, 0, 0, 0) + + epNet = if not $ bsToBool rawActive + then Nothing + else Just + ( fromIntegral $ bytesAtom rawKeyRevision + , passFromBS rawEncryptionKey rawAuthenticationKey rawCryptoSuite + , fromIntegral $ bytesAtom rawContinuityNum + , (bsToBool rawHasSponsor, + Ship (fromIntegral $ bytesAtom rawSponsor)) + , escapeState + ) + + -- I don't know what this is supposed to be, other than the old Dawn.hs and + -- dawn.c do the same thing. + epKid = case clanFromShip (Ship $ fromIntegral grqPointId) of + Ob.Galaxy -> Just (0, setToHoonSet mempty) + Ob.Star -> Just (0, setToHoonSet mempty) + _ -> Nothing + +-- Preprocess data from a point request into the form used in the galaxy table. +parseGalaxyTableEntry :: PointRequest -> Text -> (Ship, (Rift, Life, Pass)) +parseGalaxyTableEntry PointRequest{..} result = (ship, (rift, life, pass)) + where + [rawEncryptionKey, + rawAuthenticationKey, + _, _, _, _, _, + rawCryptoSuite, + rawKeyRevision, + rawContinuityNum] = parseAndChunkResultToBS result + + ship = Ship $ fromIntegral grqPointId + rift = fromIntegral $ bytesAtom rawContinuityNum + life = fromIntegral $ bytesAtom rawKeyRevision + pass = passFromBS rawEncryptionKey rawAuthenticationKey rawCryptoSuite + +removePrefix :: ByteString -> ByteString +removePrefix withOhEx + | prefix == "0x" = suffix + | otherwise = error "not prefixed with 0x" + where + (prefix, suffix) = splitAt 2 withOhEx + +chunkBytestring :: Int -> ByteString -> [ByteString] +chunkBytestring size bs + | null rest = [cur] + | otherwise = (cur : chunkBytestring size rest) + where + (cur, rest) = splitAt size bs + +data TurfRequest = TurfRequest + { trqHexBlockNum :: TextBlockNum + , trqTurfId :: Int + } deriving (Show, Eq) + +instance RequestMethod TurfRequest where + getRequestMethod TurfRequest{..} = "eth_call" + +instance ToJSON TurfRequest where + -- 0xeccc8ff1 is the dnsDomains(uint32) call. + toJSON TurfRequest{..} = + Array $ fromList [object [ "to" .= azimuthAddr + , "data" .= encodeCall "0xeccc8ff1" trqTurfId], + String trqHexBlockNum + ] + +-- This is another hack instead of a full Ethereum ABI response. +parseTurfResponse :: TurfRequest -> Text -> Turf +parseTurfResponse a raw = turf + where + without0x = removePrefix $ encodeUtf8 raw + (_, blRest) = splitAt 64 without0x + (utfLenStr, utfStr) = splitAt 64 blRest + utfLen = fromIntegral $ bytesAtom $ reverse $ toBytes $ hexString utfLenStr + dnsStr = decodeUtf8 $ BS.take utfLen $ toBytes $ hexString utfStr + turf = Turf $ fmap Cord $ reverse $ splitOn "." dnsStr + -- Azimuth Functions ----------------------------------------------------------- --- Perform a request to azimuth at a certain block number -withAzimuth :: Quantity - -> Address - -> DefaultAccount Web3 a - -> Web3 a -withAzimuth bloq azimuth action = - withAccount () $ - withParam (to .~ azimuth) $ - withParam (block .~ BlockWithNumber bloq) - action +retrievePoint :: String -> TextBlockNum -> Ship -> RIO e EthPoint +retrievePoint endpoint block ship = + dawnPostRequests provider parseEthPoint + [PointRequest block (fromIntegral ship)] >>= \case + [x] -> pure x + _ -> error "JSON server returned multiple return values." --- Retrieves the EthPoint information for an individual point. -retrievePoint :: Quantity -> Address -> Ship -> Web3 EthPoint -retrievePoint bloq azimuth ship = - withAzimuth bloq azimuth $ do - (encryptionKey, - authenticationKey, - hasSponsor, - active, - escapeRequested, - sponsor, - escapeTo, - cryptoSuite, - keyRevision, - continuityNum) <- AZ.points (fromIntegral ship) - - let escapeState = if escapeRequested - then Just $ Ship $ fromIntegral escapeTo - else Nothing - - -- The hoon version also sets this to all 0s and then does nothing with it. - let epOwn = (0, 0, 0, 0) - - let epNet = if not active - then Nothing - else Just - ( fromIntegral keyRevision - , passFromEth encryptionKey authenticationKey cryptoSuite - , fromIntegral continuityNum - , (hasSponsor, Ship (fromIntegral sponsor)) - , escapeState - ) - - -- TODO: wtf? - let epKid = case clanFromShip ship of - Ob.Galaxy -> Just (0, setToHoonSet mempty) - Ob.Star -> Just (0, setToHoonSet mempty) - _ -> Nothing - - pure EthPoint{..} - --- Retrieves information about all the galaxies from Ethereum. -retrieveGalaxyTable :: Quantity -> Address -> Web3 (Map Ship (Rift, Life, Pass)) -retrieveGalaxyTable bloq azimuth = - withAzimuth bloq azimuth $ mapFromList <$> mapM getRow [0..255] - where - getRow idx = do - (encryptionKey, authenticationKey, _, _, _, _, _, cryptoSuite, - keyRev, continuity) <- AZ.points idx - pure ( fromIntegral idx - , ( fromIntegral continuity - , fromIntegral keyRev - , passFromEth encryptionKey authenticationKey cryptoSuite - ) - ) - --- Reads the three Ames domains from Ethereum, removing duplicates -readAmesDomains :: Quantity -> Address -> Web3 [Turf] -readAmesDomains bloq azimuth = - withAzimuth bloq azimuth $ nub <$> mapM getTurf [0..2] - where - getTurf idx = - Turf . fmap Cord . reverse . splitOn "." <$> AZ.dnsDomains idx - - -validateShipAndGetImmediateSponsor :: Quantity -> Address -> Seed -> Web3 Ship -validateShipAndGetImmediateSponsor block azimuth (Seed ship life ring oaf) = +validateShipAndGetSponsor :: String -> TextBlockNum -> Seed -> RIO e Ship +validateShipAndGetSponsor endpoint block (Seed ship life ring oaf) = case clanFromShip ship of Ob.Comet -> validateComet Ob.Moon -> validateMoon @@ -161,7 +350,7 @@ validateShipAndGetImmediateSponsor block azimuth (Seed ship life ring oaf) = -- A comet address is the fingerprint of the keypair let shipFromPass = cometFingerprint $ ringToPass ring when (ship /= shipFromPass) $ - fail ("comet name doesn't match fingerprint " ++ show ship ++ " vs " ++ + fail ("comet name doesn't match fingerprint " <> show ship <> " vs " <> show shipFromPass) when (life /= 1) $ fail ("comet can never be re-keyed") @@ -174,15 +363,15 @@ validateShipAndGetImmediateSponsor block azimuth (Seed ship life ring oaf) = pure $ shipSein ship validateRest = do - putStrLn ("boot: retrieving " ++ renderShip ship ++ "'s public keys") + putStrLn ("boot: retrieving " <> renderShip ship <> "'s public keys") - whoP <- retrievePoint block azimuth ship + whoP <- retrievePoint endpoint block ship case epNet whoP of Nothing -> fail "ship not keyed" Just (netLife, pass, contNum, (hasSponsor, who), _) -> do when (netLife /= life) $ - fail ("keyfile life mismatch; keyfile claims life " ++ - show life ++ ", but Azimuth claims life " ++ + fail ("keyfile life mismatch; keyfile claims life " <> + show life <> ", but Azimuth claims life " <> show netLife) when ((ringToPass ring) /= pass) $ fail "keyfile does not match blockchain" @@ -193,62 +382,67 @@ validateShipAndGetImmediateSponsor block azimuth (Seed ship life ring oaf) = -- Walk through the sponsorship chain retrieving the actual sponsorship chain -- as it exists on Ethereum. -getSponsorshipChain :: Quantity -> Address -> Ship -> Web3 [(Ship,EthPoint)] -getSponsorshipChain block azimuth = loop +getSponsorshipChain :: String -> TextBlockNum -> Ship -> RIO e [(Ship,EthPoint)] +getSponsorshipChain endpoint block = loop where loop ship = do - putStrLn ("boot: retrieving keys for sponsor " ++ renderShip ship) - ethPoint <- retrievePoint block azimuth ship + putStrLn ("boot: retrieving keys for sponsor " <> renderShip ship) + ethPoint <- retrievePoint endpoint block ship case (clanFromShip ship, epNet ethPoint) of (Ob.Comet, _) -> fail "Comets cannot be sponsors" (Ob.Moon, _) -> fail "Moons cannot be sponsors" (_, Nothing) -> - fail $ unpack ("Ship " ++ renderShip ship ++ " not booted") + fail $ unpack ("Ship " <> renderShip ship <> " not booted") (Ob.Galaxy, Just _) -> pure [(ship, ethPoint)] (_, Just (_, _, _, (False, _), _)) -> - fail $ unpack ("Ship " ++ renderShip ship ++ " has no sponsor") + fail $ unpack ("Ship " <> renderShip ship <> " has no sponsor") (_, Just (_, _, _, (True, sponsor), _)) -> do chain <- loop sponsor - pure $ chain ++ [(ship, ethPoint)] - + pure $ chain <> [(ship, ethPoint)] -- Produces either an error or a validated boot event structure. -dawnVent :: Seed -> RIO e (Either Text Dawn) -dawnVent dSeed@(Seed ship life ring oaf) = do - ret <- runWeb3' provider $ do - block <- blockNumber - putStrLn ("boot: ethereum block #" ++ tshow block) +dawnVent :: HasLogFunc e => Seed -> RIO e (Either Text Dawn) +dawnVent dSeed@(Seed ship life ring oaf) = + -- The type checker can't figure this out on its own. + (onLeft tshow :: Either SomeException Dawn -> Either Text Dawn) <$> try do + blockResponses + <- dawnPostRequests provider parseBlockRequest [BlockRequest] - putStrLn "boot: retrieving azimuth contract" - azimuth <- withAccount () $ Ens.resolve "azimuth.eth" + hexStrBlock <- case blockResponses of + [num] -> pure num + x -> error "Unexpected multiple returns from block # request" - immediateSponsor <- validateShipAndGetImmediateSponsor block azimuth dSeed - dSponsor <- getSponsorshipChain block azimuth immediateSponsor + let dBloq = hexStrToAtom hexStrBlock + putStrLn ("boot: ethereum block #" <> tshow dBloq) + + immediateSponsor <- validateShipAndGetSponsor provider hexStrBlock dSeed + dSponsor <- getSponsorshipChain provider hexStrBlock immediateSponsor putStrLn "boot: retrieving galaxy table" - dCzar <- mapToHoonMap <$> retrieveGalaxyTable block azimuth + dCzar <- (mapToHoonMap . mapFromList) <$> + (dawnPostRequests provider parseGalaxyTableEntry $ + map (PointRequest hexStrBlock) [0..255]) putStrLn "boot: retrieving network domains" - dTurf <- readAmesDomains block azimuth + dTurf <- nub <$> (dawnPostRequests provider parseTurfResponse $ + map (TurfRequest hexStrBlock) [0..2]) - let dBloq = toBloq block let dNode = Nothing + pure $ MkDawn{..} - case ret of - Left x -> pure $ Left $ tshow x - Right y -> pure $ Right y +-- Comet List ------------------------------------------------------------------ dawnCometList :: RIO e [Ship] dawnCometList = do -- Get the jamfile with the list of stars accepting comets right now. - manager <- io $ C.newManager tlsManagerSettings + manager <- io $ C.newManager TLS.tlsManagerSettings request <- io $ C.parseRequest "https://bootstrap.urbit.org/comet-stars.jam" response <- io $ C.httpLbs (C.setRequestCheckStatus request) manager let body = toStrict $ C.responseBody response diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre.hs index 2aeb33f63..ee6d616eb 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre.hs @@ -11,7 +11,7 @@ where import Urbit.Prelude hiding (Builder) import Urbit.Arvo hiding (ServerId, reqUrl, secure) -import Urbit.King.App (HasKingId(..), HasPierEnv(..)) +import Urbit.King.App (HasKingId(..), HasMultiEyreApi(..), HasPierEnv(..)) import Urbit.King.Config import Urbit.Vere.Eyre.Multi import Urbit.Vere.Eyre.PortsFile @@ -170,15 +170,17 @@ execRespActs (Drv v) who reqId ev = readMVar v >>= \case atomically (routeRespAct who (sLiveReqs sv) reqId act) startServ - :: (HasPierConfig e, HasLogFunc e, HasNetworkConfig e) - => MultiEyreApi - -> Ship + :: (HasPierConfig e, HasLogFunc e, HasMultiEyreApi e, HasNetworkConfig e) + => Ship -> Bool -> HttpServerConf -> (EvErr -> STM ()) + -> (Text -> RIO e ()) -> RIO e Serv -startServ multi who isFake conf plan = do - logDebug (displayShow ("EYRE", "startServ")) +startServ who isFake conf plan stderr = do + logInfo (displayShow ("EYRE", "startServ")) + + multi <- view multiEyreApiL let vLive = meaLive multi @@ -219,11 +221,11 @@ startServ multi who isFake conf plan = do let onKilReq :: Ship -> Word64 -> STM () onKilReq _ship = plan . cancelEv srvId . fromIntegral - logDebug (displayShow ("EYRE", "joinMultiEyre", who, mTls, mCre)) + logInfo (displayShow ("EYRE", "joinMultiEyre", who, mTls, mCre)) atomically (joinMultiEyre multi who mCre onReq onKilReq) - logDebug $ displayShow ("EYRE", "Starting loopback server") + logInfo $ displayShow ("EYRE", "Starting loopback server") lop <- serv vLive $ ServConf { scHost = soHost (pttLop ptt) , scPort = soWhich (pttLop ptt) @@ -235,7 +237,7 @@ startServ multi who isFake conf plan = do } } - logDebug $ displayShow ("EYRE", "Starting insecure server") + logInfo $ displayShow ("EYRE", "Starting insecure server") ins <- serv vLive $ ServConf { scHost = soHost (pttIns ptt) , scPort = soWhich (pttIns ptt) @@ -248,7 +250,7 @@ startServ multi who isFake conf plan = do } mSec <- for mTls $ \tls -> do - logDebug "Starting secure server" + logInfo "Starting secure server" serv vLive $ ServConf { scHost = soHost (pttSec ptt) , scPort = soWhich (pttSec ptt) @@ -269,7 +271,11 @@ startServ multi who isFake conf plan = do let por = Ports secPor insPor lopPor fil = pierPath <> "/.http.ports" - logDebug $ displayShow ("EYRE", "All Servers Started.", srvId, por, fil) + logInfo $ displayShow ("EYRE", "All Servers Started.", srvId, por, fil) + for secPor $ \p -> + stderr ("http: secure web interface live on https://localhost:" <> tshow p) + stderr ("http: web interface live on http://localhost:" <> tshow insPor) + stderr ("http: loopback live on http://localhost:" <> tshow lopPor) pure (Serv srvId conf lop ins mSec por fil vLive) @@ -281,16 +287,18 @@ _bornFailed env _ = runRIO env $ do pure () -- TODO What should this do? eyre' - :: HasPierEnv e - => MultiEyreApi - -> Ship + :: (HasPierEnv e, HasMultiEyreApi e) + => Ship -> Bool + -> (Text -> RIO e ()) -> RIO e ([Ev], RAcquire e (DriverApi HttpServerEf)) -eyre' multi who isFake = do + +eyre' who isFake stderr = do ventQ :: TQueue EvErr <- newTQueueIO env <- ask - let (bornEvs, startDriver) = eyre env multi who (writeTQueue ventQ) isFake + let (bornEvs, startDriver) = + eyre env who (writeTQueue ventQ) isFake stderr let runDriver = do diOnEffect <- startDriver @@ -315,14 +323,15 @@ eyre :: forall e . (HasPierEnv e) => e - -> MultiEyreApi -> Ship -> (EvErr -> STM ()) -> Bool + -> (Text -> RIO e ()) -> ([Ev], RAcquire e (HttpServerEf -> IO ())) -eyre env multi who plan isFake = (initialEvents, runHttpServer) +eyre env who plan isFake stderr = (initialEvents, runHttpServer) where king = fromIntegral (env ^. kingIdL) + multi = env ^. multiEyreApiL initialEvents :: [Ev] initialEvents = [bornEv king] @@ -342,10 +351,10 @@ eyre env multi who plan isFake = (initialEvents, runHttpServer) restart :: Drv -> HttpServerConf -> RIO e Serv restart (Drv var) conf = do - logDebug "Restarting http server" - let startAct = startServ multi who isFake conf plan + logInfo "Restarting http server" + let startAct = startServ who isFake conf plan stderr res <- fromEither =<< restartService var startAct kill - logDebug "Done restating http server" + logInfo "Done restating http server" pure res liveFailed _ = pure () @@ -353,11 +362,11 @@ eyre env multi who plan isFake = (initialEvents, runHttpServer) handleEf :: Drv -> HttpServerEf -> IO () handleEf drv = runRIO env . \case HSESetConfig (i, ()) conf -> do - logDebug (displayShow ("EYRE", "%set-config")) + logInfo (displayShow ("EYRE", "%set-config")) Serv {..} <- restart drv conf - logDebug (displayShow ("EYRE", "%set-config", "Sending %live")) + logInfo (displayShow ("EYRE", "%set-config", "Sending %live")) atomically $ plan (EvErr (liveEv sServId sPorts) liveFailed) - logDebug "Write ports file" + logInfo "Write ports file" io (writePortsFile sPortsFile sPorts) HSEResponse (i, req, _seq, ()) ev -> do logDebug (displayShow ("EYRE", "%response")) diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Multi.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Multi.hs index 706afb8f3..73a0c5c55 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Multi.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Multi.hs @@ -72,7 +72,7 @@ leaveMultiEyre MultiEyreApi {..} who = do multiEyre :: HasLogFunc e => MultiEyreConf -> RIO e MultiEyreApi multiEyre conf@MultiEyreConf {..} = do - logDebug (displayShow ("EYRE", "MULTI", conf)) + logInfo (displayShow ("EYRE", "MULTI", conf)) vLive <- io emptyLiveReqs >>= newTVarIO vPlan <- newTVarIO mempty @@ -96,7 +96,7 @@ multiEyre conf@MultiEyreConf {..} = do Just cb -> cb who reqId mIns <- for mecHttpPort $ \por -> do - logDebug (displayShow ("EYRE", "MULTI", "HTTP", por)) + logInfo (displayShow ("EYRE", "MULTI", "HTTP", por)) serv vLive $ ServConf { scHost = host , scPort = SPChoices $ singleton $ fromIntegral por @@ -109,7 +109,7 @@ multiEyre conf@MultiEyreConf {..} = do } mSec <- for mecHttpsPort $ \por -> do - logDebug (displayShow ("EYRE", "MULTI", "HTTPS", por)) + logInfo (displayShow ("EYRE", "MULTI", "HTTPS", por)) serv vLive $ ServConf { scHost = host , scPort = SPChoices $ singleton $ fromIntegral por diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Serv.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Serv.hs index e007d6331..0fdab349b 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Serv.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Serv.hs @@ -164,7 +164,7 @@ tryOpenChoices tryOpenChoices hos = go where go (p :| ps) = do - logDebug (displayShow ("EYRE", "Trying to open port.", p)) + logInfo (displayShow ("EYRE", "Trying to open port.", p)) io (tryOpen hos p) >>= \case Left err -> do logError (displayShow ("EYRE", "Failed to open port.", p)) @@ -185,7 +185,7 @@ tryOpenAny hos = do pure (Right (p, s)) logDbg :: (HasLogFunc e, Show a) => [Text] -> a -> RIO e () -logDbg ctx msg = logDebug (prefix <> suffix) +logDbg ctx msg = logInfo (prefix <> suffix) where prefix = display (concat $ fmap (<> ": ") ctx) suffix = displayShow msg @@ -312,7 +312,7 @@ configCreds TlsConfig {..} = fakeServ :: HasLogFunc e => ServConf -> RIO e ServApi fakeServ conf = do let por = fakePort (scPort conf) - logDebug (displayShow ("EYRE", "SERV", "Running Fake Server", por)) + logInfo (displayShow ("EYRE", "SERV", "Running Fake Server", por)) pure $ ServApi { saKil = pure () , saPor = pure por @@ -331,7 +331,7 @@ getFirstTlsConfig (MTC var) = do realServ :: HasLogFunc e => TVar E.LiveReqs -> ServConf -> RIO e ServApi realServ vLive conf@ServConf {..} = do - logDebug (displayShow ("EYRE", "SERV", "Running Real Server")) + logInfo (displayShow ("EYRE", "SERV", "Running Real Server")) kil <- newEmptyTMVarIO por <- newEmptyTMVarIO @@ -344,7 +344,7 @@ realServ vLive conf@ServConf {..} = do } where runServ vPort = do - logDebug (displayShow ("EYRE", "SERV", "runServ")) + logInfo (displayShow ("EYRE", "SERV", "runServ")) rwith (forceOpenSocket scHost scPort) $ \(por, sok) -> do atomically (putTMVar vPort por) startServer scType scHost por sok scRedi vLive diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Service.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Service.hs index ce3bc01a9..af571afc9 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Service.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Service.hs @@ -31,21 +31,21 @@ restartService -> (s -> RIO e ()) -> RIO e (Either SomeException s) restartService vServ sstart kkill = do - logDebug "restartService" + logInfo "restartService" modifyMVar vServ $ \case Nothing -> doStart Just sv -> doRestart sv where doRestart :: s -> RIO e (Maybe s, Either SomeException s) doRestart serv = do - logDebug "doStart" + logInfo "doStart" try (kkill serv) >>= \case Left exn -> pure (Nothing, Left exn) Right () -> doStart doStart :: RIO e (Maybe s, Either SomeException s) doStart = do - logDebug "doStart" + logInfo "doStart" try sstart <&> \case Right s -> (Just s, Right s) Left exn -> (Nothing, Left exn) @@ -59,7 +59,7 @@ stopService -> (s -> RIO e ()) -> RIO e (Either SomeException ()) stopService vServ kkill = do - logDebug "stopService" + logInfo "stopService" modifyMVar vServ $ \case Nothing -> pure (Nothing, Right ()) Just sv -> do diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Wai.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Wai.hs index 3815e1ca9..147c3bd93 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Wai.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Eyre/Wai.hs @@ -179,7 +179,7 @@ streamBlocks env init getAct = send init >> loop send "" = pure () send c = do - runRIO env (logTrace (display ("sending chunk " <> tshow c))) + runRIO env (logDebug (display ("sending chunk " <> tshow c))) yield $ Chunk $ fromByteString c yield Flush diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Http/Client.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Http/Client.hs index 011ce86ac..92e36145b 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Http/Client.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Http/Client.hs @@ -140,7 +140,7 @@ client env plan = (initialEvents, runHttpClient) runReq HttpClientDrv{..} id req = async $ case cvtReq req of Nothing -> do - logDebug $ displayShow ("(malformed http client request)", id, req) + logInfo $ displayShow ("(malformed http client request)", id, req) planEvent id (Cancel ()) Just r -> do logDebug $ displayShow ("(http client request)", id, req) diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/LMDB.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/LMDB.hs index 8e544647b..691e25e5c 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/LMDB.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/LMDB.hs @@ -226,7 +226,7 @@ readRowsBatch :: ∀e. HasLogFunc e readRowsBatch env dbi first = readRows where readRows = do - logDebug $ display ("(readRowsBatch) From: " <> tshow first) + logInfo $ display ("(readRowsBatch) From: " <> tshow first) withWordPtr first $ \pIdx -> withKVPtrs' (MDB_val 8 (castPtr pIdx)) nullVal $ \pKey pVal -> rwith (readTxn env) $ \txn -> diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/NounServ.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/NounServ.hs index f23f82aa3..bc31c32e9 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/NounServ.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/NounServ.hs @@ -82,7 +82,7 @@ wsConn pre inp out wsc = do flip finally cleanup $ do res <- atomically (waitCatchSTM writer <|> waitCatchSTM reader) - logDebug $ displayShow (res :: Either SomeException ()) + logInfo $ displayShow (res :: Either SomeException ()) -------------------------------------------------------------------------------- @@ -95,7 +95,7 @@ wsClient pax por = do out <- io $ newTBMChanIO 5 con <- pure (mkConn inp out) - logDebug "NOUNSERV (wsClie) Trying to connect" + logInfo "NOUNSERV (wsClie) Trying to connect" tid <- io $ async $ WS.runClient "127.0.0.1" por (unpack pax) @@ -111,7 +111,7 @@ wsServApp :: (HasLogFunc e, ToNoun o, FromNoun i, Show i, Show o) -> WS.PendingConnection -> RIO e () wsServApp cb pen = do - logDebug "NOUNSERV (wsServer) Got connection!" + logInfo "NOUNSERV (wsServer) Got connection!" wsc <- io $ WS.acceptRequest pen inp <- io $ newTBMChanIO 5 out <- io $ newTBMChanIO 5 @@ -125,10 +125,10 @@ wsServer = do tid <- async $ do env <- ask - logDebug "NOUNSERV (wsServer) Starting server" + logInfo "NOUNSERV (wsServer) Starting server" io $ WS.runServer "127.0.0.1" 9999 $ runRIO env . wsServApp (writeTBMChan con) - logDebug "NOUNSERV (wsServer) Server died" + logInfo "NOUNSERV (wsServer) Server died" atomically $ closeTBMChan con pure $ Server (readTBMChan con) tid 9999 diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Pier.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Pier.hs index 78b6d9e5d..82da7d0df 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Pier.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Pier.hs @@ -25,12 +25,13 @@ import Urbit.King.App import Urbit.Vere.Pier.Types import Control.Monad.STM (retry) +import System.Environment (getExecutablePath) +import System.FilePath (splitFileName, ()) import System.Posix.Files (ownerModes, setFileMode) import Urbit.EventLog.LMDB (EventLog) import Urbit.King.API (TermConn) import Urbit.Noun.Time (Wen) import Urbit.TermSize (TermSize(..)) -import Urbit.Vere.Eyre.Multi (MultiEyreApi) import Urbit.Vere.Serf (Serf) import qualified Data.Text as T @@ -122,17 +123,25 @@ runSerf -> RAcquire e Serf runSerf vSlog pax = do env <- ask - Serf.withSerf (config env) + serfProg <- io getSerfProg + Serf.withSerf (config env serfProg) where slog txt = atomically (readTVar vSlog) >>= (\f -> f txt) - config env = Serf.Config - { scSerf = env ^. pierConfigL . pcSerfExe . to unpack + config env serfProg = Serf.Config + { scSerf = env ^. pierConfigL . pcSerfExe . to (maybe serfProg unpack) , scPier = pax , scFlag = env ^. pierConfigL . pcSerfFlags , scSlog = \(pri, tank) -> printTank slog pri tank , scStdr = \txt -> slog (txt <> "\r\n") , scDead = pure () -- TODO: What can be done? } + getSerfProg :: IO FilePath + getSerfProg = do + (path, filename) <- splitFileName <$> getExecutablePath + pure $ case filename of + "urbit" -> path "urbit-worker" + "urbit-king" -> path "urbit-worker" + _ -> "urbit-worker" -- Boot a new ship. ------------------------------------------------------------ @@ -169,21 +178,21 @@ bootNewShip -> RIO e () bootNewShip pill lite ship bootEv = do seq@(BootSeq ident x y) <- genBootSeq ship pill lite bootEv - logDebug "BootSeq Computed" + logInfo "BootSeq Computed" pierPath <- view pierPathL rio (setupPierDirectory pierPath) - logDebug "Directory setup." + logInfo "Directory setup." let logPath = (pierPath ".urb/log") rwith (Log.new logPath ident) $ \log -> do - logDebug "Event log onitialized." + logInfo "Event log onitialized." jobs <- (\now -> bootSeqJobs now seq) <$> io Time.now writeJobs log (fromList jobs) - logDebug "Finsihed populating event log with boot sequence" + logInfo "Finsihed populating event log with boot sequence" -- Resume an existing ship. ---------------------------------------------------- @@ -207,16 +216,16 @@ resumed vSlog replayUntil = do serf <- runSerf vSlog tap rio $ do - logDebug "Replaying events" + logInfo "Replaying events" Serf.execReplay serf log replayUntil >>= \case Left err -> error (show err) Right 0 -> do - logDebug "No work during replay so no snapshot" + logInfo "No work during replay so no snapshot" pure () Right _ -> do - logDebug "Taking snapshot" + logInfo "Taking snapshot" io (Serf.snapshot serf) - logDebug "SNAPSHOT TAKEN" + logInfo "SNAPSHOT TAKEN" pure (serf, log) @@ -242,14 +251,14 @@ acquireWorker :: HasLogFunc e => Text -> RIO e () -> RAcquire e (Async ()) acquireWorker nam act = mkRAcquire (async act) kill where kill tid = do - logDebug ("Killing worker thread: " <> display nam) + logInfo ("Killing worker thread: " <> display nam) cancel tid acquireWorkerBound :: HasLogFunc e => Text -> RIO e () -> RAcquire e (Async ()) acquireWorkerBound nam act = mkRAcquire (asyncBound act) kill where kill tid = do - logDebug ("Killing worker thread: " <> display nam) + logInfo ("Killing worker thread: " <> display nam) cancel tid @@ -260,9 +269,8 @@ pier :: (Serf, EventLog) -> TVar (Text -> IO ()) -> MVar () - -> MultiEyreApi -> RAcquire PierEnv () -pier (serf, log) vSlog startedSig multi = do +pier (serf, log) vSlog startedSig = do let logId = Log.identity log :: LogIdentity let ship = who logId :: Ship @@ -285,11 +293,11 @@ pier (serf, log) vSlog startedSig multi = do pure (res, Term.useDemux res) void $ acquireWorker "TERMSERV Listener" $ forever $ do - logDebug "TERMSERV Waiting for external terminal." + logInfo "TERMSERV Waiting for external terminal." atomically $ do ext <- Term.connClient <$> readTQueue termApiQ Term.addDemux ext demux - logDebug "TERMSERV External terminal connected." + logInfo "TERMSERV External terminal connected." -- Slogs go to both stderr and to the terminal. env <- ask @@ -311,7 +319,7 @@ pier (serf, log) vSlog startedSig multi = do let err = atomically . Term.trace muxed . (<> "\r\n") let siz = TermSize { tsWide = 80, tsTall = 24 } let fak = isFake logId - drivers env multi ship fak compute (siz, muxed) err sigint + drivers env ship fak compute (siz, muxed) err sigint scrySig <- newEmptyTMVarIO onKill <- view onKillPierSigL @@ -369,7 +377,7 @@ pier (serf, log) vSlog startedSig multi = do threadDelay 15_000_000 wen <- io Time.now let kal = \mTermNoun -> runRIO env $ do - logDebug $ displayShow ("scry result: ", mTermNoun) + logInfo $ displayShow ("scry result: ", mTermNoun) let nkt = MkKnot $ tshow $ Time.MkDate wen let pax = Path ["j", "~zod", "life", nkt, "~zod"] atomically $ putTMVar scrySig (wen, Nothing, pax, kal) @@ -412,7 +420,6 @@ data Drivers = Drivers drivers :: HasPierEnv e => e - -> MultiEyreApi -> Ship -> Bool -> (RunReq -> STM ()) @@ -420,11 +427,11 @@ drivers -> (Text -> RIO e ()) -> IO () -> RAcquire e ([Ev], RAcquire e Drivers) -drivers env multi who isFake plan termSys stderr serfSIGINT = do +drivers env who isFake plan termSys stderr serfSIGINT = do (behnBorn, runBehn) <- rio Behn.behn' (termBorn, runTerm) <- rio (Term.term' termSys serfSIGINT) (amesBorn, runAmes) <- rio (Ames.ames' who isFake stderr) - (httpBorn, runEyre) <- rio (Eyre.eyre' multi who isFake) + (httpBorn, runEyre) <- rio (Eyre.eyre' who isFake stderr) (clayBorn, runClay) <- rio Clay.clay' (irisBorn, runIris) <- rio Iris.client' @@ -494,7 +501,7 @@ router slog waitFx Drivers {..} = do logEvent :: HasLogFunc e => Ev -> RIO e () logEvent ev = do - logTrace $ "<- " <> display (summarizeEvent ev) + --logInfo $ "<- " <> display (summarizeEvent ev) logDebug $ "[EVENT]\n" <> display pretty where pretty :: Text @@ -502,7 +509,7 @@ logEvent ev = do logEffect :: HasLogFunc e => Lenient Ef -> RIO e () logEffect ef = do - logTrace $ " -> " <> display (summarizeEffect ef) + --logInfo $ " -> " <> display (summarizeEffect ef) logDebug $ display $ "[EFFECT]\n" <> pretty ef where pretty :: Lenient Ef -> Text diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Ports.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Ports.hs new file mode 100644 index 000000000..024bd391f --- /dev/null +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Ports.hs @@ -0,0 +1,314 @@ +module Urbit.Vere.Ports (HasPortControlApi(..), + PortControlApi, + buildInactivePorts, + buildNatPortsWhenPrivate, + buildNatPorts, + requestPortAccess) where + +import Control.Monad.STM (check) +import Urbit.Prelude +import Network.NatPmp +import Data.Time.Clock.POSIX +import Network.Socket + +import qualified Data.Heap as DH + +-- This module deals with ports and port requests. When a component wants to +-- ensure that it is externally reachable, possibly from outside a NAT, it +-- makes a request to this module to hole-punch. + +class HasPortControlApi a where + portControlApiL :: Lens' a PortControlApi + +data PortControlApi = PortControlApi + { pAddPortRequest :: Word16 -> IO () + , pRemovePortRequest :: Word16 -> IO () + } + +-- | Builds a PortControlApi struct which does nothing when called. +buildInactivePorts :: PortControlApi +buildInactivePorts = PortControlApi noop noop + where + noop x = pure () + +-- | Builds a PortControlApi struct which tries to hole-punch by talking to the +-- NAT gateway over NAT-PMP iff we are on a private network ip. +buildNatPortsWhenPrivate :: (HasLogFunc e) + => (Text -> RIO e ()) + -> RIO e PortControlApi +buildNatPortsWhenPrivate stderr = do + behind <- likelyBehindRouter + if behind + then buildNatPorts stderr + else pure buildInactivePorts + +-- | Builds a PortControlApi struct which tries to hole-punch by talking to the +-- NAT gateway over NAT-PMP. +buildNatPorts :: (HasLogFunc e) + => (Text -> RIO e ()) + -> RIO e PortControlApi +buildNatPorts stderr = do + q <- newTQueueIO + async $ portThread q stderr + + let addRequest port = do + resp <- newEmptyTMVarIO + atomically $ + writeTQueue q (PTMOpen port (putTMVar resp True)) + atomically $ takeTMVar resp + pure () + + let removeRequest port = atomically $ writeTQueue q (PTMClose port) + + pure $ PortControlApi addRequest removeRequest + +portLeaseLifetime :: Word32 +portLeaseLifetime = 15 * 60 + +-- Be paranoid and renew leases a full minute before they would naturally expire. +portRenewalTime :: Word32 +portRenewalTime = portLeaseLifetime - 60 + +-- Number of retries before we give up on performing nat operations. +maxRetries :: Int +maxRetries = 3 + +-- How long to wait between retries. +networkRetryDelay :: Int +networkRetryDelay = 5 * 1_000_000 + +-- Messages sent from the main thread to the port mapping communication thread. +data PortThreadMsg + = PTMOpen Word16 (STM ()) + -- ^ Does the open request, and then runs the passed in stm action to + -- signal completion to the main thread. We want to block on the initial + -- setting opening because we want the forwarding set up before we actually + -- start using the port. + + | PTMClose Word16 + -- ^ Close command. No synchronization because there's nothing we can do if + -- it fails. + +-- We get requests to acquire a port as an RAII condition, but the actual APIs +-- are timeout based, so we have to maintain a heap of the next timer to +-- rerequest port access. +data RenewAction = RenewAction Word16 + +-- The port thread is an async which reads commands from an STM queue and then +-- executes them. This thread is here to bind the semantics that we want to how +-- NAT-PMP sees the world. We want for an RAcquire to be able to start a +-- request for port forwarding and then to release it when it goes out of +-- scope. OTOH, NAT-PMP is all timeout based, and we want that timeout to be +-- fairly short, such as 15 minutes, so the portThread needs to keep track of +-- the time of the next port request. +portThread :: forall e. (HasLogFunc e) + => TQueue PortThreadMsg + -> (Text -> RIO e ()) + -> RIO e () +portThread q stderr = do + initNatPmp >>= \case + Left ErrCannotGetGateway -> do + assumeOnPublicInternet + Left err -> do + likelyIPAddress >>= \case + Just ip@(192, 168, _, _) -> warnBehindRouterAndErr ip err + Just ip@(172, x, _, _) + | (x >= 16 && x <= 31) -> warnBehindRouterAndErr ip err + Just ip@(10, _, _, _) -> warnBehindRouterAndErr ip err + _ -> assumeOnPublicInternet + Right pmp -> foundRouter pmp + where + warnBehindRouterAndErr (a, b, c, d) err = do + stderr $ "port: you appear to be behind a router since your ip " ++ + "is " ++ (tshow a) ++ "." ++ (tshow b) ++ "." ++ (tshow c) ++ + "." ++ (tshow d) ++ ", but " ++ + "we could not request port forwarding (NAT-PMP error: " ++ + (tshow err) ++ ")" + stderr $ "port: urbit performance will be degregaded unless you " ++ + "manually forward your ames port." + loopErr q + + assumeOnPublicInternet = do + stderr $ "port: couldn't find router; assuming on public internet" + loopErr q + + foundRouter :: NatPmpHandle -> RIO e () + foundRouter pmp = do + getPublicAddress pmp >>= \case + Left ErrCannotGetGateway -> assumeOnPublicInternet + Left ErrNoGatewaySupport -> assumeOnPublicInternet + Left err -> do + stderr $ "port: received error when asking router for public ip: " ++ + (tshow err) + loopErr q + Right addr -> do + let (a, b, c, d) = hostAddressToTuple addr + stderr $ "port: router reports that our public IP is " ++ (tshow a) ++ + "." ++ (tshow b) ++ "." ++ (tshow c) ++ "." ++ (tshow d) + loop pmp mempty + + loop :: NatPmpHandle -> DH.MinPrioHeap POSIXTime RenewAction -> RIO e () + loop pmp nextRenew = do + now <- io $ getPOSIXTime + delay <- case DH.viewHead nextRenew of + Nothing -> newTVarIO False + Just (fireTime, _) -> do + let timeTo = fireTime - now + let ms = round $ timeTo * 1000000 + registerDelay ms + command <- atomically $ + (Left <$> fini delay) <|> (Right <$> readTQueue q) + case command of + Left () -> handleRenew pmp nextRenew + Right msg -> handlePTM pmp msg nextRenew + + handlePTM :: NatPmpHandle + -> PortThreadMsg + -> DH.MinPrioHeap POSIXTime RenewAction + -> RIO e () + handlePTM pmp msg nextRenew = case msg of + PTMOpen p notifyComplete -> do + logInfo $ + displayShow ("port: sending initial request to NAT-PMP for port ", p) + setPortMapping pmp PTUdp p p portLeaseLifetime >>= \case + Left err | isResetAndRetry err -> do + closeNatPmp pmp + attemptReestablishNatPmpThen (\pmp -> handlePTM pmp msg nextRenew) + Left err -> do + logError $ + displayShow ("port: failed to request NAT-PMP for port ", p, + ":", err, ", disabling NAT-PMP") + loopErr q + Right _ -> do + -- Filter any existing references to this port on the heap to ensure + -- we don't double up on tasks. + let filteredHeap = filterPort p nextRenew + now <- io $ getPOSIXTime + let withRenew = + DH.insert (now + fromIntegral portRenewalTime, RenewAction p) + filteredHeap + atomically notifyComplete + loop pmp withRenew + + PTMClose p -> do + logInfo $ + displayShow ("port: releasing lease for ", p) + setPortMapping pmp PTUdp p p 0 + let removed = filterPort p nextRenew + loop pmp removed + + handleRenew :: NatPmpHandle + -> DH.MinPrioHeap POSIXTime RenewAction + -> RIO e () + handleRenew pmp nextRenew = do + case (DH.view nextRenew) of + Nothing -> error "Internal heap managing error." + Just ((_, RenewAction p), rest) -> do + logInfo $ + displayShow ("port: sending renewing request to NAT-PMP for port ", + p) + setPortMapping pmp PTUdp p p portLeaseLifetime >>= \case + Left err | isResetAndRetry err -> do + closeNatPmp pmp + attemptReestablishNatPmpThen (\pmp -> handleRenew pmp nextRenew) + Left err -> do + logError $ + displayShow ("port: failed to request NAT-PMP for port ", p, + ":", err, ". disabling NAT-PMP") + loopErr q + Right _ -> do + -- We don't need to filter the port because we just did. + now <- io $ getPOSIXTime + let withRenew = + DH.insert (now + fromIntegral portRenewalTime, RenewAction p) + rest + loop pmp withRenew + + -- If the internal natpmp socket is closed (laptop lid closed, network + -- change, etc), attempt to reestablish a connection. + attemptReestablishNatPmpThen :: (NatPmpHandle -> RIO e ()) + -> RIO e () + attemptReestablishNatPmpThen andThen = do + logInfo $ + displayShow ("port: network changed. Attempting NAT reconnect"); + loop 0 + where + loop :: Int -> RIO e () + loop tryNum = do + initNatPmp >>= \case + Left err -> do + if tryNum == maxRetries + then do + stderr $ "port: failed to reestablish a connection to your router" + loopErr q + else do + threadDelay networkRetryDelay + loop (tryNum + 1) + Right pmp -> do + andThen pmp + + filterPort :: Word16 + -> DH.MinPrioHeap POSIXTime RenewAction + -> DH.MinPrioHeap POSIXTime RenewAction + filterPort p = DH.filter okPort + where + okPort (_, RenewAction x) = p /= x + + -- block (retry) until the delay TVar is set to True + fini :: TVar Bool -> STM () + fini = check <=< readTVar + + -- The NAT system is considered "off" but we still need to signal back to + -- the main thread that blocking actions are complete. + loopErr q = forever $ do + (atomically $ readTQueue q) >>= \case + PTMOpen _ onComplete -> atomically onComplete + PTMClose _ -> pure () + +-- When we were unable to connect to a router, get the ip address on the +-- default ipv4 interface to check if we look like we're on an internal network +-- or not. +likelyIPAddress :: MonadIO m => m (Maybe (Word8, Word8, Word8, Word8)) +likelyIPAddress = liftIO do + -- Try opening a socket to 1.1.1.1 to get our own IP address. Since UDP is + -- stateless and we aren't sending anything, we aren't actually contacting + -- them in any way. + sock <- socket AF_INET Datagram 0 + connect sock (SockAddrInet 53 (tupleToHostAddress (1, 1, 1, 1))) + sockAddr <- getSocketName sock + case sockAddr of + SockAddrInet _ addr -> pure $ Just $ hostAddressToTuple addr + _ -> pure $ Nothing + +likelyBehindRouter :: MonadIO m => m Bool +likelyBehindRouter = do + likelyIPAddress >>= \case + Just ip@(192, 168, _, _) -> pure True + Just ip@(172, x, _, _) + | (x >= 16 && x <= 31) -> pure True + Just ip@(10, _, _, _) -> pure True + _ -> pure False + +-- Some of the errors that we encounter happen when the underlying sockets have +-- closed out from under us. When this happens, we want to wait a short time +-- and reset the system. +isResetAndRetry :: Error -> Bool +isResetAndRetry ErrRecvFrom = True +isResetAndRetry ErrSendErr = True +isResetAndRetry _ = False + +-- Acquire a port for the duration of the RAcquire. +requestPortAccess :: forall e. (HasPortControlApi e) => Word16 -> RAcquire e () +requestPortAccess port = do + mkRAcquire request release + where + request :: RIO e () + request = do + api <- view portControlApiL + io $ pAddPortRequest api port + + release :: () -> RIO e () + release _ = do + api <- view portControlApiL + io $ pRemovePortRequest api port + diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Serf.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Serf.hs index e03512883..6a65f2d23 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Serf.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Serf.hs @@ -40,7 +40,7 @@ withSerf config = mkRAcquire startup kill where startup = do (serf, st) <- io $ start config - logDebug (displayShow ("serf state", st)) + logInfo (displayShow ("serf state", st)) pure serf kill serf = do void $ rio $ stop serf @@ -58,7 +58,7 @@ execReplay serf log last = do where doBoot :: RIO e (Either PlayBail Word) doBoot = do - logDebug "Beginning boot sequence" + logInfo "Beginning boot sequence" let bootSeqLen = lifecycleLen (Log.identity log) @@ -72,14 +72,14 @@ execReplay serf log last = do when (numEvs /= bootSeqLen) $ do throwIO (MissingBootEventsInEventLog numEvs bootSeqLen) - logDebug $ display ("Sending " <> tshow numEvs <> " boot events to serf") + logInfo $ display ("Sending " <> tshow numEvs <> " boot events to serf") io (boot serf evs) >>= \case Just err -> do - logDebug "Error on replay, exiting" + logInfo "Error on replay, exiting" pure (Left err) Nothing -> do - logDebug "Finished boot events, moving on to more events from log." + logInfo "Finished boot events, moving on to more events from log." doReplay <&> \case Left err -> Left err Right num -> Right (num + numEvs) diff --git a/pkg/hs/urbit-king/lib/Urbit/Vere/Term.hs b/pkg/hs/urbit-king/lib/Urbit/Vere/Term.hs index 06082c142..e4b5bf0cd 100644 --- a/pkg/hs/urbit-king/lib/Urbit/Vere/Term.hs +++ b/pkg/hs/urbit-king/lib/Urbit/Vere/Term.hs @@ -30,6 +30,7 @@ import Urbit.King.API (readPortsFile) import Urbit.TermSize (TermSize(TermSize)) import Urbit.Vere.Term.API (Client(Client)) +import qualified Data.Set as S import qualified Data.ByteString.Internal as BS import qualified Data.ByteString.UTF8 as BS import qualified System.Console.ANSI as ANSI @@ -342,6 +343,8 @@ localClient doneSignal = fst <$> mkRAcquire start stop Clr () -> do T.clearScreen termRefreshLine ls Hop w -> termShowCursor ls (fromIntegral w) + Klr s -> do ls2 <- termShowClear ls + termShowStub ls2 s Lin c -> do ls2 <- termShowClear ls termShowLine ls2 (pack c) Mor () -> termShowMore ls @@ -349,6 +352,55 @@ localClient doneSignal = fst <$> mkRAcquire start stop Sav path atom -> pure ls Url url -> pure ls + termRenderDeco :: Deco -> Char + termRenderDeco = \case + DecoBr -> '1' + DecoUn -> '4' + DecoBl -> '5' + DecoNull -> '0' + + termRenderTint :: Tint -> Char + termRenderTint = \case + TintK -> '0' + TintR -> '1' + TintG -> '2' + TintY -> '3' + TintB -> '4' + TintM -> '5' + TintC -> '6' + TintW -> '7' + TintNull -> '9' + + -- Wraps the appropriate escape sequence around a piece of styled text + termRenderStubSegment :: Stye -> [Char] -> [Char] + termRenderStubSegment Stye {..} tape = + case (S.null decoset, back, fore) of + (True, TintNull, TintNull) -> tape + _ -> styled + where + decoset = setFromHoonSet deco + escape = [chr 27, '['] + + styles = intercalate ";" $ filter (not . null) + [ intersperse ';' $ fmap termRenderDeco $ toList decoset + , case back of + TintNull -> [] + tint -> ['4', termRenderTint tint] + , case fore of + TintNull -> [] + tint -> ['3', termRenderTint tint] + ] + + styled = mconcat [escape, styles, "m", tape, escape, "0m"] + + -- Displays and sets styled text as the current line + termShowStub :: LineState -> Stub -> RIO e LineState + termShowStub ls (Stub s) = do + let visualLength = sum $ fmap (length . snd) s + let outText = pack $ mconcat $ fmap (uncurry termRenderStubSegment) s + putStr outText + pure ls { lsLine = outText, lsCurPos = visualLength } + -- Moves the cursor to the requested position termShowCursor :: LineState -> Int -> RIO e LineState termShowCursor ls@LineState{..} {-line pos)-} newPos = do @@ -472,7 +524,7 @@ localClient doneSignal = fst <$> mkRAcquire start stop loop rd else if w == 3 then do -- ETX (^C) - logDebug $ displayShow "Ctrl-c interrupt" + logInfo $ displayShow "Ctrl-c interrupt" atomically $ do writeTQueue wq [Term.Trace "interrupt\r\n"] writeTQueue rq $ Ctl $ Cord "c" diff --git a/pkg/hs/urbit-king/package.yaml b/pkg/hs/urbit-king/package.yaml index d1e8b6a77..583cd6b6c 100644 --- a/pkg/hs/urbit-king/package.yaml +++ b/pkg/hs/urbit-king/package.yaml @@ -50,6 +50,8 @@ dependencies: - Glob - hashable - hashtables + - heap + - hexstring - http-client - http-client-tls - http-types @@ -64,6 +66,7 @@ dependencies: - mtl - multimap - murmur3 + - natpmp-static - network - optparse-applicative - para @@ -99,7 +102,6 @@ dependencies: - unliftio-core - unordered-containers - urbit-atom - - urbit-azimuth - urbit-eventlog-lmdb - urbit-hob - urbit-noun @@ -112,7 +114,6 @@ dependencies: - wai-websockets - warp - warp-tls - - web3 - websockets default-extensions: diff --git a/pkg/hs/urbit-king/test/AmesTests.hs b/pkg/hs/urbit-king/test/AmesTests.hs index 6be3e5c8c..41080dd14 100644 --- a/pkg/hs/urbit-king/test/AmesTests.hs +++ b/pkg/hs/urbit-king/test/AmesTests.hs @@ -15,6 +15,7 @@ import Urbit.Noun.Time import Urbit.Prelude import Urbit.Vere.Ames import Urbit.Vere.Pier.Types +import Urbit.Vere.Ports import Control.Concurrent (runInBoundThread) import Data.LargeWord (LargeKey(..)) @@ -27,7 +28,11 @@ import qualified Urbit.EventLog.LMDB as Log -------------------------------------------------------------------------------- -type HasAmes e = (HasLogFunc e, HasNetworkConfig e, HasKingId e) +type HasAmes e = + ( HasLogFunc e + , HasNetworkConfig e + , HasKingId e + , HasPortControlApi e) -- Utils ----------------------------------------------------------------------- @@ -41,9 +46,10 @@ sendEf :: Galaxy -> Wen -> Bytes -> NewtEf sendEf g w bs = NewtEfSend (0, ()) (EachYes g) bs data NetworkTestApp = NetworkTestApp - { _ntaLogFunc :: !LogFunc - , _ntaNetworkConfig :: !NetworkConfig - , _ntaKingId :: !Word16 + { _ntaLogFunc :: !LogFunc + , _ntaNetworkConfig :: !NetworkConfig + , _ntaPortControlApi :: !PortControlApi + , _ntaKingId :: !Word16 } makeLenses ''NetworkTestApp @@ -57,20 +63,25 @@ instance HasNetworkConfig NetworkTestApp where instance HasKingId NetworkTestApp where kingIdL = ntaKingId +instance HasPortControlApi NetworkTestApp where + portControlApiL = ntaPortControlApi + runNetworkApp :: RIO NetworkTestApp a -> IO a -runNetworkApp = runRIO NetworkTestApp - { _ntaLogFunc = mkLogFunc (\_ _ _ _ -> pure ()) - , _ntaKingId = 34 - , _ntaNetworkConfig = NetworkConfig { _ncNetMode = NMNormal - , _ncAmesPort = Nothing - , _ncNoAmes = False - , _ncNoHttp = False - , _ncNoHttps = False - , _ncHttpPort = Nothing - , _ncHttpsPort = Nothing - , _ncLocalPort = Nothing - } - } +runNetworkApp = + runRIO NetworkTestApp + { _ntaLogFunc = mkLogFunc (\_ _ _ _ -> pure ()) + , _ntaKingId = 34 + , _ntaPortControlApi = buildInactivePorts + , _ntaNetworkConfig = NetworkConfig { _ncNetMode = NMNormal + , _ncAmesPort = Nothing + , _ncNoAmes = False + , _ncNoHttp = False + , _ncNoHttps = False + , _ncHttpPort = Nothing + , _ncHttpsPort = Nothing + , _ncLocalPort = Nothing + } + } runGala :: forall e @@ -110,8 +121,7 @@ sendThread cb (to, val) = void $ mkRAcquire start cancel zodSelfMsg :: Property zodSelfMsg = forAll arbitrary (ioProperty . runNetworkApp . runTest) where - runTest - :: (HasLogFunc e, HasNetworkConfig e, HasKingId e) => Bytes -> RIO e Bool + runTest :: (HasAmes e) => Bytes -> RIO e Bool runTest val = runRAcquire $ do env <- ask (zodQ, zod) <- runGala 0 @@ -121,15 +131,13 @@ zodSelfMsg = forAll arbitrary (ioProperty . runNetworkApp . runTest) twoTalk :: Property twoTalk = forAll arbitrary (ioProperty . runNetworkApp . runTest) where - runTest :: (HasLogFunc e, HasNetworkConfig e, HasKingId e) - => (Word8, Word8, Bytes) -> RIO e Bool + runTest :: (HasAmes e) => (Word8, Word8, Bytes) -> RIO e Bool runTest (aliceShip, bobShip, val) = if aliceShip == bobShip then pure True else go aliceShip bobShip val - go :: (HasLogFunc e, HasNetworkConfig e, HasKingId e) - => Word8 -> Word8 -> Bytes -> RIO e Bool + go :: (HasAmes e) => Word8 -> Word8 -> Bytes -> RIO e Bool go aliceShip bobShip val = runRAcquire $ do (aliceQ, alice) <- runGala aliceShip (bobQ, bob) <- runGala bobShip diff --git a/pkg/interface/.eslintrc.js b/pkg/interface/.eslintrc.js new file mode 100644 index 000000000..0f63724ca --- /dev/null +++ b/pkg/interface/.eslintrc.js @@ -0,0 +1,186 @@ +const env = { + "browser": true, + "es6": true, + "node": true +}; + +const rules = { + "array-bracket-spacing": ["error", "never"], + "arrow-parens": [ + "error", + "as-needed", + { + "requireForBlockBody": true + } + ], + "arrow-spacing": "error", + "block-spacing": ["error", "always"], + "brace-style": ["error", "1tbs"], + "camelcase": [ + "error", + { + "properties": "never" + } + ], + "comma-dangle": ["error", "never"], + "eol-last": ["error", "always"], + "func-name-matching": "error", + "indent": [ + "off", + 2, + { + "ArrayExpression": "off", + "SwitchCase": 1, + "CallExpression": { + "arguments": "off" + }, + "FunctionDeclaration": { + "parameters": "off" + }, + "FunctionExpression": { + "parameters": "off" + }, + "MemberExpression": "off", + "ObjectExpression": "off", + "ImportDeclaration": "off" + } + ], + "handle-callback-err": "off", + "linebreak-style": ["error", "unix"], + "max-lines": [ + "error", + { + "max": 300, + "skipBlankLines": true, + "skipComments": true + } + ], + "max-lines-per-function": [ + "warn", + { + "skipBlankLines": true, + "skipComments": true + } + ], + "max-statements-per-line": [ + "error", + { + "max": 1 + } + ], + "new-cap": [ + "error", + { + "newIsCap": true, + "capIsNew": false + } + ], + "new-parens": "error", + "no-buffer-constructor": "error", + "no-console": "off", + "no-extra-semi": "off", + "no-fallthrough": "off", + "no-func-assign": "off", + "no-implicit-coercion": "error", + "no-multi-assign": "error", + "no-multiple-empty-lines": [ + "error", + { + "max": 1 + } + ], + "no-nested-ternary": "error", + "no-param-reassign": "off", + "no-return-assign": "error", + "no-return-await": "off", + "no-shadow-restricted-names": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-unused-vars": [ + "error", + { + "vars": "all", + "args": "none", + "ignoreRestSiblings": false + } + ], + "no-use-before-define": [ + "error", + { + "functions": false, + "classes": false + } + ], + "no-useless-escape": "off", + "no-var": "error", + "nonblock-statement-body-position": ["error", "below"], + "object-curly-spacing": ["error", "always"], + "padded-blocks": ["error", "never"], + "prefer-arrow-callback": "error", + "prefer-const": [ + "error", + { + "destructuring": "all", + "ignoreReadBeforeAssign": true + } + ], + "prefer-template": "off", + "quotes": ["error", "single"], + "semi": ["error", "always"], + "spaced-comment": [ + "error", + "always", + { + "exceptions": ["!"] + } + ], + "space-before-blocks": "error", + "unicode-bom": ["error", "never"], + "valid-jsdoc": "error", + "wrap-iife": ["error", "inside"], + "react/jsx-closing-bracket-location": 1, + "react/jsx-tag-spacing": 1, + "react/jsx-max-props-per-line": ["error", { "maximum": 2, "when": "multiline" }], + "react/prop-types": 0 +}; + +module.exports = { + "env": env, + "extends": [ + "plugin:react/recommended", + "eslint:recommended", + ], + "settings": { + "react": { + "version": "^16.5.2" + } + }, + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 10, + "requireConfigFile": false, + "sourceType": "module" + }, + "root": true, + "rules": rules, + "overrides": [ + { + "files": ["**/*.ts", "**/*.tsx"], + "env": env, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { "jsx": true }, + "ecmaVersion": 10, + "requireConfigFile": false, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": rules + } + ] +}; diff --git a/pkg/interface/.eslintrc.json b/pkg/interface/.eslintrc.json deleted file mode 100644 index 87ab40377..000000000 --- a/pkg/interface/.eslintrc.json +++ /dev/null @@ -1,147 +0,0 @@ -{ - "env": { - "browser": true, - "es6": true, - "node": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended" - ], - "settings": { - "react": { - "version": "^16.5.2" - } - }, - "parser": "babel-eslint", - "parserOptions": { - "ecmaVersion": 10, - "requireConfigFile": false, - "sourceType": "module" - }, - "root": true, - "rules": { - "array-bracket-spacing": ["error", "never"], - "arrow-parens": [ - "error", - "as-needed", - { - "requireForBlockBody": true - } - ], - "arrow-spacing": "error", - "block-spacing": ["error", "always"], - "brace-style": ["error", "1tbs"], - "camelcase": [ - "error", - { - "properties": "never" - } - ], - "comma-dangle": ["error", "never"], - "eol-last": ["error", "always"], - "func-name-matching": "error", - "indent": [ - "off", - 2, - { - "ArrayExpression": "off", - "SwitchCase": 1, - "CallExpression": { - "arguments": "off" - }, - "FunctionDeclaration": { - "parameters": "off" - }, - "FunctionExpression": { - "parameters": "off" - }, - "MemberExpression": "off", - "ObjectExpression": "off", - "ImportDeclaration": "off" - } - ], - "handle-callback-err": "off", - "linebreak-style": ["error", "unix"], - "max-statements-per-line": [ - "error", - { - "max": 1 - } - ], - "new-cap": [ - "error", - { - "newIsCap": true, - "capIsNew": false - } - ], - "new-parens": "error", - "no-buffer-constructor": "error", - "no-console": "off", - "no-extra-semi": "off", - "no-fallthrough": "off", - "no-func-assign": "off", - "no-implicit-coercion": "error", - "no-multi-assign": "error", - "no-multiple-empty-lines": [ - "error", - { - "max": 1 - } - ], - "no-nested-ternary": "error", - "no-param-reassign": "off", - "no-return-assign": "error", - "no-return-await": "off", - "no-shadow-restricted-names": "error", - "no-tabs": "error", - "no-trailing-spaces": "error", - "no-unused-vars": [ - "error", - { - "vars": "all", - "args": "none", - "ignoreRestSiblings": false - } - ], - "no-use-before-define": [ - "error", - { - "functions": false, - "classes": false - } - ], - "no-useless-escape": "off", - "no-var": "error", - "nonblock-statement-body-position": ["error", "below"], - "object-curly-spacing": ["error", "always"], - "padded-blocks": ["error", "never"], - "prefer-arrow-callback": "error", - "prefer-const": [ - "error", - { - "destructuring": "all", - "ignoreReadBeforeAssign": true - } - ], - "prefer-template": "off", - "quotes": ["error", "single"], - "semi": ["error", "always"], - "spaced-comment": [ - "error", - "always", - { - "exceptions": ["!"] - } - ], - "space-before-blocks": "error", - "unicode-bom": ["error", "never"], - "valid-jsdoc": "error", - "wrap-iife": ["error", "inside"], - "react/jsx-closing-bracket-location": 1, - "react/jsx-tag-spacing": 1, - "react/jsx-max-props-per-line": ["error", { "maximum": 2, "when": "multiline" }], - "react/prop-types": 0 - } -} diff --git a/pkg/interface/config/webpack.dev.js b/pkg/interface/config/webpack.dev.js index 3d5c08d31..e2cb44ea4 100644 --- a/pkg/interface/config/webpack.dev.js +++ b/pkg/interface/config/webpack.dev.js @@ -52,7 +52,7 @@ if(urbitrc.URL) { ...devServer, index: '', proxy: { - '/~landscape/js/index.js': { + '/~landscape/js/bundle/index.*.js': { target: 'http://localhost:9000', pathRewrite: (req, path) => '/index.js' }, diff --git a/pkg/interface/config/webpack.prod.js b/pkg/interface/config/webpack.prod.js index 10525a526..7a0e5a2e7 100644 --- a/pkg/interface/config/webpack.prod.js +++ b/pkg/interface/config/webpack.prod.js @@ -1,6 +1,6 @@ const path = require('path'); // const HtmlWebpackPlugin = require('html-webpack-plugin'); -// const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { mode: 'production', @@ -49,17 +49,16 @@ module.exports = { // historyApiFallback: true // }, plugins: [ - // new CleanWebpackPlugin(), + new CleanWebpackPlugin(), // new HtmlWebpackPlugin({ // title: 'Hot Module Replacement', // template: './public/index.html', // }), ], output: { - filename: 'index.js', - chunkFilename: 'index.js', - path: path.resolve(__dirname, '../../arvo/app/landscape/js'), - publicPath: '/' + filename: 'index.[contenthash].js', + path: path.resolve(__dirname, '../../arvo/app/landscape/js/bundle'), + publicPath: '/', }, optimization: { minimize: true, diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index c4c1df3b4..c3f717ee9 100644 --- a/pkg/interface/package-lock.json +++ b/pkg/interface/package-lock.json @@ -1393,6 +1393,42 @@ "tslib": "^1.11.1" } }, + "@reach/disclosure": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/disclosure/-/disclosure-0.10.5.tgz", + "integrity": "sha512-DCae28vcL7wXJNt8hySI2uaowEJ6KPDJ9U14xQMkMs0/lH7Tz8PoAO3llf7csEXk/4kzjnDpkyobDiEV3pz05g==", + "requires": { + "@reach/auto-id": "0.10.5", + "@reach/utils": "0.10.5", + "tslib": "^2.0.0" + }, + "dependencies": { + "@reach/auto-id": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.10.5.tgz", + "integrity": "sha512-we4/bwjFxJ3F+2eaddQ1HltbKvJ7AB8clkN719El7Zugpn/vOjfPMOVUiBqTmPGLUvkYrq4tpuFwLvk2HyOVHg==", + "requires": { + "@reach/utils": "0.10.5", + "tslib": "^2.0.0" + } + }, + "@reach/utils": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.10.5.tgz", + "integrity": "sha512-5E/xxQnUbmpI/LrufBAOXjunl96DnqX6B4zC2MO2KH/dRzLug5gM5VuOwV26egsp0jvsSPxojwciOhS43px3qw==", + "requires": { + "@types/warning": "^3.0.0", + "tslib": "^2.0.0", + "warning": "^4.0.3" + } + }, + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + } + } + }, "@reach/menu-button": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@reach/menu-button/-/menu-button-0.10.1.tgz", @@ -1443,6 +1479,53 @@ "tslib": "^1.11.1" } }, + "@reach/tabs": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/tabs/-/tabs-0.10.5.tgz", + "integrity": "sha512-oQJxQ9FwFsXo2HxEzJxFU/wP31bPVh4VU54NlhHW9f49uofyYkIKBbAhdF0Zb3TnaFp4cGKPHX39pXBYGPDkAQ==", + "requires": { + "@reach/auto-id": "0.10.5", + "@reach/descendants": "0.10.5", + "@reach/utils": "0.10.5", + "prop-types": "^15.7.2", + "tslib": "^2.0.0" + }, + "dependencies": { + "@reach/auto-id": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.10.5.tgz", + "integrity": "sha512-we4/bwjFxJ3F+2eaddQ1HltbKvJ7AB8clkN719El7Zugpn/vOjfPMOVUiBqTmPGLUvkYrq4tpuFwLvk2HyOVHg==", + "requires": { + "@reach/utils": "0.10.5", + "tslib": "^2.0.0" + } + }, + "@reach/descendants": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/descendants/-/descendants-0.10.5.tgz", + "integrity": "sha512-8HhN4DwS/HsPQ+Ym/Ft/XJ1spXBYdE8hqpnbYR9UcU7Nx3oDbTIdhjA6JXXt23t5avYIx2jRa8YHCtVKSHuiwA==", + "requires": { + "@reach/utils": "0.10.5", + "tslib": "^2.0.0" + } + }, + "@reach/utils": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.10.5.tgz", + "integrity": "sha512-5E/xxQnUbmpI/LrufBAOXjunl96DnqX6B4zC2MO2KH/dRzLug5gM5VuOwV26egsp0jvsSPxojwciOhS43px3qw==", + "requires": { + "@types/warning": "^3.0.0", + "tslib": "^2.0.0", + "warning": "^4.0.3" + } + }, + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + } + } + }, "@reach/utils": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.10.1.tgz", @@ -1555,15 +1638,15 @@ "@styled-system/css": "^5.1.5" } }, + "@tlon/indigo-light": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tlon/indigo-light/-/indigo-light-1.0.3.tgz", + "integrity": "sha512-3OPSdf9cejP/TSzWXuBaYbzLtAfBzQnc75SlPLkoPfwpxnv1Bvy9hiWngLY0WnKRR6lMOldnkYQCCuNWeDibYQ==" + }, "@tlon/indigo-react": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@tlon/indigo-react/-/indigo-react-1.1.12.tgz", - "integrity": "sha512-XBJjHwaslEwZA2r09qnoh84BeVLnd/jwZRkhq71KNABnRD+QRtg/dYNvtswueML4Km89Vx9QBtCIEIeujzrblw==", - "requires": { - "@reach/menu-button": "^0.10.0", - "@styled-system/css": "^5.1.5", - "@types/styled-system__css": "^5.0.5" - } + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@tlon/indigo-react/-/indigo-react-1.1.15.tgz", + "integrity": "sha512-Ao+1hAJjN5y1gDyT7GIUgXORPXTIpZKVVtrS++ZGYBemYMSq3oJFMIZertsSZbDHuh/TsVPenJrMUZBpV60law==" }, "@types/anymatch": { "version": "1.3.1", @@ -1577,6 +1660,12 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", @@ -1606,6 +1695,12 @@ "integrity": "sha512-iYCgjm1dGPRuo12+BStjd1HiVQqhlRhWDOQigNxn023HcjnhsiFz9pc6CzJj4HwDCSQca9bxTL4PxJDbkdm3PA==", "dev": true }, + "@types/json-schema": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", + "dev": true + }, "@types/lodash": { "version": "4.14.155", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.155.tgz", @@ -1667,14 +1762,6 @@ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", "dev": true }, - "@types/styled-system__css": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/@types/styled-system__css/-/styled-system__css-5.0.8.tgz", - "integrity": "sha512-skv+daDje8vWQ8wnqVV0GCzgWVKx4gI9lJpAxWE77s52Ne6k/SCPP8HGE4BFbWDvK+qi5O3p89BGWVOQ1VHjMg==", - "requires": { - "csstype": "^2.6.6" - } - }, "@types/tapable": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.5.tgz", @@ -1720,6 +1807,93 @@ "source-map": "^0.6.1" } }, + "@typescript-eslint/eslint-plugin": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.8.0.tgz", + "integrity": "sha512-lFb4VCDleFSR+eo4Ew+HvrJ37ZH1Y9ZyE+qyP7EiwBpcCVxwmUc5PAqhShCQ8N8U5vqYydm74nss+a0wrrCErw==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "3.8.0", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.8.0.tgz", + "integrity": "sha512-o8T1blo1lAJE0QDsW7nSyvZHbiDzQDjINJKyB44Z3sSL39qBy5L10ScI/XwDtaiunoyKGLiY9bzRk4YjsUZl8w==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/types": "3.8.0", + "@typescript-eslint/typescript-estree": "3.8.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.8.0.tgz", + "integrity": "sha512-u5vjOBaCsnMVQOvkKCXAmmOhyyMmFFf5dbkM3TIbg3MZ2pyv5peE4gj81UAbTHwTOXEwf7eCQTUMKrDl/+qGnA==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "3.8.0", + "@typescript-eslint/types": "3.8.0", + "@typescript-eslint/typescript-estree": "3.8.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/types": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.8.0.tgz", + "integrity": "sha512-8kROmEQkv6ss9kdQ44vCN1dTrgu4Qxrd2kXr10kz2NP5T8/7JnEfYNxCpPkArbLIhhkGLZV3aVMplH1RXQRF7Q==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.8.0.tgz", + "integrity": "sha512-MTv9nPDhlKfclwnplRNDL44mP2SY96YmPGxmMbMy6x12I+pERcxpIUht7DXZaj4mOKKtet53wYYXU0ABaiXrLw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "3.8.0", + "@typescript-eslint/visitor-keys": "3.8.0", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.8.0.tgz", + "integrity": "sha512-gfqQWyVPpT9NpLREXNR820AYwgz+Kr1GuF3nf1wxpHD6hdxI62tq03ToomFnDxY0m3pUB39IF7sil7D5TQexLA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -1918,9 +2092,9 @@ } }, "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", "dev": true }, "acorn-jsx": { @@ -2856,9 +3030,9 @@ } }, "cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "dev": true }, "cliui": { @@ -3328,7 +3502,8 @@ "csstype": { "version": "2.6.10", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.10.tgz", - "integrity": "sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==" + "integrity": "sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==", + "dev": true }, "cyclist": { "version": "1.0.1", @@ -3884,6 +4059,15 @@ } } }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, "globals": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", @@ -3899,6 +4083,12 @@ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -3957,9 +4147,9 @@ } }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -3967,9 +4157,9 @@ } }, "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" @@ -4008,9 +4198,9 @@ }, "dependencies": { "estraverse": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", - "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", "dev": true } } @@ -5333,21 +5523,21 @@ "dev": true }, "inquirer": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", - "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", + "chalk": "^4.1.0", "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", + "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", - "lodash": "^4.17.15", + "lodash": "^4.17.19", "mute-stream": "0.0.8", "run-async": "^2.4.0", - "rxjs": "^6.5.3", + "rxjs": "^6.6.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6" @@ -5364,9 +5554,9 @@ } }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5394,6 +5584,12 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true + }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", @@ -5716,9 +5912,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -5949,6 +6145,15 @@ "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==" }, + "markdown-to-jsx": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-6.11.4.tgz", + "integrity": "sha512-3lRCD5Sh+tfA52iGgfs/XZiw33f7fFX9Bn55aNnVNUd2GzLDkOWyKYYD8Yju2B1Vn+feiEdgJs8T6Tg0xNokPw==", + "requires": { + "prop-types": "^15.6.2", + "unquote": "^1.1.0" + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -5985,6 +6190,11 @@ "p-is-promise": "^2.0.0" } }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -6290,6 +6500,11 @@ "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", "integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==" }, + "mousetrap-global-bind": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mousetrap-global-bind/-/mousetrap-global-bind-1.1.0.tgz", + "integrity": "sha1-zX3pIivQZG+i4BDVTISnTCaojt0=" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -6684,9 +6899,9 @@ } }, "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.1.tgz", + "integrity": "sha512-ZpZpjcJeugQfWsfyQlshVoowIIQ1qBGSVll4rfDq6JJVO//fesjoX808hXWfBjY+ROZgpKDI5TRSRBSoJiZ8eg==", "dev": true, "requires": { "mimic-fn": "^2.1.0" @@ -7491,6 +7706,15 @@ "tiny-warning": "^1.0.0" } }, + "react-window": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.5.tgz", + "integrity": "sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==", + "requires": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -7562,9 +7786,9 @@ } }, "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, "regexpu-core": { @@ -7883,9 +8107,9 @@ } }, "rxjs": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", - "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", "dev": true, "requires": { "tslib": "^1.9.0" @@ -8727,9 +8951,9 @@ "dev": true }, "strip-json-comments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", - "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "style-loader": { @@ -9088,6 +9312,15 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -9125,6 +9358,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "dev": true + }, "unherit": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", @@ -9257,6 +9496,11 @@ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true }, + "unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=" + }, "unset-value": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-0.1.2.tgz", @@ -9393,9 +9637,9 @@ "dev": true }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "dev": true }, "value-equal": { @@ -9565,7 +9809,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -9586,12 +9831,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9606,17 +9853,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -9733,7 +9983,8 @@ "inherits": { "version": "2.0.4", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -9745,6 +9996,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9759,6 +10011,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -9766,12 +10019,14 @@ "minimist": { "version": "1.2.5", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.9.0", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -9790,6 +10045,7 @@ "version": "0.5.3", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "^1.2.5" } @@ -9851,7 +10107,8 @@ "npm-normalize-package-bin": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "npm-packlist": { "version": "1.4.8", @@ -9879,7 +10136,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -9891,6 +10149,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -9968,7 +10227,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -10004,6 +10264,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -10023,6 +10284,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -10066,12 +10328,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -10552,7 +10816,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -10573,12 +10838,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -10593,17 +10860,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -10720,7 +10990,8 @@ "inherits": { "version": "2.0.4", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -10732,6 +11003,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -10746,6 +11018,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -10753,12 +11026,14 @@ "minimist": { "version": "1.2.5", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.9.0", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -10777,6 +11052,7 @@ "version": "0.5.3", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "^1.2.5" } @@ -10838,7 +11114,8 @@ "npm-normalize-package-bin": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "npm-packlist": { "version": "1.4.8", @@ -10866,7 +11143,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -10878,6 +11156,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -10955,7 +11234,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -10991,6 +11271,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -11010,6 +11291,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -11053,12 +11335,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/pkg/interface/package.json b/pkg/interface/package.json index 2aef19ded..338828766 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -5,21 +5,27 @@ "main": "index.js", "dependencies": { "@babel/runtime": "^7.10.5", + "@reach/disclosure": "^0.10.5", "@reach/menu-button": "^0.10.1", - "@tlon/indigo-react": "^1.1.10", + "@reach/tabs": "^0.10.5", + "@tlon/indigo-light": "^1.0.3", + "@tlon/indigo-react": "^1.1.15", "classnames": "^2.2.6", "codemirror": "^5.51.0", "css-loader": "^3.5.3", "formik": "^2.1.4", "lodash": "^4.17.15", + "markdown-to-jsx": "^6.11.4", "moment": "^2.20.1", "mousetrap": "^1.6.5", + "mousetrap-global-bind": "^1.1.0", "prop-types": "^15.7.2", "react": "^16.5.2", "react-codemirror2": "^6.0.1", "react-dom": "^16.8.6", "react-markdown": "^4.3.1", "react-router-dom": "^5.0.0", + "react-window": "^1.8.5", "remark-disable-tokenizers": "^1.0.24", "style-loader": "^1.2.1", "styled-components": "^5.1.0", @@ -40,6 +46,8 @@ "@types/lodash": "^4.14.155", "@types/react": "^16.9.38", "@types/react-router-dom": "^5.1.5", + "@typescript-eslint/eslint-plugin": "^3.8.0", + "@typescript-eslint/parser": "^3.8.0", "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", "clean-webpack-plugin": "^3.0.0", @@ -51,12 +59,13 @@ "react-hot-loader": "^4.12.21", "sass": "^1.26.5", "sass-loader": "^8.0.2", + "typescript": "^3.9.7", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.3" }, "scripts": { - "lint": "eslint ./**/*.js", + "lint": "eslint ./src/**/*.{js,ts,tsx}", "lint-file": "eslint", "tsc": "tsc", "tsc:watch": "tsc --watch", diff --git a/pkg/interface/src/App.js b/pkg/interface/src/App.js index 5d057f9cb..4b13c3c2e 100644 --- a/pkg/interface/src/App.js +++ b/pkg/interface/src/App.js @@ -3,10 +3,15 @@ import 'react-hot-loader'; import * as React from 'react'; import { BrowserRouter as Router, Route, withRouter, Switch } from 'react-router-dom'; import styled, { ThemeProvider, createGlobalStyle } from 'styled-components'; +import { sigil as sigiljs, stringRenderer } from 'urbit-sigil-js'; + +import Mousetrap from 'mousetrap'; +import 'mousetrap-global-bind'; import './css/indigo-static.css'; import './css/fonts.css'; -import { light, dark, inverted, paperDark } from '@tlon/indigo-react'; +import light from './themes/light'; +import dark from './themes/old-dark'; import LaunchApp from './apps/launch/app'; import ChatApp from './apps/chat/app'; @@ -16,11 +21,14 @@ import LinksApp from './apps/links/app'; import PublishApp from './apps/publish/app'; import StatusBar from './components/StatusBar'; -import NotFound from './components/404'; +import Omnibox from './components/Omnibox'; +import ErrorComponent from './components/Error'; import GlobalStore from './store/store'; import GlobalSubscription from './subscription/global'; import GlobalApi from './api/global'; +import { uxToHex } from './lib/util'; +import { Sigil } from './lib/sigil'; // const Style = createGlobalStyle` // ${cssReset} @@ -61,6 +69,7 @@ class App extends React.Component { new GlobalSubscription(this.store, this.api, this.appChannel); this.updateTheme = this.updateTheme.bind(this); + this.setFavicon = this.setFavicon.bind(this); } componentDidMount() { @@ -69,99 +78,149 @@ class App extends React.Component { this.api.local.setDark(this.themeWatcher.matches); this.themeWatcher.addListener(this.updateTheme); this.api.local.getBaseHash(); + Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => { + e.preventDefault(); + this.api.local.setOmnibox(); + }); + this.setFavicon(); } componentWillUnmount() { this.themeWatcher.removeListener(this.updateTheme); } + componentDidUpdate(prevProps, prevState, snapshot) { + this.setFavicon(); + } + updateTheme(e) { this.api.local.setDark(e.matches); } + setFavicon() { + if (window.ship.length < 14) { + let background = '#ffffff'; + if (this.state.contacts.hasOwnProperty('/~/default')) { + background = `#${uxToHex(this.state.contacts['/~/default'][window.ship].color)}`; + } + const foreground = Sigil.foregroundFromBackground(background); + const svg = sigiljs({ + patp: window.ship, + renderer: stringRenderer, + size: 16, + colors: [background, foreground] + }); + const dataurl = 'data:image/svg+xml;base64,' + btoa(svg); + const favicon = document.querySelector('[rel=icon]'); + favicon.href = dataurl; + favicon.type = 'image/svg+xml'; + } + } + render() { const channel = window.channel; const associations = this.state.associations ? this.state.associations : { contacts: {} }; - const selectedGroups = this.state.selectedGroups ? this.state.selectedGroups : []; const { state } = this; - const theme = state.dark ? paperDark : light; + const theme = state.dark ? dark : light; return ( - + - - ( - + ( + + )} /> - )} - /> - ( - ( + + )} /> - )} - /> - ( - ( + + )} /> - )} - /> - ( - ( + + )} /> - )} - /> - ( - ( + + )} /> - )} - /> - ( - ( + + )} /> - )} + ( + + )} /> - @@ -171,5 +230,6 @@ class App extends React.Component { } } + export default process.env.NODE_ENV === 'production' ? App : hot(App); diff --git a/pkg/interface/src/api/local.ts b/pkg/interface/src/api/local.ts index 8610284c2..77ff9b2b3 100644 --- a/pkg/interface/src/api/local.ts +++ b/pkg/interface/src/api/local.ts @@ -1,6 +1,5 @@ import BaseApi from "./base"; import { StoreState } from "../store/type"; -import { SelectedGroup } from "../types/local-update"; export default class LocalApi extends BaseApi { getBaseHash() { @@ -9,16 +8,6 @@ export default class LocalApi extends BaseApi { }); } - setSelected(selected: SelectedGroup[]) { - this.store.handleEvent({ - data: { - local: { - selected - } - } - }) - } - sidebarToggle() { this.store.handleEvent({ data: { @@ -39,4 +28,14 @@ export default class LocalApi extends BaseApi { }); } + setOmnibox() { + this.store.handleEvent({ + data: { + local: { + omniboxShown: true + }, + }, + }); + } + } diff --git a/pkg/interface/src/apps/chat/app.tsx b/pkg/interface/src/apps/chat/app.tsx index 8f9adc843..8ccaabd46 100644 --- a/pkg/interface/src/apps/chat/app.tsx +++ b/pkg/interface/src/apps/chat/app.tsx @@ -53,7 +53,6 @@ export default class ChatApp extends React.Component { const unreads = {}; let totalUnreads = 0; - const selectedGroups = props.selectedGroups ? props.selectedGroups : []; const associations = props.associations ? props.associations : { chat: {}, contacts: {} }; @@ -74,14 +73,7 @@ export default class ChatApp extends React.Component { unreads[stat] = Boolean(unread); if ( unread && - stat in associations.chat && - (selectedGroups.length === 0 || - selectedGroups - .map((e) => { - return e[0]; - }) - .includes(associations.chat?.[stat]?.['group-path']) || - props.groups[associations.chat?.[stat]?.['group-path']]?.hidden) + stat in associations.chat ) { totalUnreads += unread; } @@ -111,7 +103,6 @@ export default class ChatApp extends React.Component { inbox={inbox} messagePreviews={messagePreviews} associations={associations} - selectedGroups={selectedGroups} contacts={contacts} invites={invites['/chat'] || {}} unreads={unreads} @@ -286,44 +277,6 @@ export default class ChatApp extends React.Component { ); }} /> - { - let station = `/${props.match.params.ship}/${props.match.params.station}`; - - const popout = props.match.url.includes('/popout/'); - - const association = - station in associations['chat'] ? associations.chat[station] : {}; - const groupPath = association['group-path']; - - const group = groups[groupPath] || {}; - return ( - - - - ); - }} - /> ; } export class ChatScreen extends Component { - hasAskedForMessages = false; lastNumPending = 0; - - scrollContainer: HTMLElement | null = null; - - unreadMarker = null; - scrolledToMarker = false; - activityTimeout: NodeJS.Timeout | null = null; - scrollElement: HTMLElement | null = null; - constructor(props) { super(props); this.state = { - numPages: 1, - scrollLocked: false, - read: props.read, - active: true, - // only for FF - lastScrollHeight: null, + messages: new Map(), }; - this.onScroll = this.onScroll.bind(this); - - this.setUnreadMarker = this.setUnreadMarker.bind(this); - - this.handleActivity = this.handleActivity.bind(this); - this.setInactive = this.setInactive.bind(this); - moment.updateLocale("en", { calendar: { sameDay: "[Today]", @@ -141,450 +65,68 @@ export class ChatScreen extends Component { }); } - componentDidMount() { - document.addEventListener("mousemove", this.handleActivity, false); - document.addEventListener("mousedown", this.handleActivity, false); - document.addEventListener("keypress", this.handleActivity, false); - document.addEventListener("touchmove", this.handleActivity, false); - this.activityTimeout = setTimeout(this.setInactive, ACTIVITY_TIMEOUT); - } - - componentWillUnmount() { - document.removeEventListener("mousemove", this.handleActivity, false); - document.removeEventListener("mousedown", this.handleActivity, false); - document.removeEventListener("keypress", this.handleActivity, false); - document.removeEventListener("touchmove", this.handleActivity, false); - if (this.activityTimeout) { - clearTimeout(this.activityTimeout); - } - } - - handleActivity() { - if (!this.state.active) { - this.setState({ active: true }); - } - - if (this.activityTimeout) { - clearTimeout(this.activityTimeout); - } - - this.activityTimeout = setTimeout(this.setInactive, ACTIVITY_TIMEOUT); - } - - setInactive() { - this.activityTimeout = null; - this.setState({ active: false, scrollLocked: true }); - } - - receivedNewChat() { - const { props } = this; - this.hasAskedForMessages = false; - - this.unreadMarker = null; - this.scrolledToMarker = false; - - this.setState({ read: props.read }); - - const unread = props.length - props.read; - const unreadUnloaded = unread - props.envelopes.length; - const excessUnread = unreadUnloaded > MAX_BACKLOG_SIZE; - - if (!excessUnread && unreadUnloaded + 20 > DEFAULT_BACKLOG_SIZE) { - this.askForMessages(unreadUnloaded + 20); - } else { - this.askForMessages(DEFAULT_BACKLOG_SIZE); - } - - if (excessUnread || props.read === props.length) { - this.scrolledToMarker = true; - this.setState( - { - scrollLocked: false, - }, - () => { - this.scrollToBottom(); - } - ); - } else { - this.setState({ scrollLocked: true, numPages: Math.ceil(unread / 100) }); - } - } - - componentDidUpdate(prevProps, prevState) { - const { props, state } = this; - - if ( - prevProps.match.params.station !== props.match.params.station || - prevProps.match.params.ship !== props.match.params.ship - ) { - this.receivedNewChat(); - } else if ( - props.chatInitialized && - !(props.station in props.inbox) && - Boolean(props.chatSynced) && - !(props.station in props.chatSynced) - ) { - props.history.push("/~chat"); - } else if (props.envelopes.length >= prevProps.envelopes.length + 10) { - this.hasAskedForMessages = false; - } else if ( - props.length !== prevProps.length && - prevProps.length === prevState.read && - state.active - ) { - this.setState({ read: props.length }); - this.props.api.chat.read(this.props.station); - } - - if (!prevProps.chatInitialized && props.chatInitialized) { - this.receivedNewChat(); - } - - if ( - props.length !== prevProps.length || - props.envelopes.length !== prevProps.envelopes.length || - getNumPending(props) !== this.lastNumPending || - state.numPages !== prevState.numPages - ) { - this.scrollToBottom(); - if (navigator.userAgent.includes("Firefox")) { - this.recalculateScrollTop(); - } - - this.lastNumPending = getNumPending(props); - } - } - - askForMessages(size) { - const { props, state } = this; - - if ( - props.envelopes.length >= props.length || - this.hasAskedForMessages || - props.length <= 0 - ) { - return; - } - - const start = - props.length - props.envelopes[props.envelopes.length - 1].number; - if (start > 0) { - const end = start + size < props.length ? start + size : props.length; - this.hasAskedForMessages = true; - props.api.chat.fetchMessages(start + 1, end, props.station); - } - } - - scrollToBottom() { - if (!this.state.scrollLocked && this.scrollElement) { - this.scrollElement.scrollIntoView(); - } - } - - // Restore chat position on FF when new messages come in - recalculateScrollTop() { - const { lastScrollHeight } = this.state; - if (!this.scrollContainer || !lastScrollHeight) { - return; - } - - const target = this.scrollContainer; - const newScrollTop = this.scrollContainer.scrollHeight - lastScrollHeight; - if (target.scrollTop !== 0 || newScrollTop === target.scrollTop) { - return; - } - target.scrollTop = target.scrollHeight - lastScrollHeight; - } - - onScroll(e) { - if (scrollIsAtTop(e.target)) { - // Save scroll position for FF - if (navigator.userAgent.includes("Firefox")) { - this.setState({ - lastScrollHeight: e.target.scrollHeight, - }); - } - this.setState( - { - numPages: this.state.numPages + 1, - scrollLocked: true, - }, - () => { - this.askForMessages(DEFAULT_BACKLOG_SIZE); - } - ); - } else if (scrollIsAtBottom(e.target)) { - this.dismissUnread(); - this.setState({ - numPages: 1, - scrollLocked: false, - }); - } - } - - setUnreadMarker(ref) { - if (ref && !this.scrolledToMarker) { - this.setState({ scrollLocked: true }, () => { - ref.scrollIntoView({ block: "center" }); - if (ref.offsetParent && scrollIsAtBottom(ref.offsetParent)) { - this.dismissUnread(); - this.setState({ - numPages: 1, - scrollLocked: false, - }); - } - }); - this.scrolledToMarker = true; - } - this.unreadMarker = ref; - } - - dismissUnread() { - this.props.api.chat.read(this.props.station); - } - - chatWindow(unread) { - // Replace with just the "not Firefox" implementation - // when Firefox #1042151 is patched. - - const { props, state } = this; - - let messages: IMessage[] = props.envelopes.slice(0); - const lastMsgNum = messages.length > 0 ? messages.length : 0; - - if (messages.length > 100 * state.numPages) { - messages = messages.slice(0, 100 * state.numPages); - } - - const pendingMessages: IMessage[] = ( - props.pendingMessages.get(props.station) || [] - ).map((value) => ({ ...value, pending: true })); - - if(unread !== 0) { - unread += pendingMessages.length; - } - - messages = pendingMessages.concat(messages); - - const messageElements = messages.map((msg, i) => { - // Render sigil if previous message is not by the same sender - const aut = ["author"]; - const renderSigil = - _.get(messages[i + 1], aut) !== _.get(msg, aut, msg.author); - const paddingTop = renderSigil; - const paddingBot = - _.get(messages[i - 1], aut) !== _.get(msg, aut, msg.author); - - const when = ["when"]; - const dayBreak = - moment(_.get(messages[i + 1], when)).format("YYYY.MM.DD") !== - moment(_.get(messages[i], when)).format("YYYY.MM.DD"); - - const messageElem = ( - - ); - if (unread > 0 && i === unread - 1) { - return ( - - {messageElem} -
-
-

New messages below

-
- {dayBreak && ( -

- {moment(_.get(messages[i], when)).calendar()} -

- )} -
-
-
- ); - } else if (dayBreak) { - return ( - - {messageElem} -
-

{moment(_.get(messages[i], when)).calendar()}

-
-
- ); - } else { - return messageElem; - } - }); - - if (navigator.userAgent.includes("Firefox")) { - return ( -
{ - this.scrollContainer = e; - }} - > -
-
{ - this.scrollElement = el; - }} - >
- {props.chatInitialized && !(props.station in props.inbox) && ( - - )} - {props.chatSynced && - !(props.station in props.chatSynced) && - messages.length > 0 ? ( - - ) : ( -
- )} - {messageElements} -
-
- ); - } else { - return ( -
-
{ - this.scrollElement = el; - }} - >
- {props.chatInitialized && !(props.station in props.inbox) && ( - - )} - {props.chatSynced && - !(props.station in props.chatSynced) && - messages.length > 0 ? ( - - ) : ( -
- )} - {messageElements} -
- ); - } - } - render() { const { props, state } = this; - const messages = props.envelopes.slice(0); - - const lastMsgNum = messages.length > 0 ? messages.length : 0; - - const group = Array.from(props.group.members); - - const isinPopout = props.popout ? "popout/" : ""; - + const lastMsgNum = props.envelopes.length > 0 ? props.envelopes.length : 0; const ownerContact = window.ship in props.contacts ? props.contacts[window.ship] : false; - let title = props.station.substr(1); + const pendingMessages = (props.pendingMessages.get(props.station) || []) + .map((value) => ({ + ...value, + pending: true + })); - if (props.association && "metadata" in props.association) { - title = - props.association.metadata.title !== "" - ? props.association.metadata.title - : props.station.substr(1); - } + const isChatMissing = + props.chatInitialized && + !(props.station in props.inbox) && + props.chatSynced && + !(props.station in props.chatSynced); - const unread = props.length - state.read; + const isChatLoading = + props.chatInitialized && + !(props.station in props.inbox) && + props.chatSynced && + (props.station in props.chatSynced); - const unreadMsg = unread > 0 && messages[unread - 1]; + const isChatUnsynced = + props.chatSynced && + !(props.station in props.chatSynced) && + props.envelopes.length > 0; - const showUnreadNotice = - props.length !== props.read && props.read === state.read; + const unreadCount = props.length - props.read; + const unreadMsg = unreadCount > 0 && props.envelopes[unreadCount - 1]; return (
-
- {"⟵ All Chats"} -
- -
- - -

- {title} -

- - -
- {!!unreadMsg && showUnreadNotice && ( - this.dismissUnread()} - /> - )} - {this.chatWindow(unread)} + className="h-100 w-100 overflow-hidden flex flex-column relative"> + + { ownerContact={ownerContact} envelopes={props.envelopes} contacts={props.contacts} - onEnter={() => this.setState({ scrollLocked: false })} + onUnmount={(msg: string) => this.setState({ + messages: this.state.messages.set(props.station, msg) + })} s3={props.s3} placeholder="Message..." + message={this.state.messages.get(props.station) || ""} + deleteMessage={() => this.setState({ + messages: this.state.messages.set(props.station, "") + })} />
); diff --git a/pkg/interface/src/apps/chat/components/lib/backlog-element.js b/pkg/interface/src/apps/chat/components/lib/backlog-element.js index 6e16882ff..64f8b26ca 100644 --- a/pkg/interface/src/apps/chat/components/lib/backlog-element.js +++ b/pkg/interface/src/apps/chat/components/lib/backlog-element.js @@ -1,21 +1,22 @@ import React, { Component } from 'react'; -export class BacklogElement extends Component { - render() { - return ( -
-
- -

- Past messages are being restored -

-
-
- - ); +export const BacklogElement = (props) => { + if (!props.isChatLoading) { + return null; } + return ( +
+
+ +

Past messages are being restored

+
+
+ ); } diff --git a/pkg/interface/src/apps/chat/components/lib/chat-editor.js b/pkg/interface/src/apps/chat/components/lib/chat-editor.js new file mode 100644 index 000000000..ee6dc4b6c --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/chat-editor.js @@ -0,0 +1,141 @@ +import React, { Component } from 'react'; +import { UnControlled as CodeEditor } from 'react-codemirror2'; +import CodeMirror from 'codemirror'; + +import 'codemirror/mode/markdown/markdown'; +import 'codemirror/addon/display/placeholder'; + +import 'codemirror/lib/codemirror.css'; + +const BROWSER_REGEX = + new RegExp(String(/Android|webOS|iPhone|iPad|iPod|BlackBerry/i)); + + +const MARKDOWN_CONFIG = { + name: 'markdown', + tokenTypeOverrides: { + header: 'presentation', + quote: 'presentation', + list1: 'presentation', + list2: 'presentation', + list3: 'presentation', + hr: 'presentation', + image: 'presentation', + imageAltText: 'presentation', + imageMarker: 'presentation', + formatting: 'presentation', + linkInline: 'presentation', + linkEmail: 'presentation', + linkText: 'presentation', + linkHref: 'presentation' + } +}; + +export default class ChatEditor extends Component { + constructor(props) { + super(props); + + this.state = { + message: props.message + }; + this.editor = null; + } + + componentWillUnmount() { + this.props.onUnmount(this.state.message); + } + + componentDidUpdate(prevProps) { + const { props } = this; + + if (prevProps.message !== props.message) { + this.editor.setValue(props.message); + this.editor.setOption('mode', MARKDOWN_CONFIG); + return; + } + + if (!props.inCodeMode) { + this.editor.setOption('mode', MARKDOWN_CONFIG); + this.editor.setOption('placeholder', this.props.placeholder); + } else { + this.editor.setOption('mode', null); + this.editor.setOption('placeholder', 'Code...'); + } + const value = this.editor.getValue(); + + // Force redraw of placeholder + if(value.length === 0) { + this.editor.setValue(' '); + this.editor.setValue(''); + } + } + + submit() { + if(!this.editor) { + return; + } + + let editorMessage = this.editor.getValue(); + if (editorMessage === '') { + return; + } + + this.setState({ message: '' }); + this.props.submit(editorMessage); + this.editor.setValue(''); + } + + messageChange(editor, data, value) { + if (this.state.message !== '' && value == '') { + this.setState({ + message: value + }); + } + if (value == this.props.message || value == '' || value == ' ') { + return; + } + this.setState({ + message: value + }); + } + + render() { + const { props } = this; + + const codeTheme = props.inCodeMode ? ' code' : ''; + + const options = { + mode: MARKDOWN_CONFIG, + theme: 'tlon' + codeTheme, + lineNumbers: false, + lineWrapping: true, + scrollbarStyle: 'native', + cursorHeight: 0.85, + placeholder: props.inCodeMode ? 'Code...' : props.placeholder, + extraKeys: { + 'Enter': () => { + this.submit(); + } + } + }; + + return ( +
+ this.messageChange(e, d, v)} + editorDidMount={(editor) => { + this.editor = editor; + if (!(BROWSER_REGEX.test(navigator.userAgent))) { + editor.focus(); + } + }} + /> +
+ ); + } +} diff --git a/pkg/interface/src/apps/chat/components/lib/chat-header.js b/pkg/interface/src/apps/chat/components/lib/chat-header.js new file mode 100644 index 000000000..4c20a5e25 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/chat-header.js @@ -0,0 +1,58 @@ +import React, { Component, Fragment } from "react"; +import { Link } from "react-router-dom"; + +import { ChatTabBar } from "./chat-tabbar"; +import { SidebarSwitcher } from "../../../../components/SidebarSwitch"; +import { deSig } from "../../../../lib/util"; + + +export const ChatHeader = (props) => { + const isInPopout = props.popout ? "popout/" : ""; + const group = Array.from(props.group.members); + let title = props.station.substr(1); + if (props.association && + "metadata" in props.association && + props.association.metadata.tile !== "") { + title = props.association.metadata.title + } + + return ( + +
+ {"⟵ All Chats"} +
+
+ + +

+ {title} +

+ + +
+
+ ); +} diff --git a/pkg/interface/src/apps/chat/components/lib/chat-input.js b/pkg/interface/src/apps/chat/components/lib/chat-input.js index d84a8c6e6..8223ac47c 100644 --- a/pkg/interface/src/apps/chat/components/lib/chat-input.js +++ b/pkg/interface/src/apps/chat/components/lib/chat-input.js @@ -1,157 +1,44 @@ import React, { Component } from 'react'; -import _ from 'lodash'; -import moment from 'moment'; -import { UnControlled as CodeEditor } from 'react-codemirror2'; -import CodeMirror from 'codemirror'; - -import 'codemirror/mode/markdown/markdown'; -import 'codemirror/addon/display/placeholder'; - -import 'codemirror/lib/codemirror.css'; - import { Sigil } from '../../../../lib/sigil'; -import { ShipSearch } from './ship-search'; +import ChatEditor from './chat-editor'; import { S3Upload } from './s3-upload'; - import { uxToHex } from '../../../../lib/util'; -const MARKDOWN_CONFIG = { - name: 'markdown', - tokenTypeOverrides: { - header: 'presentation', - quote: 'presentation', - list1: 'presentation', - list2: 'presentation', - list3: 'presentation', - hr: 'presentation', - image: 'presentation', - imageAltText: 'presentation', - imageMarker: 'presentation', - formatting: 'presentation', - linkInline: 'presentation', - linkEmail: 'presentation', - linkText: 'presentation', - linkHref: 'presentation' - } -}; + +const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source)); export class ChatInput extends Component { constructor(props) { super(props); this.state = { - message: '', - patpSearch: null + inCodeMode: false, }; - this.textareaRef = React.createRef(); - - this.messageSubmit = this.messageSubmit.bind(this); - this.messageChange = this.messageChange.bind(this); - - this.patpAutocomplete = this.patpAutocomplete.bind(this); - this.completePatp = this.completePatp.bind(this); - this.clearSearch = this.clearSearch.bind(this); - + this.submit = this.submit.bind(this); this.toggleCode = this.toggleCode.bind(this); - - this.editor = null; - - // perf testing: - /* let closure = () => { - let x = 0; - for (var i = 0; i < 30; i++) { - x++; - props.api.chat.message( - props.station, - `~${window.ship}`, - Date.now(), - { - text: `${x}` - } - ); - } - setTimeout(closure, 1000); - }; - this.closure = closure.bind(this);*/ - - moment.updateLocale('en', { - relativeTime : { - past: function(input) { - return input === 'just now' - ? input - : input + ' ago'; - }, - s : 'just now', - future: 'in %s', - ss : '%d sec', - m: 'a minute', - mm: '%d min', - h: 'an hr', - hh: '%d hrs', - d: 'a day', - dd: '%d days', - M: 'a month', - MM: '%d months', - y: 'a year', - yy: '%d years' - } - }); } - nextAutocompleteSuggestion(backward = false) { - const { patpSuggestions } = this.state; - let idx = patpSuggestions.findIndex(s => s === this.state.selectedSuggestion); - - idx = backward ? idx - 1 : idx + 1; - idx = idx % patpSuggestions.length; - if(idx < 0) { - idx = patpSuggestions.length - 1; - } - - this.setState({ selectedSuggestion: patpSuggestions[idx] }); + uploadSuccess(url) { + const { props } = this; + props.api.chat.message( + props.station, + `~${window.ship}`, + Date.now(), + { url } + ); } - patpAutocomplete(message) { - const match = /~([a-zA-Z\-]*)$/.exec(message); - - if (!match ) { - this.setState({ patpSearch: null }); - return; - } - this.setState({ patpSearch: match[1].toLowerCase() }); + uploadError(error) { + // no-op for now } - clearSearch() { + toggleCode() { this.setState({ - patpSearch: null + inCodeMode: !this.state.inCodeMode }); } - completePatp(suggestion) { - if(!this.editor) { - return; - } - const newMessage = this.editor.getValue().replace( - /[a-zA-Z\-]*$/, - suggestion - ); - this.editor.setValue(newMessage); - const lastRow = this.editor.lastLine(); - const lastCol = this.editor.getLineHandle(lastRow).text.length; - this.editor.setCursor(lastRow, lastCol); - this.setState({ - patpSearch: null - }); - } - - messageChange(editor, data, value) { - const { patpSearch } = this.state; - if(patpSearch !== null) { - this.patpAutocomplete(value, false); - } - } - getLetterType(letter) { if (letter.startsWith('/me ')) { letter = letter.slice(4); @@ -165,7 +52,7 @@ export class ChatInput extends Component { me: letter }; } else if (this.isUrl(letter)) { - return { + return { url: letter }; } else { @@ -177,98 +64,117 @@ export class ChatInput extends Component { isUrl(string) { try { - const websiteTest = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source) - ); - return websiteTest.test(string); + return URL_REGEX.test(string); } catch (e) { return false; } } - messageSubmit() { - if(!this.editor) { - return; - } + submit(text) { const { props, state } = this; - const editorMessage = this.editor.getValue(); - - if (editorMessage === '') { - return; - } - - props.onEnter(); - - if(state.code) { - props.api.chat.message(props.station, `~${window.ship}`, Date.now(), { - code: { - expression: editorMessage, - output: undefined - } + if (state.inCodeMode) { + this.setState({ + inCodeMode: false + }, () => { + props.api.chat.message( + props.station, + `~${window.ship}`, + Date.now(), { + code: { + expression: text, + output: undefined + } + } + ); }); - this.editor.setValue(''); return; } + + let messages = []; let message = []; - editorMessage.split(' ').map((each) => { - if (this.isUrl(each)) { - if (message.length > 0) { - message = message.join(' '); - message = this.getLetterType(message); - props.api.chat.message( - props.station, - `~${window.ship}`, - Date.now(), - message - ); - message = []; - } - const URL = this.getLetterType(each); + let isInCodeBlock = false; + let endOfCodeBlock = false; + text.split(/\r?\n/).forEach((line, index) => { + if (index !== 0) { + message.push('\n'); + } + // A line of backticks enters and exits a codeblock + if (line.startsWith('```')) { + // But we need to check if we've ended a codeblock + endOfCodeBlock = isInCodeBlock; + isInCodeBlock = (!isInCodeBlock); + } else { + endOfCodeBlock = false; + } + + if (isInCodeBlock || endOfCodeBlock) { + message.push(line); + } else { + line.split(/\s/).forEach((str) => { + if ( + (str.startsWith('`') && str !== '`') + || (str === '`' && !isInCodeBlock) + ) { + isInCodeBlock = true; + } else if ( + (str.endsWith('`') && str !== '`') + || (str === '`' && isInCodeBlock) + ) { + isInCodeBlock = false; + } + + if (this.isUrl(str) && !isInCodeBlock) { + if (message.length > 0) { + // If we're in the middle of a message, add it to the stack and reset + messages.push(message); + message = []; + } + messages.push([str]); + message = []; + } else { + message.push(str); + } + }); + } + }); + + if (message.length) { + // Add any remaining message + messages.push(message); + } + + props.deleteMessage(); + + messages.forEach((message) => { + if (message.length > 0) { + message = this.getLetterType(message.join(' ')); props.api.chat.message( props.station, `~${window.ship}`, Date.now(), - URL + message ); - } else { - return message.push(each); } }); - if (message.length > 0) { - message = message.join(' '); - message = this.getLetterType(message); - props.api.chat.message( - props.station, - `~${window.ship}`, - Date.now(), - message - ); - message = []; - } - - // perf: - // setTimeout(this.closure, 2000); - - this.editor.setValue(''); - } - - toggleCode() { - if(this.state.code) { - this.setState({ code: false }); - this.editor.setOption('mode', MARKDOWN_CONFIG); - this.editor.setOption('placeholder', this.props.placeholder); - } else { - this.setState({ code: true }); - this.editor.setOption('mode', null); - this.editor.setOption('placeholder', 'Code...'); - } - const value = this.editor.getValue(); - - // Force redraw of placeholder - if(value.length === 0) { - this.editor.setValue(' '); - this.editor.setValue(''); - } + // perf testing: + /*let closure = () => { + let x = 0; + for (var i = 0; i < 30; i++) { + x++; + props.api.chat.message( + props.station, + `~${window.ship}`, + Date.now(), + { + text: `${x}` + } + ); + } + setTimeout(closure, 1000); + }; + this.closure = closure.bind(this); + setTimeout(this.closure, 2000);*/ } uploadSuccess(url) { @@ -294,7 +200,7 @@ export class ChatInput extends Component { const sigilClass = props.ownerContact ? '' : 'mix-blend-diff'; - const img = (props.ownerContact && (props.ownerContact.avatar !== null)) + const avatar = (props.ownerContact && (props.ownerContact.avatar !== null)) ? : ; - const candidates = _.chain(this.props.envelopes) - .defaultTo([]) - .map('author') - .uniq() - .reverse() - .value(); - - const codeTheme = state.code ? ' code' : ''; - - const options = { - mode: MARKDOWN_CONFIG, - theme: 'tlon' + codeTheme, - lineNumbers: false, - lineWrapping: true, - scrollbarStyle: 'native', - cursorHeight: 0.85, - placeholder: state.code ? 'Code...' : props.placeholder, - extraKeys: { - Tab: cm => - this.patpAutocomplete(cm.getValue(), true), - 'Enter': () => { - this.messageSubmit(); - if (this.state.code) { - this.toggleCode(); - } - }, - 'Shift-3': cm => - cm.getValue().length === 0 - ? this.toggleCode() - : CodeMirror.Pass - } - }; - return ( -
- -
- {img} -
-
- { - this.editor = editor; - if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test( - navigator.userAgent - )) { - editor.focus(); - } - }} - onChange={(e, d, v) => this.messageChange(e, d, v)} - /> +
+
+ {avatar}
+
+ style={{ + height: '16px', + width: '16px', + flexBasis: 16, + marginTop: 10 + }}>
-
- +
+
); diff --git a/pkg/interface/src/apps/chat/components/lib/chat-message.tsx b/pkg/interface/src/apps/chat/components/lib/chat-message.tsx new file mode 100644 index 000000000..84b985417 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/chat-message.tsx @@ -0,0 +1,84 @@ +import React, { PureComponent, Fragment } from "react"; +import moment from "moment"; + +import { Message } from "./message"; + +type IMessage = Envelope & { pending?: boolean }; + + +export const ChatMessage = (props) => { + const { + msg, + previousMsg, + nextMsg, + isLastUnread, + group, + association, + contacts, + unreadRef + } = props; + + // Render sigil if previous message is not by the same sender + const aut = ["author"]; + const renderSigil = + _.get(nextMsg, aut) !== _.get(msg, aut, msg.author); + const paddingTop = renderSigil; + const paddingBot = + _.get(previousMsg, aut) !== _.get(msg, aut, msg.author); + + const when = ["when"]; + const dayBreak = + moment(_.get(nextMsg, when)).format("YYYY.MM.DD") !== + moment(_.get(msg, when)).format("YYYY.MM.DD"); + + const messageElem = ( + + ); + + if (props.isLastUnread) { + return ( + + {messageElem} +
+
+

New messages below

+
+ {dayBreak && ( +

+ {moment(_.get(msg, when)).calendar()} +

+ )} +
+
+
+ ); + } else if (dayBreak) { + return ( + + {messageElem} +
+

{moment(_.get(msg, when)).calendar()}

+
+
+ ); + } else { + return messageElem; + } +}; + diff --git a/pkg/interface/src/apps/chat/components/lib/chat-scroll-container.js b/pkg/interface/src/apps/chat/components/lib/chat-scroll-container.js new file mode 100644 index 000000000..68b0872c4 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/chat-scroll-container.js @@ -0,0 +1,143 @@ +import React, { Component, Fragment } from "react"; + +import { scrollIsAtTop, scrollIsAtBottom } from "../../../../lib/util"; + +// Restore chat position on FF when new messages come in +const recalculateScrollTop = (lastScrollHeight, scrollContainer) => { + if (!scrollContainer || !lastScrollHeight) { + return; + } + + const newScrollTop = scrollContainer.scrollHeight - lastScrollHeight; + if (scrollContainer.scrollTop !== 0 || + scrollContainer.scrollTop === newScrollTop) { + return; + } + + scrollContainer.scrollTop = scrollContainer.scrollHeight - lastScrollHeight; +}; + + +export class ChatScrollContainer extends Component { + constructor(props) { + super(props); + + // only for FF + this.state = { + lastScrollHeight: null + }; + + this.isTriggeredScroll = false; + + this.isAtBottom = true; + this.isAtTop = false; + + this.containerDidScroll = this.containerDidScroll.bind(this); + + this.containerRef = React.createRef(); + this.scrollRef = React.createRef(); + } + + containerDidScroll(e) { + const { props } = this; + if (scrollIsAtTop(e.target)) { + // Save scroll position for FF + if (navigator.userAgent.includes("Firefox")) { + this.setState({ + lastScrollHeight: e.target.scrollHeight, + }); + } + + if (!this.isAtTop) { + props.scrollIsAtTop(); + } + + this.isTriggeredScroll = false; + this.isAtBottom = false; + this.isAtTop = true; + } else if (scrollIsAtBottom(e.target) && !this.isTriggeredScroll) { + if (!this.isAtBottom) { + props.scrollIsAtBottom(); + } + + this.isTriggeredScroll = false; + this.isAtBottom = true; + this.isAtTop = false; + } else { + this.isAtBottom = false; + this.isAtTop = false; + this.isTriggeredScroll = false; + } + } + + render() { + // Replace with just the "not Firefox" implementation + // when Firefox #1042151 is patched. + + if (navigator.userAgent.includes("Firefox")) { + return this.firefoxScrollContainer(); + } else { + return this.normalScrollContainer(); + } + } + + firefoxScrollContainer() { + return ( +
+
+
+ {this.props.children} +
+
+ ); + } + + normalScrollContainer() { + return ( +
+
+ {this.props.children} +
+ ); + } + + scrollToBottom() { + this.isTriggeredScroll = true; + if (this.scrollRef.current) { + this.scrollRef.current.scrollIntoView(false); + } + + if (navigator.userAgent.includes("Firefox")) { + recalculateScrollTop( + this.state.lastScrollHeight, + this.scrollContainer + ); + } + } + + scrollToReference(ref) { + this.isTriggeredScroll = true; + if (this.scrollRef.current && ref.current) { + ref.current.scrollIntoView({ block: 'center' }); + } + + if (navigator.userAgent.includes("Firefox")) { + recalculateScrollTop( + this.state.lastScrollHeight, + this.scrollContainer + ); + } + } + +} diff --git a/pkg/interface/src/apps/chat/components/lib/chat-tabbar.js b/pkg/interface/src/apps/chat/components/lib/chat-tabbar.js index 97670f23e..69df556d3 100644 --- a/pkg/interface/src/apps/chat/components/lib/chat-tabbar.js +++ b/pkg/interface/src/apps/chat/components/lib/chat-tabbar.js @@ -1,66 +1,41 @@ import React, { Component } from 'react'; import { Link } from 'react-router-dom'; -export class ChatTabBar extends Component { - render() { - const props = this.props; +export const ChatTabBar = (props) => { + const { + location, + station + } = props; + let setColor = '', popout = ''; - let memColor = '', - setColor = '', - popout = ''; - - if (props.location.pathname.includes('/settings')) { - memColor = 'gray3'; - setColor = 'black white-d'; - } else if (props.location.pathname.includes('/members')) { - memColor = 'black white-d'; - setColor = 'gray3'; - } else { - memColor = 'gray3'; - setColor = 'gray3'; - } - - popout = props.location.pathname.includes('/popout') - ? 'popout/' : ''; - - const hidePopoutIcon = (this.props.popout) - ? 'dn-m dn-l dn-xl' : 'dib-m dib-l dib-xl'; - - return ( -
- {props.isOwner ? ( -
- - Members - -
- ) : ( -
- )} -
- - Settings - -
- - - -
- ); + if (location.pathname.includes('/settings')) { + setColor = 'black white-d'; + } else { + setColor = 'gray3'; } + + const hidePopoutIcon = (popout) + ? 'dn-m dn-l dn-xl' : 'dib-m dib-l dib-xl'; + + return ( +
+
+ + Settings + +
+ + + +
+ ); } diff --git a/pkg/interface/src/apps/chat/components/lib/chat-window.tsx b/pkg/interface/src/apps/chat/components/lib/chat-window.tsx new file mode 100644 index 000000000..b16f9d3ce --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/chat-window.tsx @@ -0,0 +1,194 @@ +import React, { Component, Fragment } from "react"; + +import { ChatMessage } from './chat-message'; +import { ChatScrollContainer } from "./chat-scroll-container"; +import { UnreadNotice } from "./unread-notice"; +import { ResubscribeElement } from "./resubscribe-element"; +import { BacklogElement } from "./backlog-element"; + +const MAX_BACKLOG_SIZE = 1000; +const DEFAULT_BACKLOG_SIZE = 200; +const PAGE_SIZE = 50; +const INITIAL_LOAD = 20; + + +export class ChatWindow extends Component { + constructor(props) { + super(props); + this.state = { + numPages: 1, + }; + + this.hasAskedForMessages = false; + + this.dismissUnread = this.dismissUnread.bind(this); + this.scrollIsAtBottom = this.scrollIsAtBottom.bind(this); + this.scrollIsAtTop = this.scrollIsAtTop.bind(this); + + this.scrollReference = React.createRef(); + this.unreadReference = React.createRef(); + } + + componentDidMount() { + this.initialFetch(); + + if (this.state.numPages === 1 && this.props.unreadCount < INITIAL_LOAD) { + this.dismissUnread(); + this.scrollToBottom(); + } + } + + initialFetch() { + const { props } = this; + if (props.messages.length > 0) { + const unreadUnloaded = props.unreadCount - props.messages.length; + + if (unreadUnloaded <= MAX_BACKLOG_SIZE && + unreadUnloaded + INITIAL_LOAD > DEFAULT_BACKLOG_SIZE) { + this.fetchBacklog(unreadUnloaded + INITIAL_LOAD); + } else { + this.fetchBacklog(DEFAULT_BACKLOG_SIZE); + } + } else { + setTimeout(() => { + this.initialFetch(); + }, 2000); + } + } + + componentDidUpdate(prevProps, prevState) { + const { props, state } = this; + + if (props.isChatMissing) { + props.history.push("/~chat"); + } else if (props.messages.length >= prevProps.messages.length + 10) { + this.hasAskedForMessages = false; + let numPages = props.unreadCount > 0 ? + Math.ceil(props.unreadCount / PAGE_SIZE) : this.state.numPages; + + if (this.state.numPages === numPages) { + if (props.unreadCount > 20) { + this.scrollToUnread(); + } + } else { + this.setState({ numPages }, () => { + if (props.unreadCount > 20) { + this.scrollToUnread(); + } + }); + } + } else if ( + state.numPages === 1 && + this.props.unreadCount < INITIAL_LOAD && + this.props.unreadCount > 0 + ) { + this.dismissUnread(); + this.scrollToBottom(); + } + } + + scrollIsAtTop() { + const { props, state } = this; + this.setState({ numPages: state.numPages + 1 }, () => { + if (state.numPages * PAGE_SIZE < props.length) { + this.fetchBacklog(DEFAULT_BACKLOG_SIZE); + } + }); + } + + scrollIsAtBottom() { + if (this.state.numPages !== 1) { + this.setState({ numPages: 1 }); + this.dismissUnread(); + } + } + + scrollToBottom() { + if (this.scrollReference.current) { + this.scrollReference.current.scrollToBottom(); + } + if (this.state.numPages !== 1) { + this.setState({ numPages: 1 }); + } + } + + scrollToUnread() { + if (this.scrollReference.current && this.unreadReference.current) { + this.scrollReference.current.scrollToReference(this.unreadReference); + } + } + + dismissUnread() { + this.props.api.chat.read(this.props.station); + } + + fetchBacklog(size) { + const { props } = this; + + if ( + props.messages.length >= props.length || + this.hasAskedForMessages || + props.length <= 0 + ) { + return; + } + + const start = + props.length - props.messages[props.messages.length - 1].number; + if (start > 0) { + const end = start + size < props.length ? start + size : props.length; + props.api.chat.fetchMessages(start + 1, end, props.station); + this.hasAskedForMessages = true; + } + } + + render() { + const { props, state } = this; + const sliceLength = Math.min( + state.numPages * PAGE_SIZE, + props.messages.length + props.pendingMessages.length + ); + const messages = + props.pendingMessages + .concat(props.messages) + .slice(0, sliceLength); + + return ( + + + + + + { messages.map((msg, i) => ( + 0 && + i === props.unreadCount - 1 && + state.numPages !== 1 + } + msg={msg} + previousMsg={messages[i - 1]} + nextMsg={messages[i + 1]} + association={props.association} + group={props.group} + contacts={props.contacts} /> + )) + } + + + ); + } +} + diff --git a/pkg/interface/src/apps/chat/components/lib/content/code.js b/pkg/interface/src/apps/chat/components/lib/content/code.js new file mode 100644 index 000000000..99ca099d8 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/content/code.js @@ -0,0 +1,28 @@ +import React, { Component } from 'react'; + + +export default class CodeContent extends Component { + + render() { + const { props } = this; + const content = props.content; + + const outputElement = + (Boolean(content.code.output) && + content.code.output.length && content.code.output.length > 0) ? + ( +
+          {content.code.output[0].join('\n')}
+        
+ ) : null; + + return ( +
+
+          {content.code.expression}
+        
+ {outputElement} +
+ ); + } +} diff --git a/pkg/interface/src/apps/chat/components/lib/content/text.js b/pkg/interface/src/apps/chat/components/lib/content/text.js new file mode 100644 index 000000000..f44d9bd6a --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/content/text.js @@ -0,0 +1,65 @@ +import React, { Component } from 'react'; +import { Link } from 'react-router-dom'; +import ReactMarkdown from 'react-markdown'; +import RemarkDisableTokenizers from 'remark-disable-tokenizers'; +import urbitOb from 'urbit-ob'; + +const DISABLED_BLOCK_TOKENS = [ + 'indentedCode', + 'blockquote', + 'atxHeading', + 'thematicBreak', + 'list', + 'setextHeading', + 'html', + 'definition', + 'table' +]; + +const DISABLED_INLINE_TOKENS = [ + 'autoLink', + 'url', + 'email', + 'link', + 'reference' +]; + +const MessageMarkdown = React.memo(props => ( + +)); + + +export default class TextContent extends Component { + + render() { + const { props } = this; + const content = props.content; + + const group = content.text.match( + /([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z])+([/-])?)+/ + ); + if ((group !== null) // matched possible chatroom + && (group[2].length > 2) // possible ship? + && (urbitOb.isValidPatp(group[2]) // valid patp? + && (group[0] === content.text))) { // entire message is room name? + return ( + + {content.text} + + ); + } else { + return ( +
+ +
+ ); + } + } +} diff --git a/pkg/interface/src/apps/chat/components/lib/content/url.js b/pkg/interface/src/apps/chat/components/lib/content/url.js new file mode 100644 index 000000000..21fb6dff1 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/content/url.js @@ -0,0 +1,102 @@ +import React, { Component } from 'react'; + +const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i); + +const YOUTUBE_REGEX = + new RegExp( + String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) // protocol + + /(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source // short and long-links + + /(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id + ); + +export default class UrlContent extends Component { + constructor() { + super(); + this.state = { + unfold: false, + copied: false + }; + this.unfoldEmbed = this.unfoldEmbed.bind(this); + } + + unfoldEmbed(id) { + let unfoldState = this.state.unfold; + unfoldState = !unfoldState; + this.setState({ unfold: unfoldState }); + this.iframe.setAttribute('src', this.iframe.dataset.src); + } + + render() { + const { props } = this; + const content = props.content; + const imgMatch = IMAGE_REGEX.exec(props.content.url); + const ytMatch = YOUTUBE_REGEX.exec(props.content.url); + + let contents = content.url; + if (imgMatch) { + contents = ( + + ); + return ( + + {contents} + + ); + } else if (ytMatch) { + contents = ( +
+ +
+ ); + return ( + + ); + } else { + return ( + + {contents} + + ); + } + } +} diff --git a/pkg/interface/src/apps/chat/components/lib/delete-button.js b/pkg/interface/src/apps/chat/components/lib/delete-button.js new file mode 100644 index 000000000..cb7b75ae7 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/delete-button.js @@ -0,0 +1,51 @@ +import React, { Component } from 'react'; + + +export const DeleteButton = (props) => { + const { isOwner, station, changeLoading, api } = props; + const leaveButtonClasses = (!isOwner) ? 'pointer' : 'c-default'; + const deleteButtonClasses = (isOwner) ? + 'b--red2 red2 pointer bg-gray0-d' : + 'b--gray3 gray3 bg-gray0-d c-default'; + + const deleteChat = () => { + changeLoading( + true, + true, + isOwner ? 'Deleting chat...' : 'Leaving chat...', + () => { + api.chat.delete(station); + } + ); + }; + + return ( +
+
+

Leave Chat

+

+ Remove this chat from your chat list.{' '} + You will need to request for access again. +

+ + Leave this chat + +
+
+

Delete Chat

+

+ Permanently delete this chat.{' '} + All current members will no longer see this chat. +

+ Delete this chat +
+
+ ); +}; + diff --git a/pkg/interface/src/apps/chat/components/lib/groupify-button.js b/pkg/interface/src/apps/chat/components/lib/groupify-button.js new file mode 100644 index 000000000..786c5d9a3 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/groupify-button.js @@ -0,0 +1,104 @@ +import React, { Component } from 'react'; +import Toggle from '../../../../components/toggle'; +import { InviteSearch } from '../../../../components/InviteSearch'; + + +export class GroupifyButton extends Component { + + constructor(props) { + super(props); + + this.state = { + inclusive: false, + targetGroup: null + }; + } + + changeTargetGroup(target) { + if (target.groups.length === 1) { + this.setState({ targetGroup: target.groups[0] }); + } else { + this.setState({ targetGroup: null }); + } + } + + changeInclusive(event) { + this.setState({ inclusive: Boolean(event.target.checked) }); + } + + renderInclusiveToggle() { + return this.state.targetGroup ? ( +
+ + + Add all members to group + +

+ Add chat members to the group if they aren't in it yet +

+
+ ) :
; + } + + render() { + const { inclusive, targetGroup } = this.state; + const { + api, + isOwner, + association, + associations, + contacts, + groups, + station + } = this.props; + + const groupPath = association['group-path']; + const ownedUnmanagedVillage = + isOwner && + !contacts[groupPath]; + + if (!ownedUnmanagedVillage) { + return null; + } + + return ( + + ); + } +} + diff --git a/pkg/interface/src/apps/chat/components/lib/message-content.js b/pkg/interface/src/apps/chat/components/lib/message-content.js new file mode 100644 index 000000000..865ab3af2 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/message-content.js @@ -0,0 +1,33 @@ +import React, { Component } from 'react'; + +import TextContent from './content/text'; +import CodeContent from './content/code'; +import UrlContent from './content/url'; + + +export default class MessageContent extends Component { + + render() { + const { props } = this; + + const content = props.letter; + + if ('code' in content) { + return ; + } else if ('url' in content) { + return ; + } else if ('me' in content) { + return ( +

+ {content.me} +

+ ); + } + else if ('text' in content) { + return ; + } else { + return null; + } + } + +} diff --git a/pkg/interface/src/apps/chat/components/lib/message.js b/pkg/interface/src/apps/chat/components/lib/message.js index 1c61a9eeb..f4b6b5729 100644 --- a/pkg/interface/src/apps/chat/components/lib/message.js +++ b/pkg/interface/src/apps/chat/components/lib/message.js @@ -1,275 +1,101 @@ import React, { Component } from 'react'; -import { Link } from 'react-router-dom'; import { OverlaySigil } from './overlay-sigil'; +import MessageContent from './message-content'; import { uxToHex, cite, writeText } from '../../../../lib/util'; import moment from 'moment'; -import ReactMarkdown from 'react-markdown'; -import RemarkDisableTokenizers from 'remark-disable-tokenizers'; -import urbitOb from 'urbit-ob'; -const DISABLED_BLOCK_TOKENS = [ - 'indentedCode', - 'blockquote', - 'atxHeading', - 'thematicBreak', - 'list', - 'setextHeading', - 'html', - 'definition', - 'table' -]; -const DISABLED_INLINE_TOKENS = [ - 'autoLink', - 'url', - 'email', - 'link', - 'reference' -]; +export const Message = (props) => { + const pending = props.msg.pending ? ' o-40' : ''; + const containerClass = + props.renderSigil ? + `w-100 f7 pl3 pt4 pr3 cf flex lh-copy ` + pending : + 'w-100 pr3 cf hide-child flex' + pending; -const MessageMarkdown = React.memo( - props => ()); + const timestamp = + moment.unix(props.msg.when / 1000).format( + props.renderSigil ? 'hh:mm a' : 'hh:mm' + ); -export class Message extends Component { - constructor() { - super(); - this.state = { - unfold: false, - copied: false - }; - this.unFoldEmbed = this.unFoldEmbed.bind(this); - } - - unFoldEmbed(id) { - let unfoldState = this.state.unfold; - unfoldState = !unfoldState; - this.setState({ unfold: unfoldState }); - const iframe = this.refs.iframe; - iframe.setAttribute('src', iframe.getAttribute('data-src')); - } - - renderContent() { - const { props } = this; - const letter = props.msg.letter; - - if ('code' in letter) { - const outputElement = - (Boolean(letter.code.output) && - letter.code.output.length && letter.code.output.length > 0) ? - ( -
-            {letter.code.output[0].join('\n')}
-          
- ) : null; - return ( -
-
-            {letter.code.expression}
-          
- {outputElement} -
- ); - } else if ('url' in letter) { - const imgMatch = - /(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|svg|SVG)$/ - .exec(letter.url); - const youTubeRegex = new RegExp(String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) // protocol - + /(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source // short and long-links - + /(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id - ); - const ytMatch = - youTubeRegex.exec(letter.url); - let contents = letter.url; - if (imgMatch) { - contents = ( - - ); - return ( - - {contents} - - ); - } else if (ytMatch) { - contents = ( -
- -
- ); - return ( - - ); - } else { - return ( - - {contents} - - ); - } - } else if ('me' in letter) { - return ( -

- {letter.me} -

- ); - } else { - const group = letter.text.match( - /([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z])+([/-])?)+/ - ); - if ((group !== null) // matched possible chatroom - && (group[2].length > 2) // possible ship? - && (urbitOb.isValidPatp(group[2]) // valid patp? - && (group[0] === letter.text))) { // entire message is room name? - return ( - - {letter.text} - - ); - } else { - return ( -
- -
- ); - } - } -} - - render() { - const { props, state } = this; - const pending = props.msg.pending ? ' o-40' : ''; - const datestamp = '~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D'); - - const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : ''; - - if (props.renderSigil) { - const timestamp = moment.unix(props.msg.when / 1000).format('hh:mm a'); - - const contact = props.msg.author in props.contacts - ? props.contacts[props.msg.author] : false; - let name = `~${props.msg.author}`; - let color = '#000000'; - let sigilClass = 'mix-blend-diff'; - if (contact) { - name = (contact.nickname.length > 0) - ? contact.nickname : `~${props.msg.author}`; - color = `#${uxToHex(contact.color)}`; - sigilClass = ''; - } - - if (`~${props.msg.author}` === name) { - name = cite(props.msg.author); - } - - return ( -
- -
-
-

- { - writeText(props.msg.author); - this.setState({ copied: true }); - setTimeout(() => { - this.setState({ copied: false }); - }, 800); - }} - title={`~${props.msg.author}`} - > - {state.copied && 'Copied' || name} - -

-

{timestamp}

-

{datestamp}

+ return ( +
+ { + props.renderSigil ? ( + renderWithSigil(props, timestamp) + ) : ( +
+

{timestamp}

+
+
- {this.renderContent()}
-
- ); - } else { - const timestamp = moment.unix(props.msg.when / 1000).format('hh:mm'); + ) + } +
+ ); +}; - return ( -
-

{timestamp}

-
- {this.renderContent()} -
-
- ); +const renderWithSigil = (props, timestamp) => { + const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : ''; + const datestamp = + '~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D'); + + const contact = props.msg.author in props.contacts + ? props.contacts[props.msg.author] : false; + let name = `~${props.msg.author}`; + let color = '#000000'; + let sigilClass = 'mix-blend-diff'; + if (contact) { + name = (contact.nickname.length > 0) + ? contact.nickname : `~${props.msg.author}`; + color = `#${uxToHex(contact.color)}`; + sigilClass = ''; } + + if (`~${props.msg.author}` === name) { + name = cite(props.msg.author); + } + + return ( +
+ +
+
+

+ { + writeText(props.msg.author); + }} + title={`~${props.msg.author}`} + > + {name} + +

+

{timestamp}

+

+ {datestamp} +

+
+ +
+
+ ); } -} + diff --git a/pkg/interface/src/apps/chat/components/lib/metadata-color.js b/pkg/interface/src/apps/chat/components/lib/metadata-color.js new file mode 100644 index 000000000..5faadf253 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/metadata-color.js @@ -0,0 +1,71 @@ +import React, { Component } from 'react'; + +import { uxToHex } from '../../../../lib/util'; + + +export class MetadataColor extends Component { + + constructor(props) { + super(props); + this.state = { + color: props.initialValue + }; + + this.changeColor = this.changeColor.bind(this); + this.submitColor = this.submitColor.bind(this); + } + + componentDidUpdate(prevProps) { + const { props } = this; + if (prevProps.initialValue !== props.initialValue) { + this.setState({ color: props.initialValue }); + } + } + + changeColor(event) { + this.setState({ color: event.target.value }); + } + + submitColor() { + const { props, state } = this; + + let color = state.color; + if (color.startsWith('#')) { + color = state.color.substr(1); + } + const hexExp = /([0-9A-Fa-f]{6})/; + const hexTest = hexExp.exec(color); + if (!props.isDisabled && hexTest && (state.color !== props.initialValue)) { + props.setValue(color); + } + } + + render() { + const { props, state } = this; + return ( +
+

Change color

+

Give this chat a color when viewing group channels

+
+
+ +
+
+ ); + } +} diff --git a/pkg/interface/src/apps/chat/components/lib/metadata-input.js b/pkg/interface/src/apps/chat/components/lib/metadata-input.js new file mode 100644 index 000000000..e3a5d8399 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/metadata-input.js @@ -0,0 +1,52 @@ +import React, { Component } from 'react'; + +export class MetadataInput extends Component { + + constructor(props) { + super(props); + + this.state = { + value: props.initialValue + }; + } + + componentDidUpdate(prevProps) { + const { props } = this; + if (prevProps.initialValue !== props.initialValue) { + this.setState({ value: props.initialValue }); + } + } + + render() { + const { + title, + description, + isDisabled, + setValue + } = this.props; + + return ( +
+

{title}

+

{description}

+
+ { + this.setState({ value: e.target.value }); + }} + onBlur={() => { + if (!isDisabled) { + setValue(this.state.value || ''); + } + }} + /> +
+
+ ); + } +} diff --git a/pkg/interface/src/apps/chat/components/lib/metadata-settings.js b/pkg/interface/src/apps/chat/components/lib/metadata-settings.js new file mode 100644 index 000000000..cd8a18c49 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/metadata-settings.js @@ -0,0 +1,90 @@ +import React, { Component } from 'react'; + +import { MetadataColor } from './metadata-color'; +import { MetadataInput } from './metadata-input'; +import { uxToHex } from '../../../../lib/util'; + + +export const MetadataSettings = (props) => { + const { + isOwner, + association, + changeLoading, + api, + station + } = props; + + const title = + (props.association && 'metadata' in props.association) ? + association.metadata.title : ''; + const description = + (props.association && 'metadata' in props.association) ? + association.metadata.description : ''; + const color = + (props.association && 'metadata' in props.association) ? + `#${uxToHex(props.association.metadata.color)}` : ''; + + return ( +
+ { + changeLoading(false, true, 'Editing chat...', () => { + api.metadata.metadataAdd( + 'chat', + association['app-path'], + association['group-path'], + val, + association.metadata.description, + association.metadata['date-created'], + uxToHex(association.metadata.color) + ).then(() => { + changeLoading(false, false, '', () => {}); + }); + }); + }} /> + { + changeLoading(false, true, 'Editing chat...', () => { + api.metadata.metadataAdd( + 'chat', + association['app-path'], + association['group-path'], + association.metadata.title, + val, + association.metadata['date-created'], + uxToHex(association.metadata.color) + ).then(() => { + changeLoading(false, false, '', () => {}); + }); + }); + }} /> + { + changeLoading(false, true, 'Editing chat...', () => { + props.api.metadata.metadataAdd( + 'chat', + association['app-path'], + association['group-path'], + association.metadata.title, + association.metadata.description, + association.metadata['date-created'], + val + ).then(() => { + changeLoading(false, false, '', () => {}); + }); + }); + }} /> +
+ ); +}; + diff --git a/pkg/interface/src/apps/chat/components/lib/profile-overlay.js b/pkg/interface/src/apps/chat/components/lib/profile-overlay.js index e679e9e38..b9a690bd7 100644 --- a/pkg/interface/src/apps/chat/components/lib/profile-overlay.js +++ b/pkg/interface/src/apps/chat/components/lib/profile-overlay.js @@ -46,7 +46,7 @@ export class ProfileOverlay extends Component { if (!(top || bottom)) { bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`; } - const containerStyle = { top, bottom, left: '100%' }; + const containerStyle = { top, bottom, left: '100%', maxWidth: '160px' }; const isOwn = window.ship === ship; @@ -79,7 +79,7 @@ export class ProfileOverlay extends Component {
{contact && contact.nickname && ( -
{contact.nickname}
+
{contact.nickname}
)}
{cite(`~${ship}`)}
{!isOwn && ( diff --git a/pkg/interface/src/apps/chat/components/lib/resubscribe-element.js b/pkg/interface/src/apps/chat/components/lib/resubscribe-element.js index 61186a375..6ac875705 100644 --- a/pkg/interface/src/apps/chat/components/lib/resubscribe-element.js +++ b/pkg/interface/src/apps/chat/components/lib/resubscribe-element.js @@ -9,19 +9,24 @@ export class ResubscribeElement extends Component { } render() { - return ( -
-

- Your ship has been disconnected from the chat's host. - This may be due to a bad connection, going offline, lack of permission, - or an over-the-air update. -

- - Reconnect to this chat - -
- ); + const { props } = this; + if (props.isChatUnsynced) { + return ( +
+

+ Your ship has been disconnected from the chat's host. + This may be due to a bad connection, going offline, lack of permission, + or an over-the-air update. +

+ + Reconnect to this chat + +
+ ); + } else { + return null; + } } } diff --git a/pkg/interface/src/apps/chat/components/lib/sidebar-invite.js b/pkg/interface/src/apps/chat/components/lib/sidebar-invite.js deleted file mode 100644 index de914c255..000000000 --- a/pkg/interface/src/apps/chat/components/lib/sidebar-invite.js +++ /dev/null @@ -1,38 +0,0 @@ -import React, { Component } from 'react'; - -export class SidebarInvite extends Component { - onAccept() { - this.props.api.invite.accept('/chat', this.props.uid); - } - - onDecline() { - this.props.api.invite.decline('/chat', this.props.uid); - } - - render() { - const { props } = this; - - return ( -
-
-

- {props.invite.path} -

-
- - Accept Invite - - - Decline - -
- ); - } -} - diff --git a/pkg/interface/src/apps/chat/components/lib/unread-notice.js b/pkg/interface/src/apps/chat/components/lib/unread-notice.js index e18f612ef..2724d23c9 100644 --- a/pkg/interface/src/apps/chat/components/lib/unread-notice.js +++ b/pkg/interface/src/apps/chat/components/lib/unread-notice.js @@ -1,37 +1,41 @@ import React, { Component } from 'react'; import moment from 'moment'; -export class UnreadNotice extends Component { - render() { - const { unread, unreadMsg, onRead } = this.props; +export const UnreadNotice = (props) => { + const { unreadCount, unreadMsg, dismissUnread } = props; - let datestamp = moment.unix(unreadMsg.when / 1000).format('YYYY.M.D'); - const timestamp = moment.unix(unreadMsg.when / 1000).format('HH:mm'); + if (!unreadMsg || (unreadCount === 0)) { + return null; + } - if (datestamp === moment().format('YYYY.M.D')) { - datestamp = null; - } + let datestamp = moment.unix(unreadMsg.when / 1000).format('YYYY.M.D'); + const timestamp = moment.unix(unreadMsg.when / 1000).format('HH:mm'); - return ( -
-
-

- {unread} new messages since{' '} - {datestamp && ( - <> - ~{datestamp} at{' '} - - )} - {timestamp} -

-
- Mark as Read -
+ if (datestamp === moment().format('YYYY.M.D')) { + datestamp = null; + } + + return ( +
+
+

+ {unreadCount} new messages since{' '} + {datestamp && ( + <> + ~{datestamp} at{' '} + + )} + {timestamp} +

+
+ Mark as Read
- ); - } +
+ ); } diff --git a/pkg/interface/src/apps/chat/components/member.js b/pkg/interface/src/apps/chat/components/member.js deleted file mode 100644 index 2ffb04461..000000000 --- a/pkg/interface/src/apps/chat/components/member.js +++ /dev/null @@ -1,94 +0,0 @@ -import React, { Component } from 'react'; - -import { Link } from 'react-router-dom'; - -import { deSig } from '../../../lib/util'; -import { ChatTabBar } from './lib/chat-tabbar'; -import { MemberElement } from './lib/member-element'; -import { InviteElement } from './lib/invite-element'; -import { SidebarSwitcher } from '../../../components/SidebarSwitch'; -import { GroupView } from '../../../components/Group'; -import { PatpNoSig } from '../../../types/noun'; - -export class MemberScreen extends Component { - constructor(props) { - super(props); - this.inviteShips = this.inviteShips.bind(this); - } - - inviteShips(ships) { - const { props } = this; - return props.api.chat.invite(props.station, ships.map(s => `~${s}`)); - } - - render() { - const { props } = this; - - const isinPopout = this.props.popout ? 'popout/' : ''; - - let title = props.station.substr(1); - - if (props.association && 'metadata' in props.association) { - title = - props.association.metadata.title !== '' - ? props.association.metadata.title - : props.station.substr(1); - } - - return ( -
-
- {'⟵ All Chats'} -
-
- - -

- {title} -

- - -
-
- { props.association['group-path'] && ( - )} -
-
- ); - } -} diff --git a/pkg/interface/src/apps/chat/components/settings.js b/pkg/interface/src/apps/chat/components/settings.js index 6109cff9b..d0e4716ae 100644 --- a/pkg/interface/src/apps/chat/components/settings.js +++ b/pkg/interface/src/apps/chat/components/settings.js @@ -1,462 +1,143 @@ -import React, { Component } from 'react'; -import { deSig, uxToHex, writeText } from '../../../lib/util'; +import React, { Component, Fragment } from 'react'; +import { deSig } from '../../../lib/util'; import { Link } from 'react-router-dom'; +import { ChatHeader } from './lib/chat-header'; +import { MetadataSettings } from './lib/metadata-settings'; +import { DeleteButton } from './lib/delete-button'; +import { GroupifyButton } from './lib/groupify-button'; import { Spinner } from '../../../components/Spinner'; import { ChatTabBar } from './lib/chat-tabbar'; -import { InviteSearch } from '../../../components/InviteSearch'; import SidebarSwitcher from '../../../components/SidebarSwitch'; + export class SettingsScreen extends Component { constructor(props) { super(props); this.state = { isLoading: false, - title: '', - description: '', - color: '', - // groupify settings - targetGroup: null, - inclusive: false, awaiting: false, type: 'Editing chat...' }; - this.renderDelete = this.renderDelete.bind(this); - this.changeTargetGroup = this.changeTargetGroup.bind(this); - this.changeInclusive = this.changeInclusive.bind(this); - this.changeTitle = this.changeTitle.bind(this); - this.changeDescription = this.changeDescription.bind(this); - this.changeColor = this.changeColor.bind(this); - this.submitColor = this.submitColor.bind(this); + this.changeLoading = this.changeLoading.bind(this); } componentDidMount() { - const { props } = this; - if (props.association && 'metadata' in props.association) { - this.setState({ - title: props.association.metadata.title, - description: props.association.metadata.description, - color: `#${uxToHex(props.association.metadata.color)}` - }); + if (this.state.isLoading && (this.props.station in this.props.inbox)) { + this.setState({ isLoading: false }); } } componentDidUpdate(prevProps) { const { props, state } = this; - if (Boolean(state.isLoading) && !(props.station in props.inbox)) { + if (state.isLoading && !(props.station in props.inbox)) { this.setState({ isLoading: false }, () => { props.history.push('/~chat'); }); - } - - if ((state.title === '') && (prevProps !== props)) { - if (props.association && 'metadata' in props.association) - this.setState({ - title: props.association.metadata.title, - description: props.association.metadata.description, - color: `#${uxToHex(props.association.metadata.color)}` - }); + } else if (state.isLoading && (props.station in props.inbox)) { + this.setState({ isLoading: false }); } } - changeTargetGroup(target) { - if (target.groups.length === 1) { - this.setState({ targetGroup: target.groups[0] }); - } else { - this.setState({ targetGroup: null }); - } - } - - changeInclusive(event) { - this.setState({ inclusive: Boolean(event.target.checked) }); - } - - changeTitle() { - this.setState({ title: event.target.value }); - } - - changeDescription() { - this.setState({ description: event.target.value }); - } - - changeColor() { - this.setState({ color: event.target.value }); - } - - submitColor() { - const { props, state } = this; - - let color = state.color; - if (color.startsWith('#')) { - color = state.color.substr(1); - } - const hexExp = /([0-9A-Fa-f]{6})/; - const hexTest = hexExp.exec(color); - let currentColor = '000000'; - if (props.association && 'metadata' in props.association) { - currentColor = uxToHex(props.association.metadata.color); - } - if (hexTest && (hexTest[1] !== currentColor)) { - const chatOwner = (deSig(props.match.params.ship) === window.ship); - const association = - (props.association) && ('metadata' in props.association) - ? props.association : {}; - - if (chatOwner) { - this.setState({ awaiting: true, type: 'Editing chat...' }, (() => { - props.api.metadata.metadataAdd( - 'chat', - association['app-path'], - association['group-path'], - association.metadata.title, - association.metadata.description, - association.metadata['date-created'], - color - ).then(() => { - this.setState({ awaiting: false }); - }); - })); - } - } - } - - deleteChat() { - const { props } = this; - + changeLoading(isLoading, awaiting, type, closure) { this.setState({ - isLoading: true, - awaiting: true, - type: (deSig(props.match.params.ship) === window.ship) - ? 'Deleting chat...' - : 'Leaving chat...' - }, (() => { - props.api.chat.delete(props.station); - })); + isLoading, + awaiting, + type + }, closure); } - groupifyChat() { - const { props, state } = this; - - this.setState({ - isLoading: true, - awaiting: true, - type: 'Converting chat...' - }, (() => { - props.api.chat.groupify( - props.station, state.targetGroup, state.inclusive - ).then(() => this.setState({ awaiting: false })); - })); - } - - renderDelete() { - const { props } = this; - - const chatOwner = (deSig(props.match.params.ship) === window.ship); - - const deleteButtonClasses = (chatOwner) ? 'b--red2 red2 pointer bg-gray0-d' : 'b--gray3 gray3 bg-gray0-d c-default'; - const leaveButtonClasses = (!chatOwner) ? 'pointer' : 'c-default'; - + renderLoading() { return ( -
-
-

Leave Chat

-

Remove this chat from your chat list. You will need to request for access again.

- Leave this chat -
-
-

Delete Chat

-

Permanently delete this chat. All current members will no longer see this chat.

- Delete this chat -
-
+ ); } - renderGroupify() { - const { props, state } = this; + renderNormal() { + const { state } = this; + const { + associations, + association, + contacts, + groups, + api, + station, + match + } = this.props; + const isOwner = deSig(match.params.ship) === window.ship; - const chatOwner = (deSig(props.match.params.ship) === window.ship); - - const groupPath = props.association['group-path']; - const ownedUnmanagedVillage = - chatOwner && - !props.contacts[groupPath]; - - if (!ownedUnmanagedVillage) { - return null; - } else { - let inclusiveToggle =
; - if (state.targetGroup) { - // TODO toggle component into /lib - const inclusiveClasses = state.inclusive - ? 'relative checked bg-green2 br3 h1 toggle v-mid z-0' - : 'relative bg-gray4 bg-gray1-d br3 h1 toggle v-mid z-0'; - inclusiveToggle = ( -
- - - Add all members to group - -

- Add chat members to the group if they aren't in it yet -

-
- ); - } - - return ( -
-
-

Convert Chat

-

- Convert this chat into a group with associated chat, or select a - group to add this chat to. -

- - {inclusiveToggle} - - Convert to group - -
-
- ); - } - } - - renderMetadataSettings() { - const { props, state } = this; - - const chatOwner = (deSig(props.match.params.ship) === window.ship); - - const association = (props.association) && ('metadata' in props.association) - ? props.association : {}; - - return( -
-
-

Rename

-

Change the name of this chat

-
- { - if (chatOwner) { - this.setState({ awaiting: true, type: 'Editing chat...' }, (() => { - props.api.metadata.metadataAdd( - 'chat', - association['app-path'], - association['group-path'], - state.title, - association.metadata.description, - association.metadata['date-created'], - uxToHex(association.metadata.color) - ).then(() => { - this.setState({ awaiting: false }); - }); - })); - } - }} - /> -
-

Change description

-

Change the description of this chat

-
- { - if (chatOwner) { - this.setState({ awaiting: true, type: 'Editing chat...' }, (() => { - props.api.metadata.metadataAdd( - 'chat', - association['app-path'], - association['group-path'], - association.metadata.title, - state.description, - association.metadata['date-created'], - uxToHex(association.metadata.color) - ).then(() => { - this.setState({ awaiting: false }); - }); - })); - } - }} - /> -
-

Change color

-

Give this chat a color when viewing group channels

-
-
- -
-
-
+ return ( + +

Chat Settings

+ + + + +
); } render() { - const { props, state } = this; - const isinPopout = this.props.popout ? 'popout/' : ''; + const { state } = this; + const { + api, + group, + association, + station, + popout, + sidebarShown, + match, + location + } = this.props; - const permission = Array.from(props.group.members.values()); - - if (state.isLoading) { - let title = props.station.substr(1); - - if ((props.association) && ('metadata' in props.association)) { - title = (props.association.metadata.title !== '') - ? props.association.metadata.title : props.station.substr(1); - } - - return ( -
-
- {'⟵ All Chats'} -
-
- - -

- {title} -

- - -
-
- -
-
- ); - } - - let title = props.station.substr(1); - - if ((props.association) && ('metadata' in props.association)) { - title = (props.association.metadata.title !== '') - ? props.association.metadata.title : props.station.substr(1); - } + const isInPopout = popout ? "popout/" : ""; + const title = + ( association && + ('metadata' in association) && + (association.metadata.title !== '') + ) ? association.metadata.title : station.substr(1); return (
-
- {'⟵ All Chats'} -
-
- - -

- {title} -

- - -
+
-

Chat Settings

- {this.renderGroupify()} - {this.renderDelete()} - {this.renderMetadataSettings()} - + {(state.isLoading) ? this.renderLoading() : this.renderNormal() }
); diff --git a/pkg/interface/src/apps/chat/components/sidebar.js b/pkg/interface/src/apps/chat/components/sidebar.js index b94e62c12..95abbca9a 100644 --- a/pkg/interface/src/apps/chat/components/sidebar.js +++ b/pkg/interface/src/apps/chat/components/sidebar.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import Welcome from './lib/welcome'; import { alphabetiseAssociations } from '../../../lib/util'; -import { SidebarInvite } from './lib/sidebar-invite'; +import SidebarInvite from '../../../components/SidebarInvite'; import { GroupItem } from './lib/group-item'; export class Sidebar extends Component { @@ -13,8 +13,6 @@ export class Sidebar extends Component { render() { const { props } = this; - const selectedGroups = props.selectedGroups ? props.selectedGroups : []; - const contactAssoc = (props.associations && 'contacts' in props.associations) ? alphabetiseAssociations(props.associations.contacts) : {}; @@ -51,25 +49,16 @@ export class Sidebar extends Component { .map((uid) => { return ( props.api.invite.accept('/chat', uid)} + onDecline={() => props.api.invite.decline('/chat', uid)} /> ); }); const groupedItems = Object.keys(contactAssoc) .filter(each => (groupedChannels[each] || []).length !== 0) - .filter((each) => { - if (selectedGroups.length === 0) { - return true; - } - const selectedPaths = selectedGroups.map((e) => { - return e[0]; - }); - return selectedPaths.includes(each); - }) .map((each, i) => { const channels = groupedChannels[each] || []; return( diff --git a/pkg/interface/src/apps/chat/components/skeleton.js b/pkg/interface/src/apps/chat/components/skeleton.js index 4c31e26e2..8fa175712 100644 --- a/pkg/interface/src/apps/chat/components/skeleton.js +++ b/pkg/interface/src/apps/chat/components/skeleton.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import classnames from 'classnames'; import { Link } from 'react-router-dom'; +import ErrorBoundary from '../../../components/ErrorBoundary'; export class Skeleton extends Component { render() { @@ -61,7 +62,9 @@ export class Skeleton extends Component { width: 'calc(100% - 300px)' }} > - {this.props.children} + + {this.props.children} +
diff --git a/pkg/interface/src/apps/groups/app.tsx b/pkg/interface/src/apps/groups/app.tsx index a88da3eb3..cefd0d6ec 100644 --- a/pkg/interface/src/apps/groups/app.tsx +++ b/pkg/interface/src/apps/groups/app.tsx @@ -48,7 +48,6 @@ export default class GroupsApp extends Component { const invites = (Boolean(props.invites) && '/contacts' in props.invites) ? props.invites['/contacts'] : {}; - const selectedGroups = props.selectedGroups ? props.selectedGroups : []; const s3 = props.s3 ? props.s3 : {}; const groups = props.groups || {}; const associations = props.associations || {}; @@ -62,7 +61,6 @@ export default class GroupsApp extends Component { return ( { return ( { return ( { return ( { return ( { { { 0) && !state.group)) { const incomingGroup = `${props.ship}/${props.name}`; // push to group if already exists if (`/ship/${incomingGroup}` in props.groups) { @@ -48,10 +46,8 @@ export class JoinScreen extends Component { } } - onClickJoin() { const { props, state } = this; - console.log('i am joining'); const { group } = state; const [ship, name] = group.split('/'); @@ -101,14 +97,13 @@ export class JoinScreen extends Component {

Enter a ~ship/group-name

Group names use lowercase, hyphens, and slashes.