├── .gitignore
├── assets
├── logo.png
├── pin.png
├── how-to.png
├── delegate-trust.png
├── delegate-vote.png
├── list-shortages.png
├── list-solutions.png
├── pin-to-local-node.png
└── suggest-solution.png
├── makefile
├── .github
└── FUNDING.yml
├── LICENSE
├── go.mod
├── README.md
├── go.sum
└── cyber-acid.go
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | web/app.wasm
3 | cyber-acid
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/logo.png
--------------------------------------------------------------------------------
/assets/pin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/pin.png
--------------------------------------------------------------------------------
/assets/how-to.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/how-to.png
--------------------------------------------------------------------------------
/assets/delegate-trust.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/delegate-trust.png
--------------------------------------------------------------------------------
/assets/delegate-vote.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/delegate-vote.png
--------------------------------------------------------------------------------
/assets/list-shortages.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/list-shortages.png
--------------------------------------------------------------------------------
/assets/list-solutions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/list-solutions.png
--------------------------------------------------------------------------------
/assets/pin-to-local-node.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/pin-to-local-node.png
--------------------------------------------------------------------------------
/assets/suggest-solution.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stateless-minds/cyber-acid/HEAD/assets/suggest-solution.png
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | build:
2 | GOARCH=wasm GOOS=js go build -o web/app.wasm
3 | go build
4 |
5 | run: build
6 | ./cyber-acid
--------------------------------------------------------------------------------
/.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-acid
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 | # Cyber Acid
3 |
4 |
5 |
6 | 
7 |
8 | "A lot of people say if you care about global problems just be a politician. But is only the politician allowed to care about such issues?
9 | Isn't every living human being having that same right which is taken by the politicians with no other options presented? That's what representative democracy is all about. Full unconditional representation until the next election vote. Immense transfer of power with no guarantees whatsoever."
10 |
11 | Cyber Acid is a political simulator based on the liquid democracy concept. It is designed as an integration module that works with Cyber Stasis - the moneyless economy simulator.
12 |
13 |
14 | ## Screenshots
15 |
16 |
17 |
18 |
19 |
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 | ## Features
65 |
66 |
67 |
68 | * **Check shortages** - Review pressing issues.
69 |
70 | * **Suggest a solution** - Contribute with your expertise.
71 |
72 | * **Vote for solutions** - Vote for the best solution.
73 |
74 | * **Delegate your vote** - Not competent? Delegate your vote.
75 |
76 | * **Infinite delegation** - Delegation can be chained for maximum participation.
77 |
78 | * **Cross delegation** - Cross delegation is also supported.
79 |
80 | ## Community
81 |
82 | https://www.reddit.com/r/CyberAcid/
83 |
84 |
85 | ## How to Play
86 |
87 |
88 |
89 | The game runs on the public IPFS network. In order to play it follow the steps below:
90 |
91 |
92 |
93 | 1. Install the official IPFS Desktop http://docs.ipfs.io/install/ipfs-desktop/
94 |
95 | 2. Install IPFS Companion http://docs.ipfs.io/install/ipfs-companion/
96 |
97 | 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`
98 |
99 | 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`:
100 |
101 | ```{
102 |
103 | "API": {
104 |
105 | "HTTPHeaders": {
106 |
107 | "Access-Control-Allow-Origin": ["webui://-", "http://localhost:3000", "http://k51qzi5uqu5dktpxn6nobipyd1i5q8kc4316ybqevx8ardb3diamyvtixshh0m.ipns.localhost:8080", "http://127.0.0.1:5001", "https://webui.ipfs.io"],
108 |
109 | "Access-Control-Allow-Credentials": ["true"],
110 |
111 | "Access-Control-Allow-Methods": ["PUT", "POST"]
112 |
113 | }
114 |
115 | },
116 |
117 | ```
118 |
119 | 6. Navigate to Cyber Acid and let's simulate the future together!
120 |
121 | 7. If you like the game consider pinning it to your local node so that you become a permanent host of it while you have IPFS daemon running
122 |
123 | 
124 |
125 | 
126 |
127 |
128 |
129 | Please note the game 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%.
130 |
131 |
132 | ## Roadmap
133 |
134 | 1. Make reputation index context/category based
135 |
136 | 2. Make voting time-restricted
137 |
138 |
139 |
140 | ## Acknowledgments
141 |
142 |
143 |
144 | 1. go-app
145 |
146 | 2. IPFS
147 |
148 | 3. Berty
149 |
150 | 4. All the rest of the authors who worked on the dependencies used! Thanks a lot!
151 |
152 |
153 |
154 | ## Contributing
155 |
156 |
157 |
158 | Open an issue
159 |
160 |
161 |
162 | ## License
163 |
164 |
165 |
166 | Stateless Minds (c) 2022 and contributors
167 |
168 |
169 |
170 | MIT License
171 |
--------------------------------------------------------------------------------
/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-acid.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 dbNameIssue = "issue"
19 |
20 | const dbNameCitizenReputation = "citizen_reputation"
21 |
22 | const typeShortage = "shortage"
23 |
24 | const (
25 | topicCritical = "critical"
26 | topicIssue = "issue"
27 | )
28 |
29 | const (
30 | NotificationSuccess NotificationStatus = "positive"
31 | NotificationInfo NotificationStatus = "info"
32 | NotificationWarning NotificationStatus = "warning"
33 | NotificationDanger NotificationStatus = "negative"
34 | SuccessHeader = "Success"
35 | ErrorHeader = "Error"
36 | )
37 |
38 | const (
39 | asideTitleCreate = "Suggest Solution"
40 | asideTitleList = "List Solutions"
41 | )
42 |
43 | // pubsub is a component that does a simple pubsub on ipfs. A component is a
44 | // customizable, independent, and reusable UI element. It is created by
45 | // embedding app.Compo into a struct.
46 | type acid struct {
47 | app.Compo
48 | sh *shell.Shell
49 | sub *shell.PubSubSubscription
50 | citizenID string
51 | issues []Issue
52 | categoryIssues map[string][]Issue
53 | ranks []CitizenReputation
54 | delegates []Delegate
55 | currentIssueInSlice int
56 | Solutions []Solution
57 | currentSolutionDescription string
58 | notifications map[string]notification
59 | notificationID int
60 | AsideTitle string
61 | }
62 |
63 | type NotificationStatus string
64 |
65 | type notification struct {
66 | id int
67 | status string
68 | header string
69 | message string
70 | }
71 |
72 | type Issue struct {
73 | ID string `mapstructure:"_id" json:"_id" validate:"uuid_rfc4122"`
74 | Type string `mapstructure:"type" json:"type" validate:"uuid_rfc4122"`
75 | Category string `mapstructure:"category" json:"category" validate:"uuid_rfc4122"`
76 | Description string `mapstructure:"description" json:"description" validate:"uuid_rfc4122"`
77 | Delegates []Delegate `mapstructure:"delegates" json:"delegates" validate:"uuid_rfc4122"`
78 | Solutions []Solution `mapstructure:"solutions" json:"solutions" validate:"uuid_rfc4122"`
79 | Voters []string `mapstructure:"voters" json:"voters" validate:"uuid_rfc4122"`
80 | }
81 |
82 | type Solution struct {
83 | ID string `mapstructure:"_id" json:"_id" validate:"uuid_rfc4122"`
84 | Description string `mapstructure:"description" json:"description" validate:"uuid_rfc4122"`
85 | Votes int `mapstructure:"votes" json:"votes" validate:"uuid_rfc4122"`
86 | }
87 |
88 | type Delegate struct {
89 | CitizenID string `mapstructure:"citizenId" json:"citizenId" validate:"uuid_rfc4122"`
90 | Votes int `mapstructure:"votes" json:"votes" validate:"uuid_rfc4122"`
91 | Selected int `mapstructure:"selected" json:"selected" validate:"uuid_rfc4122"`
92 | OwnVote bool `mapstructure:"voted" json:"voted" validate:"uuid_rfc4122"`
93 | }
94 |
95 | type CitizenReputation struct {
96 | ID string `mapstructure:"_id" json:"_id" validate:"uuid_rfc4122"`
97 | Type string `mapstructure:"type" json:"type" validate:"uuid_rfc4122"`
98 | CitizenID string `mapstructure:"citizenId" json:"citizenId" validate:"uuid_rfc4122"`
99 | ReputationIndex float64 `mapstructure:"reputationIndex" json:"reputationIndex" validate:"uuid_rfc4122"`
100 | }
101 |
102 | func (a *acid) OnMount(ctx app.Context) {
103 | sh := shell.NewShell("localhost:5001")
104 | a.sh = sh
105 | myPeer, err := a.sh.ID()
106 | if err != nil {
107 | log.Fatal(err)
108 | }
109 |
110 | citizenID := myPeer.ID[len(myPeer.ID)-8:]
111 | // replace password with your own
112 | password := "mysecretpassword"
113 |
114 | a.citizenID = mixer.EncodeString(password, citizenID)
115 | a.subscribeToCriticalTopic(ctx)
116 | a.subscribeToIssueTopic(ctx)
117 | a.notifications = make(map[string]notification)
118 | a.categoryIssues = make(map[string][]Issue)
119 | ctx.Async(func() {
120 | // err := a.sh.OrbitDocsDelete(dbNameIssue, "all")
121 | // if err != nil {
122 | // log.Fatal(err)
123 | // }
124 |
125 | // err := a.sh.OrbitDocsDelete(dbNameCitizenReputation, "4")
126 | // if err != nil {
127 | // log.Fatal(err)
128 | // }
129 |
130 | cr, err := a.sh.OrbitDocsQuery(dbNameCitizenReputation, "type", "reputation")
131 | if err != nil {
132 | log.Fatal(err)
133 | }
134 |
135 | var cc []interface{}
136 | err = json.Unmarshal(cr, &cc)
137 | if err != nil {
138 | log.Fatal(err)
139 | }
140 |
141 | for _, zz := range cc {
142 | r := CitizenReputation{}
143 | err = mapstructure.Decode(zz, &r)
144 | if err != nil {
145 | log.Fatal(err)
146 | }
147 | ctx.Dispatch(func(ctx app.Context) {
148 | a.ranks = append(a.ranks, r)
149 | sort.SliceStable(a.ranks, func(i, j int) bool {
150 | return a.ranks[i].ID < a.ranks[j].ID
151 | })
152 | })
153 | }
154 |
155 | v, err := a.sh.OrbitDocsQuery(dbNameIssue, "type", "shortage")
156 | if err != nil {
157 | log.Fatal(err)
158 | }
159 |
160 | var vv []interface{}
161 | err = json.Unmarshal(v, &vv)
162 | if err != nil {
163 | log.Fatal(err)
164 | }
165 |
166 | for _, ii := range vv {
167 | i := Issue{}
168 | err = mapstructure.Decode(ii, &i)
169 | if err != nil {
170 | log.Fatal(err)
171 | }
172 | ctx.Dispatch(func(ctx app.Context) {
173 | a.categoryIssues[i.Category] = append(a.categoryIssues[i.Category], i)
174 | a.issues = append(a.issues, i)
175 | sort.SliceStable(a.issues, func(i, j int) bool {
176 | return a.issues[i].ID < a.issues[j].ID
177 | })
178 | })
179 | }
180 | })
181 | }
182 |
183 | func (a *acid) subscribeToCriticalTopic(ctx app.Context) {
184 | ctx.Async(func() {
185 | topic := topicCritical
186 | subscription, err := a.sh.PubSubSubscribe(topic)
187 | if err != nil {
188 | log.Fatal(err)
189 | }
190 | a.sub = subscription
191 | a.subscriptionCritical(ctx)
192 | })
193 | }
194 |
195 | func (a *acid) subscribeToIssueTopic(ctx app.Context) {
196 | ctx.Async(func() {
197 | topic := topicIssue
198 | subscription, err := a.sh.PubSubSubscribe(topic)
199 | if err != nil {
200 | log.Fatal(err)
201 | }
202 | a.sub = subscription
203 | a.subscriptionIssue(ctx)
204 | })
205 | }
206 |
207 | func (a *acid) subscriptionIssue(ctx app.Context) {
208 | ctx.Async(func() {
209 | defer a.sub.Cancel()
210 | // wait on pubsub
211 | res, err := a.sub.Next()
212 | if err != nil {
213 | log.Fatal(err)
214 | }
215 | // Decode the string data.
216 | str := string(res.Data)
217 | log.Println("Subscriber of topic issue received message: " + str)
218 | ctx.Async(func() {
219 | a.subscribeToIssueTopic(ctx)
220 | })
221 |
222 | s := Issue{}
223 | err = json.Unmarshal([]byte(str), &s)
224 | if err != nil {
225 | log.Fatal(err)
226 | }
227 |
228 | id, err := strconv.Atoi(s.ID)
229 | if err != nil {
230 | log.Fatal(err)
231 | }
232 |
233 | ctx.Dispatch(func(ctx app.Context) {
234 | a.issues[id-1] = s
235 | })
236 | })
237 | }
238 |
239 | func (a *acid) subscriptionCritical(ctx app.Context) {
240 | ctx.Async(func() {
241 | defer a.sub.Cancel()
242 | // wait on pubsub
243 | res, err := a.sub.Next()
244 | if err != nil {
245 | log.Fatal(err)
246 | }
247 | // Decode the string data.
248 | str := string(res.Data)
249 | log.Println("Subscriber of topic critical received message: " + str)
250 | ctx.Async(func() {
251 | a.subscribeToCriticalTopic(ctx)
252 | })
253 |
254 | s := Issue{}
255 | err = json.Unmarshal([]byte(str), &s)
256 | if err != nil {
257 | log.Fatal(err)
258 | }
259 |
260 | var lastID int
261 | unique := true
262 | for n, i := range a.issues {
263 | a.categoryIssues[i.Category] = append(a.categoryIssues[i.Category], i)
264 | if s.Description == i.Description {
265 | unique = false
266 | }
267 |
268 | if n == 0 {
269 | lastID, err = strconv.Atoi(i.ID)
270 | if err != nil {
271 | log.Fatal(err)
272 | }
273 | } else {
274 | currentID, err := strconv.Atoi(i.ID)
275 | if err != nil {
276 | log.Fatal(err)
277 | }
278 | previousID, err := strconv.Atoi(a.issues[n-1].ID)
279 | if err != nil {
280 | log.Fatal(err)
281 | }
282 | if currentID > previousID {
283 | lastID = currentID
284 | }
285 | }
286 |
287 | }
288 | if unique {
289 | newID := lastID + 1
290 | issue := Issue{
291 | ID: strconv.Itoa(newID),
292 | Type: typeShortage,
293 | Category: s.Category,
294 | Description: s.Description,
295 | Solutions: []Solution{},
296 | }
297 |
298 | i, err := json.Marshal(issue)
299 | if err != nil {
300 | log.Fatal(err)
301 | }
302 |
303 | err = a.sh.OrbitDocsPut(dbNameIssue, i)
304 | if err != nil {
305 | log.Fatal(err)
306 | }
307 |
308 | err = a.sh.PubSubPublish(topicIssue, string(i))
309 | if err != nil {
310 | log.Fatal(err)
311 | }
312 |
313 | ctx.Dispatch(func(ctx app.Context) {
314 | a.issues = append(a.issues, issue)
315 | })
316 | }
317 | })
318 | }
319 |
320 | // The Render method is where the component appearance is defined. Here, a
321 | // "pubsub World!" is displayed as a heading.
322 | func (a *acid) Render() app.UI {
323 | return app.Div().Class("l-application").Role("presentation").Body(
324 | app.Link().Rel("stylesheet").Href("https://assets.ubuntu.com/v1/vanilla-framework-version-3.8.0.min.css"),
325 | app.Link().Rel("stylesheet").Href("https://use.fontawesome.com/releases/v6.2.0/css/all.css"),
326 | app.Link().Rel("stylesheet").Href("/app.css"),
327 | app.Header().Class("l-navigation is-collapsed").Body(
328 | app.Div().Class("l-navigation__drawer").Body(
329 | app.Div().Class("p-panel is-dark").Body(
330 | app.Div().Class("p-panel__header is-sticky").Body(
331 | app.A().Class("p-panel__logo").Href("#").Body(
332 | app.H5().Class("p-heading--2").Text("Cyber Acid"),
333 | ),
334 | ),
335 | app.Hr(),
336 | app.P().Class("p-heading--6").Body(
337 | app.Text("Liquid democracy politics simulator based on the automated data feed from the moneyless economy simulator "),
338 | app.A().Href("https://github.com/stateless-minds/cyber-stasis").Text("Cyber Stasis"),
339 | ).Style("padding", "0 10%;"),
340 | app.Hr(),
341 | app.Div().Class("p-panel__content").Body(
342 | app.Div().Class("p-side-navigation--icons is-dark").ID("drawer-icons").Body(
343 | app.Nav().Aria("label", "Main"),
344 | app.Ul().Class("p-side-navigation__list").Body(
345 | app.Li().Class("p-side-navigation__item--title").Body(
346 | app.A().Class("p-side-navigation__link").Href("#").Body(
347 | app.I().Class("p-icon--help is-light p-side-navigation__icon"),
348 | app.Span().Class("p-side-navigation__label").Text("How to play"),
349 | ).OnClick(a.openHowToDialog),
350 | app.A().Class("p-side-navigation__link").Href("#").Body(
351 | app.I().Class("p-icon--warning is-light p-side-navigation__icon"),
352 | app.Span().Class("p-side-navigation__label").Text("Shortages"),
353 | ).Aria("current", "page"),
354 | app.A().Class("p-side-navigation__link").Href("#").Body(
355 | app.I().Class("p-icon--share is-light p-side-navigation__icon"),
356 | app.Span().Class("p-side-navigation__label").Text("Delegate rankings"),
357 | ).OnClick(a.openRankingsDialog),
358 | ),
359 | ),
360 | ),
361 | ),
362 | ),
363 | ),
364 | ),
365 | app.Main().Class("l-main").Body(
366 | app.Div().Class("p-panel").Body(
367 | app.If(len(a.notifications) > 0, func() app.UI {
368 | return app.Range(a.notifications).Map(func(s string) app.UI {
369 | return app.Div().Class("p-notification--" + a.notifications[s].status).Body(
370 | app.Div().Class("p-notification__content").Body(
371 | app.H5().Class("p-notification__title").Text(a.notifications[s].header),
372 | app.P().Class("p-notification__message").Text(a.notifications[s].message),
373 | ),
374 | )
375 | })
376 | }),
377 | app.Div().Class("p-panel__header").Body(
378 | app.H4().Class("p-panel__title").Text("Open Issues"),
379 | ),
380 | app.Div().Class("p-panel__content").Body(
381 | app.Div().Class("u-fixed-width").Body(
382 | app.If(len(a.categoryIssues) > 0, func() app.UI {
383 | return app.Range(a.categoryIssues).Map(func(s string) app.UI {
384 | return app.Table().Aria("label", "Issues table").Class("p-main-table").Body(
385 | app.THead().Body(
386 | app.Tr().Body(
387 | app.Th().Body(
388 | app.Span().Class("status-icon is-running").Text("Category "+s),
389 | ),
390 | app.Th().Text("Actions"),
391 | ),
392 | ),
393 | app.If(len(a.categoryIssues[s]) > 0, func() app.UI {
394 | return app.TBody().Body(
395 | app.Range(a.categoryIssues[s]).Slice(func(i int) app.UI {
396 | return app.Tr().DataSet("id", i).Body(
397 | app.Td().DataSet("column", "issue").Body(
398 | app.Div().Text(a.categoryIssues[s][i].Description),
399 | ),
400 | app.Td().DataSet("column", "action").Body(
401 | app.Div().Body(
402 | app.Button().Class("u-no-margin--bottom").Text("List Solutions").Value(a.categoryIssues[s][i].ID).OnMouseOver(a.asidePreloadList).OnClick(a.asideOpenList),
403 | app.Button().Class("u-no-margin--bottom").Text("Suggest Solution").Value(a.categoryIssues[s][i].ID).OnMouseOver(a.asidePreloadCreate).OnClick(a.asideOpenCreate),
404 | ),
405 | ),
406 | )
407 | }),
408 | )
409 | }),
410 | )
411 | })
412 | }),
413 | ),
414 | app.Div().Class("p-modal").ID("howto-modal").Style("display", "none").Body(
415 | app.Section().Class("p-modal__dialog").Role("dialog").Aria("modal", true).Aria("labelledby", "modal-title").Aria("describedby", "modal-description").Body(
416 | app.Header().Class("p-modal__header").Body(
417 | app.H2().Class("p-modal__title").ID("modal-title").Text("How to play"),
418 | app.Button().Class("p-modal__close").Aria("label", "Close active modal").Aria("controls", "modal").OnClick(a.closeHowToModal),
419 | ),
420 | app.Div().Class("p-heading-icon--small").Body(
421 | app.Aside().Class("p-accordion").Body(
422 | app.Ul().Class("p-accordion__list").Body(
423 | app.Li().Class("p-accordion__group").Body(
424 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
425 | app.Button().Type("button").Class("p-accordion__tab").ID("tab1").Aria("controls", "tab1-section").Aria("expanded", true).Text("What is Cyber Acid").Value("tab1-section").OnClick(a.toggleAccordion),
426 | ),
427 | app.Section().Class("p-accordion__panel").ID("tab1-section").Aria("hidden", false).Aria("labelledby", "tab1").Body(
428 | app.P().Text("Cyber Acid is a political simulator based on the liquid democracy concept. It is designed as an integration module that works with Cyber Stasis - the moneyless economy simulator."),
429 | ),
430 | ),
431 | app.Li().Class("p-accordion__group").Body(
432 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
433 | app.Button().Type("button").Class("p-accordion__tab").ID("tab2").Aria("controls", "tab2-section").Aria("expanded", true).Text("What is liquid democracy").Value("tab2-section").OnClick(a.toggleAccordion),
434 | ),
435 | app.Section().Class("p-accordion__panel").ID("tab2-section").Aria("hidden", true).Aria("labelledby", "tab1").Body(
436 | app.P().Text("Liquid democracy meets the transparency and accountability of direct democracy with the easy of use of representative democracy. Vote directly for what you want and delegate one-time voting rights per topic."),
437 | ),
438 | ),
439 | app.Li().Class("p-accordion__group").Body(
440 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
441 | app.Button().Type("button").Class("p-accordion__tab").ID("tab3").Aria("controls", "tab3-section").Aria("expanded", true).Text("How it works").Value("tab3-section").OnClick(a.toggleAccordion),
442 | ),
443 | app.Section().Class("p-accordion__panel").ID("tab3-section").Aria("hidden", true).Aria("labelledby", "tab3").Body(
444 | app.P().Text("The simulator receives live data from Cyber Stasis about critical shortages of production and resources. The goal of all participants is to suggest solutions to those issues. For example - replacing a resource with another one, researching new technologies etc."),
445 | ),
446 | ),
447 | app.Li().Class("p-accordion__group").Body(
448 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
449 | app.Button().Type("button").Class("p-accordion__tab").ID("tab4").Aria("controls", "tab4-section").Aria("expanded", true).Text("Features").Value("tab4-section").OnClick(a.toggleAccordion),
450 | ),
451 | app.Section().Class("p-accordion__panel").ID("tab4-section").Aria("hidden", true).Aria("labelledby", "tab3").Body(
452 | app.Ul().Class("p-matrix").Body(
453 | app.Li().Class("p-matrix__item").Body(
454 | app.Div().Class("p-matrix__content").Body(
455 | app.H3().Class("p-matrix__title").Text("Check shortages"),
456 | app.Div().Class("p-matrix__desc").Body(
457 | app.P().Text("Review pressing issues."),
458 | ),
459 | ),
460 | ),
461 | app.Li().Class("p-matrix__item").Body(
462 | app.Div().Class("p-matrix__content").Body(
463 | app.H3().Class("p-matrix__title").Text("Suggest a solution"),
464 | app.Div().Class("p-matrix__desc").Body(
465 | app.P().Text("Contribute with your expertise."),
466 | ),
467 | ),
468 | ),
469 | app.Li().Class("p-matrix__item").Body(
470 | app.Div().Class("p-matrix__content").Body(
471 | app.H3().Class("p-matrix__title").Text("Vote for solutions"),
472 | app.Div().Class("p-matrix__desc").Body(
473 | app.P().Text("Vote for the best solution."),
474 | ),
475 | ),
476 | ),
477 | app.Li().Class("p-matrix__item").Body(
478 | app.Div().Class("p-matrix__content").Body(
479 | app.H3().Class("p-matrix__title").Text("Delegate your vote"),
480 | app.Div().Class("p-matrix__desc").Body(
481 | app.P().Text("Not competent? Delegate your vote."),
482 | ),
483 | ),
484 | ),
485 | app.Li().Class("p-matrix__item").Body(
486 | app.Div().Class("p-matrix__content").Body(
487 | app.H3().Class("p-matrix__title").Text("Infinite delegation"),
488 | app.Div().Class("p-matrix__desc").Body(
489 | app.P().Text("Delegation can be chained for maximum participation."),
490 | ),
491 | ),
492 | ),
493 | app.Li().Class("p-matrix__item").Body(
494 | app.Div().Class("p-matrix__content").Body(
495 | app.H3().Class("p-matrix__title").Text("Cross delegation"),
496 | app.Div().Class("p-matrix__desc").Body(
497 | app.P().Text("Cross delegation is also supported."),
498 | ),
499 | ),
500 | ),
501 | ),
502 | ),
503 | ),
504 | app.Li().Class("p-accordion__group").Body(
505 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
506 | app.Button().Type("button").Class("p-accordion__tab").ID("tab5").Aria("controls", "tab5-section").Aria("expanded", true).Text("Support us").Value("tab5-section").OnClick(a.toggleAccordion),
507 | ),
508 | app.Section().Class("p-accordion__panel").ID("tab5-section").Aria("hidden", true).Aria("labelledby", "tab5").Body(
509 | app.A().Href("https://opencollective.com/stateless-minds-collective").Text("https://opencollective.com/stateless-minds-collective"),
510 | ),
511 | ),
512 | app.Li().Class("p-accordion__group").Body(
513 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
514 | 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(a.toggleAccordion),
515 | ),
516 | app.Section().Class("p-accordion__panel").ID("tab6-section").Aria("hidden", true).Aria("labelledby", "tab6").Body(
517 | app.Div().Class("p-card").Body(
518 | app.H3().Text("Introduction"),
519 | app.P().Class("p-card__content").Text("Cyber Acid is a liquid democracy political simulator in the form of a fictional game based on real-time data from Cyber Stasis. By using the application you are implicitly agreeing to share your peer id with the IPFS public network."),
520 | ),
521 | app.Div().Class("p-card").Body(
522 | app.H3().Text("Application Hosting"),
523 | app.P().Class("p-card__content").Text("Cyber Acid 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."),
524 | ),
525 | app.Div().Class("p-card").Body(
526 | app.H3().Text("User-Generated Content"),
527 | app.P().Class("p-card__content").Text("All published content is user-generated, fictional and creators are not responsible for it."),
528 | ),
529 | ),
530 | ),
531 | app.Li().Class("p-accordion__group").Body(
532 | app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
533 | app.Button().Type("button").Class("p-accordion__tab").ID("tab7").Aria("controls", "tab7-section").Aria("expanded", true).Text("Privacy policy").Value("tab7-section").OnClick(a.toggleAccordion),
534 | ),
535 | app.Section().Class("p-accordion__panel").ID("tab7-section").Aria("hidden", true).Aria("labelledby", "tab7").Body(
536 | app.Div().Class("p-card").Body(
537 | app.H3().Text("Personal data"),
538 | app.P().Class("p-card__content").Text("There is no personal information collected within Cyber Acid. We store a small portion of your peer ID encrypted as a non-unique identifier which is used for displaying the ranks interface."),
539 | ),
540 | app.Div().Class("p-card").Body(
541 | app.H3().Text("Coookies"),
542 | app.P().Class("p-card__content").Text("Cyber Acid does not use cookies."),
543 | ),
544 | app.Div().Class("p-card").Body(
545 | app.H3().Text("Links to Cyber Stasis"),
546 | app.P().Class("p-card__content").Text("Cyber Acid contains links to its sister project Cyber Stasis and depends on its data to function properly."),
547 | ),
548 | app.Div().Class("p-card").Body(
549 | app.H3().Text("Changes to this privacy policy"),
550 | 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."),
551 | ),
552 | ),
553 | ),
554 | ),
555 | ),
556 | ),
557 | ).Style("left", "10%").Style("width", "80%"),
558 | ),
559 | app.Div().Class("p-modal").ID("rankings-modal").Style("display", "none").Body(
560 | app.Section().Class("p-modal__dialog").Role("dialog").Aria("modal", true).Aria("labelledby", "modal-title").Aria("describedby", "modal-description").Body(
561 | app.Header().Class("p-modal__header").Body(
562 | app.H2().Class("p-modal__title").ID("modal-title").Text("Delegate rankings"),
563 | app.Button().Class("p-modal__close").Aria("label", "Close active modal").Aria("controls", "modal").OnClick(a.closeRankingsModal),
564 | ),
565 | app.Table().Aria("label", "Rankings table").Class("p-main-table").Body(
566 | app.THead().Body(
567 | app.Tr().Body(
568 | app.Th().Body(
569 | app.Span().Class("status-icon is-blocked").Text("Delegate ID"),
570 | ),
571 | app.Th().Text("Trust"),
572 | ),
573 | ),
574 | app.If(len(a.delegates) > 0, func() app.UI {
575 | return app.TBody().Body(
576 | app.Range(a.delegates).Slice(func(i int) app.UI {
577 | return app.Tr().DataSet("id", i).Body(
578 | app.Td().DataSet("column", "delegate").Body(
579 | app.Div().Text(a.delegates[i].CitizenID),
580 | ),
581 | app.Td().DataSet("column", "trust").Body(
582 | app.Div().Text(a.delegates[i].Selected),
583 | ),
584 | )
585 | }),
586 | )
587 | }),
588 | ),
589 | ).Style("left", "10%").Style("width", "80%"),
590 | ),
591 | ),
592 | ),
593 | ),
594 | app.Aside().Class("l-aside is-collapsed").ID("aside-panel").Body(
595 | app.Div().Class("p-panel").Body(
596 | app.Div().Class("p-panel__header").Body(
597 | app.H4().Class("p-panel__title").Text(a.AsideTitle),
598 | app.Div().Class("p-panel__controls").Body(
599 | app.Button().Class("p-button--base u-no-margin--bottom has-icon").Body(app.I().Class("p-icon--close")).OnClick(a.asideClose),
600 | ),
601 | ),
602 | app.If(a.AsideTitle == asideTitleCreate, func() app.UI {
603 | return app.Div().Class("p-panel__content").Body(
604 | app.Div().Class("p-form p-form--stacked").Body(
605 | app.Div().Class("p-form__group row").Body(
606 | app.Textarea().ID("solution").Name("solution").Rows(3).OnKeyUp(a.onSolution),
607 | ),
608 | ),
609 | app.Div().Class("row").Body(
610 | app.Div().Class("col-12").Body(
611 | app.Button().Class("p-button--positive u-float-right").Name("submit-solution").Text("Submit Solution").OnClick(a.submitSolution),
612 | ),
613 | ),
614 | )
615 | }).ElseIf(a.AsideTitle == asideTitleList, func() app.UI {
616 | return app.Div().Class("p-panel__content").Body(
617 | app.Ul().Class("p-list-tree").Aria("multiselectable", true).Role("tree").Body(
618 | app.Li().Class("p-list-tree__item p-list-tree__item--group").Role("treeitem").Body(
619 | app.Button().Class("p-list-tree__toggle").ID("sub-1-btn").Aria("controls", "sub-1").Aria("expanded", true).Text("Suggested Solutions"),
620 | app.Ul().Class("p-list-tree").Role("group").ID("sub-1").Aria("hidden", false).Aria("labelledby", "sub-1-btn").Body(
621 | app.Range(a.Solutions).Slice(func(i int) app.UI {
622 | return app.Li().Class("p-list-tree__item").Role("treeitem").Body(
623 | app.P().Text(a.Solutions[i].Description),
624 | app.If(len(a.issues[a.currentIssueInSlice].Voters) > 0, func() app.UI {
625 | return app.If(sliceContains(a.issues[a.currentIssueInSlice].Voters, a.citizenID), func() app.UI {
626 | return app.Button().
627 | Class("p-button is-small is-inline").
628 | Text("Vote").
629 | Value(a.Solutions[i].ID).
630 | OnClick(a.vote).
631 | Disabled(true).
632 | Body(
633 | app.I().Class("fa-solid fa-thumbs-up"), // Icon
634 | app.Span().
635 | Class("p-badge").
636 | Aria("label", strconv.Itoa(a.Solutions[i].Votes)+" votes").
637 | Text(strconv.Itoa(a.Solutions[i].Votes)), // Votes count
638 | )
639 | }).Else( func() app.UI {
640 | return app.Div().Class("vote-delegate-container").Body(
641 | // Vote Button with Icon
642 | app.Button().
643 | Class("p-button is-small is-inline").
644 | Text("Vote").
645 | Value(a.Solutions[i].ID).
646 | OnClick(a.vote).
647 | Body(
648 | app.I().Class("fa-regular fa-thumbs-up"), // Icon for Vote
649 | ),
650 |
651 | // Delegate Button
652 | app.Button().
653 | Class("p-button is-small is-inline").
654 | ID("show-modal").
655 | Text("Delegate...").
656 | Aria("controls", "modal").
657 | Value(a.Solutions[i].ID).
658 | OnClick(a.openDelegateDialog),
659 |
660 | // Modal for Delegation
661 | app.Div().
662 | Class("p-modal").
663 | ID("delegate-modal").
664 | Style("display", "none"). // Initially hidden
665 | Body(
666 | app.Section().
667 | Class("p-modal__dialog").
668 | Role("dialog").
669 | Aria("modal", true).
670 | Aria("labelledby", "modal-title").
671 | Aria("describedby", "modal-description").
672 | Body(
673 | app.Header().
674 | Class("p-modal__header").
675 | Body(
676 | app.H2().
677 | Class("p-modal__title").
678 | ID("modal-title").
679 | Text("Delegate"),
680 | app.Button().
681 | Class("p-modal__close").
682 | Aria("label", "Close active modal").
683 | Aria("controls", "modal").
684 | Value(a.Solutions[i].ID).
685 | OnClick(a.closeDelegateModal),
686 | ),
687 | app.P().
688 | ID("modal-description").
689 | Text("Select a citizen to represent your vote for this issue:"),
690 | app.Div().
691 | Class("p-heading-icon--small").
692 | Body(
693 | app.Range(a.ranks).Slice(func(i int) app.UI {
694 | return app.If(a.ranks[i].CitizenID != a.citizenID, func() app.UI {
695 | return app.Div().Class("p-heading-icon__header").Body(
696 | app.Button().
697 | Class("p-chip").
698 | Aria("pressed", true).
699 | Disabled(true).
700 | Body(
701 | app.Span().Class("p-chip__value").Text("Citizen"),
702 | app.Span().Class("p-badge").Aria("label", "Citizen").Text(a.ranks[i].CitizenID),
703 | ),
704 | app.Button().
705 | Class("p-chip").
706 | Aria("pressed", true).
707 | Disabled(true).
708 | Body(
709 | app.Span().Class("p-chip__value").Text("Reputation"),
710 | app.Span().Class("p-badge").Aria("label", "Reputation").Text(a.ranks[i].ReputationIndex),
711 | ),
712 | app.Button().
713 | Class("p-chip").
714 | Body(
715 | app.Span().Class("p-chip__value").Text("Select"),
716 | ).Value(a.ranks[i].CitizenID).OnClick(a.delegate),
717 | )
718 | })
719 | }),
720 | ),
721 | ),
722 | ),
723 | )
724 | })
725 | }).Else( func() app.UI {
726 | return app.Div().Class("vote-delegate-container").Body(
727 | // Vote Button with Icon
728 | app.Button().
729 | Class("p-button is-small is-inline").
730 | Text("Vote").
731 | Value(a.Solutions[i].ID).
732 | OnClick(a.vote).
733 | Body(
734 | app.I().Class("fa-regular fa-thumbs-up"), // Icon for Vote
735 | ),
736 |
737 | // Delegate Button
738 | app.Button().
739 | Class("p-button is-small is-inline").
740 | ID("show-modal").
741 | Text("Delegate...").
742 | Aria("controls", "modal").
743 | Value(a.Solutions[i].ID).
744 | OnClick(a.openDelegateDialog),
745 |
746 | // Modal for Delegation
747 | app.Div().
748 | Class("p-modal").
749 | ID("delegate-modal").
750 | Style("display", "none"). // Initially hidden
751 | Body(
752 | app.Section().
753 | Class("p-modal__dialog").
754 | Role("dialog").
755 | Aria("modal", true).
756 | Aria("labelledby", "modal-title").
757 | Aria("describedby", "modal-description").
758 | Body(
759 | app.Header().
760 | Class("p-modal__header").
761 | Body(
762 | app.H2().
763 | Class("p-modal__title").
764 | ID("modal-title").
765 | Text("Delegate"),
766 | app.Button().
767 | Class("p-modal__close").
768 | Aria("label", "Close active modal").
769 | Aria("controls", "modal").
770 | Value(a.Solutions[i].ID).
771 | OnClick(a.closeDelegateModal),
772 | ),
773 | app.P().
774 | ID("modal-description").
775 | Text("Select a citizen to represent your vote for this issue:"),
776 | app.Div().
777 | Class("p-heading-icon--small").
778 | Body(
779 | app.Range(a.ranks).Slice(func(i int) app.UI {
780 | return app.If(a.ranks[i].CitizenID != a.citizenID, func() app.UI {
781 | return app.Div().Class("p-heading-icon__header").Body(
782 | app.Button().
783 | Class("p-chip").
784 | Aria("pressed", true).
785 | Disabled(true).
786 | Body(
787 | app.Span().Class("p-chip__value").Text("Citizen"),
788 | app.Span().Class("p-badge").Aria("label", "Citizen").Text(a.ranks[i].CitizenID),
789 | ),
790 | app.Button().
791 | Class("p-chip").
792 | Aria("pressed", true).
793 | Disabled(true).
794 | Body(
795 | app.Span().Class("p-chip__value").Text("Reputation"),
796 | app.Span().Class("p-badge").Aria("label", "Reputation").Text(a.ranks[i].ReputationIndex),
797 | ),
798 | app.Button().
799 | Class("p-chip").
800 | Body(
801 | app.Span().Class("p-chip__value").Text("Select"),
802 | ).Value(a.ranks[i].CitizenID).OnClick(a.delegate),
803 | )
804 | })
805 | }),
806 | ),
807 | ),
808 | ),
809 | )
810 | }),
811 | )
812 | }),
813 | ),
814 | ),
815 | ),
816 | )
817 | }),
818 | ),
819 | ),
820 | )
821 | }
822 |
823 | func (a *acid) asidePreloadList(ctx app.Context, e app.Event) {
824 | issueID := ctx.JSSrc().Get("value").String()
825 | issueIDInt, err := strconv.Atoi(issueID)
826 | if err != nil {
827 | log.Fatal(err)
828 | }
829 | a.currentIssueInSlice = issueIDInt - 1
830 | a.Solutions = a.issues[issueIDInt-1].Solutions
831 | a.AsideTitle = asideTitleList
832 | }
833 |
834 | func (a *acid) asideOpenList(ctx app.Context, e app.Event) {
835 | app.Window().Get("document").Call("querySelector", ".l-aside").Get("classList").Call("remove", "is-collapsed")
836 | }
837 |
838 | func (a *acid) asidePreloadCreate(ctx app.Context, e app.Event) {
839 | a.AsideTitle = asideTitleCreate
840 | }
841 |
842 | func (a *acid) asideOpenCreate(ctx app.Context, e app.Event) {
843 | app.Window().Get("document").Call("querySelector", ".l-aside").Get("classList").Call("remove", "is-collapsed")
844 | app.Window().Get("document").Call("querySelector", ".p-button--positive").Call("setAttribute", "id", ctx.JSSrc().Get("value").String())
845 | }
846 |
847 | func (a *acid) asideClose(ctx app.Context, e app.Event) {
848 | app.Window().Get("document").Call("querySelector", ".l-aside").Get("classList").Call("add", "is-collapsed")
849 | }
850 |
851 | func (a *acid) onSolution(ctx app.Context, e app.Event) {
852 | a.currentSolutionDescription = ctx.JSSrc().Get("value").String()
853 | }
854 |
855 | func (a *acid) vote(ctx app.Context, e app.Event) {
856 | ctx.JSSrc().Get("firstChild").Get("classList").Call("remove", "fa-regular")
857 |
858 | val := ctx.JSSrc().Get("value").String()
859 | solutionID, err := strconv.Atoi(val)
860 | if err != nil {
861 | log.Fatal(err)
862 | }
863 |
864 | currentIssue := a.issues[a.currentIssueInSlice]
865 | var delegate bool
866 | var delegatedVotes int
867 | var ownVote bool
868 | // delegated voting logic
869 | for i, d := range currentIssue.Delegates {
870 | if a.citizenID == d.CitizenID {
871 | delegate = true
872 | ownVote = currentIssue.Delegates[i].OwnVote
873 | if !d.OwnVote {
874 | currentIssue.Delegates[i].OwnVote = true
875 |
876 | }
877 | delegatedVotes = d.Votes
878 | currentIssue.Delegates[i].Votes = 0
879 | }
880 | }
881 |
882 | currentIssue.Voters = append(currentIssue.Voters, a.citizenID)
883 |
884 | if delegate {
885 | if !ownVote {
886 | currentIssue.Solutions[solutionID-1].Votes += delegatedVotes + 1
887 | } else {
888 | currentIssue.Solutions[solutionID-1].Votes += delegatedVotes
889 | }
890 |
891 | } else {
892 | currentIssue.Solutions[solutionID-1].Votes++
893 | }
894 |
895 | i, err := json.Marshal(currentIssue)
896 | if err != nil {
897 | log.Fatal(err)
898 | }
899 | ctx.Async(func() {
900 | err = a.sh.OrbitDocsPut(dbNameIssue, i)
901 | if err != nil {
902 | ctx.Dispatch(func(ctx app.Context) {
903 | a.createNotification(ctx, NotificationDanger, ErrorHeader, "Could not vote for solution. Try again later.")
904 | log.Fatal(err)
905 | })
906 | }
907 | err = a.sh.PubSubPublish(topicIssue, string(i))
908 | if err != nil {
909 | log.Fatal(err)
910 | }
911 | ctx.Dispatch(func(ctx app.Context) {
912 | a.issues[a.currentIssueInSlice] = currentIssue
913 | a.createNotification(ctx, NotificationSuccess, SuccessHeader, "Vote accepted.")
914 | })
915 | })
916 | }
917 |
918 | func (a *acid) openRankingsDialog(ctx app.Context, e app.Event) {
919 | for _, i := range a.issues {
920 | a.delegates = append(a.delegates, i.Delegates...)
921 | }
922 | sort.SliceStable(a.delegates, func(i, j int) bool {
923 | return a.delegates[i].Selected > a.delegates[j].Selected
924 | })
925 | app.Window().GetElementByID("rankings-modal").Set("style", "display:flex")
926 | }
927 |
928 | func (a *acid) openHowToDialog(ctx app.Context, e app.Event) {
929 | app.Window().GetElementByID("howto-modal").Set("style", "display:flex")
930 | }
931 |
932 | func (a *acid) openDelegateDialog(ctx app.Context, e app.Event) {
933 | app.Window().GetElementByID("delegate-modal").Set("style", "display:flex")
934 | }
935 |
936 | func (a *acid) delegate(ctx app.Context, e app.Event) {
937 | citizenID := ctx.JSSrc().Get("value").String()
938 | issue := a.issues[a.currentIssueInSlice]
939 |
940 | var delegateExists bool
941 | var delegate Delegate
942 | votesTransfer := 1
943 | if len(issue.Delegates) > 0 {
944 | for ii, dd := range issue.Delegates {
945 | // recursive delegation logic
946 | if dd.CitizenID == a.citizenID {
947 | // transfer origin votes to recipient plus own vote
948 | votesTransfer = dd.Votes + 1
949 | // set origin delegator's votes to zero
950 | issue.Delegates[ii].Votes = 0
951 | // set origin delegator as voted
952 | issue.Delegates[ii].OwnVote = true
953 | }
954 | }
955 |
956 | for i, d := range issue.Delegates {
957 | if d.CitizenID == citizenID {
958 | issue.Delegates[i].Votes += votesTransfer
959 | issue.Delegates[i].Selected++
960 | delegateExists = true
961 | }
962 | }
963 | }
964 |
965 | if len(issue.Delegates) == 0 || !delegateExists {
966 | delegate = Delegate{
967 | CitizenID: citizenID,
968 | Votes: votesTransfer,
969 | Selected: 1,
970 | }
971 | issue.Delegates = append(issue.Delegates, delegate)
972 | }
973 | issue.Voters = append(issue.Voters, a.citizenID)
974 | var voters []string
975 | for _, v := range issue.Voters {
976 | // if the delegate already voted previously remove from voters so he can vote again on new delegation
977 | if citizenID != v {
978 | voters = append(voters, v)
979 | }
980 | }
981 | issue.Voters = voters
982 |
983 | i, err := json.Marshal(issue)
984 | if err != nil {
985 | log.Fatal(err)
986 | }
987 |
988 | ctx.Async(func() {
989 | err = a.sh.OrbitDocsPut(dbNameIssue, i)
990 | if err != nil {
991 | log.Fatal(err)
992 | }
993 |
994 | err = a.sh.PubSubPublish(topicIssue, string(i))
995 | if err != nil {
996 | log.Fatal(err)
997 | }
998 |
999 | ctx.Dispatch(func(ctx app.Context) {
1000 | a.issues[a.currentIssueInSlice] = issue
1001 | a.closeDelegateModal(ctx, e)
1002 | a.createNotification(ctx, NotificationSuccess, SuccessHeader, "Vote delegated.")
1003 | })
1004 | })
1005 | }
1006 |
1007 | func (a *acid) toggleAccordion(ctx app.Context, e app.Event) {
1008 | id := ctx.JSSrc().Get("value").String()
1009 | attr := app.Window().GetElementByID(id).Get("attributes")
1010 | aria := attr.Get("aria-hidden").Get("value").String()
1011 | if aria == "false" {
1012 | app.Window().GetElementByID(id).Call("setAttribute", "aria-hidden", "true")
1013 | } else {
1014 | app.Window().GetElementByID(id).Call("setAttribute", "aria-hidden", "false")
1015 | }
1016 | }
1017 |
1018 | func (a *acid) closeRankingsModal(ctx app.Context, e app.Event) {
1019 | app.Window().GetElementByID("rankings-modal").Set("style", "display:none")
1020 | }
1021 |
1022 | func (a *acid) closeHowToModal(ctx app.Context, e app.Event) {
1023 | app.Window().GetElementByID("howto-modal").Set("style", "display:none")
1024 | }
1025 |
1026 | func (a *acid) closeDelegateModal(ctx app.Context, e app.Event) {
1027 | app.Window().GetElementByID("delegate-modal").Set("style", "display:none")
1028 | }
1029 |
1030 | func (a *acid) submitSolution(ctx app.Context, e app.Event) {
1031 | idStr := ctx.JSSrc().Get("id").String()
1032 | id, err := strconv.Atoi(idStr)
1033 | if err != nil {
1034 | log.Fatal(err)
1035 | }
1036 |
1037 | lastSolutionID := 0
1038 | unique := true
1039 | if len(a.issues[id-1].Solutions) > 0 {
1040 | solutions := a.issues[id-1].Solutions
1041 | for n, s := range solutions {
1042 | if s.Description == a.currentSolutionDescription {
1043 | unique = false
1044 | }
1045 | if n > 0 {
1046 | currentID, err := strconv.Atoi(s.ID)
1047 | if err != nil {
1048 | log.Fatal(err)
1049 | }
1050 | previousID, err := strconv.Atoi(solutions[n-1].ID)
1051 | if err != nil {
1052 | log.Fatal(err)
1053 | }
1054 | if currentID > previousID {
1055 | lastSolutionID = currentID
1056 | }
1057 | } else {
1058 | lastSolutionID = 1
1059 | }
1060 | }
1061 | }
1062 |
1063 | if unique {
1064 | solution := Solution{
1065 | ID: strconv.Itoa(lastSolutionID + 1),
1066 | Description: a.currentSolutionDescription,
1067 | Votes: 0,
1068 | }
1069 |
1070 | a.issues[id-1].Solutions = append(a.issues[id-1].Solutions, solution)
1071 |
1072 | i, err := json.Marshal(a.issues[id-1])
1073 | if err != nil {
1074 | log.Fatal(err)
1075 | }
1076 |
1077 | ctx.Async(func() {
1078 | err = a.sh.OrbitDocsPut(dbNameIssue, i)
1079 | if err != nil {
1080 | ctx.Dispatch(func(ctx app.Context) {
1081 | a.createNotification(ctx, NotificationDanger, ErrorHeader, "Could not create solution. Try again later.")
1082 | log.Fatal(err)
1083 | })
1084 | }
1085 | err = a.sh.PubSubPublish(topicIssue, string(i))
1086 | if err != nil {
1087 | log.Fatal(err)
1088 | }
1089 |
1090 | ctx.Dispatch(func(ctx app.Context) {
1091 | app.Window().Get("document").Call("querySelector", ".l-aside").Get("classList").Call("add", "is-collapsed")
1092 | a.createNotification(ctx, NotificationSuccess, SuccessHeader, "Solution submited.")
1093 | })
1094 | })
1095 | }
1096 | }
1097 |
1098 | func (a *acid) createNotification(ctx app.Context, s NotificationStatus, h string, msg string) {
1099 | a.notificationID++
1100 | a.notifications[strconv.Itoa(a.notificationID)] = notification{
1101 | id: a.notificationID,
1102 | status: string(s),
1103 | header: h,
1104 | message: msg,
1105 | }
1106 |
1107 | ntfs := a.notifications
1108 | ctx.Async(func() {
1109 | for n := range ntfs {
1110 | time.Sleep(5 * time.Second)
1111 | delete(ntfs, n)
1112 | ctx.Async(func() {
1113 | ctx.Dispatch(func(ctx app.Context) {
1114 | a.notifications = ntfs
1115 | })
1116 | })
1117 | }
1118 | })
1119 | }
1120 |
1121 | // https://play.golang.org/p/Qg_uv_inCek
1122 | // contains checks if a string is present in a slice
1123 | func sliceContains(s []string, str string) bool {
1124 | for _, v := range s {
1125 | if v == str {
1126 | return true
1127 | }
1128 | }
1129 |
1130 | return false
1131 | }
1132 |
1133 | // The main function is the entry point where the app is configured and started.
1134 | // It is executed in 2 different environments: A client (the web browser) and a
1135 | // server.
1136 | func main() {
1137 | // The first thing to do is to associate the hello component with a path.
1138 | //
1139 | // This is done by calling the Route() function, which tells go-app what
1140 | // component to display for a given path, on both client and server-side.
1141 | app.Route("/", func() app.Composer{
1142 | return &acid{}
1143 | })
1144 |
1145 | // Once the routes set up, the next thing to do is to either launch the app
1146 | // or the server that serves the app.
1147 | //
1148 | // When executed on the client-side, the RunWhenOnBrowser() function
1149 | // launches the app, starting a loop that listens for app events and
1150 | // executes client instructions. Since it is a blocking call, the code below
1151 | // it will never be executed.
1152 | //
1153 | // When executed on the server-side, RunWhenOnBrowser() does nothing, which
1154 | // lets room for server implementation without the need for precompiling
1155 | // instructions.
1156 | app.RunWhenOnBrowser()
1157 |
1158 | // Finally, launching the server that serves the app is done by using the Go
1159 | // standard HTTP package.
1160 | //
1161 | // The Handler is an HTTP handler that serves the client and all its
1162 | // required resources to make it work into a web browser. Here it is
1163 | // configured to handle requests with a path that starts with "/".
1164 |
1165 | withGz := gziphandler.GzipHandler(&app.Handler{
1166 | Name: "cyber-acid",
1167 | Description: "Cyber Acid - Liquid democracy politics simulator based on personal reputation index",
1168 | Styles: []string{
1169 | "https://assets.ubuntu.com/v1/vanilla-framework-version-3.8.0.min.css",
1170 | "https://use.fontawesome.com/releases/v6.2.0/css/all.css",
1171 | },
1172 | Scripts: []string{},
1173 | })
1174 | http.Handle("/", withGz)
1175 |
1176 | if err := http.ListenAndServe(":7000", nil); err != nil {
1177 | log.Fatal(err)
1178 | }
1179 | }
1180 |
--------------------------------------------------------------------------------