diff --git a/Makefile b/Makefile index 4a6c40fd662d82ea2d2060f874c3a88ca266a0e3..916f6fd97a7a4b8e1018f3dd904f0cef7c0fc756 100644 --- a/Makefile +++ b/Makefile @@ -12,8 +12,8 @@ build: go mod tidy update_release: - GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@release - GOFLAGS="" go get gitlab.com/elixxir/crypto@release + GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@project/adminCommands + GOFLAGS="" go get gitlab.com/elixxir/crypto@project/adminCommands GOFLAGS="" go get gitlab.com/elixxir/primitives@release GOFLAGS="" go get gitlab.com/xx_network/crypto@release GOFLAGS="" go get gitlab.com/xx_network/primitives@release diff --git a/go.mod b/go.mod index 99834562d11b4df8a145098e671b1b798647fd64..e120361848d44dba0ad70b1d388cd72bcc66260e 100644 --- a/go.mod +++ b/go.mod @@ -7,15 +7,16 @@ require ( github.com/hack-pad/go-indexeddb v0.2.0 github.com/pkg/errors v0.9.1 github.com/spf13/jwalterweatherman v1.1.0 - gitlab.com/elixxir/client/v4 v4.3.11 - gitlab.com/elixxir/crypto v0.0.7-0.20221214192244-6783272c04a0 + gitlab.com/elixxir/client/v4 v4.3.12-0.20230104175249-e265a4ca4e58 + gitlab.com/elixxir/crypto v0.0.7-0.20230104175234-604a6cd56b98 gitlab.com/elixxir/primitives v0.0.3-0.20221214192222-988b44a6958a gitlab.com/xx_network/crypto v0.0.5-0.20221121220724-8eefdbb0eb46 - gitlab.com/xx_network/primitives v0.0.4-0.20221209210320-376735467d58 + gitlab.com/xx_network/primitives v0.0.4-0.20221219230308-4b5550a9247d golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa ) require ( + filippo.io/edwards25519 v1.0.0 // indirect git.xx.network/elixxir/grpc-web-go-client v0.0.0-20221215181401-0b8a26d47532 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/badoux/checkmail v1.2.1 // indirect @@ -33,6 +34,8 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/oasisprotocol/curve25519-voi v0.0.0-20221003100820-41fad3beba17 // indirect + github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/rs/cors v1.8.2 // indirect github.com/sethvargo/go-diceware v0.3.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect @@ -46,10 +49,13 @@ require ( gitlab.com/elixxir/ekv v0.2.1 // indirect gitlab.com/xx_network/comms v0.0.4-0.20221215214252-1275cef8760e // indirect gitlab.com/xx_network/ring v0.0.3-0.20220902183151-a7d3b15bc981 // indirect + gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect + gitlab.com/yawning/nyquist.git v0.0.0-20221003103146-de5645224a22 // indirect + gitlab.com/yawning/x448.git v0.0.0-20221003101044-617eb9b7d9b7 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c // indirect - golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect + golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect google.golang.org/grpc v1.49.0 // indirect diff --git a/go.sum b/go.sum index 394f9f31793bcf8c7a5ae66374cada0b18345486..f6a9c5771a5f13d839a4b85d08a8f282d284262c 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= git.xx.network/elixxir/grpc-web-go-client v0.0.0-20221215181401-0b8a26d47532 h1:EH4TFLgXGgofV2MsUOgNDmn3X+qfhbQ2RV6zOYRaSdU= git.xx.network/elixxir/grpc-web-go-client v0.0.0-20221215181401-0b8a26d47532/go.mod h1:uFKw2wmgtlYMdiIm08dM0Vj4XvX9ZKVCj71c8O7SAPo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -255,6 +257,10 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/oasisprotocol/curve25519-voi v0.0.0-20221003100820-41fad3beba17 h1:lpwUgSIAfvJJ9sQ9BB//ZCjqzzFSuSg8mYOf+8L96+E= +github.com/oasisprotocol/curve25519-voi v0.0.0-20221003100820-41fad3beba17/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= +github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= +github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -341,12 +347,15 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0= github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w= @@ -369,12 +378,12 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f h1:yXGvNBqzZwAhDYlSnxPRbgor6JWoOt1Z7s3z1O9JR40= gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k= -gitlab.com/elixxir/client/v4 v4.3.11 h1:LGXdAjnGdWbK1eVtrAn5Fwage7vrlrgPXsoEtUJ4Lyg= -gitlab.com/elixxir/client/v4 v4.3.11/go.mod h1:gxRW2YXDxCzESBnYqMDKH2I25TGv226TDujdQno3JQw= +gitlab.com/elixxir/client/v4 v4.3.12-0.20230104175249-e265a4ca4e58 h1:cZFtKQQT3FA6/calIKcDkwPhNjRWW/S9rHF788D03qc= +gitlab.com/elixxir/client/v4 v4.3.12-0.20230104175249-e265a4ca4e58/go.mod h1:IbslBQ3B9cAoWiHhucRSLcDiF6CMIDmbRlHfoO0sEDA= gitlab.com/elixxir/comms v0.0.4-0.20221215214627-7807bfdde33a h1:DuqDqWc5cWjZ3qk98K1Bf9y1dYlyCeIigFmkHWDKc1Q= gitlab.com/elixxir/comms v0.0.4-0.20221215214627-7807bfdde33a/go.mod h1:B2Yek4mCbtN2aXZkyZcUffd3sTEZ5WgKD0mRBSVYtF8= -gitlab.com/elixxir/crypto v0.0.7-0.20221214192244-6783272c04a0 h1:dwCf7wKv2DCuYZZ394bSQWdUOXiABLsEyDvXZUOo83o= -gitlab.com/elixxir/crypto v0.0.7-0.20221214192244-6783272c04a0/go.mod h1:oRh3AwveOEvpk9E3kRcMGK8fImcEnN0PY4jr9HDgQE8= +gitlab.com/elixxir/crypto v0.0.7-0.20230104175234-604a6cd56b98 h1:4uAPUenpRJYdnVGJzC3QXPX+dQkztL3YOMC1C0lJGN4= +gitlab.com/elixxir/crypto v0.0.7-0.20230104175234-604a6cd56b98/go.mod h1:7whUm4bnEdEoiVfMnu3TbHgvlrz0Ywp/Tekqg2Wl7vw= gitlab.com/elixxir/ekv v0.2.1 h1:dtwbt6KmAXG2Tik5d60iDz2fLhoFBgWwST03p7T+9Is= gitlab.com/elixxir/ekv v0.2.1/go.mod h1:USLD7xeDnuZEavygdrgzNEwZXeLQJK/w1a+htpN+JEU= gitlab.com/elixxir/primitives v0.0.3-0.20221214192222-988b44a6958a h1:F17FfEjS+/uDI/TTYQD21S5JvNZ9+p9bieau2nyLCzo= @@ -383,10 +392,16 @@ gitlab.com/xx_network/comms v0.0.4-0.20221215214252-1275cef8760e h1:l+FiCBP2Lc1+ gitlab.com/xx_network/comms v0.0.4-0.20221215214252-1275cef8760e/go.mod h1:FR/OyruSuob6+xzSZtk+rXlncbRr6nDKFypX3vwtkFc= gitlab.com/xx_network/crypto v0.0.5-0.20221121220724-8eefdbb0eb46 h1:6AHgUpWdJ72RVTTdJSvfThZiYTQNUnrPaTCl/EkRLpg= gitlab.com/xx_network/crypto v0.0.5-0.20221121220724-8eefdbb0eb46/go.mod h1:acWUBKCpae/XVaQF7J9RnLAlBT13i5r7gnON+mrIxBk= -gitlab.com/xx_network/primitives v0.0.4-0.20221209210320-376735467d58 h1:HpeUIf1gIIelLH3LHxEf3/GalecbbtZnOnIegJHALoc= -gitlab.com/xx_network/primitives v0.0.4-0.20221209210320-376735467d58/go.mod h1:wUxbEBGOBJZ/RkAiVAltlC1uIlIrU0dE113Nq7HiOhw= +gitlab.com/xx_network/primitives v0.0.4-0.20221219230308-4b5550a9247d h1:D9hEtiQ7xj0yFBkDkb4X4S95RfNoeXxtB1eE4UuFHtk= +gitlab.com/xx_network/primitives v0.0.4-0.20221219230308-4b5550a9247d/go.mod h1:wUxbEBGOBJZ/RkAiVAltlC1uIlIrU0dE113Nq7HiOhw= gitlab.com/xx_network/ring v0.0.3-0.20220902183151-a7d3b15bc981 h1:1s0vX9BbkiD0IVXwr3LOaTBcq1wBrWcUWMBK0s8r0Z0= gitlab.com/xx_network/ring v0.0.3-0.20220902183151-a7d3b15bc981/go.mod h1:aLzpP2TiZTQut/PVHR40EJAomzugDdHXetbieRClXIM= +gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= +gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= +gitlab.com/yawning/nyquist.git v0.0.0-20221003103146-de5645224a22 h1:25fLWFlW+5awvcQhZj4drwVHP4Cmb+iZpfiS6LgE8f0= +gitlab.com/yawning/nyquist.git v0.0.0-20221003103146-de5645224a22/go.mod h1:VvFd4eOUakA3ieUDzIpPT5GwkBTS/NKvIWr0SlNI8U4= +gitlab.com/yawning/x448.git v0.0.0-20221003101044-617eb9b7d9b7 h1:ITrNVw6uSwSdEap0RR4us4RV1CHPBHvBZApENRcDk3c= +gitlab.com/yawning/x448.git v0.0.0-20221003101044-617eb9b7d9b7/go.mod h1:BC2R0OW0tAYTMNLB4UMXwkk7WKokoDZP5n73hyLPyCo= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= @@ -413,6 +428,7 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -475,6 +491,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -492,10 +509,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220325203850-36772127a21f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220731174439-a90be440212d h1:Sv5ogFZatcgIMMtBSTTAgMYsicp25MXBubjXNDKwm80= -golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -588,6 +606,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/indexedDb/channels/implementation.go b/indexedDb/channels/implementation.go index f867e4b2f4ff590cc206562a5bdff87d40fbe03a..26c78490d6c522e16a3ef2df816af4f8e5af6998 100644 --- a/indexedDb/channels/implementation.go +++ b/indexedDb/channels/implementation.go @@ -13,11 +13,13 @@ import ( "crypto/ed25519" "encoding/base64" "encoding/json" - "gitlab.com/elixxir/xxdk-wasm/indexedDb" + "strings" "sync" "syscall/js" "time" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" + "github.com/hack-pad/go-indexeddb/idb" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" @@ -26,6 +28,7 @@ import ( "gitlab.com/elixxir/client/v4/cmix/rounds" cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/message" "gitlab.com/elixxir/xxdk-wasm/utils" "gitlab.com/xx_network/primitives/id" ) @@ -40,6 +43,13 @@ type wasmModel struct { updateMux sync.Mutex } +// DeleteMessage removes a message with the given messageID from storage. +func (w *wasmModel) DeleteMessage(messageID message.ID) error { + msgId := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes())) + return indexedDb.DeleteIndex(w.db, messageStoreName, + messageStoreMessageIndex, pkeyName, msgId) +} + // JoinChannel is called whenever a channel is joined locally. func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { parentErr := errors.New("failed to JoinChannel") @@ -76,35 +86,12 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { func (w *wasmModel) LeaveChannel(channelID *id.ID) { parentErr := errors.New("failed to LeaveChannel") - // Prepare the Transaction - txn, err := w.db.Transaction(idb.TransactionReadWrite, channelsStoreName) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to create Transaction: %+v", err)) - return - } - store, err := txn.ObjectStore(channelsStoreName) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to get ObjectStore: %+v", err)) - return - } - - // Perform the operation - _, err = store.Delete(js.ValueOf(channelID.String())) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to Delete Channel: %+v", err)) - return - } - - // Wait for the operation to return - ctx, cancel := indexedDb.NewContext() - err = txn.Await(ctx) - cancel() + // Delete the channel from storage + err := indexedDb.Delete(w.db, channelsStoreName, + js.ValueOf(channelID.String())) if err != nil { jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Deleting Channel failed: %+v", err)) + "Unable to delete Channel: %+v", err)) return } @@ -165,11 +152,10 @@ func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error { // // It may be called multiple times on the same message; it is incumbent on the // user of the API to filter such called by message ID. -func (w *wasmModel) ReceiveMessage(channelID *id.ID, - messageID cryptoChannel.MessageID, nickname, text string, - pubKey ed25519.PublicKey, codeset uint8, - timestamp time.Time, lease time.Duration, round rounds.Round, - mType channels.MessageType, status channels.SentStatus) uint64 { +func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID, + nickname, text string, pubKey ed25519.PublicKey, dmToken uint32, + codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, + mType channels.MessageType, status channels.SentStatus, hidden bool) uint64 { textBytes := []byte(text) var err error @@ -184,7 +170,8 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, msgToInsert := buildMessage( channelID.Marshal(), messageID.Bytes(), nil, nickname, - textBytes, pubKey, codeset, timestamp, lease, round.ID, mType, status) + textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, + false, hidden, status) uuid, err := w.receiveHelper(msgToInsert, false) if err != nil { @@ -201,11 +188,11 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, // // Messages may arrive our of order, so a reply, in theory, can arrive before // the initial message. As a result, it may be important to buffer replies. -func (w *wasmModel) ReceiveReply(channelID *id.ID, - messageID cryptoChannel.MessageID, replyTo cryptoChannel.MessageID, - nickname, text string, pubKey ed25519.PublicKey, codeset uint8, - timestamp time.Time, lease time.Duration, round rounds.Round, - mType channels.MessageType, status channels.SentStatus) uint64 { +func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID, + replyTo message.ID, nickname, text string, pubKey ed25519.PublicKey, + dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, + round rounds.Round, mType channels.MessageType, status channels.SentStatus, + hidden bool) uint64 { textBytes := []byte(text) var err error @@ -219,8 +206,8 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, } msgToInsert := buildMessage(channelID.Marshal(), messageID.Bytes(), - replyTo.Bytes(), nickname, textBytes, pubKey, codeset, - timestamp, lease, round.ID, mType, status) + replyTo.Bytes(), nickname, textBytes, pubKey, dmToken, codeset, + timestamp, lease, round.ID, mType, hidden, false, status) uuid, err := w.receiveHelper(msgToInsert, false) @@ -237,11 +224,11 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, // // Messages may arrive our of order, so a reply, in theory, can arrive before // the initial message. As a result, it may be important to buffer reactions. -func (w *wasmModel) ReceiveReaction(channelID *id.ID, - messageID cryptoChannel.MessageID, reactionTo cryptoChannel.MessageID, - nickname, reaction string, pubKey ed25519.PublicKey, codeset uint8, - timestamp time.Time, lease time.Duration, round rounds.Round, - mType channels.MessageType, status channels.SentStatus) uint64 { +func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID, + reactionTo message.ID, nickname, reaction string, pubKey ed25519.PublicKey, + dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, + round rounds.Round, mType channels.MessageType, status channels.SentStatus, + hidden bool) uint64 { textBytes := []byte(reaction) var err error @@ -256,7 +243,8 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, msgToInsert := buildMessage( channelID.Marshal(), messageID.Bytes(), reactionTo.Bytes(), nickname, - textBytes, pubKey, codeset, timestamp, lease, round.ID, mType, status) + textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, + false, hidden, status) uuid, err := w.receiveHelper(msgToInsert, false) if err != nil { @@ -266,15 +254,54 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, return uuid } -// UpdateSentStatus is called whenever the [channels.SentStatus] of a message -// has changed. At this point the message ID goes from empty/unknown to -// populated. +// UpdateFromMessageID is called whenever a message with the message ID is +// modified. +// +// The API needs to return the UUID of the modified message that can be +// referenced at a later time. +// +// timestamp, round, pinned, and hidden are all nillable and may be updated +// based upon the UUID at a later date. If a nil value is passed, then make +// no update. +func (w *wasmModel) UpdateFromMessageID(messageID message.ID, + timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, + status *channels.SentStatus) uint64 { + parentErr := errors.New("failed to UpdateFromMessageID") + + // FIXME: this is a bit of race condition without the mux. + // This should be done via the transactions (i.e., make a + // special version of receiveHelper) + w.updateMux.Lock() + defer w.updateMux.Unlock() + + msgIDStr := base64.StdEncoding.EncodeToString(messageID.Marshal()) + currentMsgObj, err := indexedDb.GetIndex(w.db, messageStoreName, + messageStoreMessageIndex, js.ValueOf(msgIDStr)) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Failed to get message by index: %+v", err)) + return 0 + } + + currentMsg := utils.JsToJson(currentMsgObj) + uuid, err := w.updateMessage(currentMsg, &messageID, timestamp, + round, pinned, hidden, status) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to updateMessage: %+v", err)) + } + return uuid +} + +// UpdateFromUUID is called whenever a message at the UUID is modified. // -// TODO: Potential race condition due to separate get/update operations. -func (w *wasmModel) UpdateSentStatus(uuid uint64, - messageID cryptoChannel.MessageID, timestamp time.Time, round rounds.Round, - status channels.SentStatus) { - parentErr := errors.New("failed to UpdateSentStatus") +// messageID, timestamp, round, pinned, and hidden are all nillable and may be +// updated based upon the UUID at a later date. If a nil value is passed, then +// make no update. +func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, + timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, + status *channels.SentStatus) { + parentErr := errors.New("failed to UpdateFromUUID") // FIXME: this is a bit of race condition without the mux. // This should be done via the transactions (i.e., make a @@ -293,36 +320,58 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, return } - // Extract the existing Message and update the Status - newMessage := &Message{} - err = json.Unmarshal([]byte(utils.JsToJson(currentMsg)), newMessage) + _, err = w.updateMessage(utils.JsToJson(currentMsg), messageID, timestamp, + round, pinned, hidden, status) if err != nil { jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Could not JSON unmarshal message: %+v", err)) - return + "Unable to updateMessage: %+v", err)) + } +} + +// updateMessage is a helper for updating a stored message. +func (w *wasmModel) updateMessage(currentMsgJson string, messageID *message.ID, + timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, + status *channels.SentStatus) (uint64, error) { + + newMessage := &Message{} + err := json.Unmarshal([]byte(currentMsgJson), newMessage) + if err != nil { + return 0, err } - newMessage.Status = uint8(status) - if !messageID.Equals(cryptoChannel.MessageID{}) { + if status != nil { + newMessage.Status = uint8(*status) + } + if messageID != nil { newMessage.MessageID = messageID.Bytes() } - if round.ID != 0 { + if round != nil { newMessage.Round = uint64(round.ID) } - if !timestamp.Equal(time.Time{}) { - newMessage.Timestamp = timestamp + if timestamp != nil { + newMessage.Timestamp = *timestamp + } + + if pinned != nil { + newMessage.Pinned = *pinned + } + + if hidden != nil { + newMessage.Hidden = *hidden } // Store the updated Message - _, err = w.receiveHelper(newMessage, true) + uuid, err := w.receiveHelper(newMessage, true) if err != nil { - jww.ERROR.Printf("%+v", errors.Wrap(parentErr, err.Error())) + return 0, err } channelID := &id.ID{} copy(channelID[:], newMessage.ChannelID) go w.receivedMessageCB(uuid, channelID, true) + + return uuid, nil } // buildMessage is a private helper that converts typical [channels.EventModel] @@ -332,8 +381,9 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, // autoincrement key by default. If you are trying to overwrite an existing // message, then you need to set it manually yourself. func buildMessage(channelID, messageID, parentID []byte, nickname string, - text []byte, pubKey ed25519.PublicKey, codeset uint8, timestamp time.Time, - lease time.Duration, round id.Round, mType channels.MessageType, + text []byte, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, + timestamp time.Time, lease time.Duration, round id.Round, + mType channels.MessageType, pinned, hidden bool, status channels.SentStatus) *Message { return &Message{ MessageID: messageID, @@ -343,20 +393,21 @@ func buildMessage(channelID, messageID, parentID []byte, nickname string, Timestamp: timestamp, Lease: lease, Status: uint8(status), - Hidden: false, - Pinned: false, + Hidden: hidden, + Pinned: pinned, Text: text, Type: uint16(mType), Round: uint64(round), // User Identity Info Pubkey: pubKey, + DmToken: dmToken, CodesetVersion: codeset, } } // receiveHelper is a private helper for receiving any sort of message. -func (w *wasmModel) receiveHelper(newMessage *Message, isUpdate bool) (uint64, - error) { +func (w *wasmModel) receiveHelper( + newMessage *Message, isUpdate bool) (uint64, error) { // Convert to jsObject newMessageJson, err := json.Marshal(newMessage) if err != nil { @@ -374,45 +425,91 @@ func (w *wasmModel) receiveHelper(newMessage *Message, isUpdate bool) (uint64, } // Store message to database - addReq, err := indexedDb.Put(w.db, messageStoreName, messageObj) - if err != nil { + result, err := indexedDb.Put(w.db, messageStoreName, messageObj) + if err != nil && !strings.Contains(err.Error(), + "at least one key does not satisfy the uniqueness requirements") { + // Only return non-unique constraint errors so that the case + // below this one can be hit and handle duplicate entries properly. return 0, errors.Errorf("Unable to put Message: %+v", err) } - res, err := addReq.Result() - if err != nil { - return 0, errors.Errorf("Unable to get Message result: %+v", err) - } // NOTE: Sometimes the insert fails to return an error but hits a duplicate // insert, so this fallthrough returns the UUID entry in that case. - if res.IsUndefined() { - msgID := cryptoChannel.MessageID{} + if result.IsUndefined() { + msgID := message.ID{} copy(msgID[:], newMessage.MessageID) - uuid, errLookup := w.msgIDLookup(msgID) - if uuid != 0 && errLookup == nil { - return uuid, nil + msg, errLookup := w.msgIDLookup(msgID) + if errLookup == nil && msg.ID != 0 { + return msg.ID, nil } return 0, errors.Errorf("uuid lookup failure: %+v", err) } - uuid := uint64(res.Int()) + uuid := uint64(result.Int()) jww.DEBUG.Printf("Successfully stored message %d", uuid) return uuid, nil } +// GetMessage returns the message with the given [channel.MessageID]. +func (w *wasmModel) GetMessage( + messageID message.ID) (channels.ModelMessage, error) { + lookupResult, err := w.msgIDLookup(messageID) + if err != nil { + return channels.ModelMessage{}, err + } + + var channelId *id.ID + if lookupResult.ChannelID != nil { + channelId, err = id.Unmarshal(lookupResult.ChannelID) + if err != nil { + return channels.ModelMessage{}, err + } + } + + var parentMsgId message.ID + if lookupResult.ParentMessageID != nil { + parentMsgId, err = message.UnmarshalID(lookupResult.ParentMessageID) + if err != nil { + return channels.ModelMessage{}, err + } + } + + return channels.ModelMessage{ + UUID: lookupResult.ID, + Nickname: lookupResult.Nickname, + MessageID: messageID, + ChannelID: channelId, + ParentMessageID: parentMsgId, + Timestamp: lookupResult.Timestamp, + Lease: lookupResult.Lease, + Status: channels.SentStatus(lookupResult.Status), + Hidden: lookupResult.Hidden, + Pinned: lookupResult.Pinned, + Content: lookupResult.Text, + Type: channels.MessageType(lookupResult.Type), + Round: id.Round(lookupResult.Round), + PubKey: lookupResult.Pubkey, + CodesetVersion: lookupResult.CodesetVersion, + }, nil +} + // msgIDLookup gets the UUID of the Message with the given messageID. -func (w *wasmModel) msgIDLookup(messageID cryptoChannel.MessageID) (uint64, - error) { +func (w *wasmModel) msgIDLookup(messageID message.ID) (*Message, error) { msgIDStr := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes())) resultObj, err := indexedDb.GetIndex(w.db, messageStoreName, messageStoreMessageIndex, msgIDStr) if err != nil { - return 0, err + return nil, err + } else if resultObj.IsUndefined() { + return nil, errors.Errorf("no message for %s found", msgIDStr) } - uuid := uint64(0) - if !resultObj.IsUndefined() { - uuid = uint64(resultObj.Get("id").Int()) + // Process result into string + resultMsg := &Message{} + err = json.Unmarshal([]byte(utils.JsToJson(resultObj)), resultMsg) + if err != nil { + return nil, err } - return uuid, nil + return resultMsg, nil + } diff --git a/indexedDb/channels/implementation_test.go b/indexedDb/channels/implementation_test.go index 27f33649eac4d02514a09b6e2117fb148c6b764e..79a02de774ae701b6b579d356690f931f7a53457 100644 --- a/indexedDb/channels/implementation_test.go +++ b/indexedDb/channels/implementation_test.go @@ -13,6 +13,7 @@ import ( "encoding/json" "fmt" "github.com/hack-pad/go-indexeddb/idb" + "gitlab.com/elixxir/crypto/message" "gitlab.com/elixxir/xxdk-wasm/indexedDb" "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/xx_network/crypto/csprng" @@ -26,7 +27,6 @@ import ( "gitlab.com/elixxir/client/v4/channels" "gitlab.com/elixxir/client/v4/cmix/rounds" cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" - "gitlab.com/elixxir/crypto/channel" cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/xx_network/primitives/id" ) @@ -40,59 +40,107 @@ func dummyCallback(uint64, *id.ID, bool) {} // Happy path, insert message and look it up func TestWasmModel_msgIDLookup(t *testing.T) { - cipher, err := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + cipher, err := cryptoChannel.NewCipher( + []byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for _, c := range []cryptoChannel.Cipher{nil, cipher} { cs := "" - if cipher != nil { + if c != nil { cs = "_withCipher" } t.Run(fmt.Sprintf("TestWasmModel_msgIDLookup%s", cs), func(t *testing.T) { storage.GetLocalStorage().Clear() - testString := "test" - testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1}) + testString := "TestWasmModel_msgIDLookup" + cs + testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString)) + eventModel, err := newWASMModel(testString, c, dummyCallback) if err != nil { t.Fatalf("%+v", err) } testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(), - time.Second, 0, 0, channels.Sent) + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, + netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) _, err = eventModel.receiveHelper(testMsg, false) if err != nil { t.Fatalf("%+v", err) } - uuid, err := eventModel.msgIDLookup(testMsgId) + msg, err := eventModel.msgIDLookup(testMsgId) if err != nil { t.Fatalf("%+v", err) } - if uuid == 0 { + if msg.ID == 0 { t.Fatalf("Expected to get a UUID!") } }) } } +// Happy path, insert message and delete it +func TestWasmModel_DeleteMessage(t *testing.T) { + storage.GetLocalStorage().Clear() + testString := "TestWasmModel_DeleteMessage" + testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString)) + eventModel, err := newWASMModel(testString, nil, dummyCallback) + if err != nil { + t.Fatalf("%+v", err) + } + + // Insert a message + testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), + time.Second, 0, 0, false, false, channels.Sent) + _, err = eventModel.receiveHelper(testMsg, false) + if err != nil { + t.Fatalf("%+v", err) + } + + // Check the resulting status + results, err := indexedDb.Dump(eventModel.db, messageStoreName) + if err != nil { + t.Fatalf("%+v", err) + } + if len(results) != 1 { + t.Fatalf("Expected 1 message to exist") + } + + // Delete the message + err = eventModel.DeleteMessage(testMsgId) + if err != nil { + t.Fatalf("%+v", err) + } + + // Check the resulting status + results, err = indexedDb.Dump(eventModel.db, messageStoreName) + if err != nil { + t.Fatalf("%+v", err) + } + if len(results) != 0 { + t.Fatalf("Expected no messages to exist") + } +} + // Test wasmModel.UpdateSentStatus happy path and ensure fields don't change. func Test_wasmModel_UpdateSentStatus(t *testing.T) { - cipher, err := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + cipher, err := cryptoChannel.NewCipher( + []byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for _, c := range []cryptoChannel.Cipher{nil, cipher} { cs := "" - if cipher != nil { + if c != nil { cs = "_withCipher" } - t.Run(fmt.Sprintf("Test_wasmModel_UpdateSentStatus%s", cs), func(t *testing.T) { + t.Run("Test_wasmModel_UpdateSentStatus"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() - testString := "test" - testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1}) + testString := "Test_wasmModel_UpdateSentStatus" + cs + testMsgId := message.DeriveChannelMessageID( + &id.ID{1}, 0, []byte(testString)) eventModel, err := newWASMModel(testString, c, dummyCallback) if err != nil { t.Fatalf("%+v", err) @@ -100,8 +148,8 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { // Store a test message testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(), - time.Second, 0, 0, channels.Sent) + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), + time.Second, 0, 0, false, false, channels.Sent) uuid, err := eventModel.receiveHelper(testMsg, false) if err != nil { t.Fatalf("%+v", err) @@ -118,8 +166,8 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { // Update the sentStatus expectedStatus := channels.Failed - eventModel.UpdateSentStatus(uuid, testMsgId, netTime.Now(), - rounds.Round{ID: 8675309}, expectedStatus) + eventModel.UpdateFromUUID( + uuid, nil, nil, nil, nil, nil, &expectedStatus) // Check the resulting status results, err = indexedDb.Dump(eventModel.db, messageStoreName) @@ -148,16 +196,17 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { // Smoke test wasmModel.JoinChannel/wasmModel.LeaveChannel happy paths. func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { - cipher, err := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + cipher, err := cryptoChannel.NewCipher( + []byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for _, c := range []cryptoChannel.Cipher{nil, cipher} { cs := "" - if cipher != nil { + if c != nil { cs = "_withCipher" } - t.Run(fmt.Sprintf("Test_wasmModel_JoinChannel_LeaveChannel%s", cs), func(t *testing.T) { + t.Run("Test_wasmModel_JoinChannel_LeaveChannel"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() eventModel, err := newWASMModel("test", c, dummyCallback) if err != nil { @@ -199,18 +248,19 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { // Test UUID gets returned when different messages are added. func Test_wasmModel_UUIDTest(t *testing.T) { - cipher, err := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + cipher, err := cryptoChannel.NewCipher( + []byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for _, c := range []cryptoChannel.Cipher{nil, cipher} { cs := "" - if cipher != nil { + if c != nil { cs = "_withCipher" } - t.Run(fmt.Sprintf("Test_wasmModel_UUIDTest%s", cs), func(t *testing.T) { + t.Run("Test_wasmModel_UUIDTest"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() - testString := "testHello" + testString := "testHello" + cs eventModel, err := newWASMModel(testString, c, dummyCallback) if err != nil { t.Fatalf("%+v", err) @@ -221,12 +271,12 @@ func Test_wasmModel_UUIDTest(t *testing.T) { for i := 0; i < 10; i++ { // Store a test message channelID := id.NewIdFromBytes([]byte(testString), t) - msgID := channel.MessageID{} + msgID := message.ID{} copy(msgID[:], testString+fmt.Sprintf("%d", i)) rnd := rounds.Round{ID: id.Round(42)} uuid := eventModel.ReceiveMessage(channelID, msgID, "test", - testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, - netTime.Now(), time.Hour, rnd, 0, channels.Sent) + testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0, + netTime.Now(), time.Hour, rnd, 0, channels.Sent, false) uuids[i] = uuid } @@ -244,16 +294,17 @@ func Test_wasmModel_UUIDTest(t *testing.T) { // Tests if the same message ID being sent always returns the same UUID. func Test_wasmModel_DuplicateReceives(t *testing.T) { - cipher, err := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + cipher, err := cryptoChannel.NewCipher( + []byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for _, c := range []cryptoChannel.Cipher{nil, cipher} { cs := "" - if cipher != nil { + if c != nil { cs = "_withCipher" } - t.Run(fmt.Sprintf("Test_wasmModel_DuplicateReceives%s", cs), func(t *testing.T) { + t.Run("Test_wasmModel_DuplicateReceives"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() testString := "testHello" eventModel, err := newWASMModel(testString, c, dummyCallback) @@ -263,15 +314,15 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) { uuids := make([]uint64, 10) - msgID := channel.MessageID{} + msgID := message.ID{} copy(msgID[:], testString) for i := 0; i < 10; i++ { // Store a test message channelID := id.NewIdFromBytes([]byte(testString), t) rnd := rounds.Round{ID: id.Round(42)} uuid := eventModel.ReceiveMessage(channelID, msgID, "test", - testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, - netTime.Now(), time.Hour, rnd, 0, channels.Sent) + testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0, + netTime.Now(), time.Hour, rnd, 0, channels.Sent, false) uuids[i] = uuid } @@ -285,22 +336,22 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) { } }) } - } // Happy path: Inserts many messages, deletes some, and checks that the final // result is as expected. func Test_wasmModel_deleteMsgByChannel(t *testing.T) { - cipher, err := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + cipher, err := cryptoChannel.NewCipher( + []byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for _, c := range []cryptoChannel.Cipher{nil, cipher} { cs := "" - if cipher != nil { + if c != nil { cs = "_withCipher" } - t.Run(fmt.Sprintf("Test_wasmModel_deleteMsgByChannel%s", cs), func(t *testing.T) { + t.Run("Test_wasmModel_deleteMsgByChannel"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() testString := "test_deleteMsgByChannel" totalMessages := 10 @@ -324,10 +375,12 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) { thisChannel = keepChannel } - testMsgId := channel.MakeMessageID([]byte(testStr), &id.ID{1}) - eventModel.ReceiveMessage(thisChannel, testMsgId, testStr, testStr, - []byte{8, 6, 7, 5}, 0, netTime.Now(), time.Second, - rounds.Round{ID: id.Round(0)}, 0, channels.Sent) + testMsgId := message.DeriveChannelMessageID( + &id.ID{byte(i)}, 0, []byte(testStr)) + eventModel.ReceiveMessage(thisChannel, testMsgId, testStr, + testStr, []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), + time.Second, rounds.Round{ID: id.Round(0)}, 0, + channels.Sent, false) } // Check pre-results @@ -360,16 +413,17 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) { // This test is designed to prove the behavior of unique indexes. // Inserts will not fail, they simply will not happen. func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { - cipher, err := cryptoChannel.NewCipher([]byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) + cipher, err := cryptoChannel.NewCipher( + []byte("testpass"), []byte("testsalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } for i, c := range []cryptoChannel.Cipher{nil, cipher} { cs := "" - if cipher != nil { + if c != nil { cs = "_withCipher" } - t.Run(fmt.Sprintf("TestWasmModel_receiveHelper_UniqueIndex%s", cs), func(t *testing.T) { + t.Run("TestWasmModel_receiveHelper_UniqueIndex"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() testString := fmt.Sprintf("test_receiveHelper_UniqueIndex_%d", i) eventModel, err := newWASMModel(testString, c, dummyCallback) @@ -398,55 +452,49 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { } // First message insert should succeed - testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1}) + testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString)) testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(), - time.Second, 0, 0, channels.Sent) - _, err = eventModel.receiveHelper(testMsg, false) + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, + netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) + uuid, err := eventModel.receiveHelper(testMsg, false) if err != nil { t.Fatal(err) } - // The duplicate entry won't fail, but it just silently shouldn't happen - _, err = eventModel.receiveHelper(testMsg, false) - if err != nil { - t.Fatalf("%+v", err) - } - results, err := indexedDb.Dump(eventModel.db, messageStoreName) + // The duplicate entry should return the same UUID + duplicateUuid, err := eventModel.receiveHelper(testMsg, false) if err != nil { - t.Fatalf("%+v", err) + t.Fatal(err) } - if len(results) != 1 { - t.Fatalf("Expected only a single message, got %d", len(results)) + if uuid != duplicateUuid { + t.Fatalf("Expected UUID %d to match %d", uuid, duplicateUuid) } // Now insert a message with a different message ID from the first - testMsgId2 := channel.MakeMessageID([]byte(testString), &id.ID{2}) + testMsgId2 := message.DeriveChannelMessageID( + &id.ID{2}, 0, []byte(testString)) testMsg = buildMessage([]byte(testString), testMsgId2.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, 0, netTime.Now(), - time.Second, 0, 0, channels.Sent) - primaryKey, err := eventModel.receiveHelper(testMsg, false) + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, + netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) + uuid2, err := eventModel.receiveHelper(testMsg, false) if err != nil { t.Fatal(err) } + if uuid2 == uuid { + t.Fatalf("Expected UUID %d to NOT match %d", uuid, duplicateUuid) + } // Except this time, we update the second entry to have the same // message ID as the first - testMsg.ID = primaryKey + testMsg.ID = uuid testMsg.MessageID = testMsgId.Bytes() - _, err = eventModel.receiveHelper(testMsg, true) + duplicateUuid2, err := eventModel.receiveHelper(testMsg, true) if err != nil { t.Fatal(err) } - - // The update to duplicate message ID won't fail, - // but it just silently shouldn't happen - results, err = indexedDb.Dump(eventModel.db, messageStoreName) - if err != nil { - t.Fatalf("%+v", err) + if duplicateUuid2 != duplicateUuid { + t.Fatalf("Expected UUID %d to match %d", uuid, duplicateUuid) } - // TODO: Convert JSON to Message, ensure Message ID fields differ - }) } } diff --git a/indexedDb/channels/model.go b/indexedDb/channels/model.go index 02a3ebd42479a26cffa7e758c8d317a21a906099..078d6bd67f7c43d9e50373b7801f69937cfce2ac 100644 --- a/indexedDb/channels/model.go +++ b/indexedDb/channels/model.go @@ -61,6 +61,7 @@ type Message struct { // User cryptographic Identity struct -- could be pulled out Pubkey []byte `json:"pubkey"` + DmToken uint32 `json:"dm_token"` CodesetVersion uint8 `json:"codeset_version"` } diff --git a/indexedDb/dm/implementation.go b/indexedDb/dm/implementation.go new file mode 100644 index 0000000000000000000000000000000000000000..afd4e3bff44d739d7a8d3d687563c390fde7f743 --- /dev/null +++ b/indexedDb/dm/implementation.go @@ -0,0 +1,379 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package channelEventModel + +import ( + "crypto/ed25519" + "encoding/json" + "strings" + "sync" + "syscall/js" + "time" + + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/cmix/rounds" + "gitlab.com/elixxir/client/v4/dm" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" + "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/xx_network/primitives/id" + + "github.com/hack-pad/go-indexeddb/idb" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/message" +) + +// wasmModel implements [dm.Receiver] interface, which uses the channels +// system passed an object that adheres to in order to get events on the +// channel. +type wasmModel struct { + db *idb.Database + cipher cryptoChannel.Cipher + receivedMessageCB MessageReceivedCallback + updateMux sync.Mutex +} + +// joinConversation is used for joining new conversations. +func (w *wasmModel) joinConversation(nickname string, + pubKey ed25519.PublicKey, dmToken uint32, codeset uint8) error { + parentErr := errors.New("failed to joinConversation") + + // Build object + newConvo := Conversation{ + Pubkey: pubKey, + Nickname: nickname, + Token: dmToken, + CodesetVersion: codeset, + Blocked: false, + } + + // Convert to jsObject + newConvoJson, err := json.Marshal(&newConvo) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to marshal Conversation: %+v", err) + } + convoObj, err := utils.JsonToJS(newConvoJson) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to marshal Conversation: %+v", err) + } + + _, err = indexedDb.Put(w.db, conversationStoreName, convoObj) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to put Conversation: %+v", err) + } + return nil +} + +// buildMessage is a private helper that converts typical [dm.Receiver] +// inputs into a basic Message structure for insertion into storage. +// +// NOTE: ID is not set inside this function because we want to use the +// autoincrement key by default. If you are trying to overwrite an existing +// message, then you need to set it manually yourself. +func buildMessage(messageID, parentID []byte, text []byte, + pubKey ed25519.PublicKey, timestamp time.Time, round id.Round, + mType dm.MessageType, status dm.Status) *Message { + return &Message{ + MessageID: messageID, + ConversationPubKey: pubKey, + ParentMessageID: parentID, + Timestamp: timestamp, + Status: uint8(status), + Text: text, + Type: uint16(mType), + Round: uint64(round), + } +} + +func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte, + pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, + timestamp time.Time, round rounds.Round, mType dm.MessageType, + status dm.Status) uint64 { + parentErr := errors.New("failed to Receive") + + // If there is no extant Conversation, create one. + _, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)) + if err != nil { + if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + if err != nil { + jww.ERROR.Printf("%+v", err) + } + } else { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + } + return 0 + } else { + jww.DEBUG.Printf("Conversation with %s already joined", nickname) + } + + // Handle encryption, if it is present + if w.cipher != nil { + text, err = w.cipher.Encrypt(text) + if err != nil { + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 + } + } + + msgToInsert := buildMessage(messageID.Bytes(), nil, text, + pubKey, timestamp, round.ID, mType, status) + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) + } + + go w.receivedMessageCB(uuid, pubKey, false) + return uuid +} + +func (w *wasmModel) ReceiveText(messageID message.ID, nickname, text string, + pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, + timestamp time.Time, round rounds.Round, status dm.Status) uint64 { + parentErr := errors.New("failed to ReceiveText") + + // If there is no extant Conversation, create one. + _, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)) + if err != nil { + if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + if err != nil { + jww.ERROR.Printf("%+v", err) + } + } else { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + } + return 0 + } else { + jww.DEBUG.Printf("Conversation with %s already joined", nickname) + } + + // Handle encryption, if it is present + textBytes := []byte(text) + if w.cipher != nil { + textBytes, err = w.cipher.Encrypt(textBytes) + if err != nil { + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 + } + } + + msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes, + pubKey, timestamp, round.ID, dm.TextType, status) + + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) + } + + go w.receivedMessageCB(uuid, pubKey, false) + return uuid +} + +func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname, + text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, + timestamp time.Time, round rounds.Round, status dm.Status) uint64 { + parentErr := errors.New("failed to ReceiveReply") + + // If there is no extant Conversation, create one. + _, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)) + if err != nil { + if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + if err != nil { + jww.ERROR.Printf("%+v", err) + } + } else { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + } + return 0 + } else { + jww.DEBUG.Printf("Conversation with %s already joined", nickname) + } + + // Handle encryption, if it is present + textBytes := []byte(text) + if w.cipher != nil { + textBytes, err = w.cipher.Encrypt(textBytes) + if err != nil { + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 + } + } + + msgToInsert := buildMessage(messageID.Bytes(), reactionTo.Marshal(), textBytes, + pubKey, timestamp, round.ID, dm.TextType, status) + + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) + } + + go w.receivedMessageCB(uuid, pubKey, false) + return uuid +} + +func (w *wasmModel) ReceiveReaction(messageID, reactionTo message.ID, nickname, + reaction string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, + timestamp time.Time, round rounds.Round, status dm.Status) uint64 { + parentErr := errors.New("failed to ReceiveText") + + // If there is no extant Conversation, create one. + _, err := indexedDb.Get(w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)) + if err != nil { + if strings.Contains(err.Error(), indexedDb.ErrDoesNotExist) { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + if err != nil { + jww.ERROR.Printf("%+v", err) + } + } else { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + } + return 0 + } else { + jww.DEBUG.Printf("Conversation with %s already joined", nickname) + } + + // Handle encryption, if it is present + textBytes := []byte(reaction) + if w.cipher != nil { + textBytes, err = w.cipher.Encrypt(textBytes) + if err != nil { + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 + } + } + + msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes, + pubKey, timestamp, round.ID, dm.ReactionType, status) + + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) + } + + go w.receivedMessageCB(uuid, pubKey, false) + return uuid +} + +func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID, + timestamp time.Time, round rounds.Round, status dm.Status) { + parentErr := errors.New("failed to UpdateSentStatus") + + // FIXME: this is a bit of race condition without the mux. + // This should be done via the transactions (i.e., make a + // special version of receiveHelper) + w.updateMux.Lock() + defer w.updateMux.Unlock() + + // Convert messageID to the key generated by json.Marshal + key := js.ValueOf(uuid) + + // Use the key to get the existing Message + currentMsg, err := indexedDb.Get(w.db, messageStoreName, key) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get message: %+v", err)) + return + } + + // Extract the existing Message and update the Status + newMessage := &Message{} + err = json.Unmarshal([]byte(utils.JsToJson(currentMsg)), newMessage) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Could not JSON unmarshal message: %+v", err)) + return + } + + newMessage.Status = uint8(status) + if !messageID.Equals(message.ID{}) { + newMessage.MessageID = messageID.Bytes() + } + + if round.ID != 0 { + newMessage.Round = uint64(round.ID) + } + + if !timestamp.Equal(time.Time{}) { + newMessage.Timestamp = timestamp + } + + // Store the updated Message + _, err = w.receiveHelper(newMessage, true) + if err != nil { + jww.ERROR.Printf("%+v", errors.Wrap(parentErr, err.Error())) + } + go w.receivedMessageCB(uuid, newMessage.ConversationPubKey, true) +} + +// receiveHelper is a private helper for receiving any sort of message. +func (w *wasmModel) receiveHelper( + newMessage *Message, isUpdate bool) (uint64, error) { + // Convert to jsObject + newMessageJson, err := json.Marshal(newMessage) + if err != nil { + return 0, errors.Errorf("Unable to marshal Message: %+v", err) + } + messageObj, err := utils.JsonToJS(newMessageJson) + if err != nil { + return 0, errors.Errorf("Unable to marshal Message: %+v", err) + } + + // Unset the primaryKey for inserts so that it can be auto-populated and + // incremented + if !isUpdate { + messageObj.Delete("id") + } + + // Store message to database + result, err := indexedDb.Put(w.db, messageStoreName, messageObj) + if err != nil { + return 0, errors.Errorf("Unable to put Message: %+v", err) + } + + // NOTE: Sometimes the insert fails to return an error but hits a duplicate + // insert, so this fallthrough returns the UUID entry in that case. + if result.IsUndefined() { + msgID := message.ID{} + copy(msgID[:], newMessage.MessageID) + uuid, errLookup := w.msgIDLookup(msgID) + if uuid != 0 && errLookup == nil { + return uuid, nil + } + return 0, errors.Errorf("uuid lookup failure: %+v", err) + } + uuid := uint64(result.Int()) + jww.DEBUG.Printf("Successfully stored message %d", uuid) + + return uuid, nil +} + +// msgIDLookup gets the UUID of the Message with the given messageID. +func (w *wasmModel) msgIDLookup(messageID message.ID) (uint64, error) { + resultObj, err := indexedDb.GetIndex(w.db, messageStoreName, + messageStoreMessageIndex, utils.CopyBytesToJS(messageID.Marshal())) + if err != nil { + return 0, err + } + + uuid := uint64(0) + if !resultObj.IsUndefined() { + uuid = uint64(resultObj.Get("id").Int()) + } + return uuid, nil +} diff --git a/indexedDb/dm/init.go b/indexedDb/dm/init.go new file mode 100644 index 0000000000000000000000000000000000000000..380df647c810d7fb6239cee9c4e7c1fc4181e4ca --- /dev/null +++ b/indexedDb/dm/init.go @@ -0,0 +1,182 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package channelEventModel + +import ( + "crypto/ed25519" + "syscall/js" + + "github.com/hack-pad/go-indexeddb/idb" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/dm" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" + "gitlab.com/elixxir/xxdk-wasm/storage" +) + +const ( + // databaseSuffix is the suffix to be appended to the name of + // the database. + databaseSuffix = "_speakeasy_dm" + + // currentVersion is the current version of the IndexDb + // runtime. Used for migration purposes. + currentVersion uint = 1 +) + +// MessageReceivedCallback is called any time a message is received or updated. +// +// update is true if the row is old and was edited. +type MessageReceivedCallback func(uuid uint64, pubKey ed25519.PublicKey, update bool) + +// NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel. +// The name should be a base64 encoding of the users public key. +func NewWASMEventModel(path string, encryption cryptoChannel.Cipher, + cb MessageReceivedCallback) (dm.EventModel, error) { + databaseName := path + databaseSuffix + return newWASMModel(databaseName, encryption, cb) +} + +// newWASMModel creates the given [idb.Database] and returns a wasmModel. +func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, + cb MessageReceivedCallback) (*wasmModel, error) { + // Attempt to open database object + ctx, cancel := indexedDb.NewContext() + defer cancel() + openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion, + func(db *idb.Database, oldVersion, newVersion uint) error { + if oldVersion == newVersion { + jww.INFO.Printf("IndexDb version is current: v%d", + newVersion) + return nil + } + + jww.INFO.Printf("IndexDb upgrade required: v%d -> v%d", + oldVersion, newVersion) + + if oldVersion == 0 && newVersion >= 1 { + err := v1Upgrade(db) + if err != nil { + return err + } + oldVersion = 1 + } + + // if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 } + return nil + }) + if err != nil { + return nil, err + } + + // Wait for database open to finish + db, err := openRequest.Await(ctx) + if err != nil { + return nil, err + } + + // Save the encryption status to storage + encryptionStatus := encryption != nil + loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus( + databaseName, encryptionStatus) + if err != nil { + return nil, err + } + + // Verify encryption status does not change + if encryptionStatus != loadedEncryptionStatus { + return nil, errors.New( + "Cannot load database with different encryption status.") + } else if !encryptionStatus { + jww.WARN.Printf("IndexedDb encryption disabled!") + } + + // Attempt to ensure the database has been properly initialized + openRequest, err = idb.Global().Open(ctx, databaseName, currentVersion, + func(db *idb.Database, oldVersion, newVersion uint) error { + return nil + }) + if err != nil { + return nil, err + } + // Wait for database open to finish + db, err = openRequest.Await(ctx) + if err != nil { + return nil, err + } + wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption} + + return wrapper, nil +} + +// v1Upgrade performs the v0 -> v1 database upgrade. +// +// This can never be changed without permanently breaking backwards +// compatibility. +func v1Upgrade(db *idb.Database) error { + indexOpts := idb.IndexOptions{ + Unique: false, + MultiEntry: false, + } + + // Build Message ObjectStore and Indexes + messageStoreOpts := idb.ObjectStoreOptions{ + KeyPath: js.ValueOf(msgPkeyName), + AutoIncrement: true, + } + messageStore, err := db.CreateObjectStore(messageStoreName, messageStoreOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreMessageIndex, + js.ValueOf(messageStoreMessage), + idb.IndexOptions{ + Unique: true, + MultiEntry: false, + }) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreConversationIndex, + js.ValueOf(messageStoreConversation), indexOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreParentIndex, + js.ValueOf(messageStoreParent), indexOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreTimestampIndex, + js.ValueOf(messageStoreTimestamp), indexOpts) + if err != nil { + return err + } + + // Build Channel ObjectStore + conversationStoreOpts := idb.ObjectStoreOptions{ + KeyPath: js.ValueOf(convoPkeyName), + AutoIncrement: false, + } + _, err = db.CreateObjectStore(conversationStoreName, conversationStoreOpts) + if err != nil { + return err + } + + // Get the database name and save it to storage + if databaseName, err := db.Name(); err != nil { + return err + } else if err = storage.StoreIndexedDb(databaseName); err != nil { + return err + } + + return nil +} diff --git a/indexedDb/dm/model.go b/indexedDb/dm/model.go new file mode 100644 index 0000000000000000000000000000000000000000..bb6f34588aa2976c1689b629c6df31219de6b585 --- /dev/null +++ b/indexedDb/dm/model.go @@ -0,0 +1,63 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package channelEventModel + +import ( + "time" +) + +const ( + // Text representation of primary key value (keyPath). + msgPkeyName = "id" + convoPkeyName = "pub_key" + + // Text representation of the names of the various [idb.ObjectStore]. + messageStoreName = "messages" + conversationStoreName = "conversations" + + // Message index names. + messageStoreMessageIndex = "message_id_index" + messageStoreConversationIndex = "conversation_id_index" + messageStoreParentIndex = "parent_message_id_index" + messageStoreTimestampIndex = "timestamp_index" + + // Message keyPath names (must match json struct tags). + messageStoreMessage = "message_id" + messageStoreConversation = "conversation_id" + messageStoreParent = "parent_message_id" + messageStoreTimestamp = "timestamp" +) + +// Message defines the IndexedDb representation of a single Message. +// +// A Message belongs to one Conversation. +// A Message may belong to one Message (Parent). +type Message struct { + ID uint64 `json:"id"` // Matches msgPkeyName + MessageID []byte `json:"message_id"` // Index + ConversationPubKey []byte `json:"conversation_pub_key"` // Index + ParentMessageID []byte `json:"parent_message_id"` // Index + Timestamp time.Time `json:"timestamp"` // Index + Status uint8 `json:"status"` + Text []byte `json:"text"` + Type uint16 `json:"type"` + Round uint64 `json:"round"` +} + +// Conversation defines the IndexedDb representation of a single +// message exchange between two recipients. +// A Conversation has many Message. +type Conversation struct { + Pubkey []byte `json:"pub_key"` // Matches convoPkeyName + Nickname string `json:"nickname"` + Token uint32 `json:"token"` + CodesetVersion uint8 `json:"codeset_version"` + Blocked bool `json:"blocked"` +} diff --git a/indexedDb/utils.go b/indexedDb/utils.go index 1aa6ff45805c1c65b9ad27e51a8b0590e7b8201d..a5bef78685ef34452aca396e1aea3f5206cf4ab6 100644 --- a/indexedDb/utils.go +++ b/indexedDb/utils.go @@ -22,9 +22,13 @@ import ( "time" ) -// dbTimeout is the global timeout for operations with the storage -// [context.Context]. -const dbTimeout = time.Second +const ( + // dbTimeout is the global timeout for operations with the storage + // [context.Context]. + dbTimeout = time.Second + // ErrDoesNotExist is an error string for got undefined on Get operations. + ErrDoesNotExist = "result is undefined" +) // NewContext builds a context for indexedDb operations. func NewContext() (context.Context, context.CancelFunc) { @@ -32,6 +36,7 @@ func NewContext() (context.Context, context.CancelFunc) { } // Get is a generic helper for getting values from the given [idb.ObjectStore]. +// Only usable by primary key. func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, error) { parentErr := errors.Errorf("failed to Get %s/%s", objectStoreName, key) @@ -62,8 +67,8 @@ func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, erro return js.Undefined(), errors.WithMessagef(parentErr, "Unable to get from ObjectStore: %+v", err) } else if resultObj.IsUndefined() { - return js.Undefined(), errors.WithMessage(parentErr, - "Unable to get from ObjectStore: result is undefined") + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to get from ObjectStore: %s", ErrDoesNotExist) } // Process result into string @@ -74,7 +79,7 @@ func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, erro // GetIndex is a generic helper for getting values from the given // [idb.ObjectStore] using the given [idb.Index]. -func GetIndex(db *idb.Database, objectStoreName string, +func GetIndex(db *idb.Database, objectStoreName, indexName string, key js.Value) (js.Value, error) { parentErr := errors.Errorf("failed to GetIndex %s/%s/%s", objectStoreName, indexName, key) @@ -111,8 +116,8 @@ func GetIndex(db *idb.Database, objectStoreName string, return js.Undefined(), errors.WithMessagef(parentErr, "Unable to get from ObjectStore: %+v", err) } else if resultObj.IsUndefined() { - return js.Undefined(), errors.WithMessage(parentErr, - "Unable to get from ObjectStore: result is undefined") + return js.Undefined(), errors.WithMessagef(parentErr, + "Unable to get from ObjectStore: %s", ErrDoesNotExist) } // Process result into string @@ -123,41 +128,42 @@ func GetIndex(db *idb.Database, objectStoreName string, // Put is a generic helper for putting values into the given [idb.ObjectStore]. // Equivalent to insert if not exists else update. -func Put(db *idb.Database, objectStoreName string, value js.Value) (*idb.Request, error) { +func Put(db *idb.Database, objectStoreName string, value js.Value) (js.Value, error) { // Prepare the Transaction txn, err := db.Transaction(idb.TransactionReadWrite, objectStoreName) if err != nil { - return nil, errors.Errorf("Unable to create Transaction: %+v", err) + return js.Undefined(), errors.Errorf("Unable to create Transaction: %+v", err) } store, err := txn.ObjectStore(objectStoreName) if err != nil { - return nil, errors.Errorf("Unable to get ObjectStore: %+v", err) + return js.Undefined(), errors.Errorf("Unable to get ObjectStore: %+v", err) } // Perform the operation request, err := store.Put(value) if err != nil { - return nil, errors.Errorf("Unable to Put: %+v", err) + return js.Undefined(), errors.Errorf("Unable to Put: %+v", err) } // Wait for the operation to return ctx, cancel := NewContext() - err = txn.Await(ctx) + result, err := request.Await(ctx) cancel() if err != nil { - return nil, errors.Errorf("Putting value failed: %+v", err) + return js.Undefined(), errors.Errorf("Putting value failed: %+v", err) } - jww.DEBUG.Printf("Successfully put value in %s: %v", + jww.DEBUG.Printf("Successfully put value in %s: %s", objectStoreName, utils.JsToJson(value)) - return request, nil + return result, nil } -// Delete is a generic helper for removing values from the given [idb.ObjectStore]. +// Delete is a generic helper for removing values from the given +// [idb.ObjectStore]. Only usable by primary key. func Delete(db *idb.Database, objectStoreName string, key js.Value) error { parentErr := errors.Errorf("failed to Delete %s/%s", objectStoreName, key) // Prepare the Transaction - txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName) + txn, err := db.Transaction(idb.TransactionReadWrite, objectStoreName) if err != nil { return errors.WithMessagef(parentErr, "Unable to create Transaction: %+v", err) @@ -169,20 +175,44 @@ func Delete(db *idb.Database, objectStoreName string, key js.Value) error { } // Perform the operation - deleteRequest, err := store.Delete(key) + _, err = store.Delete(key) if err != nil { return errors.WithMessagef(parentErr, - "Unable to Get from ObjectStore: %+v", err) + "Unable to Delete from ObjectStore: %+v", err) } // Wait for the operation to return ctx, cancel := NewContext() - err = deleteRequest.Await(ctx) + err = txn.Await(ctx) cancel() if err != nil { return errors.WithMessagef(parentErr, - "Unable to delete from ObjectStore: %+v", err) + "Unable to Delete from ObjectStore: %+v", err) + } + jww.DEBUG.Printf("Successfully deleted value at %s/%s", + objectStoreName, utils.JsToJson(key)) + return nil +} + +// DeleteIndex is a generic helper for removing values from the +// given [idb.ObjectStore] using the given [idb.Index]. Requires passing +// in the name of the primary key for the store. +func DeleteIndex(db *idb.Database, objectStoreName, + indexName, pkeyName string, key js.Value) error { + parentErr := errors.Errorf("failed to DeleteIndex %s/%s", objectStoreName, key) + + value, err := GetIndex(db, objectStoreName, indexName, key) + if err != nil { + return errors.WithMessagef(parentErr, "%+v", err) } + + err = Delete(db, objectStoreName, value.Get(pkeyName)) + if err != nil { + return errors.WithMessagef(parentErr, "%+v", err) + } + + jww.DEBUG.Printf("Successfully deleted value at %s/%s/%s", + objectStoreName, indexName, utils.JsToJson(key)) return nil } diff --git a/main.go b/main.go index d06333fb8dd96ea545ad3d00fb182e8849ae1dad..de379dbdbf5eb3132e8cc546df5a6b601665e5e6 100644 --- a/main.go +++ b/main.go @@ -11,13 +11,14 @@ package main import ( "fmt" + "os" + "syscall/js" + jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/bindings" "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/elixxir/xxdk-wasm/utils" "gitlab.com/elixxir/xxdk-wasm/wasm" - "os" - "syscall/js" ) func init() { @@ -77,18 +78,25 @@ func main() { js.FuncOf(wasm.LoadChannelsManagerWithIndexedDbUnsafe)) js.Global().Set("NewChannelsManagerWithIndexedDbUnsafe", js.FuncOf(wasm.NewChannelsManagerWithIndexedDbUnsafe)) - js.Global().Set("GenerateChannel", js.FuncOf(wasm.GenerateChannel)) - js.Global().Set("GetSavedChannelPrivateKeyUNSAFE", - js.FuncOf(wasm.GetSavedChannelPrivateKeyUNSAFE)) js.Global().Set("DecodePublicURL", js.FuncOf(wasm.DecodePublicURL)) js.Global().Set("DecodePrivateURL", js.FuncOf(wasm.DecodePrivateURL)) js.Global().Set("GetChannelJSON", js.FuncOf(wasm.GetChannelJSON)) js.Global().Set("GetChannelInfo", js.FuncOf(wasm.GetChannelInfo)) js.Global().Set("GetShareUrlType", js.FuncOf(wasm.GetShareUrlType)) + js.Global().Set("ValidForever", js.FuncOf(wasm.ValidForever)) js.Global().Set("IsNicknameValid", js.FuncOf(wasm.IsNicknameValid)) js.Global().Set("NewChannelsDatabaseCipher", js.FuncOf(wasm.NewChannelsDatabaseCipher)) + // wasm/dm.go + js.Global().Set("NewDMClient", js.FuncOf(wasm.NewDMClient)) + js.Global().Set("NewDMClientWithIndexedDb", + js.FuncOf(wasm.NewDMClientWithIndexedDb)) + js.Global().Set("NewDMClientWithIndexedDbUnsafe", + js.FuncOf(wasm.NewDMClientWithIndexedDbUnsafe)) + js.Global().Set("NewDMsDatabaseCipher", + js.FuncOf(wasm.NewDMsDatabaseCipher)) + // wasm/cmix.go js.Global().Set("NewCmix", js.FuncOf(wasm.NewCmix)) js.Global().Set("LoadCmix", js.FuncOf(wasm.LoadCmix)) diff --git a/utils/utils.go b/utils/utils.go index 19399f5a62c5f41269739440d0a9948a7c244200..cb1e46f3859695e7c77fbf0c9403eb24b7b10ff4 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -74,9 +74,11 @@ func CreatePromise(f PromiseFn) any { return Promise.New(handler) } -// Await waits on a Javascript value. It returns the results of the then and -// catch functions once it resolves. -func Await(awaitable js.Value) ([]js.Value, []js.Value) { +// Await waits on a Javascript value. It blocks until the awaitable successfully +// resolves to the result or rejects to err. +// +// If there is a result, err will be nil and vice versa. +func Await(awaitable js.Value) (result []js.Value, err []js.Value) { then := make(chan []js.Value) defer close(then) thenFunc := js.FuncOf(func(this js.Value, args []js.Value) any { @@ -96,9 +98,9 @@ func Await(awaitable js.Value) ([]js.Value, []js.Value) { awaitable.Call("then", thenFunc).Call("catch", catchFunc) select { - case result := <-then: + case result = <-then: return result, nil - case err := <-catch: + case err = <-catch: return nil, err } } diff --git a/wasm/channels.go b/wasm/channels.go index cf09b017d0263550e6f7da7a4f4f8825567439cc..d32737321c0efbab9ff732917b185a7b2cfbea99 100644 --- a/wasm/channels.go +++ b/wasm/channels.go @@ -11,7 +11,10 @@ package wasm import ( "encoding/base64" - "gitlab.com/elixxir/xxdk-wasm/indexedDb/channels" + "encoding/json" + "errors" + "gitlab.com/elixxir/client/v4/channels" + channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/channels" "gitlab.com/xx_network/primitives/id" "sync" "syscall/js" @@ -36,11 +39,12 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any { cm := ChannelsManager{api} channelsManagerMap := map[string]any{ // Basic Channel API - "GetID": js.FuncOf(cm.GetID), - "JoinChannel": js.FuncOf(cm.JoinChannel), - "GetChannels": js.FuncOf(cm.GetChannels), - "LeaveChannel": js.FuncOf(cm.LeaveChannel), - "ReplayChannel": js.FuncOf(cm.ReplayChannel), + "GetID": js.FuncOf(cm.GetID), + "GenerateChannel": js.FuncOf(cm.GenerateChannel), + "JoinChannel": js.FuncOf(cm.JoinChannel), + "GetChannels": js.FuncOf(cm.GetChannels), + "LeaveChannel": js.FuncOf(cm.LeaveChannel), + "ReplayChannel": js.FuncOf(cm.ReplayChannel), // Share URL "GetShareURL": js.FuncOf(cm.GetShareURL), @@ -51,12 +55,22 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any { "SendMessage": js.FuncOf(cm.SendMessage), "SendReply": js.FuncOf(cm.SendReply), "SendReaction": js.FuncOf(cm.SendReaction), + "DeleteMessage": js.FuncOf(cm.DeleteMessage), + "PinMessage": js.FuncOf(cm.PinMessage), + "MuteUser": js.FuncOf(cm.MuteUser), "GetIdentity": js.FuncOf(cm.GetIdentity), "ExportPrivateIdentity": js.FuncOf(cm.ExportPrivateIdentity), "GetStorageTag": js.FuncOf(cm.GetStorageTag), "SetNickname": js.FuncOf(cm.SetNickname), "DeleteNickname": js.FuncOf(cm.DeleteNickname), "GetNickname": js.FuncOf(cm.GetNickname), + "Muted": js.FuncOf(cm.Muted), + "GetMutedUsers": js.FuncOf(cm.GetMutedUsers), + "IsChannelAdmin": js.FuncOf(cm.IsChannelAdmin), + "ExportChannelAdminKey": js.FuncOf(cm.ExportChannelAdminKey), + "VerifyChannelAdminKey": js.FuncOf(cm.VerifyChannelAdminKey), + "ImportChannelAdminKey": js.FuncOf(cm.ImportChannelAdminKey), + "DeleteChannelAdminKey": js.FuncOf(cm.DeleteChannelAdminKey), // Channel Receiving Logic and Callback Registration "RegisterReceiveHandler": js.FuncOf(cm.RegisterReceiveHandler), @@ -70,16 +84,19 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any { // // Returns: // - Tracker ID (int). -func (ch *ChannelsManager) GetID(js.Value, []js.Value) any { - return ch.api.GetID() +func (cm *ChannelsManager) GetID(js.Value, []js.Value) any { + return cm.api.GetID() } // GenerateChannelIdentity creates a new private channel identity -// ([channel.PrivateIdentity]). The public component can be retrieved as JSON -// via [GetPublicChannelIdentityFromPrivate]. +// ([channel.PrivateIdentity]) from scratch and assigns it a codename. +// +// The public component can be retrieved as JSON via +// [GetPublicChannelIdentityFromPrivate]. // // Parameters: -// - args[0] - ID of [Cmix] object in tracker (int). +// - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved +// using [Cmix.GetID]. // // Returns: // - Marshalled bytes of [channel.PrivateIdentity] (Uint8Array). @@ -97,8 +114,8 @@ func GenerateChannelIdentity(_ js.Value, args []js.Value) any { // identityMap stores identities previously generated by ConstructIdentity. var identityMap sync.Map -// ConstructIdentity constructs a [channel.Identity] from a user's public key -// and codeset version. +// ConstructIdentity creates a codename in a public [channel.Identity] from an +// extant identity for a given codeset version. // // Parameters: // - args[0] - The Ed25519 public key (Uint8Array). @@ -157,7 +174,8 @@ func constructIdentity(_ js.Value, args []js.Value) any { // // Parameters: // - args[0] - The password used to encrypt the identity (string). -// - args[2] - The encrypted data (Uint8Array). +// - args[2] - The encrypted data from [ChannelsManager.ExportPrivateIdentity] +// (Uint8Array). // // Returns: // - JSON of [channel.PrivateIdentity] (Uint8Array). @@ -202,7 +220,7 @@ func GetPublicChannelIdentity(_ js.Value, args []js.Value) any { // // Parameters: // - args[0] - Bytes of the private identity -// (channel.PrivateIdentity]) (Uint8Array). +// ([channel.PrivateIdentity]) (Uint8Array). // // Returns: // - JSON of the public identity ([channel.Identity]) (Uint8Array). @@ -220,26 +238,6 @@ func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) any { return utils.CopyBytesToJS(identity) } -// eventModelBuilder adheres to the [bindings.EventModelBuilder] interface. -type eventModelBuilder struct { - build func(args ...any) js.Value -} - -// Build initializes and returns the event model. It wraps a Javascript object -// that has all the methods in [bindings.EventModel] to make it adhere to the Go -// interface [bindings.EventModel]. -func (emb *eventModelBuilder) Build(path string) bindings.EventModel { - emJs := emb.build(path) - return &eventModel{ - joinChannel: utils.WrapCB(emJs, "JoinChannel"), - leaveChannel: utils.WrapCB(emJs, "LeaveChannel"), - receiveMessage: utils.WrapCB(emJs, "ReceiveMessage"), - receiveReply: utils.WrapCB(emJs, "ReceiveReply"), - receiveReaction: utils.WrapCB(emJs, "ReceiveReaction"), - updateSentStatus: utils.WrapCB(emJs, "UpdateSentStatus"), - } -} - // NewChannelsManager creates a new [ChannelsManager] from a new private // identity ([channel.PrivateIdentity]). // @@ -274,7 +272,8 @@ func NewChannelsManager(_ js.Value, args []js.Value) any { return newChannelsManagerJS(cm) } -// LoadChannelsManager loads an existing [ChannelsManager]. +// LoadChannelsManager loads an existing [ChannelsManager] for the given storage +// tag. // // This is for loading a manager for an identity that has already been created. // The channel manager should have previously been created with @@ -394,7 +393,7 @@ func newChannelsManagerWithIndexedDb(cmixID int, privateIdentity []byte, cb.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update) } - model := channels.NewWASMEventModelBuilder(cipher, messageReceivedCB) + model := channelsDb.NewWASMEventModelBuilder(cipher, messageReceivedCB) promiseFn := func(resolve, reject func(args ...any) js.Value) { cm, err := bindings.NewChannelsManagerGoEventModel( @@ -493,7 +492,7 @@ func loadChannelsManagerWithIndexedDb(cmixID int, storageTag string, cb.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), updated) } - model := channels.NewWASMEventModelBuilder(cipher, messageReceivedCB) + model := channelsDb.NewWASMEventModelBuilder(cipher, messageReceivedCB) promiseFn := func(resolve, reject func(args ...any) js.Value) { cm, err := bindings.LoadChannelsManagerGoEventModel( @@ -508,76 +507,9 @@ func loadChannelsManagerWithIndexedDb(cmixID int, storageTag string, return utils.CreatePromise(promiseFn) } -// GenerateChannel is used to create a channel a new channel of which you are -// the admin. It is only for making new channels, not joining existing ones. -// -// It returns a pretty print of the channel and the private key. -// -// The name cannot be longer that __ characters. The description cannot be -// longer than __ and can only use ______ characters. -// -// Parameters: -// - args[0] - ID of [Cmix] object in tracker (int). -// - args[1] - The name of the new channel (string). The name must be between -// 3 and 24 characters inclusive. It can only include upper and lowercase -// unicode letters, digits 0 through 9, and underscores (_). It cannot be -// changed once a channel is created. -// - args[2] - The description of a channel (string). The description is -// optional but cannot be longer than 144 characters and can include all -// unicode characters. It cannot be changed once a channel is created. -// - args[3] - The [broadcast.PrivacyLevel] of the channel (int). 0 = public, -// 1 = private, and 2 = secret. Refer to the comment below for more -// information. -// -// Returns: -// - JSON of [bindings.ChannelGeneration], which describes a generated -// channel. It contains both the public channel info and the private key for -// the channel in PEM format (Uint8Array). -// - Throws a TypeError if generating the channel fails. -// -// The [broadcast.PrivacyLevel] of a channel indicates the level of channel -// information revealed when sharing it via URL. For any channel besides public -// channels, the secret information is encrypted and a password is required to -// share and join a channel. -// - A privacy level of [broadcast.Public] reveals all the information -// including the name, description, privacy level, public key and salt. -// - A privacy level of [broadcast.Private] reveals only the name and -// description. -// - A privacy level of [broadcast.Secret] reveals nothing. -func GenerateChannel(_ js.Value, args []js.Value) any { - gen, err := bindings.GenerateChannel( - args[0].Int(), args[1].String(), args[2].String(), args[3].Int()) - if err != nil { - utils.Throw(utils.TypeError, err) - return nil - } - - return utils.CopyBytesToJS(gen) -} - -// GetSavedChannelPrivateKeyUNSAFE loads the private key from storage for the -// given channel ID. -// -// NOTE: This function is unsafe and only for debugging purposes only. -// -// Parameters: -// - args[0] - ID of [Cmix] object in tracker (int). -// - args[1] - The [id.ID] of the channel in base 64 encoding (string). -// -// Returns: -// - The PEM file of the private key (string). -// - Throws a TypeError if retrieving the [Cmix] object or the private key -// fails. -func GetSavedChannelPrivateKeyUNSAFE(_ js.Value, args []js.Value) any { - privKey, err := bindings.GetSavedChannelPrivateKeyUNSAFE( - args[0].Int(), args[1].String()) - if err != nil { - utils.Throw(utils.TypeError, err) - return nil - } - - return privKey -} +//////////////////////////////////////////////////////////////////////////////// +// Channel Actions // +//////////////////////////////////////////////////////////////////////////////// // DecodePublicURL decodes the channel URL into a channel pretty print. This // function can only be used for public channel URLs. To get the privacy level @@ -675,65 +607,86 @@ func GetChannelInfo(_ js.Value, args []js.Value) any { return utils.CopyBytesToJS(ci) } -// JoinChannel joins the given channel. It will fail if the channel has already -// been joined. -// -// Parameters: -// - args[0] - A portable channel string. Should be received from another user -// or generated via [GenerateChannel] (string). +// GenerateChannel creates a new channel with the user as the admin and returns +// the pretty print of the channel. This function only create a channel and does +// not join it. // -// The pretty print will be of the format: +// The private key is saved to storage and can be accessed with +// [ChannelsManager.ExportChannelAdminKey]. // -// <Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=> +// Parameters: +// - args[0] - The name of the new channel (string). The name must be between +// 3 and 24 characters inclusive. It can only include upper and lowercase +// Unicode letters, digits 0 through 9, and underscores (_). It cannot be +// changed once a channel is created. +// - args[1] - The description of a channel (string). The description is +// optional but cannot be longer than 144 characters and can include all +// Unicode characters. It cannot be changed once a channel is created. +// - args[2] - The [broadcast.PrivacyLevel] of the channel (int). 0 = public, +// 1 = private, and 2 = secret. Refer to the comment below for more +// information. // // Returns: -// - JSON of [bindings.ChannelInfo], which describes all relevant channel info -// (Uint8Array). -// - Throws a TypeError if joining the channel fails. -func (ch *ChannelsManager) JoinChannel(_ js.Value, args []js.Value) any { - ci, err := ch.api.JoinChannel(args[0].String()) +// - The pretty print of the channel (string). +// - Throws a TypeError if generating the channel fails. +// +// The [broadcast.PrivacyLevel] of a channel indicates the level of channel +// information revealed when sharing it via URL. For any channel besides public +// channels, the secret information is encrypted and a password is required to +// share and join a channel. +// - A privacy level of [broadcast.Public] reveals all the information +// including the name, description, privacy level, public key and salt. +// - A privacy level of [broadcast.Private] reveals only the name and +// description. +// - A privacy level of [broadcast.Secret] reveals nothing. +func (cm *ChannelsManager) GenerateChannel(_ js.Value, args []js.Value) any { + prettyPrint, err := cm.api.GenerateChannel( + args[0].String(), args[1].String(), args[2].Int()) if err != nil { utils.Throw(utils.TypeError, err) return nil } - return utils.CopyBytesToJS(ci) + return prettyPrint } -// GetChannels returns the IDs of all channels that have been joined. +// JoinChannel joins the given channel. It will return the error +// [channels.ChannelAlreadyExistsErr] if the channel has already been joined. // -// Returns: -// - JSON of an array of marshalled [id.ID] (Uint8Array). -// - Throws a TypeError if getting the channels fails. +// Parameters: +// - args[0] - A portable channel string. Should be received from another user +// or generated via [ChannelsManager.GenerateChannel] (string). // -// JSON Example: +// The pretty print will be of the format: // -// { -// "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID", -// "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD" -// } -func (ch *ChannelsManager) GetChannels(js.Value, []js.Value) any { - channelList, err := ch.api.GetChannels() +// <Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=> +// +// Returns: +// - JSON of [bindings.ChannelInfo], which describes all relevant channel info +// (Uint8Array). +// - Throws a TypeError if joining the channel fails. +func (cm *ChannelsManager) JoinChannel(_ js.Value, args []js.Value) any { + ci, err := cm.api.JoinChannel(args[0].String()) if err != nil { utils.Throw(utils.TypeError, err) return nil } - return utils.CopyBytesToJS(channelList) + return utils.CopyBytesToJS(ci) } -// LeaveChannel leaves the given channel. It will return an error if the channel -// was not previously joined. +// LeaveChannel leaves the given channel. It will return the error +// [channels.ChannelDoesNotExistsErr] if the channel was not previously joined. // // Parameters: // - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). // // Returns: // - Throws a TypeError if the channel does not exist. -func (ch *ChannelsManager) LeaveChannel(_ js.Value, args []js.Value) any { +func (cm *ChannelsManager) LeaveChannel(_ js.Value, args []js.Value) any { marshalledChanId := utils.CopyBytesToGo(args[0]) - err := ch.api.LeaveChannel(marshalledChanId) + err := cm.api.LeaveChannel(marshalledChanId) if err != nil { utils.Throw(utils.TypeError, err) return nil @@ -745,15 +698,18 @@ func (ch *ChannelsManager) LeaveChannel(_ js.Value, args []js.Value) any { // ReplayChannel replays all messages from the channel within the network's // memory (~3 weeks) over the event model. // +// Returns the error [channels.ChannelDoesNotExistsErr] if the channel was not +// previously joined. +// // Parameters: -// - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array). // // Returns: // - Throws a TypeError if the replay fails. -func (ch *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any { +func (cm *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any { marshalledChanId := utils.CopyBytesToGo(args[0]) - err := ch.api.ReplayChannel(marshalledChanId) + err := cm.api.ReplayChannel(marshalledChanId) if err != nil { utils.Throw(utils.TypeError, err) return nil @@ -762,6 +718,28 @@ func (ch *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any { return nil } +// GetChannels returns the IDs of all channels that have been joined. +// +// Returns: +// - JSON of an array of marshalled [id.ID] (Uint8Array). +// - Throws a TypeError if getting the channels fails. +// +// JSON Example: +// +// { +// "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID", +// "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD" +// } +func (cm *ChannelsManager) GetChannels(js.Value, []js.Value) any { + channelList, err := cm.api.GetChannels() + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return utils.CopyBytesToJS(channelList) +} + //////////////////////////////////////////////////////////////////////////////// // Channel Share URL // //////////////////////////////////////////////////////////////////////////////// @@ -786,8 +764,8 @@ type ShareURL struct { // channel. If it is set to 0, then it can be shared unlimited times. The max // uses is set as a URL parameter using the key [broadcast.MaxUsesKey]. Note // that this number is also encoded in the secret data for private and secret -// URLs, so if the number is changed in the URL, is will be verified when -// calling [DecodePublicURL] or [DecodePrivateURL]. There is no enforcement for +// URLs, so if the number is changed in the URL, it will be verified when +// calling [DecodePublicURL] and [DecodePrivateURL]. There is no enforcement for // public URLs. // // Parameters: @@ -800,13 +778,13 @@ type ShareURL struct { // Returns: // - JSON of [bindings.ShareURL] (Uint8Array). // - Throws a TypeError if generating the URL fails. -func (ch *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any { +func (cm *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any { cmixID := args[0].Int() host := args[1].String() maxUses := args[2].Int() marshalledChanId := utils.CopyBytesToGo(args[3]) - su, err := ch.api.GetShareURL(cmixID, host, maxUses, marshalledChanId) + su, err := cm.api.GetShareURL(cmixID, host, maxUses, marshalledChanId) if err != nil { utils.Throw(utils.TypeError, err) return nil @@ -823,7 +801,7 @@ func (ch *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any { // // Returns: // - An int that corresponds to the [broadcast.PrivacyLevel] as outlined -// below. +// below (int). // - Throws a TypeError if parsing the URL fails. // // Possible returns: @@ -845,84 +823,54 @@ func GetShareUrlType(_ js.Value, args []js.Value) any { // Channel Sending Methods and Reports // //////////////////////////////////////////////////////////////////////////////// +// ValidForever returns the value to use for validUntil when you want a message +// to be available for the maximum amount of time. +// +// Returns: +// - The maximum amount of time (int). +func ValidForever(js.Value, []js.Value) any { + return bindings.ValidForever() +} + // SendGeneric is used to send a raw message over a channel. In general, it -// should be wrapped in a function which defines the wire protocol. If the final -// message, before being sent over the wire, is too long, this will return an -// error. Due to the underlying encoding using compression, it isn't possible to -// define the largest payload that can be sent, but it will always be possible -// to send a payload of 802 bytes at minimum. The meaning of validUntil depends -// on the use case. +// should be wrapped in a function that defines the wire protocol. +// +// If the final message, before being sent over the wire, is too long, this +// will return an error. Due to the underlying encoding using compression, +// it is not possible to define the largest payload that can be sent, but it +// will always be possible to send a payload of 802 bytes at minimum. // // Parameters: // - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). // - args[1] - The message type of the message. This will be a valid // [channels.MessageType] (int). // - args[2] - The contents of the message (Uint8Array). -// - args[3] - The lease of the message. This will be how long the message is -// valid until, in milliseconds. As per the [channels.Manager] -// documentation, this has different meanings depending on the use case. -// These use cases may be generic enough that they will not be enumerated -// here (int). -// - args[4] - JSON of [xxdk.CMIXParams]. If left empty +// - args[3] - The lease of the message. This will be how long the +// message is available from the network, in milliseconds (int). As per the +// [channels.Manager] documentation, this has different meanings depending +// on the use case. These use cases may be generic enough that they will not +// be enumerated here. Use [ValidForever] to last the max message life. +// - args[4] - Set tracked to true if the message should be tracked in the +// sendTracker, which allows messages to be shown locally before they are +// received on the network. In general, all messages that will be displayed +// to the user should be tracked while all actions should not be (boolean). +// - args[5] - JSON of [xxdk.CMIXParams]. If left empty // [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). // // Returns a promise: // - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). // - Rejected with an error if sending fails. -func (ch *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any { +func (cm *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any { marshalledChanId := utils.CopyBytesToGo(args[0]) messageType := args[1].Int() message := utils.CopyBytesToGo(args[2]) leaseTimeMS := int64(args[3].Int()) - cmixParamsJSON := utils.CopyBytesToGo(args[4]) - - promiseFn := func(resolve, reject func(args ...any) js.Value) { - sendReport, err := ch.api.SendGeneric( - marshalledChanId, messageType, message, leaseTimeMS, cmixParamsJSON) - if err != nil { - reject(utils.JsTrace(err)) - } else { - resolve(utils.CopyBytesToJS(sendReport)) - } - } - - return utils.CreatePromise(promiseFn) -} - -// SendAdminGeneric is used to send a raw message over a channel encrypted with -// admin keys, identifying it as sent by the admin. In general, it should be -// wrapped in a function that defines the wire protocol. If the final message, -// before being sent over the wire, is too long, this will return an error. The -// message must be at most 510 bytes long. -// -// Parameters: -// - args[0] - The PEM-encode admin RSA private key (Uint8Array). -// - args[1] - Marshalled bytes of the channel [id.ID] (Uint8Array). -// - args[2] - The message type of the message. This will be a valid -// [channels.MessageType] (int). -// - args[3] - The contents of the message (Uint8Array). -// - args[4] - The lease of the message. This will be how long the message is -// valid until, in milliseconds. As per the [channels.Manager] -// documentation, this has different meanings depending on the use case. -// These use cases may be generic enough that they will not be enumerated -// here (int). -// - args[5] - JSON of [xxdk.CMIXParams]. If left empty -// [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). -// -// Returns a promise: -// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). -// - Rejected with an error if sending fails. -func (ch *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) any { - adminPrivateKey := utils.CopyBytesToGo(args[0]) - marshalledChanId := utils.CopyBytesToGo(args[1]) - messageType := args[2].Int() - message := utils.CopyBytesToGo(args[3]) - leaseTimeMS := int64(args[4].Int()) + tracked := args[4].Bool() cmixParamsJSON := utils.CopyBytesToGo(args[5]) promiseFn := func(resolve, reject func(args ...any) js.Value) { - sendReport, err := ch.api.SendAdminGeneric(adminPrivateKey, - marshalledChanId, messageType, message, leaseTimeMS, cmixParamsJSON) + sendReport, err := cm.api.SendGeneric(marshalledChanId, messageType, + message, leaseTimeMS, tracked, cmixParamsJSON) if err != nil { reject(utils.JsTrace(err)) } else { @@ -934,6 +882,7 @@ func (ch *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) any { } // SendMessage is used to send a formatted message over a channel. +// // Due to the underlying encoding using compression, it isn't possible to define // the largest payload that can be sent, but it will always be possible to send // a payload of 798 bytes at minimum. @@ -944,25 +893,25 @@ func (ch *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) any { // Parameters: // - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). // - args[1] - The contents of the message (string). -// - args[2] - The lease of the message. This will be how long the message is -// valid until, in milliseconds. As per the [channels.Manager] -// documentation, this has different meanings depending on the use case. -// These use cases may be generic enough that they will not be enumerated -// here (int). +// - args[2] - The lease of the message. This will be how long the +// message is available from the network, in milliseconds (int). As per the +// [channels.Manager] documentation, this has different meanings depending +// on the use case. These use cases may be generic enough that they will not +// be enumerated here. Use [ValidForever] to last the max message life. // - args[3] - JSON of [xxdk.CMIXParams]. If left empty // [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). // // Returns a promise: // - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). // - Rejected with an error if sending fails. -func (ch *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any { +func (cm *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any { marshalledChanId := utils.CopyBytesToGo(args[0]) message := args[1].String() leaseTimeMS := int64(args[2].Int()) cmixParamsJSON := utils.CopyBytesToGo(args[3]) promiseFn := func(resolve, reject func(args ...any) js.Value) { - sendReport, err := ch.api.SendMessage( + sendReport, err := cm.api.SendMessage( marshalledChanId, message, leaseTimeMS, cmixParamsJSON) if err != nil { reject(utils.JsTrace(err)) @@ -974,15 +923,14 @@ func (ch *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any { return utils.CreatePromise(promiseFn) } -// SendReply is used to send a formatted message over a channel. Due to the -// underlying encoding using compression, it isn't possible to define the -// largest payload that can be sent, but it will always be possible to send a -// payload of 766 bytes at minimum. +// SendReply is used to send a formatted message over a channel. +// +// Due to the underlying encoding using compression, it is not possible to +// define the largest payload that can be sent, but it will always be possible +// to send a payload of 766 bytes at minimum. // -// If the message ID the reply is sent to is nonexistent, the other side will -// post the message as a normal message and not a reply. The message will auto -// delete validUntil after the round it is sent in, lasting forever if -// [channels.ValidForever] is used. +// If the message ID that the reply is sent to does not exist, then the other +// side will post the message as a normal message and not as a reply. // // Parameters: // - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). @@ -994,18 +942,18 @@ func (ch *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any { // your own. Alternatively, if reacting to another user's message, you may // retrieve it via the [bindings.ChannelMessageReceptionCallback] registered // using RegisterReceiveHandler (Uint8Array). -// - args[3] - The lease of the message. This will be how long the message is -// valid until, in milliseconds. As per the [channels.Manager] -// documentation, this has different meanings depending on the use case. -// These use cases may be generic enough that they will not be enumerated -// here (int). +// - args[3] - The lease of the message. This will be how long the +// message is available from the network, in milliseconds (int). As per the +// [channels.Manager] documentation, this has different meanings depending +// on the use case. These use cases may be generic enough that they will not +// be enumerated here. Use [ValidForever] to last the max message life. // - args[4] - JSON of [xxdk.CMIXParams]. If left empty // [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). // // Returns a promise: // - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). // - Rejected with an error if sending fails. -func (ch *ChannelsManager) SendReply(_ js.Value, args []js.Value) any { +func (cm *ChannelsManager) SendReply(_ js.Value, args []js.Value) any { marshalledChanId := utils.CopyBytesToGo(args[0]) message := args[1].String() messageToReactTo := utils.CopyBytesToGo(args[2]) @@ -1013,7 +961,7 @@ func (ch *ChannelsManager) SendReply(_ js.Value, args []js.Value) any { cmixParamsJSON := utils.CopyBytesToGo(args[4]) promiseFn := func(resolve, reject func(args ...any) js.Value) { - sendReport, err := ch.api.SendReply(marshalledChanId, message, + sendReport, err := cm.api.SendReply(marshalledChanId, message, messageToReactTo, leaseTimeMS, cmixParamsJSON) if err != nil { reject(utils.JsTrace(err)) @@ -1025,10 +973,11 @@ func (ch *ChannelsManager) SendReply(_ js.Value, args []js.Value) any { return utils.CreatePromise(promiseFn) } -// SendReaction is used to send a reaction to a message over a channel. -// The reaction must be a single emoji with no other characters, and will -// be rejected otherwise. -// Users will drop the reaction if they do not recognize the reactTo message. +// SendReaction is used to send a reaction to a message over a channel. The +// reaction must be a single emoji with no other characters, and will be +// rejected otherwise. +// +// Clients will drop the reaction if they do not recognize the reactTo message. // // Parameters: // - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). @@ -1045,14 +994,14 @@ func (ch *ChannelsManager) SendReply(_ js.Value, args []js.Value) any { // Returns a promise: // - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). // - Rejected with an error if sending fails. -func (ch *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any { +func (cm *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any { marshalledChanId := utils.CopyBytesToGo(args[0]) reaction := args[1].String() messageToReactTo := utils.CopyBytesToGo(args[2]) cmixParamsJSON := utils.CopyBytesToGo(args[3]) promiseFn := func(resolve, reject func(args ...any) js.Value) { - sendReport, err := ch.api.SendReaction( + sendReport, err := cm.api.SendReaction( marshalledChanId, reaction, messageToReactTo, cmixParamsJSON) if err != nil { reject(utils.JsTrace(err)) @@ -1064,14 +1013,197 @@ func (ch *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any { return utils.CreatePromise(promiseFn) } -// GetIdentity returns the marshaled public identity ([channel.Identity]) that -// the channel is using. +//////////////////////////////////////////////////////////////////////////////// +// Admin Sending // +//////////////////////////////////////////////////////////////////////////////// + +// SendAdminGeneric is used to send a raw message over a channel encrypted with +// admin keys, identifying it as sent by the admin. In general, it should be +// wrapped in a function that defines the wire protocol. +// +// If the final message, before being sent over the wire, is too long, this will +// return an error. The message must be at most 510 bytes long. +// +// If the user is not an admin of the channel (i.e. does not have a private key +// for the channel saved to storage), then the error [channels.NotAnAdminErr] is +// returned. +// +// Parameters: +// - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - args[1] - The message type of the message. This will be a valid +// [channels.MessageType] (int). +// - args[2] - The contents of the message (Uint8Array). The message should be +// at most 510 bytes. +// - args[3] - The lease of the message. This will be how long the +// message is available from the network, in milliseconds (int). As per the +// [channels.Manager] documentation, this has different meanings depending +// on the use case. These use cases may be generic enough that they will not +// be enumerated here. Use [ValidForever] to last the max message life. +// - args[4] - Set tracked to true if the message should be tracked in the +// sendTracker, which allows messages to be shown locally before they are +// received on the network. In general, all messages that will be displayed +// to the user should be tracked while all actions should not be (boolean). +// - args[5] - JSON of [xxdk.CMIXParams]. If left empty +// [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). +// +// Returns a promise: +// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). +// - Rejected with an error if sending fails. +func (cm *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) any { + marshalledChanId := utils.CopyBytesToGo(args[0]) + messageType := args[1].Int() + message := utils.CopyBytesToGo(args[2]) + leaseTimeMS := int64(args[3].Int()) + tracked := args[4].Bool() + cmixParamsJSON := utils.CopyBytesToGo(args[5]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := cm.api.SendAdminGeneric(marshalledChanId, + messageType, message, leaseTimeMS, tracked, cmixParamsJSON) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(sendReport)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// DeleteMessage deletes the targeted message from user's view. Users may delete +// their own messages but only the channel admin can delete other user's +// messages. If the user is not an admin of the channel or if they are not the +// sender of the targetMessage, then the error [channels.NotAnAdminErr] is +// returned. +// +// If undoAction is true, then the targeted message is un-deleted. +// +// Clients will drop the deletion if they do not recognize the target +// message. +// +// Parameters: +// - args[0] - Marshalled bytes of channel [id.ID] (Uint8Array). +// - args[1] - The marshalled [channel.MessageID] of the message you want to +// delete (Uint8Array). +// - args[2] - JSON of [xxdk.CMIXParams]. This may be empty, and +// [GetDefaultCMixParams] will be used internally (Uint8Array). +// +// Returns a promise: +// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). +// - Rejected with an error if sending fails. +func (cm *ChannelsManager) DeleteMessage(_ js.Value, args []js.Value) any { + channelIdBytes := utils.CopyBytesToGo(args[0]) + targetMessageIdBytes := utils.CopyBytesToGo(args[1]) + cmixParamsJSON := utils.CopyBytesToGo(args[2]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := cm.api.DeleteMessage( + channelIdBytes, targetMessageIdBytes, cmixParamsJSON) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(sendReport)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// PinMessage pins the target message to the top of a channel view for all users +// in the specified channel. Only the channel admin can pin user messages; if +// the user is not an admin of the channel, then the error +// [channels.NotAnAdminErr] is returned. +// +// If undoAction is true, then the targeted message is unpinned. +// +// Clients will drop the pin if they do not recognize the target message. +// +// Parameters: +// - args[0] - Marshalled bytes of channel [id.ID] (Uint8Array). +// - args[1] - The marshalled [channel.MessageID] of the message you want to +// pin (Uint8Array). +// - args[2] - Set to true to unpin the message (boolean). +// - args[3] - The time, in milliseconds, that the message should be pinned +// (int). To remain pinned indefinitely, use [ValidForever]. +// - args[4] - JSON of [xxdk.CMIXParams]. This may be empty, and +// [GetDefaultCMixParams] will be used internally (Uint8Array). +// +// Returns a promise: +// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). +// - Rejected with an error if sending fails. +func (cm *ChannelsManager) PinMessage(_ js.Value, args []js.Value) any { + channelIdBytes := utils.CopyBytesToGo(args[0]) + targetMessageIdBytes := utils.CopyBytesToGo(args[1]) + undoAction := args[2].Bool() + validUntilMS := args[3].Int() + cmixParamsJSON := utils.CopyBytesToGo(args[4]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := cm.api.PinMessage(channelIdBytes, + targetMessageIdBytes, undoAction, validUntilMS, cmixParamsJSON) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(sendReport)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// MuteUser is used to mute a user in a channel. Muting a user will cause all +// future messages from the user being dropped on reception. Muted users are +// also unable to send messages. Only the channel admin can mute a user; if the +// user is not an admin of the channel, then the error [channels.NotAnAdminErr] +// is returned. +// +// If undoAction is true, then the targeted user will be unmuted. +// +// Parameters: +// - args[0] - Marshalled bytes of channel [id.ID] (Uint8Array). +// - args[1] - The [ed25519.PublicKey] of the user you want to mute +// (Uint8Array). +// - args[2] - Set to true to unmute the message (boolean). +// - args[3] - The time, in milliseconds, that the user should be muted (int). +// To remain muted indefinitely, use [ValidForever]. +// - args[4] - JSON of [xxdk.CMIXParams]. This may be empty, and +// [GetDefaultCMixParams] will be used internally (Uint8Array). +// +// Returns a promise: +// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). +// - Rejected with an error if sending fails. +func (cm *ChannelsManager) MuteUser(_ js.Value, args []js.Value) any { + channelIdBytes := utils.CopyBytesToGo(args[0]) + mutedUserPubKeyBytes := utils.CopyBytesToGo(args[1]) + undoAction := args[2].Bool() + validUntilMS := args[3].Int() + cmixParamsJSON := utils.CopyBytesToGo(args[4]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := cm.api.MuteUser(channelIdBytes, mutedUserPubKeyBytes, + undoAction, validUntilMS, cmixParamsJSON) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(sendReport)) + } + } + + return utils.CreatePromise(promiseFn) +} + +//////////////////////////////////////////////////////////////////////////////// +// Other Channel Actions // +//////////////////////////////////////////////////////////////////////////////// + +// GetIdentity returns the public identity ([channel.Identity]) of the user +// associated with this channel manager. // // Returns: // - JSON of the [channel.Identity] (Uint8Array). // - Throws TypeError if marshalling the identity fails. -func (ch *ChannelsManager) GetIdentity(js.Value, []js.Value) any { - i, err := ch.api.GetIdentity() +func (cm *ChannelsManager) GetIdentity(js.Value, []js.Value) any { + i, err := cm.api.GetIdentity() if err != nil { utils.Throw(utils.TypeError, err) return nil @@ -1080,17 +1212,17 @@ func (ch *ChannelsManager) GetIdentity(js.Value, []js.Value) any { return utils.CopyBytesToJS(i) } -// ExportPrivateIdentity encrypts and exports the private identity to a portable -// string. +// ExportPrivateIdentity encrypts the private identity using the password and +// exports it to a portable string. // // Parameters: -// - args[0] - Password to encrypt the identity with (string). +// - password - The password used to encrypt the private identity (string). // // Returns: -// - JSON of the encrypted private identity (Uint8Array). +// - Encrypted portable private identity (Uint8Array). // - Throws TypeError if exporting the identity fails. -func (ch *ChannelsManager) ExportPrivateIdentity(_ js.Value, args []js.Value) any { - i, err := ch.api.ExportPrivateIdentity(args[0].String()) +func (cm *ChannelsManager) ExportPrivateIdentity(_ js.Value, args []js.Value) any { + i, err := cm.api.ExportPrivateIdentity(args[0].String()) if err != nil { utils.Throw(utils.TypeError, err) return nil @@ -1099,12 +1231,13 @@ func (ch *ChannelsManager) ExportPrivateIdentity(_ js.Value, args []js.Value) an return utils.CopyBytesToJS(i) } -// GetStorageTag returns the storage tag needed to reload the manager. +// GetStorageTag returns the tag where this manager is stored. To be used when +// loading the manager. The storage tag is derived from the public key. // // Returns: // - Storage tag (string). -func (ch *ChannelsManager) GetStorageTag(js.Value, []js.Value) any { - return ch.api.GetStorageTag() +func (cm *ChannelsManager) GetStorageTag(js.Value, []js.Value) any { + return cm.api.GetStorageTag() } // SetNickname sets the nickname for a given channel. The nickname must be valid @@ -1117,8 +1250,8 @@ func (ch *ChannelsManager) GetStorageTag(js.Value, []js.Value) any { // Returns: // - Throws TypeError if unmarshalling the ID fails or the nickname is // invalid. -func (ch *ChannelsManager) SetNickname(_ js.Value, args []js.Value) any { - err := ch.api.SetNickname(args[0].String(), utils.CopyBytesToGo(args[1])) +func (cm *ChannelsManager) SetNickname(_ js.Value, args []js.Value) any { + err := cm.api.SetNickname(args[0].String(), utils.CopyBytesToGo(args[1])) if err != nil { utils.Throw(utils.TypeError, err) return nil @@ -1127,15 +1260,16 @@ func (ch *ChannelsManager) SetNickname(_ js.Value, args []js.Value) any { return nil } -// DeleteNickname deletes the nickname for a given channel. +// DeleteNickname removes the nickname for a given channel. The name will revert +// back to the codename for this channel instead. // // Parameters: // - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array). // // Returns: // - Throws TypeError if deleting the nickname fails. -func (ch *ChannelsManager) DeleteNickname(_ js.Value, args []js.Value) any { - err := ch.api.DeleteNickname(utils.CopyBytesToGo(args[0])) +func (cm *ChannelsManager) DeleteNickname(_ js.Value, args []js.Value) any { + err := cm.api.DeleteNickname(utils.CopyBytesToGo(args[0])) if err != nil { utils.Throw(utils.TypeError, err) return nil @@ -1151,10 +1285,10 @@ func (ch *ChannelsManager) DeleteNickname(_ js.Value, args []js.Value) any { // - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array). // // Returns: -// - The nickname (string). +// - The nickname set for the channel (string). // - Throws TypeError if the channel has no nickname set. -func (ch *ChannelsManager) GetNickname(_ js.Value, args []js.Value) any { - nickname, err := ch.api.GetNickname(utils.CopyBytesToGo(args[0])) +func (cm *ChannelsManager) GetNickname(_ js.Value, args []js.Value) any { + nickname, err := cm.api.GetNickname(utils.CopyBytesToGo(args[0])) if err != nil { utils.Throw(utils.TypeError, err) return nil @@ -1184,6 +1318,201 @@ func IsNicknameValid(_ js.Value, args []js.Value) any { return nil } +// Muted returns true if the user is currently muted in the given channel. +// +// Parameters: +// - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array). +// +// Returns: +// - Returns true if the user is muted in the channel and false otherwise +// (boolean). +// - Throws a TypeError if the channel ID cannot be unmarshalled. +func (cm *ChannelsManager) Muted(_ js.Value, args []js.Value) any { + channelIDBytes := utils.CopyBytesToGo(args[0]) + + muted, err := cm.api.Muted(channelIDBytes) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return muted +} + +// GetMutedUsers returns the list of the public keys for each muted user in +// the channel. If there are no muted user or if the channel does not exist, +// an empty list is returned. +// +// Parameters: +// - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array). +// +// Returns: +// - JSON of an array of ed25519.PublicKey (Uint8Array). Look below for an +// example. +// - Throws a TypeError if the channel ID cannot be unmarshalled. +// +// Example return: +// +// ["k2IrybDXjJtqxjS6Tx/6m3bXvT/4zFYOJnACNWTvESE=","ocELv7KyeCskLz4cm0klLWhmFLYvQL2FMDco79GTXYw=","mmxoDgoTEYwaRyEzq5Npa24IIs+3B5LXhll/8K5yCv0="] +func (cm *ChannelsManager) GetMutedUsers(_ js.Value, args []js.Value) any { + channelIDBytes := utils.CopyBytesToGo(args[0]) + mutedUsers, err := cm.api.GetMutedUsers(channelIDBytes) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return utils.CopyBytesToJS(mutedUsers) +} + +//////////////////////////////////////////////////////////////////////////////// +// Admin Management // +//////////////////////////////////////////////////////////////////////////////// + +// IsChannelAdmin returns true if the user is an admin of the channel. +// +// Parameters: +// - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array) +// +// Returns: +// - True if the user is an admin in the channel and false otherwise +// (boolean). +// - Throws a TypeError if the channel ID cannot be unmarshalled. +func (cm *ChannelsManager) IsChannelAdmin(_ js.Value, args []js.Value) any { + isAdmin, err := cm.api.IsChannelAdmin(utils.CopyBytesToGo(args[0])) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return isAdmin +} + +// ExportChannelAdminKey gets the private key for the given channel ID, encrypts +// it with the provided encryptionPassword, and exports it into a portable +// format. Returns an error if the user is not an admin of the channel. +// +// This key can be provided to other users in a channel to grant them admin +// access using [ChannelsManager.ImportChannelAdminKey]. +// +// The private key is encrypted using a key generated from the password using +// Argon2. Each call to ExportChannelAdminKey produces a different encrypted +// packet regardless if the same password is used for the same channel. It +// cannot be determined which channel the payload is for nor that two payloads +// are for the same channel. +// +// The passwords between each call are not related. They can be the same or +// different with no adverse impact on the security properties. +// +// Parameters: +// - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array). +// - args[1] - The password used to encrypt the private key (string). The +// passwords between each call are not related. They can be the same or +// different with no adverse impact on the security properties. +// +// Returns: +// - Portable string of the channel private key encrypted with the password +// (Uint8Array). +// - Throws a TypeError if the user is not an admin for the channel. +func (cm *ChannelsManager) ExportChannelAdminKey(_ js.Value, args []js.Value) any { + pk, err := cm.api.ExportChannelAdminKey( + utils.CopyBytesToGo(args[0]), args[1].String()) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + return utils.CopyBytesToJS(pk) +} + +// VerifyChannelAdminKey verifies that the encrypted private key can be +// decrypted and that it matches the expected channel. Returns false if private +// key does not belong to the given channel. +// +// Parameters: +// - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array). +// - args[1] - The password used to encrypt the private key (string) +// - args[2] - The encrypted channel private key packet (Uint8Array). +// +// Returns: +// - Returns false if private key does not belong to the given channel ID +// (boolean). +// - Throws a TypeError if the password is invalid. +// +// Returns: +// - bool - True if the private key belongs to the channel and false +// otherwise. +// - Throws a TypeError with the message [channels.WrongPasswordErr] for an +// invalid password. +// - Throws a TypeError with the message [channels.ChannelDoesNotExistsErr] i +// the channel has not already been joined. +func (cm *ChannelsManager) VerifyChannelAdminKey(_ js.Value, args []js.Value) any { + channelID := utils.CopyBytesToGo(args[0]) + encryptionPassword := args[1].String() + encryptedPrivKey := utils.CopyBytesToGo(args[2]) + valid, err := cm.api.VerifyChannelAdminKey( + channelID, encryptionPassword, encryptedPrivKey) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return valid +} + +// ImportChannelAdminKey decrypts and imports the given encrypted private key +// and grants the user admin access to the channel the private key belongs to. +// Returns an error if the private key cannot be decrypted or if the private key +// is for the wrong channel. +// +// Parameters: +// - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array). +// - args[1] - The password used to encrypt the private key (string) +// - args[2] - The encrypted channel private key packet (Uint8Array). +// +// Returns: +// - Throws a TypeError if the password is invalid or the private key does +// not match the channel ID. +// - Throws a TypeError with the message [channels.WrongPasswordErr] for an +// invalid password. +// - Throws a TypeError with the message [channels.ChannelDoesNotExistsErr] if +// the channel has not already been joined. +// - Throws a TypeError with the message [channels.WrongPrivateKeyErr] if the +// private key does not belong to the channel. +func (cm *ChannelsManager) ImportChannelAdminKey(_ js.Value, args []js.Value) any { + channelID := utils.CopyBytesToGo(args[0]) + encryptionPassword := args[1].String() + encryptedPrivKey := utils.CopyBytesToGo(args[2]) + err := cm.api.ImportChannelAdminKey( + channelID, encryptionPassword, encryptedPrivKey) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return nil +} + +// DeleteChannelAdminKey deletes the private key for the given channel. +// +// CAUTION: This will remove admin access. This cannot be undone. If the +// private key is deleted, it cannot be recovered and the channel can never +// have another admin. +// +// Parameters: +// - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array) +// +// Returns: +// - Throws a TypeError if the deletion fails. +func (cm *ChannelsManager) DeleteChannelAdminKey(_ js.Value, args []js.Value) any { + err := cm.api.DeleteChannelAdminKey(utils.CopyBytesToGo(args[0])) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return nil +} + //////////////////////////////////////////////////////////////////////////////// // Channel Receiving Logic and Callback Registration // //////////////////////////////////////////////////////////////////////////////// @@ -1225,15 +1554,28 @@ func (cmrCB *channelMessageReceptionCallback) Callback( // - args[1] - Javascript object that has functions that implement the // [bindings.ChannelMessageReceptionCallback] interface. This callback will // be executed when a channel message of the messageType is received. +// - args[2] - A name describing what type of messages the listener picks up. +// This is used for debugging and logging (string). +// - args[3] - Set to true if this listener can receive messages from normal +// users (boolean). +// - args[4] - Set to true if this listener can receive messages from admins +// (boolean). +// - args[5] - Set to true if this listener can receive messages from muted +// users (boolean). // // Returns: // - Throws a TypeError if registering the handler fails. -func (ch *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) any { +func (cm *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) any { messageType := args[0].Int() listenerCb := &channelMessageReceptionCallback{ utils.WrapCB(args[1], "Callback")} + name := args[2].String() + userSpace := args[3].Bool() + adminSpace := args[4].Bool() + mutedSpace := args[5].Bool() - err := ch.api.RegisterReceiveHandler(messageType, listenerCb) + err := cm.api.RegisterReceiveHandler( + messageType, listenerCb, name, userSpace, adminSpace, mutedSpace) if err != nil { utils.Throw(utils.TypeError, err) return nil @@ -1246,15 +1588,41 @@ func (ch *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) a // Event Model Logic // //////////////////////////////////////////////////////////////////////////////// +// eventModelBuilder adheres to the [bindings.EventModelBuilder] interface. +type eventModelBuilder struct { + build func(args ...any) js.Value +} + +// Build initializes and returns the event model. It wraps a Javascript object +// that has all the methods in [bindings.EventModel] to make it adhere to the Go +// interface [bindings.EventModel]. +func (emb *eventModelBuilder) Build(path string) bindings.EventModel { + emJs := emb.build(path) + return &eventModel{ + joinChannel: utils.WrapCB(emJs, "JoinChannel"), + leaveChannel: utils.WrapCB(emJs, "LeaveChannel"), + receiveMessage: utils.WrapCB(emJs, "ReceiveMessage"), + receiveReply: utils.WrapCB(emJs, "ReceiveReply"), + receiveReaction: utils.WrapCB(emJs, "ReceiveReaction"), + updateFromUUID: utils.WrapCB(emJs, "UpdateFromUUID"), + updateFromMessageID: utils.WrapCB(emJs, "UpdateFromMessageID"), + getMessage: utils.WrapCB(emJs, "GetMessage"), + deleteMessage: utils.WrapCB(emJs, "DeleteMessage"), + } +} + // eventModel wraps Javascript callbacks to adhere to the [bindings.EventModel] // interface. type eventModel struct { - joinChannel func(args ...any) js.Value - leaveChannel func(args ...any) js.Value - receiveMessage func(args ...any) js.Value - receiveReply func(args ...any) js.Value - receiveReaction func(args ...any) js.Value - updateSentStatus func(args ...any) js.Value + joinChannel func(args ...any) js.Value + leaveChannel func(args ...any) js.Value + receiveMessage func(args ...any) js.Value + receiveReply func(args ...any) js.Value + receiveReaction func(args ...any) js.Value + updateFromUUID func(args ...any) js.Value + updateFromMessageID func(args ...any) js.Value + getMessage func(args ...any) js.Value + deleteMessage func(args ...any) js.Value } // JoinChannel is called whenever a channel is joined locally. @@ -1284,6 +1652,7 @@ func (em *eventModel) LeaveChannel(channelID []byte) { // - nickname - The nickname of the sender of the message (string). // - text - The content of the message (string). // - pubKey - The sender's Ed25519 public key (Uint8Array). +// - dmToken - The dmToken (int32). // - codeset - The codeset version (int). // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). @@ -1302,12 +1671,12 @@ func (em *eventModel) LeaveChannel(channelID []byte) { // - A non-negative unique UUID for the message that it can be referenced by // later with [eventModel.UpdateSentStatus]. func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname, - text string, pubKey []byte, codeset int, timestamp, lease, roundId, msgType, - status int64) int64 { + text string, pubKey []byte, dmToken int32, codeset int, timestamp, lease, roundId, msgType, + status int64, hidden bool) int64 { uuid := em.receiveMessage(utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(messageID), nickname, text, - utils.CopyBytesToJS(pubKey), codeset, timestamp, lease, roundId, - msgType, status) + utils.CopyBytesToJS(pubKey), dmToken, codeset, timestamp, lease, roundId, + msgType, status, hidden) return int64(uuid.Int()) } @@ -1328,6 +1697,7 @@ func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname, // - senderUsername - The username of the sender of the message (string). // - text - The content of the message (string). // - pubKey - The sender's Ed25519 public key (Uint8Array). +// - dmToken - The dmToken (int32). // - codeset - The codeset version (int). // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). @@ -1346,12 +1716,12 @@ func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname, // - A non-negative unique UUID for the message that it can be referenced by // later with [eventModel.UpdateSentStatus]. func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte, - senderUsername, text string, pubKey []byte, codeset int, timestamp, lease, - roundId, msgType, status int64) int64 { + senderUsername, text string, pubKey []byte, dmToken int32, codeset int, timestamp, lease, + roundId, msgType, status int64, hidden bool) int64 { uuid := em.receiveReply(utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo), - senderUsername, text, utils.CopyBytesToJS(pubKey), codeset, - timestamp, lease, roundId, msgType, status) + senderUsername, text, utils.CopyBytesToJS(pubKey), dmToken, codeset, + timestamp, lease, roundId, msgType, status, hidden) return int64(uuid.Int()) } @@ -1372,6 +1742,7 @@ func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte, // - senderUsername - The username of the sender of the message (string). // - reaction - The contents of the reaction message (string). // - pubKey - The sender's Ed25519 public key (Uint8Array). +// - dmToken - The dmToken (int32). // - codeset - The codeset version (int). // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). @@ -1390,37 +1761,119 @@ func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte, // - A non-negative unique UUID for the message that it can be referenced by // later with [eventModel.UpdateSentStatus]. func (em *eventModel) ReceiveReaction(channelID, messageID, reactionTo []byte, - senderUsername, reaction string, pubKey []byte, codeset int, timestamp, - lease, roundId, msgType, status int64) int64 { + senderUsername, reaction string, pubKey []byte, dmToken int32, codeset int, timestamp, + lease, roundId, msgType, status int64, hidden bool) int64 { uuid := em.receiveReaction(utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo), - senderUsername, reaction, utils.CopyBytesToJS(pubKey), codeset, - timestamp, lease, roundId, msgType, status) + senderUsername, reaction, utils.CopyBytesToJS(pubKey), dmToken, codeset, + timestamp, lease, roundId, msgType, status, hidden) return int64(uuid.Int()) } -// UpdateSentStatus is called whenever the sent status of a message has -// changed. +// UpdateFromUUID is called whenever a message at the UUID is modified. +// +// Parameters: +// - uuid - The unique identifier of the message in the database (int). +// - messageUpdateInfoJSON - JSON of [bindings.MessageUpdateInfo] +// (Uint8Array). +func (em *eventModel) UpdateFromUUID(uuid int64, messageUpdateInfoJSON []byte) { + em.updateFromUUID(uuid, utils.CopyBytesToJS(messageUpdateInfoJSON)) +} + +// UpdateFromMessageID is called whenever a message with the message ID is +// modified. // // Parameters: -// - uuid - The unique identifier for the message (int). // - messageID - The bytes of the [channel.MessageID] of the received message // (Uint8Array). -// - timestamp - Time the message was received; represented as nanoseconds -// since unix epoch (int). -// - roundId - The ID of the round that the message was received on (int). -// - status - The [channels.SentStatus] of the message (int). +// - messageUpdateInfoJSON - JSON of [bindings.MessageUpdateInfo +// (Uint8Array). // -// Statuses will be enumerated as such: +// Returns: +// - A non-negative unique uuid for the modified message by which it can be +// referenced later with [EventModel.UpdateFromUUID] int). +func (em *eventModel) UpdateFromMessageID( + messageID []byte, messageUpdateInfoJSON []byte) int64 { + return int64(em.updateFromMessageID(utils.CopyBytesToJS(messageID), + utils.CopyBytesToJS(messageUpdateInfoJSON)).Int()) +} + +// GetMessage returns the message with the given [channel.MessageID]. // -// Sent = 0 -// Delivered = 1 -// Failed = 2 -func (em *eventModel) UpdateSentStatus( - uuid int64, messageID []byte, timestamp, roundID, status int64) { - em.updateSentStatus( - uuid, utils.CopyBytesToJS(messageID), timestamp, roundID, status) +// Note for developers: The internal Javascript function must return JSON of +// MessageAndError, which includes the returned [channels.ModelMessage] or any +// error that occurs during lookup. +// +// Parameters: +// - messageID - The bytes of the [channel.MessageID] of the message +// (Uint8Array). +// +// Returns: +// - JSON of [channels.ModelMessage] (Uint8Array). +func (em *eventModel) GetMessage(messageID []byte) ([]byte, error) { + messageAndErrorBytes := + utils.CopyBytesToGo(em.getMessage(utils.CopyBytesToJS(messageID))) + + var mae MessageAndError + err := json.Unmarshal(messageAndErrorBytes, &mae) + if err != nil { + return nil, err + } + + if mae.Error != "" { + return nil, errors.New(mae.Error) + } + + return json.Marshal(mae.ModelMessage) +} + +// DeleteMessage deletes the message with the given [channel.MessageID] from +// the database. +// +// Parameters: +// - messageID - The bytes of the [channel.MessageID] of the message. +func (em *eventModel) DeleteMessage(messageID []byte) error { + err := em.deleteMessage(utils.CopyBytesToJS(messageID)) + if !err.IsUndefined() { + return js.Error{Value: err} + } + + return nil +} + +// MessageAndError contains a message returned by eventModel.GetMessage or any +// possible error that occurs during lookup. Only one field should be present at +// a time; if an error occurs, ModelMessage should be empty. +// +// Example JSON: +// +// { +// "ModelMessage": { +// "UUID": 50, +// "Nickname": "Nickname", +// "MessageID": "ODg5goFIBvpvqPzuoYGqmvxFYBgj0MMiQxAB51Q2nPs=", +// "ChannelID": "R+xKJTH6m4YRS4f0JggK3fTu10sANmtahS0Qtc8yi/AD", +// "ParentMessageID": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", +// "Timestamp": "1955-11-05T12:01:00-07:00", +// "Lease": 21600000000000, +// "Status": 2, +// "Hidden": false, +// "Pinned": false, +// "Content": "VGhpcyBpcyBzb21lIG1lc3NhZ2UgY29udGVudC4=", +// "Type": 1, +// "Round": 7, +// "PubKey": "QyTtpndOf3sDZehVpOBQzQNBe1R2Eae7qlAEDZJ2mLg=", +// "CodesetVersion": 0 +// }, +// "Error": "" +// } +type MessageAndError struct { + // MessageJSON should contain the JSON of channels.ModelMessage. + ModelMessage channels.ModelMessage + + // Error should only be filled when an error occurs on message lookup. + Error string } //////////////////////////////////////////////////////////////////////////////// @@ -1542,8 +1995,7 @@ func (c *ChannelDbCipher) MarshalJSON(js.Value, []js.Value) any { return utils.CopyBytesToJS(data) } -// UnmarshalJSON unmarshalls JSON into the cipher. This function adheres to the -// json.Unmarshaler interface. +// UnmarshalJSON unmarshalls JSON into the cipher. // // Note that this function does not transfer the internal RNG. Use // [channel.NewCipherFromJSON] to properly reconstruct a cipher from JSON. diff --git a/wasm/connect.go b/wasm/connect.go index 5d8e347dbb34c9adf4f724eee13a3eda43fc4f57..bfb95a6d882f808277a856dfbda9937c3bb3d687 100644 --- a/wasm/connect.go +++ b/wasm/connect.go @@ -80,10 +80,6 @@ func (c *Cmix) Connect(_ js.Value, args []js.Value) any { // SendE2E is a wrapper for sending specifically to the [Connection]'s // [partner.Manager]. // -// Returns: -// - []byte - the JSON marshalled bytes of the E2ESendReport object, which can -// be passed into [Cmix.WaitForRoundResult] to see if the send succeeded. -// // Parameters: // - args[0] - Message type from [catalog.MessageType] (int). // - args[1] - Message payload (Uint8Array). diff --git a/wasm/dm.go b/wasm/dm.go new file mode 100644 index 0000000000000000000000000000000000000000..678ca74f529f1ccce1a0a8cac63242b5073ed835 --- /dev/null +++ b/wasm/dm.go @@ -0,0 +1,872 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package wasm + +import ( + "crypto/ed25519" + "syscall/js" + + indexDB "gitlab.com/elixxir/xxdk-wasm/indexedDb/dm" + + "encoding/base64" + + "gitlab.com/elixxir/client/v4/bindings" + "gitlab.com/elixxir/crypto/codename" + "gitlab.com/elixxir/xxdk-wasm/utils" +) + +//////////////////////////////////////////////////////////////////////////////// +// Basic Channel API // +//////////////////////////////////////////////////////////////////////////////// + +// DMClient wraps the [bindings.DMClient] object so its methods +// can be wrapped to be Javascript compatible. +type DMClient struct { + api *bindings.DMClient +} + +// newDMClientJS creates a new Javascript compatible object +// (map[string]any) that matches the [DMClient] structure. +func newDMClientJS(api *bindings.DMClient) map[string]any { + cm := DMClient{api} + dmClientMap := map[string]any{ + // Basic Channel API + "GetID": js.FuncOf(cm.GetID), + + // Identity and Nickname Controls + "GetPublicKey": js.FuncOf(cm.GetPublicKey), + "GetToken": js.FuncOf(cm.GetToken), + "GetIdentity": js.FuncOf(cm.GetIdentity), + "ExportPrivateIdentity": js.FuncOf(cm.ExportPrivateIdentity), + "SetNickname": js.FuncOf(cm.SetNickname), + "GetNickname": js.FuncOf(cm.GetNickname), + + // DM Sending Methods and Reports + "SendText": js.FuncOf(cm.SendText), + "SendReply": js.FuncOf(cm.SendReply), + "SendReaction": js.FuncOf(cm.SendReaction), + "Send": js.FuncOf(cm.Send), + } + + return dmClientMap +} + +// GetPublicKey returns the ecdh Public Key for this [DMClient] in the +// [DMClient] tracker. +// +// Returns: +// - Tracker ID (int). +func (ch *DMClient) GetID(js.Value, []js.Value) any { + return ch.api.GetID() +} + +func (ch *DMClient) GetPublicKey(js.Value, []js.Value) any { + return ch.api.GetPublicKey() +} + +func (ch *DMClient) GetToken(js.Value, []js.Value) any { + return ch.api.GetToken() +} + +// dmReceiverBuilder adheres to the [bindings.DMReceiverBuilder] interface. +type dmReceiverBuilder struct { + build func(args ...any) js.Value +} + +// Build initializes and returns the event model. It wraps a Javascript object +// that has all the methods in [bindings.EventModel] to make it adhere to the Go +// interface [bindings.EventModel]. +func (emb *dmReceiverBuilder) Build(path string) bindings.DMReceiver { + emJs := emb.build(path) + return &dmReceiver{ + receive: utils.WrapCB(emJs, "ReceiveText"), + receiveText: utils.WrapCB(emJs, "ReceiveText"), + receiveReply: utils.WrapCB(emJs, "ReceiveReply"), + receiveReaction: utils.WrapCB(emJs, "ReceiveReaction"), + updateSentStatus: utils.WrapCB(emJs, "UpdateSentStatus"), + } +} + +// NewDMClient creates a new [DMClient] from a new private +// identity ([channel.PrivateIdentity]). +// +// This is for creating a manager for an identity for the first time. For +// generating a new one channel identity, use [GenerateChannelIdentity]. To +// reload this channel manager, use [LoadDMClient], passing in the +// storage tag retrieved by [DMClient.GetStorageTag]. +// +// Parameters: +// - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved +// using [Cmix.GetID]. +// - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is +// generated by [GenerateChannelIdentity] (Uint8Array). +// - args[2] - A function that initialises and returns a Javascript object +// that matches the [bindings.EventModel] interface. The function must match +// the Build function in [bindings.EventModelBuilder]. +// +// Returns: +// - Javascript representation of the [DMClient] object. +// - Throws a TypeError if creating the manager fails. +func NewDMClient(_ js.Value, args []js.Value) any { + privateIdentity := utils.CopyBytesToGo(args[1]) + + em := &dmReceiverBuilder{args[2].Invoke} + + cm, err := bindings.NewDMClient(args[0].Int(), privateIdentity, em) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return newDMClientJS(cm) +} + +// NewDMClientWithIndexedDb creates a new [DMClient] from a new +// private identity ([channel.PrivateIdentity]) and using indexedDb as a backend +// to manage the event model. +// +// This is for creating a manager for an identity for the first time. For +// generating a new one channel identity, use [GenerateChannelIdentity]. To +// reload this channel manager, use [LoadDMClientWithIndexedDb], passing +// in the storage tag retrieved by [DMClient.GetStorageTag]. +// +// This function initialises an indexedDb database. +// +// Parameters: +// - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved +// using [Cmix.GetID]. +// - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is +// generated by [GenerateChannelIdentity] (Uint8Array). +// - args[2] - Function that takes in the same parameters as +// [indexedDb.MessageReceivedCallback]. On the Javascript side, the UUID is +// returned as an int and the channelID as a Uint8Array. The row in the +// database that was updated can be found using the UUID. The channel ID is +// provided so that the recipient can filter if they want to the processes +// the update now or not. An "update" bool is present which tells you if the +// row is new or if it is an edited old row. +// - args[3] - ID of [ChannelDbCipher] object in tracker (int). Create this +// object with [NewChannelsDatabaseCipher] and get its id with +// [ChannelDbCipher.GetID]. +// +// Returns a promise: +// - Resolves to a Javascript representation of the [DMClient] object. +// - Rejected with an error if loading indexedDb or the manager fails. +// - Throws a TypeError if the cipher ID does not correspond to a cipher. +func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any { + cmixID := args[0].Int() + privateIdentity := utils.CopyBytesToGo(args[1]) + messageReceivedCB := args[2] + cipherID := args[3].Int() + + cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID) + if err != nil { + utils.Throw(utils.TypeError, err) + } + + return newDMClientWithIndexedDb( + cmixID, privateIdentity, messageReceivedCB, cipher) +} + +// NewDMClientWithIndexedDbUnsafe creates a new [DMClient] from a +// new private identity ([channel.PrivateIdentity]) and using indexedDb as a +// backend to manage the event model. However, the data is written in plain text +// and not encrypted. It is recommended that you do not use this in production. +// +// This is for creating a manager for an identity for the first time. For +// generating a new one channel identity, use [GenerateChannelIdentity]. To +// reload this channel manager, use [LoadDMClientWithIndexedDbUnsafe], +// passing in the storage tag retrieved by [DMClient.GetStorageTag]. +// +// This function initialises an indexedDb database. +// +// Parameters: +// - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved +// using [Cmix.GetID]. +// - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is +// generated by [GenerateChannelIdentity] (Uint8Array). +// - args[2] - Function that takes in the same parameters as +// [indexedDb.MessageReceivedCallback]. On the Javascript side, the UUID is +// returned as an int and the channelID as a Uint8Array. The row in the +// database that was updated can be found using the UUID. The channel ID is +// provided so that the recipient can filter if they want to the processes +// the update now or not. An "update" bool is present which tells you if +// the row is new or if it is an edited old row +// +// Returns a promise: +// - Resolves to a Javascript representation of the [DMClient] object. +// - Rejected with an error if loading indexedDb or the manager fails. +func NewDMClientWithIndexedDbUnsafe(_ js.Value, args []js.Value) any { + cmixID := args[0].Int() + privateIdentity := utils.CopyBytesToGo(args[1]) + messageReceivedCB := args[2] + + return newDMClientWithIndexedDb( + cmixID, privateIdentity, messageReceivedCB, nil) +} + +func newDMClientWithIndexedDb(cmixID int, privateIdentity []byte, + cb js.Value, cipher *bindings.ChannelDbCipher) any { + + messageReceivedCB := func(uuid uint64, pubKey ed25519.PublicKey, + update bool) { + cb.Invoke(uuid, utils.CopyBytesToJS(pubKey[:]), update) + } + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + + pi, err := codename.UnmarshalPrivateIdentity(privateIdentity) + if err != nil { + reject(utils.JsTrace(err)) + } + dmPath := base64.RawStdEncoding.EncodeToString(pi.PubKey[:]) + model, err := indexDB.NewWASMEventModel(dmPath, cipher, + messageReceivedCB) + if err != nil { + reject(utils.JsTrace(err)) + } + + cm, err := bindings.NewDMClientWithGoEventModel( + cmixID, privateIdentity, model) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(newDMClientJS(cm)) + } + } + + return utils.CreatePromise(promiseFn) +} + +//////////////////////////////////////////////////////////////////////////////// +// Channel Sending Methods and Reports // +//////////////////////////////////////////////////////////////////////////////// + +// SendGeneric is used to send a raw message over a channel. In general, it +// should be wrapped in a function which defines the wire protocol. If the final +// message, before being sent over the wire, is too long, this will return an +// error. Due to the underlying encoding using compression, it isn't possible to +// define the largest payload that can be sent, but it will always be possible +// to send a payload of 802 bytes at minimum. The meaning of validUntil depends +// on the use case. +// +// Parameters: +// - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - args[1] - The message type of the message. This will be a valid +// [channels.MessageType] (int). +// - args[2] - The contents of the message (Uint8Array). +// - args[3] - The lease of the message. This will be how long the message is +// valid until, in milliseconds. As per the [channels.Manager] +// documentation, this has different meanings depending on the use case. +// These use cases may be generic enough that they will not be enumerated +// here (int). +// - args[4] - JSON of [xxdk.CMIXParams]. If left empty +// [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). +// +// Returns a promise: +// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). +// - Rejected with an error if sending fails. +func (ch *DMClient) Send(_ js.Value, args []js.Value) any { + messageType := args[0].Int() + partnerPubKeyBytes := utils.CopyBytesToGo(args[1]) + partnerToken := args[2].Int() + message := utils.CopyBytesToGo(args[3]) + leaseTimeMS := int64(args[4].Int()) + cmixParamsJSON := utils.CopyBytesToGo(args[5]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := ch.api.Send(messageType, partnerPubKeyBytes, + uint32(partnerToken), message, leaseTimeMS, + cmixParamsJSON) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(sendReport)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// SendMessage is used to send a formatted message over a channel. +// Due to the underlying encoding using compression, it isn't possible to define +// the largest payload that can be sent, but it will always be possible to send +// a payload of 798 bytes at minimum. +// +// The message will auto delete validUntil after the round it is sent in, +// lasting forever if [channels.ValidForever] is used. +// +// Parameters: +// - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - args[1] - The contents of the message (string). +// - args[2] - The lease of the message. This will be how long the message is +// valid until, in milliseconds. As per the [channels.Manager] +// documentation, this has different meanings depending on the use case. +// These use cases may be generic enough that they will not be enumerated +// here (int). +// - args[3] - JSON of [xxdk.CMIXParams]. If left empty +// [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). +// +// Returns a promise: +// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). +// - Rejected with an error if sending fails. +func (ch *DMClient) SendText(_ js.Value, args []js.Value) any { + partnerPubKeyBytes := utils.CopyBytesToGo(args[0]) + partnerToken := args[1].Int() + message := args[2].String() + leaseTimeMS := int64(args[3].Int()) + cmixParamsJSON := utils.CopyBytesToGo(args[4]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := ch.api.SendText(partnerPubKeyBytes, + uint32(partnerToken), message, leaseTimeMS, + cmixParamsJSON) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(sendReport)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// SendReply is used to send a formatted message over a channel. Due to the +// underlying encoding using compression, it isn't possible to define the +// largest payload that can be sent, but it will always be possible to send a +// payload of 766 bytes at minimum. +// +// If the message ID the reply is sent to is nonexistent, the other side will +// post the message as a normal message and not a reply. The message will auto +// delete validUntil after the round it is sent in, lasting forever if +// [channels.ValidForever] is used. +// +// Parameters: +// - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - args[1] - The contents of the message. The message should be at most 510 +// bytes. This is expected to be Unicode, and thus a string data type is +// expected (string). +// - args[2] - JSON of [channel.MessageID] of the message you wish to reply +// to. This may be found in the [bindings.ChannelSendReport] if replying to +// your own. Alternatively, if reacting to another user's message, you may +// retrieve it via the [bindings.ChannelMessageReceptionCallback] registered +// using RegisterReceiveHandler (Uint8Array). +// - args[3] - The lease of the message. This will be how long the message is +// valid until, in milliseconds. As per the [channels.Manager] +// documentation, this has different meanings depending on the use case. +// These use cases may be generic enough that they will not be enumerated +// here (int). +// - args[4] - JSON of [xxdk.CMIXParams]. If left empty +// [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). +// +// Returns a promise: +// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). +// - Rejected with an error if sending fails. +func (ch *DMClient) SendReply(_ js.Value, args []js.Value) any { + partnerPubKeyBytes := utils.CopyBytesToGo(args[0]) + partnerToken := args[1].Int() + replyID := utils.CopyBytesToGo(args[2]) + message := args[3].String() + leaseTimeMS := int64(args[4].Int()) + cmixParamsJSON := utils.CopyBytesToGo(args[5]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := ch.api.SendReply(partnerPubKeyBytes, + uint32(partnerToken), message, replyID, leaseTimeMS, + cmixParamsJSON) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(sendReport)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// SendReaction is used to send a reaction to a message over a channel. +// The reaction must be a single emoji with no other characters, and will +// be rejected otherwise. +// Users will drop the reaction if they do not recognize the reactTo message. +// +// Parameters: +// - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - args[1] - The user's reaction. This should be a single emoji with no +// other characters. As such, a Unicode string is expected (string). +// - args[2] - JSON of [channel.MessageID] of the message you wish to reply +// to. This may be found in the [bindings.ChannelSendReport] if replying to +// your own. Alternatively, if reacting to another user's message, you may +// retrieve it via the ChannelMessageReceptionCallback registered using +// RegisterReceiveHandler (Uint8Array). +// - args[3] - JSON of [xxdk.CMIXParams]. If left empty +// [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). +// +// Returns a promise: +// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). +// - Rejected with an error if sending fails. +func (ch *DMClient) SendReaction(_ js.Value, args []js.Value) any { + partnerPubKeyBytes := utils.CopyBytesToGo(args[0]) + partnerToken := args[1].Int() + replyID := utils.CopyBytesToGo(args[2]) + message := args[3].String() + cmixParamsJSON := utils.CopyBytesToGo(args[4]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := ch.api.SendReaction(partnerPubKeyBytes, + uint32(partnerToken), message, replyID, + cmixParamsJSON) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(sendReport)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// GetIdentity returns the marshaled public identity ([codename.Identity]) that +// the client is using. +// +// Returns: +// - JSON of the [channel.Identity] (Uint8Array). +// - Throws TypeError if marshalling the identity fails. +func (ch *DMClient) GetIdentity(js.Value, []js.Value) any { + i := ch.api.GetIdentity() + + return utils.CopyBytesToJS(i) +} + +// ExportPrivateIdentity encrypts and exports the private identity to a portable +// string. +// +// Parameters: +// - args[0] - Password to encrypt the identity with (string). +// +// Returns: +// - JSON of the encrypted private identity (Uint8Array). +// - Throws TypeError if exporting the identity fails. +func (ch *DMClient) ExportPrivateIdentity(_ js.Value, args []js.Value) any { + i, err := ch.api.ExportPrivateIdentity(args[0].String()) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return utils.CopyBytesToJS(i) +} + +// SetNickname sets the nickname for a given channel. The nickname must be valid +// according to [IsNicknameValid]. +// +// Parameters: +// - args[0] - The nickname to set (string). +// - args[1] - Marshalled bytes if the channel's [id.ID] (Uint8Array). +// +// Returns: +// - Throws TypeError if unmarshalling the ID fails or the nickname is +// invalid. +func (ch *DMClient) SetNickname(_ js.Value, args []js.Value) any { + ch.api.SetNickname(args[0].String()) + return nil +} + +// GetNickname returns the nickname set for a given channel. Returns an error if +// there is no nickname set. +// +// Parameters: +// - args[0] - Marshalled bytes if the channel's [id.ID] (Uint8Array). +// +// Returns: +// - The nickname (string). +// - Throws TypeError if the channel has no nickname set. +func (ch *DMClient) GetNickname(_ js.Value, args []js.Value) any { + nickname, err := ch.api.GetNickname(utils.CopyBytesToGo(args[0])) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return nickname +} + +//////////////////////////////////////////////////////////////////////////////// +// Channel Receiving Logic and Callback Registration // +//////////////////////////////////////////////////////////////////////////////// + +// channelMessageReceptionCallback wraps Javascript callbacks to adhere to the +// [bindings.ChannelMessageReceptionCallback] interface. +type dmReceptionCallback struct { + callback func(args ...any) js.Value +} + +// Callback returns the context for a channel message. +// +// Parameters: +// - receivedChannelMessageReport - Returns the JSON of +// [bindings.ReceivedChannelMessageReport] (Uint8Array). +// - err - Returns an error on failure (Error). +// +// Returns: +// - It must return a unique UUID for the message that it can be referenced by +// later (int). +func (cmrCB *dmReceptionCallback) Callback( + receivedChannelMessageReport []byte, err error) int { + uuid := cmrCB.callback( + utils.CopyBytesToJS(receivedChannelMessageReport), + utils.JsTrace(err)) + + return uuid.Int() +} + +//////////////////////////////////////////////////////////////////////////////// +// Event Model Logic // +//////////////////////////////////////////////////////////////////////////////// + +// dmReceiver wraps Javascript callbacks to adhere to the [bindings.EventModel] +// interface. +type dmReceiver struct { + receive func(args ...any) js.Value + receiveText func(args ...any) js.Value + receiveReply func(args ...any) js.Value + receiveReaction func(args ...any) js.Value + updateSentStatus func(args ...any) js.Value +} + +// ReceiveMessage is called whenever a message is received on a given channel. +// It may be called multiple times on the same message. It is incumbent on the +// user of the API to filter such called by message ID. +// +// Parameters: +// - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - messageID - The bytes of the [channel.MessageID] of the received message +// (Uint8Array). +// - nickname - The nickname of the sender of the message (string). +// - text - The content of the message (string). +// - pubKey - The sender's Ed25519 public key (Uint8Array). +// - dmToken - The dmToken (int32). +// - codeset - The codeset version (int). +// - timestamp - Time the message was received; represented as nanoseconds +// since unix epoch (int). +// - lease - The number of nanoseconds that the message is valid for (int). +// - roundId - The ID of the round that the message was received on (int). +// - msgType - The type of message ([channels.MessageType]) to send (int). +// - status - The [channels.SentStatus] of the message (int). +// +// Statuses will be enumerated as such: +// +// Sent = 0 +// Delivered = 1 +// Failed = 2 +// +// Returns: +// - A non-negative unique UUID for the message that it can be referenced by +// later with [dmReceiver.UpdateSentStatus]. +func (em *dmReceiver) Receive(messageID []byte, nickname string, + text []byte, pubKey []byte, dmToken int32, codeset int, timestamp, + roundId, mType, status int64) int64 { + uuid := em.receive(messageID, nickname, text, pubKey, dmToken, + codeset, timestamp, roundId, mType, status) + + return int64(uuid.Int()) +} + +// ReceiveText is called whenever a message is received that is a reply on a +// given channel. It may be called multiple times on the same message. It is +// incumbent on the user of the API to filter such called by message ID. +// +// Messages may arrive our of order, so a reply in theory can arrive before the +// initial message. As a result, it may be important to buffer replies. +// +// Parameters: +// - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - messageID - The bytes of the [channel.MessageID] of the received message +// (Uint8Array). +// - reactionTo - The [channel.MessageID] for the message that received a +// reply (Uint8Array). +// - senderUsername - The username of the sender of the message (string). +// - text - The content of the message (string). +// - pubKey - The sender's Ed25519 public key (Uint8Array). +// - dmToken - The dmToken (int32). +// - codeset - The codeset version (int). +// - timestamp - Time the message was received; represented as nanoseconds +// since unix epoch (int). +// - lease - The number of nanoseconds that the message is valid for (int). +// - roundId - The ID of the round that the message was received on (int). +// - msgType - The type of message ([channels.MessageType]) to send (int). +// - status - The [channels.SentStatus] of the message (int). +// +// Statuses will be enumerated as such: +// +// Sent = 0 +// Delivered = 1 +// Failed = 2 +// +// Returns: +// - A non-negative unique UUID for the message that it can be referenced by +// later with [dmReceiver.UpdateSentStatus]. +func (em *dmReceiver) ReceiveText(messageID []byte, nickname, text string, + pubKey []byte, dmToken int32, codeset int, timestamp, + roundId, status int64) int64 { + + uuid := em.receiveText(messageID, nickname, text, pubKey, dmToken, + codeset, timestamp, roundId, status) + + return int64(uuid.Int()) +} + +// ReceiveReply is called whenever a message is received that is a reply on a +// given channel. It may be called multiple times on the same message. It is +// incumbent on the user of the API to filter such called by message ID. +// +// Messages may arrive our of order, so a reply in theory can arrive before the +// initial message. As a result, it may be important to buffer replies. +// +// Parameters: +// - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - messageID - The bytes of the [channel.MessageID] of the received message +// (Uint8Array). +// - reactionTo - The [channel.MessageID] for the message that received a +// reply (Uint8Array). +// - senderUsername - The username of the sender of the message (string). +// - text - The content of the message (string). +// - pubKey - The sender's Ed25519 public key (Uint8Array). +// - dmToken - The dmToken (int32). +// - codeset - The codeset version (int). +// - timestamp - Time the message was received; represented as nanoseconds +// since unix epoch (int). +// - lease - The number of nanoseconds that the message is valid for (int). +// - roundId - The ID of the round that the message was received on (int). +// - msgType - The type of message ([channels.MessageType]) to send (int). +// - status - The [channels.SentStatus] of the message (int). +// +// Statuses will be enumerated as such: +// +// Sent = 0 +// Delivered = 1 +// Failed = 2 +// +// Returns: +// - A non-negative unique UUID for the message that it can be referenced by +// later with [dmReceiver.UpdateSentStatus]. +func (em *dmReceiver) ReceiveReply(messageID, replyTo []byte, nickname, + text string, pubKey []byte, dmToken int32, codeset int, + timestamp, roundId, status int64) int64 { + uuid := em.receiveReply(messageID, replyTo, nickname, text, pubKey, + dmToken, codeset, timestamp, roundId, status) + + return int64(uuid.Int()) +} + +// ReceiveReaction is called whenever a reaction to a message is received on a +// given channel. It may be called multiple times on the same reaction. It is +// incumbent on the user of the API to filter such called by message ID. +// +// Messages may arrive our of order, so a reply in theory can arrive before the +// initial message. As a result, it may be important to buffer reactions. +// +// Parameters: +// - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - messageID - The bytes of the [channel.MessageID] of the received message +// (Uint8Array). +// - reactionTo - The [channel.MessageID] for the message that received a +// reply (Uint8Array). +// - senderUsername - The username of the sender of the message (string). +// - reaction - The contents of the reaction message (string). +// - pubKey - The sender's Ed25519 public key (Uint8Array). +// - dmToken - The dmToken (int32). +// - codeset - The codeset version (int). +// - timestamp - Time the message was received; represented as nanoseconds +// since unix epoch (int). +// - lease - The number of nanoseconds that the message is valid for (int). +// - roundId - The ID of the round that the message was received on (int). +// - msgType - The type of message ([channels.MessageType]) to send (int). +// - status - The [channels.SentStatus] of the message (int). +// +// Statuses will be enumerated as such: +// +// Sent = 0 +// Delivered = 1 +// Failed = 2 +// +// Returns: +// - A non-negative unique UUID for the message that it can be referenced by +// later with [dmReceiver.UpdateSentStatus]. +func (em *dmReceiver) ReceiveReaction(messageID, reactionTo []byte, + nickname, reaction string, pubKey []byte, dmToken int32, + codeset int, timestamp, roundId, + status int64) int64 { + uuid := em.receiveReaction(messageID, reactionTo, nickname, reaction, + pubKey, dmToken, codeset, timestamp, roundId, status) + + return int64(uuid.Int()) +} + +// UpdateSentStatus is called whenever the sent status of a message has +// changed. +// +// Parameters: +// - uuid - The unique identifier for the message (int). +// - messageID - The bytes of the [channel.MessageID] of the received message +// (Uint8Array). +// - timestamp - Time the message was received; represented as nanoseconds +// since unix epoch (int). +// - roundId - The ID of the round that the message was received on (int). +// - status - The [channels.SentStatus] of the message (int). +// +// Statuses will be enumerated as such: +// +// Sent = 0 +// Delivered = 1 +// Failed = 2 +func (em *dmReceiver) UpdateSentStatus(uuid int64, messageID []byte, + timestamp, roundID, status int64) { + em.updateSentStatus(uuid, utils.CopyBytesToJS(messageID), + timestamp, roundID, status) +} + +//////////////////////////////////////////////////////////////////////////////// +// DM DB Cipher // +//////////////////////////////////////////////////////////////////////////////// + +// DMDbCipher wraps the [bindings.DMDbCipher] object so its methods +// can be wrapped to be Javascript compatible. +type DMDbCipher struct { + api *bindings.DMDbCipher +} + +// newDMDbCipherJS creates a new Javascript compatible object +// (map[string]any) that matches the [DMDbCipher] structure. +func newDMDbCipherJS(api *bindings.DMDbCipher) map[string]any { + c := DMDbCipher{api} + channelDbCipherMap := map[string]any{ + "GetID": js.FuncOf(c.GetID), + "Encrypt": js.FuncOf(c.Encrypt), + "Decrypt": js.FuncOf(c.Decrypt), + "MarshalJSON": js.FuncOf(c.MarshalJSON), + "UnmarshalJSON": js.FuncOf(c.UnmarshalJSON), + } + + return channelDbCipherMap +} + +// NewDMsDatabaseCipher constructs a [DMDbCipher] object. +// +// Parameters: +// - args[0] - The tracked [Cmix] object ID (int). +// - args[1] - The password for storage. This should be the same password +// passed into [NewCmix] (Uint8Array). +// - args[2] - The maximum size of a payload to be encrypted. A payload passed +// into [DMDbCipher.Encrypt] that is larger than this value will result +// in an error (int). +// +// Returns: +// - JavaScript representation of the [DMDbCipher] object. +// - Throws a TypeError if creating the cipher fails. +func NewDMsDatabaseCipher(_ js.Value, args []js.Value) any { + cmixId := args[0].Int() + password := utils.CopyBytesToGo(args[1]) + plaintTextBlockSize := args[2].Int() + + cipher, err := bindings.NewDMsDatabaseCipher( + cmixId, password, plaintTextBlockSize) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return newDMDbCipherJS(cipher) +} + +// GetID returns the ID for this [bindings.DMDbCipher] in the +// channelDbCipherTracker. +// +// Returns: +// - Tracker ID (int). +func (c *DMDbCipher) GetID(js.Value, []js.Value) any { + return c.api.GetID() +} + +// Encrypt will encrypt the raw data. It will return a ciphertext. Padding is +// done on the plaintext so all encrypted data looks uniform at rest. +// +// Parameters: +// - args[0] - The data to be encrypted (Uint8Array). This must be smaller +// than the block size passed into [NewDMsDatabaseCipher]. If it is +// larger, this will return an error. +// +// Returns: +// - The ciphertext of the plaintext passed in (Uint8Array). +// - Throws a TypeError if it fails to encrypt the plaintext. +func (c *DMDbCipher) Encrypt(_ js.Value, args []js.Value) any { + ciphertext, err := c.api.Encrypt(utils.CopyBytesToGo(args[0])) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return utils.CopyBytesToJS(ciphertext) +} + +// Decrypt will decrypt the passed in encrypted value. The plaintext will be +// returned by this function. Any padding will be discarded within this +// function. +// +// Parameters: +// - args[0] - the encrypted data returned by [DMDbCipher.Encrypt] +// (Uint8Array). +// +// Returns: +// - The plaintext of the ciphertext passed in (Uint8Array). +// - Throws a TypeError if it fails to encrypt the plaintext. +func (c *DMDbCipher) Decrypt(_ js.Value, args []js.Value) any { + plaintext, err := c.api.Decrypt(utils.CopyBytesToGo(args[0])) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return utils.CopyBytesToJS(plaintext) +} + +// MarshalJSON marshals the cipher into valid JSON. +// +// Returns: +// - JSON of the cipher (Uint8Array). +// - Throws a TypeError if marshalling fails. +func (c *DMDbCipher) MarshalJSON(js.Value, []js.Value) any { + data, err := c.api.MarshalJSON() + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return utils.CopyBytesToJS(data) +} + +// UnmarshalJSON unmarshalls JSON into the cipher. This function adheres to the +// json.Unmarshaler interface. +// +// Note that this function does not transfer the internal RNG. Use +// [channel.NewCipherFromJSON] to properly reconstruct a cipher from JSON. +// +// Parameters: +// - args[0] - JSON data to unmarshal (Uint8Array). +// +// Returns: +// - JSON of the cipher (Uint8Array). +// - Throws a TypeError if marshalling fails. +func (c *DMDbCipher) UnmarshalJSON(_ js.Value, args []js.Value) any { + err := c.api.UnmarshalJSON(utils.CopyBytesToGo(args[0])) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + return nil +} diff --git a/wasm/docs.go b/wasm/docs.go index 33b0d56ed2951fa2e98c6217f09e77ebe4024fb4..b2d6f4894014b15873b2649118e2af09a8cd7091 100644 --- a/wasm/docs.go +++ b/wasm/docs.go @@ -10,6 +10,7 @@ package wasm import ( + "crypto/ed25519" "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/auth" "gitlab.com/elixxir/client/v4/catalog" @@ -23,11 +24,11 @@ import ( "gitlab.com/elixxir/client/v4/restlike" "gitlab.com/elixxir/client/v4/single" "gitlab.com/elixxir/crypto/broadcast" - "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/crypto/fileTransfer" "gitlab.com/elixxir/crypto/group" + cryptoMessage "gitlab.com/elixxir/crypto/message" "gitlab.com/elixxir/primitives/fact" "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/id" @@ -50,7 +51,7 @@ var ( _ = connect.Callback(nil) _ = partner.Manager(nil) _ = ndf.NetworkDefinition{} - _ = channel.MessageID{} + _ = cryptoMessage.ID{} _ = channels.SentStatus(0) _ = ftE2e.Params{} _ = fileTransfer.TransferID{} @@ -67,4 +68,5 @@ var ( _ = broadcast.PrivacyLevel(0) _ = broadcast.Channel{} _ = netTime.Now + _ = ed25519.PublicKey{} ) diff --git a/wasm_test.go b/wasm_test.go index 97aab01dceb11834f53a541c57a4be2de0ccc069..68cc545ba79c45b67cf1eefdb596e28c69b5eba3 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -48,6 +48,12 @@ func TestPublicFunctions(t *testing.T) { // client versions "GetGitVersion": {}, "GetDependencies": {}, + + // DM Functions these are used but not exported by + // WASM bindins, so are not exposed. + "NewDMReceiver": {}, + "NewDMClientWithGoEventModel": {}, + "GetDMDbCipherTrackerFromID": {}, } wasmFuncs := getPublicFunctions("wasm", t) bindingsFuncs := getPublicFunctions(