├── .gitignore ├── assets ├── pin.png ├── home.png ├── logo.png ├── news.png ├── rumors.png ├── confirm-rumor.png ├── how-it-works.png ├── report-event.png ├── p2p-news.drawio.png └── pin-to-local-node.png ├── makefile ├── .github └── FUNDING.yml ├── LICENSE ├── go.mod ├── README.md ├── go.sum └── cyber-witness.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | web/app.wasm 3 | cyber-witness -------------------------------------------------------------------------------- /assets/pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-witness/HEAD/assets/pin.png -------------------------------------------------------------------------------- /assets/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-witness/HEAD/assets/home.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-witness/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/news.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-witness/HEAD/assets/news.png -------------------------------------------------------------------------------- /assets/rumors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-witness/HEAD/assets/rumors.png -------------------------------------------------------------------------------- /assets/confirm-rumor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-witness/HEAD/assets/confirm-rumor.png -------------------------------------------------------------------------------- /assets/how-it-works.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-witness/HEAD/assets/how-it-works.png -------------------------------------------------------------------------------- /assets/report-event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-witness/HEAD/assets/report-event.png -------------------------------------------------------------------------------- /assets/p2p-news.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-witness/HEAD/assets/p2p-news.drawio.png -------------------------------------------------------------------------------- /assets/pin-to-local-node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stateless-minds/cyber-witness/HEAD/assets/pin-to-local-node.png -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | build: 2 | GOARCH=wasm GOOS=js go build -o web/app.wasm 3 | go build 4 | 5 | run: build 6 | ./cyber-witness -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: stateless-minds-collective 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Stateless Minds 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stateless-minds/cyber-witness 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.3 6 | 7 | require ( 8 | github.com/NYTimes/gziphandler v1.1.1 9 | github.com/foolin/mixer v0.0.8 10 | github.com/maxence-charriere/go-app/v10 v10.0.8 11 | github.com/mitchellh/mapstructure v1.5.0 12 | github.com/stateless-minds/go-ipfs-api v0.7.5 13 | ) 14 | 15 | require ( 16 | github.com/benbjohnson/clock v1.3.5 // indirect 17 | github.com/blang/semver/v4 v4.0.0 // indirect 18 | github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf // indirect 19 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/ipfs/go-cid v0.4.1 // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 23 | github.com/libp2p/go-buffer-pool v0.1.0 // indirect 24 | github.com/libp2p/go-flow-metrics v0.2.0 // indirect 25 | github.com/libp2p/go-libp2p v0.37.0 // indirect 26 | github.com/minio/sha256-simd v1.0.1 // indirect 27 | github.com/mitchellh/go-homedir v1.1.0 // indirect 28 | github.com/mr-tron/base58 v1.2.0 // indirect 29 | github.com/multiformats/go-base32 v0.1.0 // indirect 30 | github.com/multiformats/go-base36 v0.2.0 // indirect 31 | github.com/multiformats/go-multiaddr v0.13.0 // indirect 32 | github.com/multiformats/go-multibase v0.2.0 // indirect 33 | github.com/multiformats/go-multicodec v0.9.0 // indirect 34 | github.com/multiformats/go-multihash v0.2.3 // indirect 35 | github.com/multiformats/go-multistream v0.5.0 // indirect 36 | github.com/multiformats/go-varint v0.0.7 // indirect 37 | github.com/spaolacci/murmur3 v1.1.0 // indirect 38 | github.com/stateless-minds/boxo v0.24.3 // indirect 39 | golang.org/x/crypto v0.28.0 // indirect 40 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect 41 | golang.org/x/sys v0.26.0 // indirect 42 | google.golang.org/protobuf v1.35.1 // indirect 43 | lukechampine.com/blake3 v1.3.0 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Cyber Witness 5 | 6 | 7 | 8 | 9 | 10 | ![Logo](./assets/p2p-news.drawio.png) 11 | 12 | Over the past few years we are seeing the end of free speech and the battle for communication control. Censorship, fact checking and surveillance are quickly becoming the norm. The end of free speech leads to the end of democracy as we know it. 13 | 14 | Cyber Witness is a P2P community of independent reporters and witnesses - an alternative to mass media. Reporters publish events they have personally seen with no interpretation. Until confirmed they show up as rumors. Witnesses confirm rumors they have witnessed and add their own details. Event details aggregate and become more accurate with the input of each new witness. Once a rumor has been confirmed by at least 2 witnesses it becomes news. The more witnesses the greater accuracy of news. 15 | 16 | 17 | 18 | 19 | ## Screenshots 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Screenshot of how-it-works 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Screenshot of home 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Screenshot of report event 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | Screenshot of confirm rumor 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | Screenshot of rumors 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | Screenshot of news 100 | 101 | 102 | 103 | 104 | 105 | ## Features 106 | 107 | 108 | 109 | 110 | 111 | * ### Report events 112 | 113 | Provide details about the event. 114 | 115 | - ### Confirm rumors 116 | 117 | Browse reported events, confirm what you have witnessed and provide more details. 118 | 119 | - ### Event details aggregation 120 | 121 | The more witnesses the better the accuracy the higher the chance a rumor is real news. 122 | 123 | - ### Read the real news 124 | 125 | Your personal news feed at your fingertips. All witnessed. No ads, paywalls, censorship or fact checkers. 126 | 127 | - ### Anonymity by default 128 | 129 | Anonymity guarantees everyone is protected. 130 | 131 | - ### Flat interactions 132 | 133 | No centralized control, no fact checkers and no ads. 134 | 135 | ## Community 136 | 137 | 138 | 139 | https://www.reddit.com/r/CyberWitness/ 140 | 141 | 142 | 143 | 144 | ## How to Play 145 | 146 | 147 | 148 | 149 | 150 | The simulator runs on the public IPFS network. In order to play it follow the steps below: 151 | 152 | 153 | 154 | 155 | 156 | 1. Install the official IPFS Desktop http://docs.ipfs.io/install/ipfs-desktop/ 157 | 158 | 159 | 160 | 2. Install IPFS Companion http://docs.ipfs.io/install/ipfs-companion/ 161 | 162 | 163 | 164 | 3. Clone https://github.com/stateless-minds/kubo to your local machine, build it with `make build` and run it with the following command: `~/cmd/ipfs/ipfs daemon --enable-pubsub-experiment` 165 | 166 | 167 | 168 | 4. Follow the instructions here to open your config file: https://github.com/ipfs/kubo/blob/master/docs/config.md. Usually it's `~/.ipfs/config` on Linux. Add the following snippet to the `HTTPHeaders`: 169 | 170 | 171 | 172 | ```{ 173 | 174 | 175 | 176 | "API": { 177 | 178 | 179 | 180 | "HTTPHeaders": { 181 | 182 | 183 | 184 | "Access-Control-Allow-Origin": ["webui://-", "http://localhost:3000", "http://k51qzi5uqu5dlk6jzkipcho28z9v1v1fogi2sw5il1t1tilklfn573kbtuw6o3.ipns.localhost:8080", "http://127.0.0.1:5001", "https://webui.ipfs.io"], 185 | 186 | 187 | 188 | "Access-Control-Allow-Credentials": ["true"], 189 | 190 | 191 | 192 | "Access-Control-Allow-Methods": ["PUT", "POST"] 193 | 194 | 195 | 196 | } 197 | 198 | 199 | 200 | }, 201 | 202 | 203 | 204 | ``` 205 | 206 | 207 | 208 | 5. Navigate to Cyber Witness and let's simulate the future together! 209 | 210 | 211 | 212 | 6. If you like the simulator consider pinning it to your local node so that you become a permanent host of it while you have IPFS daemon running 213 | 214 | 215 | 216 | ![SetPinning](./assets/pin.png) 217 | 218 | 219 | 220 | ![PinToLocalNode](./assets/pin-to-local-node.png) 221 | 222 | 223 | 224 | 225 | 226 | Please note the simulator has been developed on a WQHD resolution(2560x1440) and is currently not responsive or optimized for mobile devices. For best gaming experience if you play in FHD(1920x1080) please set your browser zoom settings to 150%. 227 | 228 | ## Acknowledgments 229 | 230 | 231 | 232 | 233 | 234 | 1. go-app 235 | 236 | 237 | 238 | 2. IPFS 239 | 240 | 241 | 242 | 3. Berty 243 | 244 | 245 | 246 | 4. All the rest of the authors who worked on the dependencies used! Thanks a lot! 247 | 248 | 249 | 250 | 251 | 252 | ## Contributing 253 | 254 | 255 | 256 | 257 | 258 | Open an issue 259 | 260 | 261 | 262 | 263 | 264 | ## License 265 | 266 | 267 | 268 | 269 | 270 | Stateless Minds (c) 2022 and contributors 271 | 272 | 273 | 274 | 275 | 276 | MIT License 277 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= 2 | github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 3 | github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= 4 | github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 5 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 6 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 7 | github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764= 8 | github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= 9 | github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf h1:dwGgBWn84wUS1pVikGiruW+x5XM4amhjaZO20vCjay4= 10 | github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= 15 | github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 16 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= 17 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 18 | github.com/foolin/mixer v0.0.8 h1:uGd94/kdPkk+GxMkUdfbBfXsYLNY7OEmKer1xNsKRVI= 19 | github.com/foolin/mixer v0.0.8/go.mod h1:Hgm3f6NIGGVT+BM9L26yauJuFmZiTSxTqTXurFz0RDE= 20 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 21 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 22 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 23 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 24 | github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 25 | github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 26 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 27 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 28 | github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= 29 | github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 30 | github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= 31 | github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= 32 | github.com/libp2p/go-libp2p v0.37.0 h1:8K3mcZgwTldydMCNOiNi/ZJrOB9BY+GlI3UxYzxBi9A= 33 | github.com/libp2p/go-libp2p v0.37.0/go.mod h1:GOKmSN99scDuYGTwaTbQPR8Nt6dxrK3ue7OjW2NGDg4= 34 | github.com/maxence-charriere/go-app/v10 v10.0.8 h1:ZbHTaIN1nMTzMvWmx5/wAMioDxnftNmkYfcX3O4lleE= 35 | github.com/maxence-charriere/go-app/v10 v10.0.8/go.mod h1:VyjGLeTiK6hfAQ/Q5ZVcLbuJ9vOZhKcewSC/ZwI+6WM= 36 | github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 37 | github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 38 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 39 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 40 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 41 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 42 | github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 43 | github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 44 | github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= 45 | github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 46 | github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 47 | github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 48 | github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ= 49 | github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII= 50 | github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 51 | github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 52 | github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= 53 | github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= 54 | github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 55 | github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 56 | github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= 57 | github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= 58 | github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 59 | github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 60 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 61 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 62 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 63 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 64 | github.com/stateless-minds/boxo v0.24.3 h1:7UU9NDYv8z7EAeH64Pw7TDEqslp2J4io2DJWRZcFeVU= 65 | github.com/stateless-minds/boxo v0.24.3/go.mod h1:vmyA+qT8lTQByYf63nGOn5d4mwjW9QkNYL9oIc3kGfI= 66 | github.com/stateless-minds/go-ipfs-api v0.7.5 h1:nAqpcnUoDp/ZcqxTs+m5gtCeCDKl8eb/6V7pYCl6Yx4= 67 | github.com/stateless-minds/go-ipfs-api v0.7.5/go.mod h1:hwH/Up3TNtuwnAXvcSUilOKCfgJ85+c9faWa0KkKwAA= 68 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 69 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 70 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 71 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 72 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 73 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 74 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= 75 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= 76 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 78 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 79 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 80 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 81 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 82 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 83 | lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= 84 | lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= 85 | -------------------------------------------------------------------------------- /cyber-witness.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "sort" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/NYTimes/gziphandler" 12 | "github.com/foolin/mixer" 13 | "github.com/maxence-charriere/go-app/v10/pkg/app" 14 | "github.com/mitchellh/mapstructure" 15 | shell "github.com/stateless-minds/go-ipfs-api" 16 | ) 17 | 18 | const dbNameEvent = "event" 19 | 20 | const ( 21 | topicCreateEvent = "create-event" 22 | topicUpdateEvent = "update-event" 23 | eventType = "event" 24 | ) 25 | 26 | const ( 27 | NotificationSuccess NotificationStatus = "positive" 28 | NotificationInfo NotificationStatus = "info" 29 | NotificationWarning NotificationStatus = "warning" 30 | NotificationDanger NotificationStatus = "negative" 31 | SuccessHeader = "Success" 32 | ErrorHeader = "Error" 33 | ) 34 | 35 | // pubsub is a component that does a simple pubsub on ipfs. A component is a 36 | // customizable, independent, and reusable UI element. It is created by 37 | // embedding app.Compo into a struct. 38 | type witness struct { 39 | app.Compo 40 | sh *shell.Shell 41 | sub *shell.PubSubSubscription 42 | citizenID string 43 | events []Event 44 | eventTitle string 45 | eventDetails string 46 | eventLocation string 47 | notifications map[string]notification 48 | notificationID int 49 | noNews bool 50 | isWitness bool 51 | } 52 | 53 | type NotificationStatus string 54 | 55 | type notification struct { 56 | id int 57 | status string 58 | header string 59 | message string 60 | } 61 | 62 | type Event struct { 63 | ID string `mapstructure:"_id" json:"_id" validate:"uuid_rfc4122"` 64 | Type string `mapstructure:"type" json:"type" validate:"uuid_rfc4122"` 65 | ConfirmedBy int `mapstructure:"confirmedBy" json:"confirmedBy" validate:"uuid_rfc4122"` 66 | Title string `mapstructure:"title" json:"title" validate:"uuid_rfc4122"` 67 | Details []string `mapstructure:"details" json:"details" validate:"uuid_rfc4122"` 68 | Location string `mapstructure:"location" json:"location" validate:"uuid_rfc4122"` 69 | Reporter string `mapstructure:"reporter" json:"reporter" validate:"uuid_rfc4122"` 70 | Witnesses []string `mapstructure:"witnesses" json:"witnesses" validate:"uuid_rfc4122"` 71 | } 72 | 73 | func (w *witness) OnMount(ctx app.Context) { 74 | sh := shell.NewShell("localhost:5001") 75 | w.sh = sh 76 | myPeer, err := w.sh.ID() 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | 81 | citizenID := myPeer.ID[len(myPeer.ID)-8:] 82 | // replace password with your own 83 | password := "mysecretpassword" 84 | 85 | w.citizenID = mixer.EncodeString(password, citizenID) 86 | w.citizenID = "10" 87 | 88 | w.subscribeToCreateEventTopic(ctx) 89 | w.subscribeToUpdateEventTopic(ctx) 90 | w.notifications = make(map[string]notification) 91 | 92 | // set defaults 93 | w.noNews = true 94 | 95 | ctx.Async(func() { 96 | // err := w.sh.OrbitDocsDelete(dbNameEvent, "all") 97 | // if err != nil { 98 | // log.Fatal(err) 99 | // } 100 | 101 | v, err := w.sh.OrbitDocsQuery(dbNameEvent, "type", "event") 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | 106 | var vv []interface{} 107 | err = json.Unmarshal(v, &vv) 108 | if err != nil { 109 | log.Fatal(err) 110 | } 111 | 112 | for _, ii := range vv { 113 | e := Event{} 114 | err = mapstructure.Decode(ii, &e) 115 | if err != nil { 116 | log.Fatal(err) 117 | } 118 | ctx.Dispatch(func(ctx app.Context) { 119 | if e.ConfirmedBy > 1 { 120 | w.noNews = false 121 | } 122 | for _, v := range e.Witnesses { 123 | if w.citizenID == v { 124 | w.isWitness = true 125 | } 126 | } 127 | w.events = append(w.events, e) 128 | sort.SliceStable(w.events, func(i, j int) bool { 129 | return w.events[i].ID < w.events[j].ID 130 | }) 131 | }) 132 | } 133 | }) 134 | } 135 | 136 | func (w *witness) subscribeToCreateEventTopic(ctx app.Context) { 137 | ctx.Async(func() { 138 | topic := topicCreateEvent 139 | subscription, err := w.sh.PubSubSubscribe(topic) 140 | if err != nil { 141 | log.Fatal(err) 142 | } 143 | w.sub = subscription 144 | w.subscriptionCreateEvent(ctx) 145 | }) 146 | } 147 | 148 | func (w *witness) subscribeToUpdateEventTopic(ctx app.Context) { 149 | ctx.Async(func() { 150 | topic := topicUpdateEvent 151 | subscription, err := w.sh.PubSubSubscribe(topic) 152 | if err != nil { 153 | log.Fatal(err) 154 | } 155 | w.sub = subscription 156 | w.subscriptionUpdateEvent(ctx) 157 | }) 158 | } 159 | 160 | func (w *witness) subscriptionCreateEvent(ctx app.Context) { 161 | ctx.Async(func() { 162 | defer w.sub.Cancel() 163 | // wait on pubsub 164 | res, err := w.sub.Next() 165 | if err != nil { 166 | log.Fatal(err) 167 | } 168 | // Decode the string data. 169 | str := string(res.Data) 170 | log.Println("Subscriber of topic create-event received message: " + str) 171 | ctx.Async(func() { 172 | w.subscribeToCreateEventTopic(ctx) 173 | }) 174 | 175 | e := Event{} 176 | err = json.Unmarshal([]byte(str), &e) 177 | if err != nil { 178 | log.Fatal(err) 179 | } 180 | 181 | ctx.Dispatch(func(ctx app.Context) { 182 | w.events = append(w.events, e) 183 | }) 184 | }) 185 | } 186 | 187 | func (w *witness) subscriptionUpdateEvent(ctx app.Context) { 188 | ctx.Async(func() { 189 | defer w.sub.Cancel() 190 | // wait on pubsub 191 | res, err := w.sub.Next() 192 | if err != nil { 193 | log.Fatal(err) 194 | } 195 | // Decode the string data. 196 | str := string(res.Data) 197 | log.Println("Subscriber of topic update-event received message: " + str) 198 | ctx.Async(func() { 199 | w.subscribeToUpdateEventTopic(ctx) 200 | }) 201 | 202 | e := Event{} 203 | err = json.Unmarshal([]byte(str), &e) 204 | if err != nil { 205 | log.Fatal(err) 206 | } 207 | 208 | ctx.Dispatch(func(ctx app.Context) { 209 | if e.ConfirmedBy > 1 { 210 | w.noNews = false 211 | } 212 | for _, v := range e.Witnesses { 213 | if w.citizenID == v { 214 | w.isWitness = true 215 | } 216 | } 217 | 218 | id, err := strconv.Atoi(e.ID) 219 | if err != nil { 220 | log.Fatal(err) 221 | } 222 | w.events[id-1] = e 223 | }) 224 | }) 225 | } 226 | 227 | // The Render method is where the component appearance is defined. Here, a 228 | // "pubsub World!" is displayed as a heading. 229 | func (w *witness) Render() app.UI { 230 | return app.Div().Class("l-application").Role("presentation").Body( 231 | app.Link().Rel("stylesheet").Href("https://assets.ubuntu.com/v1/vanilla-framework-version-3.8.0.min.css"), 232 | app.Link().Rel("stylesheet").Href("https://use.fontawesome.com/releases/v6.2.0/css/all.css"), 233 | app.Link().Rel("stylesheet").Href("/app.css"), 234 | app.If(len(w.notifications) > 0, func() app.UI { 235 | return app.Range(w.notifications).Map(func(s string) app.UI { 236 | return app.Div().Class("p-notification--"+w.notifications[s].status).Body( 237 | app.Div().Class("p-notification__content").Body( 238 | app.H5().Class("p-notification__title").Text(w.notifications[s].header), 239 | app.P().Class("p-notification__message").Text(w.notifications[s].message), 240 | ), 241 | ).Style("position", "fixed").Style("width", "100%").Style("z-index", "999") 242 | }) 243 | }), 244 | app.Section().Class("p-strip--suru").Body( 245 | app.Div().Class("row u-vertically-center").Body( 246 | app.Div().Class("col-12").Body( 247 | app.H1().Text("Cyber Witness - the news as they should be"), 248 | app.P().Text("P2P community of independent reporters and witnesses - an alternative to mass media. Reporters publish events they have personally seen with no interpretation. Until confirmed they show up as rumors. Witnesses confirm rumors they have witnessed and add their own details. Event details aggregate and become more accurate with the input of each new witness. Once a rumor has been confirmed by at least 2 witnesses it becomes news. The more witnesses the greater accuracy of news."), 249 | app.Button().Text("How it works").OnClick(w.openHowToDialog), 250 | ), 251 | ), 252 | ), 253 | app.Section().Class("p-strip--suru").Body( 254 | app.Div().Class("row u-vertically-center").Body( 255 | app.Div().Class("col-12").Body( 256 | app.H1().Text("Have an event to report?"), 257 | app.Div().Class("p-form p-form--stacked").Body( 258 | app.Div().Class("p-form__group row").Body( 259 | // app.Div().Class("p-form__group row").Body( 260 | app.P().Class("p-form-help-text").ID("reportEvent").Text("Check rumors first as it may already exist.").Style("color", "#fff").Style("margin-top", "0").Style("margin-bottom", "10px"), 261 | // ), 262 | app.Label().For("title").Text("Title"), 263 | app.Input().ID("title").Name("title").OnKeyUp(w.onEventTitle), 264 | ), 265 | app.Div().Class("p-form__group row").Body( 266 | app.Label().For("details").Text("Details"), 267 | app.Textarea().Class("is-dense").ID("details").Name("details").Rows(2).OnKeyUp(w.onEventDetails), 268 | ), 269 | app.Div().Class("p-form__group row").Body( 270 | app.Label().For("location").Text("Location"), 271 | app.Textarea().Class("is-dense").ID("location").Name("location").Rows(2).OnKeyUp(w.onEventLocation), 272 | ), 273 | // app.Div().Class("p-form__group row").Body( 274 | // app.Label().For("file").Text("Optional Image/Video Evidence"), 275 | // app.Input().Class("is-dense").ID("file").Name("file").Type("file"), 276 | // ), 277 | app.Div().Class("p-form__group row").Body( 278 | app.Button().Class("u-vertically-centered").Text("Report event").OnClick(w.onSubmitEvent), 279 | ), 280 | ), 281 | ), 282 | ), 283 | ).Style("background-image", "linear-gradient(to bottom right, rgba(205, 205, 205, 0.55) 0%, rgba(205, 205, 205, 0.55) 49.8%, transparent 50%, transparent 100%),linear-gradient(to bottom left, rgba(205, 205, 205, 0.55) 0%, rgba(205, 205, 205, 0.55) 49.8%, transparent 50%, transparent 100%),linear-gradient(to top right, #fff 0%, #fff 49%, transparent 50%, transparent 100%),linear-gradient(#fff 0%, #fff 100%),linear-gradient(111deg, #00C6CF 10%, #00C6CF 37%, #00C6CF 100%)"), 284 | app.Section().Class("p-strip--suru").Body( 285 | app.Div().Class("row u-vertically-center").Body( 286 | app.Div().Class("col-12").Body( 287 | app.H1().Text("Been a witness of an event?"), 288 | app.Button().Text("Confirm rumors").OnClick(w.openRumorsDialog), 289 | ), 290 | ), 291 | app.Div().Class("row u-vertically-center").Body( 292 | app.Div().Class("col-12").Body( 293 | app.H1().Text("Just want to read the news?"), 294 | app.P().Text("Your personal news feed at your fingertips. All witnessed. No ads, paywalls, censorship or fact checkers."), 295 | app.Button().Text("Read the news").OnClick(w.openNewsDialog), 296 | ), 297 | ), 298 | ).Style("background-image", "linear-gradient(to bottom right, rgba(205, 205, 205, 0.55) 0%, rgba(205, 205, 205, 0.55) 49.8%, transparent 50%, transparent 100%),linear-gradient(to bottom left, rgba(205, 205, 205, 0.55) 0%, rgba(205, 205, 205, 0.55) 49.8%, transparent 50%, transparent 100%),linear-gradient(to top right, #fff 0%, #fff 49%, transparent 50%, transparent 100%),linear-gradient(#fff 0%, #fff 100%),linear-gradient(111deg, #2F4858 10%, #2F4858 37%, #2F4858 100%)"), 299 | app.Div().Class("p-modal").ID("rumors-modal").Style("display", "none").Body( 300 | app.Section().Class("p-modal__dialog").Role("dialog").Aria("modal", true).Aria("labelledby", "modal-title").Aria("describedby", "modal-description").Body( 301 | app.Header().Class("p-modal__header").Body( 302 | app.H2().Class("p-modal__title").ID("modal-title").Text("Rumors"), 303 | app.Button().Class("p-modal__close").Aria("label", "Close active modal").Aria("controls", "modal").OnClick(w.closeRumorsModal), 304 | ), 305 | app.Table().Aria("label", "rumors-table").Class("p-table--expanding").Body( 306 | app.THead().Body( 307 | app.Tr().Body( 308 | app.Th().Body( 309 | app.Span().Class("status-icon is-blocked").Text("Title"), 310 | ), 311 | app.Th().Text("Location"), 312 | app.Th().Text("Action"), 313 | app.Th().Class("u-align--right").Text("Details"), 314 | ), 315 | ), 316 | app.If(len(w.events) > 0, func() app.UI { 317 | return app.TBody().Body( 318 | app.Range(w.events).Slice(func(i int) app.UI { 319 | return app.Tr().DataSet("title", i).Body( 320 | app.Td().Class("has-overflow").DataSet("column", "title").Body( 321 | app.Div().Text(w.events[i].Title), 322 | ), 323 | app.Td().Class("has-overflow").DataSet("column", "location").Body( 324 | app.Div().Text(w.events[i].Location), 325 | ), 326 | app.Td().Class("has-overflow").DataSet("column", "action").Body( 327 | app.If(w.citizenID == w.events[i].Reporter || w.isWitness, func() app.UI { 328 | return app.Button().Class("is-dense").Value(w.events[i].ID).Text("Confirm").Disabled(true).OnClick(w.confirmRumor) 329 | }).Else(func() app.UI { 330 | return app.Button().Class("is-dense").Value(w.events[i].ID).Text("Confirm").OnClick(w.confirmRumor) 331 | }), 332 | ), 333 | app.Td().Class("has-overflow u-align--right").DataSet("column", "details").Body( 334 | app.Button().Class("u-toggle is-dense").Aria("controls", "expanded-row").Aria("expanded", "true").DataSet("shown-text", "Hide").DataSet("hidden-text", "Show").Value(w.events[i].ID).Text("Hide").OnClick(w.expandDetails), 335 | ), 336 | app.Td().ID("expanded-row-"+w.events[i].ID).Class("has-overflow p-table__expanding-panel").Aria("hidden", "false").Body( 337 | app.H4().Text("Details"), 338 | app.Range(w.events[i].Details).Slice(func(n int) app.UI { 339 | return app.Div().Class("row").Body( 340 | app.Div().Class("col-8 p-card").Body( 341 | app.P().Text(w.events[i].Details[n]), 342 | ), 343 | ) 344 | }), 345 | app.If(w.citizenID != w.events[i].Reporter && !w.isWitness, func() app.UI { 346 | return app.Div().Class("p-form p-form--stacked").Body( 347 | app.H4().Text("Add new details: "), 348 | app.Div().Class("p-form__group row").Body( 349 | app.Textarea().Class("is-dense").ID("details").Name("details").Rows(2).OnKeyUp(w.onEventDetails), 350 | ), 351 | app.Div().Class("p-form__group row").Body( 352 | app.Button().Class("u-vertically-centered").Value(w.events[i].ID).Text("Add details").OnClick(w.onAddDetails), 353 | ), 354 | ) 355 | }), 356 | ), 357 | ) 358 | }), 359 | ) 360 | }).Else(func() app.UI { 361 | return app.Caption().Class("p-strip").Body( 362 | app.Div().Class("row").Body( 363 | app.Div().Class("u-align--left col-8 col-medium-4 col-small-3").Body( 364 | app.P().Class("p-heading--4 u-no-margin--bottom").Text("No recent rumors"), 365 | app.P().Text("Check back later or report an event"), 366 | ), 367 | ), 368 | ) 369 | }), 370 | ), 371 | ), 372 | ), 373 | app.Div().Class("p-modal").ID("news-modal").Style("display", "none").Body( 374 | app.Section().Class("p-modal__dialog").Role("dialog").Aria("modal", true).Aria("labelledby", "modal-title").Aria("describedby", "modal-description").Body( 375 | app.Header().Class("p-modal__header").Body( 376 | app.H2().Class("p-modal__title").ID("modal-title").Text("News"), 377 | app.Button().Class("p-modal__close").Aria("label", "Close active modal").Aria("controls", "modal").OnClick(w.closeNewsModal), 378 | ), 379 | app.Table().Aria("label", "news-table").Class("p-table--expanding").Body( 380 | app.THead().Body( 381 | app.Tr().Body( 382 | app.Th().Body( 383 | app.Span().Class("status-icon is-blocked").Text("Title"), 384 | ), 385 | app.Th().Text("Location"), 386 | app.Th().Text("Confirmed By"), 387 | app.Th().Class("u-align--right").Text("Details"), 388 | ), 389 | ), 390 | app.If(!w.noNews, func() app.UI { 391 | return app.TBody().Body( 392 | app.Range(w.events).Slice(func(i int) app.UI { 393 | return app.If(w.events[i].ConfirmedBy > 1, func() app.UI { 394 | return app.Tr().DataSet("title", i).Body( 395 | app.Td().Class("has-overflow").DataSet("column", "title").Body( 396 | app.Div().Text(w.events[i].Title), 397 | ), 398 | app.Td().Class("has-overflow").DataSet("column", "location").Body( 399 | app.Div().Text(w.events[i].Location), 400 | ), 401 | app.Td().Class("has-overflow").DataSet("column", "confirmedBy").Body( 402 | app.Div().Text(w.events[i].ConfirmedBy), 403 | ), 404 | app.Td().Class("has-overflow u-align--right").DataSet("column", "details").Body( 405 | app.Button().Class("u-toggle is-dense").Aria("controls", "expanded-row").Aria("expanded", "true").DataSet("shown-text", "Hide").DataSet("hidden-text", "Show").Value(w.events[i].ID).Text("Hide").OnClick(w.expandDetails), 406 | ), 407 | app.Td().ID("expanded-row-"+w.events[i].ID).Class("has-overflow p-table__expanding-panel").Aria("hidden", "false").Body( 408 | app.H4().Text("Details"), 409 | app.Range(w.events[i].Details).Slice(func(n int) app.UI { 410 | return app.Div().Class("row").Body( 411 | app.Div().Class("col-8 p-card").Body( 412 | app.P().Text(w.events[i].Details[n]), 413 | ), 414 | ) 415 | }), 416 | ), 417 | ) 418 | }) 419 | }), 420 | ) 421 | }).Else(func() app.UI { 422 | return app.Caption().Class("p-strip").Body( 423 | app.Div().Class("row").Body( 424 | app.Div().Class("u-align--left col-8 col-medium-4 col-small-3").Body( 425 | app.P().Class("p-heading--4 u-no-margin--bottom").Text("No recent news"), 426 | app.P().Text("Check back later or report an event"), 427 | ), 428 | ), 429 | ) 430 | }), 431 | ), 432 | ), 433 | ), 434 | app.Div().Class("p-modal").ID("howto-modal").Style("display", "none").Body( 435 | app.Section().Class("p-modal__dialog").Role("dialog").Aria("modal", true).Aria("labelledby", "modal-title").Aria("describedby", "modal-description").Body( 436 | app.Header().Class("p-modal__header").Body( 437 | app.H2().Class("p-modal__title").ID("modal-title").Text("How to play"), 438 | app.Button().Class("p-modal__close").Aria("label", "Close active modal").Aria("controls", "modal").OnClick(w.closeHowToModal), 439 | ), 440 | app.Div().Class("p-heading-icon--small").Body( 441 | app.Aside().Class("p-accordion").Body( 442 | app.Ul().Class("p-accordion__list").Body( 443 | app.Li().Class("p-accordion__group").Body( 444 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 445 | app.Button().Type("button").Class("p-accordion__tab").ID("tab1").Aria("controls", "tab1-section").Aria("expanded", true).Text("What is Cyber Witness").Value("tab1-section").OnClick(w.toggleAccordion), 446 | ), 447 | app.Section().Class("p-accordion__panel").ID("tab1-section").Aria("hidden", false).Aria("labelledby", "tab1").Body( 448 | app.P().Text("Cyber Witness is a p2p media simulator based on the reporter and witnesses concept."), 449 | ), 450 | ), 451 | app.Li().Class("p-accordion__group").Body( 452 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 453 | app.Button().Type("button").Class("p-accordion__tab").ID("tab2").Aria("controls", "tab2-section").Aria("expanded", true).Text("What's the problem with mass media?").Value("tab2-section").OnClick(w.toggleAccordion), 454 | ), 455 | app.Section().Class("p-accordion__panel").ID("tab2-section").Aria("hidden", true).Aria("labelledby", "tab1").Body( 456 | app.P().Text("It's centralized, censored, fact-checked, unaccountable and non-trasparent."), 457 | ), 458 | ), 459 | app.Li().Class("p-accordion__group").Body( 460 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 461 | app.Button().Type("button").Class("p-accordion__tab").ID("tab3").Aria("controls", "tab3-section").Aria("expanded", true).Text("How Cyber Witness replaces mass media").Value("tab3-section").OnClick(w.toggleAccordion), 462 | ), 463 | app.Section().Class("p-accordion__panel").ID("tab3-section").Aria("hidden", true).Aria("labelledby", "tab3").Body( 464 | app.P().Text("By switching to p2p interactions and the reporter and witnesses model we emulate a transparent environment with a feedback loop."), 465 | ), 466 | ), 467 | app.Li().Class("p-accordion__group").Body( 468 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 469 | app.Button().Type("button").Class("p-accordion__tab").ID("tab4").Aria("controls", "tab4-section").Aria("expanded", true).Text("Features").Value("tab4-section").OnClick(w.toggleAccordion), 470 | ), 471 | app.Section().Class("p-accordion__panel").ID("tab4-section").Aria("hidden", true).Aria("labelledby", "tab3").Body( 472 | app.Ul().Class("p-matrix").Body( 473 | app.Li().Class("p-matrix__item").Body( 474 | app.Div().Class("p-matrix__content").Body( 475 | app.H3().Class("p-matrix__title").Text("Report events"), 476 | app.Div().Class("p-matrix__desc").Body( 477 | app.P().Text("Provide details about the event."), 478 | ), 479 | ), 480 | ), 481 | app.Li().Class("p-matrix__item").Body( 482 | app.Div().Class("p-matrix__content").Body( 483 | app.H3().Class("p-matrix__title").Text("Confirm rumors"), 484 | app.Div().Class("p-matrix__desc").Body( 485 | app.P().Text("Browse reported events, confirm what you have witnessed and provide more details."), 486 | ), 487 | ), 488 | ), 489 | app.Li().Class("p-matrix__item").Body( 490 | app.Div().Class("p-matrix__content").Body( 491 | app.H3().Class("p-matrix__title").Text("Event details aggregation"), 492 | app.Div().Class("p-matrix__desc").Body( 493 | app.P().Text("The more witnesses the better the accuracy the higher the chance a rumor is real news."), 494 | ), 495 | ), 496 | ), 497 | app.Li().Class("p-matrix__item").Body( 498 | app.Div().Class("p-matrix__content").Body( 499 | app.H3().Class("p-matrix__title").Text("Read the real news"), 500 | app.Div().Class("p-matrix__desc").Body( 501 | app.P().Text("Your personal news feed at your fingertips. All witnessed. No ads, paywalls, censorship or fact checkers."), 502 | ), 503 | ), 504 | ), 505 | app.Li().Class("p-matrix__item").Body( 506 | app.Div().Class("p-matrix__content").Body( 507 | app.H3().Class("p-matrix__title").Text("Anonymity by default"), 508 | app.Div().Class("p-matrix__desc").Body( 509 | app.P().Text("Anonymity guarantees everyone is protected."), 510 | ), 511 | ), 512 | ), 513 | app.Li().Class("p-matrix__item").Body( 514 | app.Div().Class("p-matrix__content").Body( 515 | app.H3().Class("p-matrix__title").Text("Flat interactions"), 516 | app.Div().Class("p-matrix__desc").Body( 517 | app.P().Text("No centralized control, no fact checkers and no ads."), 518 | ), 519 | ), 520 | ), 521 | ), 522 | ), 523 | ), 524 | app.Li().Class("p-accordion__group").Body( 525 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 526 | app.Button().Type("button").Class("p-accordion__tab").ID("tab5").Aria("controls", "tab5-section").Aria("expanded", true).Text("Support us").Value("tab5-section").OnClick(w.toggleAccordion), 527 | ), 528 | app.Section().Class("p-accordion__panel").ID("tab5-section").Aria("hidden", true).Aria("labelledby", "tab5").Body( 529 | app.A().Href("https://opencollective.com/stateless-minds-collective").Text("https://opencollective.com/stateless-minds-collective"), 530 | ), 531 | ), 532 | app.Li().Class("p-accordion__group").Body( 533 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 534 | app.Button().Type("button").Class("p-accordion__tab").ID("tab6").Aria("controls", "tab6-section").Aria("expanded", true).Text("Terms of service").Value("tab6-section").OnClick(w.toggleAccordion), 535 | ), 536 | app.Section().Class("p-accordion__panel").ID("tab6-section").Aria("hidden", true).Aria("labelledby", "tab6").Body( 537 | app.Div().Class("p-card").Body( 538 | app.H3().Text("Introduction"), 539 | app.P().Class("p-card__content").Text("Cyber Witness is a p2p media simulator based on the reporter and witnesses concept in the form of a fictional game based on real-time data. By using the application you are implicitly agreeing to share your peer id with the IPFS public network."), 540 | ), 541 | app.Div().Class("p-card").Body( 542 | app.H3().Text("Application Hosting"), 543 | app.P().Class("p-card__content").Text("Cyber Witness is a decentralized application and is hosted on a public peer to peer network. By using the application you agree to host it on the public IPFS network free of charge for as long as your usage is."), 544 | ), 545 | app.Div().Class("p-card").Body( 546 | app.H3().Text("User-Generated Content"), 547 | app.P().Class("p-card__content").Text("All published content is user-generated, fictional and creators are not responsible for it."), 548 | ), 549 | ), 550 | ), 551 | app.Li().Class("p-accordion__group").Body( 552 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body( 553 | app.Button().Type("button").Class("p-accordion__tab").ID("tab7").Aria("controls", "tab7-section").Aria("expanded", true).Text("Privacy policy").Value("tab7-section").OnClick(w.toggleAccordion), 554 | ), 555 | app.Section().Class("p-accordion__panel").ID("tab7-section").Aria("hidden", true).Aria("labelledby", "tab7").Body( 556 | app.Div().Class("p-card").Body( 557 | app.H3().Text("Personal data"), 558 | app.P().Class("p-card__content").Text("There is no personal information collected within Cyber Witness. We store a small portion of your peer ID encrypted as a non-unique identifier which is used for displaying the ranks interface."), 559 | ), 560 | app.Div().Class("p-card").Body( 561 | app.H3().Text("Coookies"), 562 | app.P().Class("p-card__content").Text("Cyber Witness does not use cookies."), 563 | ), 564 | app.Div().Class("p-card").Body( 565 | app.H3().Text("Changes to this privacy policy"), 566 | app.P().Class("p-card__content").Text("This Privacy Policy might be updated from time to time. Thus, it is advised to review this page periodically for any changes. You will be notified of any changes from this page. Changes are effective immediately after they are posted on this page."), 567 | ), 568 | ), 569 | ), 570 | ), 571 | ), 572 | ), 573 | ), 574 | ), 575 | ) 576 | } 577 | 578 | func (w *witness) onEventTitle(ctx app.Context, e app.Event) { 579 | w.eventTitle = ctx.JSSrc().Get("value").String() 580 | } 581 | 582 | func (w *witness) onEventDetails(ctx app.Context, e app.Event) { 583 | w.eventDetails = ctx.JSSrc().Get("value").String() 584 | } 585 | 586 | func (w *witness) onEventLocation(ctx app.Context, e app.Event) { 587 | w.eventLocation = ctx.JSSrc().Get("value").String() 588 | } 589 | 590 | func (w *witness) onSubmitEvent(ctx app.Context, e app.Event) { 591 | lastSolutionID := 0 592 | unique := true 593 | for n, ev := range w.events { 594 | if w.eventTitle == ev.Title { 595 | unique = false 596 | } 597 | 598 | if n > 0 { 599 | currentID, err := strconv.Atoi(ev.ID) 600 | if err != nil { 601 | log.Fatal(err) 602 | } 603 | previousID, err := strconv.Atoi(w.events[n-1].ID) 604 | if err != nil { 605 | log.Fatal(err) 606 | } 607 | if currentID > previousID { 608 | lastSolutionID = currentID 609 | } 610 | } else { 611 | lastSolutionID = 1 612 | } 613 | } 614 | 615 | if unique { 616 | event := Event{ 617 | ID: strconv.Itoa(lastSolutionID + 1), 618 | Type: eventType, 619 | Title: w.eventTitle, 620 | Location: w.eventLocation, 621 | Reporter: w.citizenID, 622 | } 623 | 624 | event.Details = append(event.Details, w.eventDetails) 625 | 626 | ev, err := json.Marshal(event) 627 | if err != nil { 628 | log.Fatal(err) 629 | } 630 | 631 | ctx.Async(func() { 632 | err = w.sh.OrbitDocsPut(dbNameEvent, ev) 633 | if err != nil { 634 | ctx.Dispatch(func(ctx app.Context) { 635 | w.createNotification(ctx, NotificationDanger, ErrorHeader, "Could not create event. Try again later.") 636 | log.Fatal(err) 637 | }) 638 | } 639 | err = w.sh.PubSubPublish(topicCreateEvent, string(ev)) 640 | if err != nil { 641 | log.Fatal(err) 642 | } 643 | 644 | ctx.Dispatch(func(ctx app.Context) { 645 | w.createNotification(ctx, NotificationSuccess, SuccessHeader, "Event submited.") 646 | }) 647 | }) 648 | } 649 | } 650 | 651 | func (w *witness) onAddDetails(ctx app.Context, e app.Event) { 652 | id := ctx.JSSrc().Get("value").String() 653 | idInt, err := strconv.Atoi(id) 654 | if err != nil { 655 | log.Fatal(err) 656 | } 657 | 658 | // add new details to slice 659 | w.events[idInt-1].Details = append(w.events[idInt-1].Details, w.eventDetails) 660 | 661 | ev, err := json.Marshal(w.events[idInt-1]) 662 | if err != nil { 663 | log.Fatal(err) 664 | } 665 | 666 | ctx.Async(func() { 667 | err = w.sh.OrbitDocsPut(dbNameEvent, ev) 668 | if err != nil { 669 | ctx.Dispatch(func(ctx app.Context) { 670 | w.createNotification(ctx, NotificationDanger, ErrorHeader, "Could not add details. Try again later.") 671 | log.Fatal(err) 672 | }) 673 | } 674 | err = w.sh.PubSubPublish(topicUpdateEvent, string(ev)) 675 | if err != nil { 676 | log.Fatal(err) 677 | } 678 | 679 | ctx.Dispatch(func(ctx app.Context) { 680 | w.createNotification(ctx, NotificationSuccess, SuccessHeader, "Event details added.") 681 | }) 682 | }) 683 | } 684 | 685 | func (w *witness) createNotification(ctx app.Context, s NotificationStatus, h string, msg string) { 686 | w.notificationID++ 687 | w.notifications[strconv.Itoa(w.notificationID)] = notification{ 688 | id: w.notificationID, 689 | status: string(s), 690 | header: h, 691 | message: msg, 692 | } 693 | 694 | ntfs := w.notifications 695 | ctx.Async(func() { 696 | for n := range ntfs { 697 | time.Sleep(5 * time.Second) 698 | delete(ntfs, n) 699 | ctx.Async(func() { 700 | ctx.Dispatch(func(ctx app.Context) { 701 | w.notifications = ntfs 702 | }) 703 | }) 704 | } 705 | }) 706 | } 707 | 708 | func (w *witness) openRumorsDialog(ctx app.Context, e app.Event) { 709 | app.Window().GetElementByID("rumors-modal").Set("style", "display:flex") 710 | } 711 | 712 | func (w *witness) openNewsDialog(ctx app.Context, e app.Event) { 713 | app.Window().GetElementByID("news-modal").Set("style", "display:flex") 714 | } 715 | 716 | func (w *witness) openHowToDialog(ctx app.Context, e app.Event) { 717 | app.Window().GetElementByID("howto-modal").Set("style", "display:flex") 718 | } 719 | 720 | func (w *witness) closeRumorsModal(ctx app.Context, e app.Event) { 721 | app.Window().GetElementByID("rumors-modal").Set("style", "display:none") 722 | } 723 | 724 | func (w *witness) closeNewsModal(ctx app.Context, e app.Event) { 725 | app.Window().GetElementByID("news-modal").Set("style", "display:none") 726 | } 727 | 728 | func (w *witness) closeHowToModal(ctx app.Context, e app.Event) { 729 | app.Window().GetElementByID("howto-modal").Set("style", "display:none") 730 | } 731 | 732 | func (w *witness) expandDetails(ctx app.Context, e app.Event) { 733 | id := ctx.JSSrc().Get("value").String() 734 | attrButton := ctx.JSSrc().Get("attributes") 735 | dataShownText := attrButton.Get("data-shown-text").Get("value").String() 736 | dataHiddenText := attrButton.Get("data-hidden-text").Get("value").String() 737 | ariaButton := attrButton.Get("aria-expanded").Get("value").String() 738 | attrRow := app.Window().GetElementByID("expanded-row-" + id).Get("attributes") 739 | ariaRow := attrRow.Get("aria-hidden").Get("value").String() 740 | 741 | if ariaButton == "false" { 742 | ctx.JSSrc().Call("setAttribute", "aria-expanded", "true") 743 | ctx.JSSrc().Set("innerHTML", dataShownText) 744 | } else { 745 | ctx.JSSrc().Call("setAttribute", "aria-expanded", "false") 746 | ctx.JSSrc().Set("innerHTML", dataHiddenText) 747 | } 748 | 749 | if ariaRow == "false" { 750 | app.Window().GetElementByID("expanded-row-"+id).Call("setAttribute", "aria-hidden", "true") 751 | } else { 752 | app.Window().GetElementByID("expanded-row-"+id).Call("setAttribute", "aria-hidden", "false") 753 | } 754 | } 755 | 756 | func (w *witness) confirmRumor(ctx app.Context, e app.Event) { 757 | id := ctx.JSSrc().Get("value").String() 758 | idInt, err := strconv.Atoi(id) 759 | if err != nil { 760 | log.Fatal(err) 761 | } 762 | 763 | event := w.events[idInt-1] 764 | 765 | if w.citizenID != event.Reporter { 766 | // increment confirmedBy counter 767 | event.ConfirmedBy++ 768 | event.Witnesses = append(event.Witnesses, w.citizenID) 769 | w.isWitness = true 770 | } else { 771 | // return if reporter somehow made a request 772 | return 773 | } 774 | 775 | ev, err := json.Marshal(event) 776 | if err != nil { 777 | log.Fatal(err) 778 | } 779 | 780 | ctx.Async(func() { 781 | err = w.sh.OrbitDocsPut(dbNameEvent, ev) 782 | if err != nil { 783 | ctx.Dispatch(func(ctx app.Context) { 784 | w.createNotification(ctx, NotificationDanger, ErrorHeader, "Could not confirm rumor. Try again later.") 785 | log.Fatal(err) 786 | }) 787 | } 788 | err = w.sh.PubSubPublish(topicUpdateEvent, string(ev)) 789 | if err != nil { 790 | log.Fatal(err) 791 | } 792 | 793 | ctx.Dispatch(func(ctx app.Context) { 794 | w.events[idInt-1] = event 795 | w.createNotification(ctx, NotificationSuccess, SuccessHeader, "Rumor confirmed.") 796 | }) 797 | }) 798 | } 799 | 800 | func (w *witness) toggleAccordion(ctx app.Context, e app.Event) { 801 | id := ctx.JSSrc().Get("value").String() 802 | attr := app.Window().GetElementByID(id).Get("attributes") 803 | aria := attr.Get("aria-hidden").Get("value").String() 804 | if aria == "false" { 805 | app.Window().GetElementByID(id).Call("setAttribute", "aria-hidden", "true") 806 | } else { 807 | app.Window().GetElementByID(id).Call("setAttribute", "aria-hidden", "false") 808 | } 809 | } 810 | 811 | // The main function is the entry point where the app is configured and started. 812 | // It is executed in 2 different environments: A client (the web browser) and a 813 | // server. 814 | func main() { 815 | // The first thing to do is to associate the hello component with a path. 816 | // 817 | // This is done by calling the Route() function, which tells go-app what 818 | // component to display for a given path, on both client and server-side. 819 | app.Route("/", func() app.Composer{ 820 | return &witness{} 821 | }) 822 | 823 | // Once the routes set up, the next thing to do is to either launch the app 824 | // or the server that serves the app. 825 | // 826 | // When executed on the client-side, the RunWhenOnBrowser() function 827 | // launches the app, starting a loop that listens for app events and 828 | // executes client instructions. Since it is a blocking call, the code below 829 | // it will never be executed. 830 | // 831 | // When executed on the server-side, RunWhenOnBrowser() does nothing, which 832 | // lets room for server implementation without the need for precompiling 833 | // instructions. 834 | app.RunWhenOnBrowser() 835 | 836 | // Finally, launching the server that serves the app is done by using the Go 837 | // standard HTTP package. 838 | // 839 | // The Handler is an HTTP handler that serves the client and all its 840 | // required resources to make it work into a web browser. Here it is 841 | // configured to handle requests with a path that starts with "/". 842 | 843 | withGz := gziphandler.GzipHandler(&app.Handler{ 844 | Name: "cyber-witness", 845 | Description: "Cyber Witness - Liquid democracy politics simulator based on personal reputation index", 846 | Styles: []string{ 847 | "https://assets.ubuntu.com/v1/vanilla-framework-version-3.8.0.min.css", 848 | "https://use.fontawesome.com/releases/v6.2.0/css/all.css", 849 | }, 850 | Scripts: []string{}, 851 | }) 852 | http.Handle("/", withGz) 853 | 854 | if err := http.ListenAndServe(":7000", nil); err != nil { 855 | log.Fatal(err) 856 | } 857 | } 858 | --------------------------------------------------------------------------------