├── .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 | 
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 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
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 | 
217 |
218 |
219 |
220 | 
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 |
--------------------------------------------------------------------------------