Initial commit for Seekia Version 0.50. For earlier history, download the SeekiaGitHistory-v0.50.bundle file. Instructions for doing this are available at the bottom of the Changelog.md file.
This commit is contained in:
commit
63a27d2fb8
4053 changed files with 276722 additions and 0 deletions
73
Changelog.md
Normal file
73
Changelog.md
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
This document attempts to describe the history of changes to Seekia.
|
||||||
|
|
||||||
|
Small and insignificant changes may not be included in this log.
|
||||||
|
|
||||||
|
## Before Version 0.50
|
||||||
|
|
||||||
|
After Version 0.50, I deleted all Git history to start fresh.
|
||||||
|
|
||||||
|
Part of the reason for doing this was to reduce the compressed size of the Git repository from ~14MB to ~6 MB.
|
||||||
|
|
||||||
|
All changes to Seekia preceding Version 0.50 are available in the Seekia v0.50 Git History bundle file.
|
||||||
|
|
||||||
|
### Download Git History File
|
||||||
|
|
||||||
|
To get the file, you must download it from the IPFS network.
|
||||||
|
|
||||||
|
**File name:**
|
||||||
|
|
||||||
|
`SeekiaGitHistory-v0.50.bundle`
|
||||||
|
|
||||||
|
**File SHA-256 Checksum:**
|
||||||
|
|
||||||
|
`91edbccf32d90abe9670aeb5aed800d21c3c1ec94f0791df2a9f443ff6b85f17`
|
||||||
|
|
||||||
|
**File IPFS Content ID:**
|
||||||
|
|
||||||
|
`QmPbDpE9UcNbKFkr1uZQN8JrR3RPr6khJyBdwg7kJuHsNh`
|
||||||
|
|
||||||
|
**IPFS Gateway Download Link:**
|
||||||
|
|
||||||
|
[ipfs.io/ipfs/bafybeiass5adiqiw65i67ox3jt3rx3r54h5rhvwrepd2rbjp2ybftww6ci](https://ipfs.io/ipfs/bafybeiass5adiqiw65i67ox3jt3rx3r54h5rhvwrepd2rbjp2ybftww6ci)
|
||||||
|
|
||||||
|
### Verify Git History File Signature
|
||||||
|
|
||||||
|
**File PGP Signature:**
|
||||||
|
|
||||||
|
```
|
||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQIkBAAWCAHMFiEEESzO4XzetwSe5Wii+FQEUgumnSwFAmYWeWfA7SYAmDMEZH98
|
||||||
|
qBYJKwYBBAHaRw8BAQdAwEUyZVL64eDCk4KUWn6FNBB6Ruufmfl9yiO7QbRXIo+0
|
||||||
|
H1NpbW9uIFNhcmFzb3ZhIDxzaW1vbkBzYXJhc292YT6ImQQTFgoAQRYhBBEszuF8
|
||||||
|
3rcEnuVoovhUBFILpp0sBQJkf3yoAhsDBQkDwmcABQsJCAcCAiICBhUKCQgLAgQW
|
||||||
|
AgMBAh4HAheAAAoJEPhUBFILpp0shhEA/2DtSWWuhKZ7bpawKSkACQn5dZT/J6cm
|
||||||
|
GchXsyvidqfWAQD+Hm7eKnKCat8aina43gfNflXjLOONHRgZIOdbUtytDLg4BGR/
|
||||||
|
fKgSCisGAQQBl1UBBQEBB0AAVG4K4ZcExTjPuGmPwCTVphvzezrm33W021KOJUpV
|
||||||
|
bwMBCAeIfgQYFgoAJhYhBBEszuF83rcEnuVoovhUBFILpp0sBQJkf3yoAhsMBQkD
|
||||||
|
wmcAAAoJEPhUBFILpp0sDEQA/0ddlnCH2dn2h0ctZL3hyP7FG+uHESAwG12AiiQE
|
||||||
|
LxhsAP9py6k+vLCaOpj3M2qxR+XZdpENwWr8stqYnWRa1JlFBgAKCRD4VARSC6ad
|
||||||
|
LAUXAQDTeq5kX5amMHgVh0sbW03iub7P9Dx3hPxHk2mOCsIm/wD+IqGhvUWmfSMP
|
||||||
|
j8LTstfvqJ4YleEaNCXidI3JuVheTAI=
|
||||||
|
=74o/
|
||||||
|
-----END PGP SIGNATURE-----
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify PGP Signature:**
|
||||||
|
|
||||||
|
Save the above signature in a file called `SeekiaGitHistory-v0.50.bundle.asc`
|
||||||
|
|
||||||
|
Verify the signature by running the following command in a terminal:
|
||||||
|
|
||||||
|
`gpg --verify SeekiaGitHistory-v0.50.bundle.asc SeekiaGitHistory-v0.50.bundle`
|
||||||
|
|
||||||
|
Simon Sarasova's PGP Signing key: `112CCEE17CDEB7049EE568A2F85404520BA69D2C`
|
||||||
|
|
||||||
|
### Unpack Git History File
|
||||||
|
|
||||||
|
Upon downloading the file, run the following command in a terminal to extract the bundle and browse its contents:
|
||||||
|
|
||||||
|
`git clone SeekiaGitHistory-v0.50.bundle`
|
||||||
|
|
12
Contributors.md
Normal file
12
Contributors.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# Contributors
|
||||||
|
|
||||||
|
This document describes the contributors of Seekia.
|
||||||
|
|
||||||
|
Contributors are people who have committed code to the Seekia codebase.
|
||||||
|
|
||||||
|
Many other people have written code for modules which are imported by Seekia. They are not listed here.
|
||||||
|
|
||||||
|
|
||||||
|
Name | Date Of First Commit | Number Of Commits
|
||||||
|
--- | --- | ---
|
||||||
|
Simon Sarasova | June 13, 2023 | 227
|
118
Imports.md
Normal file
118
Imports.md
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
|
||||||
|
# Imports
|
||||||
|
|
||||||
|
Seekia is released into the public domain using the Unlicense. (Available at `/Licenses/Unlicense.md`)
|
||||||
|
|
||||||
|
I encourage copying Seekia's code and ideas, for free and commercial uses, with or without attribution.
|
||||||
|
|
||||||
|
The adoption of race and genetics aware mate discovery technology is profoundly important for humanity.
|
||||||
|
|
||||||
|
Seekia includes within it content that originates from other sources.
|
||||||
|
|
||||||
|
Below are the included pieces of content, along with their licenses.
|
||||||
|
|
||||||
|
The licenses are stored in the `Licenses` folder.
|
||||||
|
|
||||||
|
## Golang
|
||||||
|
|
||||||
|
Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.
|
||||||
|
|
||||||
|
[golang.org](https://golang.org)
|
||||||
|
|
||||||
|
## Fyne
|
||||||
|
|
||||||
|
Fyne is an easy-to-use UI toolkit and app API.
|
||||||
|
|
||||||
|
[fyne.io](https://fyne.io)
|
||||||
|
|
||||||
|
## CIRCL
|
||||||
|
|
||||||
|
CIRCL is a collection of cryptographic primitives.
|
||||||
|
|
||||||
|
[github.com/cloudflare/circl](https://github.com/cloudflare/circl)
|
||||||
|
|
||||||
|
## BadgerDB
|
||||||
|
|
||||||
|
BadgerDB is an embeddable, persistent and fast key-value database.
|
||||||
|
|
||||||
|
[github.com/dgraph-io/badger](https://github.com/dgraph-io/badger)
|
||||||
|
|
||||||
|
## go-effects
|
||||||
|
|
||||||
|
Parallelized image manipulation effects.
|
||||||
|
|
||||||
|
[github.com/markdaws/go-effects](https://github.com/markdaws/go-effects)
|
||||||
|
|
||||||
|
## Gift
|
||||||
|
|
||||||
|
Gift provides a set of useful image processing filters.
|
||||||
|
|
||||||
|
[github.com/disintegration/gift](https://github.com/disintegration/gift)
|
||||||
|
|
||||||
|
## Geodist
|
||||||
|
|
||||||
|
A package to calculate distance between latitude/longitude points.
|
||||||
|
|
||||||
|
[github.com/jftuga/geodist](https://github.com/jftuga/geodist)
|
||||||
|
|
||||||
|
## Countries States Cities Database
|
||||||
|
|
||||||
|
A package that contains names, locations and other information about world countries, cities, and states.
|
||||||
|
|
||||||
|
[github.com/dr5hn/countries-states-cities-database/](https://github.com/dr5hn/countries-states-cities-database/)
|
||||||
|
|
||||||
|
## Openmoji
|
||||||
|
|
||||||
|
An open-source emoji and icon project.
|
||||||
|
|
||||||
|
[openmoji.org](https://openmoji.org)
|
||||||
|
|
||||||
|
## go-ethereum
|
||||||
|
|
||||||
|
The official Golang implementation of the Ethereum protocol.
|
||||||
|
|
||||||
|
[geth.ethereum.org](https://geth.ethereum.org)
|
||||||
|
|
||||||
|
## Blake3
|
||||||
|
|
||||||
|
Pure Go implementation of the BLAKE3 hash function.
|
||||||
|
|
||||||
|
[github.com/zeebo/blake3](https://github.com/zeebo/blake3)
|
||||||
|
|
||||||
|
## webp
|
||||||
|
|
||||||
|
Golang Webp library for encoding and decoding, using C binding for Google libwebp.
|
||||||
|
|
||||||
|
[github.com/chai2010/webp](https://github.com/chai2010/webp)
|
||||||
|
|
||||||
|
## msgpack
|
||||||
|
|
||||||
|
MessagePack encoder and decoder.
|
||||||
|
|
||||||
|
[github.com/vmihailenco/msgpack](https://github.com/vmihailenco/msgpack)
|
||||||
|
|
||||||
|
## Charts
|
||||||
|
|
||||||
|
A simple charting library that supports timeseries and continuous line charts.
|
||||||
|
|
||||||
|
[github.com/wcharczuk/go-chart](https://github.com/wcharczuk/go-chart)
|
||||||
|
|
||||||
|
## oksvg
|
||||||
|
|
||||||
|
oksvg is a rasterizer for a partial implementation of the SVG2.0 specification.
|
||||||
|
|
||||||
|
[github.com/srwiley/oksvg](https://github.com/srwiley/oksvg)
|
||||||
|
|
||||||
|
## btcd
|
||||||
|
|
||||||
|
btcd is an alternative full node bitcoin implementation written in Go.
|
||||||
|
|
||||||
|
[github.com/btcsuite/btcd](https://github.com/btcsuite/btcd)
|
||||||
|
|
||||||
|
## Gorgonia
|
||||||
|
|
||||||
|
Gorgonia is a library that helps facilitate machine learning in Go.
|
||||||
|
|
||||||
|
[gorgonia.org](https://gorgonia.org)
|
||||||
|
|
||||||
|
|
134
ReadMe.md
Normal file
134
ReadMe.md
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
# Seekia
|
||||||
|
|
||||||
|
![Seekia Banner](./resources/markdownImages/seekiaLogoWithSubtitle.jpg)
|
||||||
|
|
||||||
|
## What is Seekia?
|
||||||
|
|
||||||
|
![Seekia Homepage](./resources/markdownImages/seekiaHomepage.jpg)
|
||||||
|
|
||||||
|
*Cure racial loneliness. Beautify the human species. Seekia: Be race aware.*
|
||||||
|
|
||||||
|
**Seekia is a race aware mate discovery network.**
|
||||||
|
|
||||||
|
Seekia is a mate discovery network where users can find a mate while having a deep awareness of each potential partner's race.
|
||||||
|
|
||||||
|
Users can share racial information in their profiles such as their eye, skin, and hair color; hair texture; genetic ancestry; haplogroups; and the alleles of their genes which effect physical traits.
|
||||||
|
|
||||||
|
Seekia enables users to browse and filter potential mates by their racial attributes. Seekia can also calculate the racial characteristics for prospective offspring between users. Seekia allows for users to predict and control the race of their offspring by selecting a mate who is the most capable and likely to produce offspring of their desired race.
|
||||||
|
|
||||||
|
Seekia aims to cure racial loneliness by helping users to find members of their own race to mate with.
|
||||||
|
|
||||||
|
Seekia also provides users with the ability to mate in a genetics aware manner.
|
||||||
|
|
||||||
|
Users can view information about the health and physical traits of their prospective offspring for each user.
|
||||||
|
|
||||||
|
Seekia aims to improve the genetic quality of humanity by making humans healthier, more beautiful, and more intelligent. Seekia aims to facilitate eugenic breeding by helping to create mate pairings which are the most likely to produce healthy, beautiful, and intelligent offspring.
|
||||||
|
|
||||||
|
Users can choose to mate with users with whom their offspring has a lower probability of having diseases and a higher probability of having certain traits.
|
||||||
|
|
||||||
|
The goal of Seekia is to accelerate the world's adoption of race and genetics aware mate discovery technology, and to help the world mate in a race and genetics aware manner.
|
||||||
|
|
||||||
|
### Learn More
|
||||||
|
|
||||||
|
Access Seekia's clearnet website at [Seekia.net](https://seekia.net).
|
||||||
|
|
||||||
|
Access Seekia's Ethereum IPFS ENS website at [Seekia.eth](ipns://seekia.eth). This site can be accessed through Brave Browser.
|
||||||
|
|
||||||
|
Access Seekia's Tor website at [seekia77v2rqfp4i4flavj425txtqjpn2yldadngdr45fjitr72fakid.onion](http://seekia77v2rqfp4i4flavj425txtqjpn2yldadngdr45fjitr72fakid.onion).
|
||||||
|
|
||||||
|
Read the whitepaper at `/documentation/Whitepaper.pdf`
|
||||||
|
|
||||||
|
Read the documentation at `/documentation/Documentation.md`
|
||||||
|
|
||||||
|
Learn how to contribute at `/documentation/Contributing.md`
|
||||||
|
|
||||||
|
## How To Run
|
||||||
|
|
||||||
|
To run Seekia, you must first install Golang and Fyne dependencies.
|
||||||
|
|
||||||
|
The instructions are described below.
|
||||||
|
|
||||||
|
### Install Golang
|
||||||
|
|
||||||
|
Golang is an open source programming language that makes it easy to build simple, reliable, and efficient software.
|
||||||
|
|
||||||
|
Install it by following the instructions on this website: [go.dev/doc/install](https://go.dev/doc/install)
|
||||||
|
|
||||||
|
### Install Fyne Dependencies
|
||||||
|
|
||||||
|
Fyne is a user interface toolkit and app API written in Golang.
|
||||||
|
|
||||||
|
You will need to install `gcc` and the graphics library header files.
|
||||||
|
|
||||||
|
One of the following commands will probably work:
|
||||||
|
|
||||||
|
* **Debian / Ubuntu:**
|
||||||
|
* `sudo apt install gcc libgl1-mesa-dev xorg-dev`
|
||||||
|
* **Fedora:**
|
||||||
|
* `sudo dnf install gcc libXcursor-devel libXrandr-devel mesa-libGL-devel libXi-devel libXinerama-devel libXxf86vm-devel`
|
||||||
|
* **Arch Linux:**
|
||||||
|
* `sudo pacman -S xorg-server-devel libxcursor libxrandr libxinerama libxi`
|
||||||
|
* **Solus:**
|
||||||
|
* `sudo eopkg it -c system.devel mesalib-devel libxrandr-devel libxcursor-devel libxi-devel libxinerama-devel`
|
||||||
|
* **openSUSE:**
|
||||||
|
* `sudo zypper install gcc libXcursor-devel libXrandr-devel Mesa-libGL-devel libXi-devel libXinerama-devel libXxf86vm-devel`
|
||||||
|
* **Void Linux:**
|
||||||
|
* `sudo xbps-install -S base-devel xorg-server-devel libXrandr-devel libXcursor-devel libXinerama-devel`
|
||||||
|
* **Alpine Linux**
|
||||||
|
* `sudo apk add gcc libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev linux-headers mesa-dev`
|
||||||
|
* **Windows/Mac**
|
||||||
|
* Visit [developer.fyne.io/started](https://developer.fyne.io/started/) to learn the instructions to install Fyne for Windows and Mac.
|
||||||
|
|
||||||
|
### Run Seekia
|
||||||
|
|
||||||
|
Open a terminal and navigate inside of the Seekia folder.
|
||||||
|
|
||||||
|
Once there, run the following command:
|
||||||
|
|
||||||
|
`go run main.go`
|
||||||
|
|
||||||
|
If you are running for the first time, the `run` command will download the Golang packages that Seekia uses.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Use the following command to run Seekia tests:
|
||||||
|
|
||||||
|
`go test ./...`
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
Seekia is not fully operational.
|
||||||
|
|
||||||
|
Hosts and clients will not connect to the internet, and you will not be able to download profiles or chat with users.
|
||||||
|
|
||||||
|
There are many TODOs throughout the code, and the `/documentation/Future Plans.md` document describes many features that need to be built.
|
||||||
|
|
||||||
|
## Simulating Use
|
||||||
|
|
||||||
|
You can create fake profiles and messages to simulate what it is like to use Seekia.
|
||||||
|
|
||||||
|
The easiest way to do this is to build your profile on the Profile - Build page and then view your profile on the Profile - View page.
|
||||||
|
|
||||||
|
To create fake user profiles and receive fake messages, run the `generateContent.go` file in the `/utilities/generateContent/` folder.
|
||||||
|
|
||||||
|
You must first create an app user and a Mate identity.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
You can contact Seekia's creator and lead developer, Simon Sarasova.
|
||||||
|
|
||||||
|
His Seekia identity hash is: `simonx5yudleks5jhwhnck5s28m`
|
||||||
|
|
||||||
|
You can use the Seekia application to cryptographically verify Seekia memos are authored by Simon's identity hash. You can do this by navigating to Settings -> Tools -> Verify Memo.
|
||||||
|
|
||||||
|
Get Simon's contact information by visiting his website at [SimonSarasova.eth](ipns://SimonSarasova.eth)
|
||||||
|
|
||||||
|
You can use Brave browser to access a .eth IPFS website.
|
||||||
|
|
||||||
|
You can also use an IPFS gateway service if you do not have Brave Browser. These services are operated by third parties, so you should access his website from multiple gateways to make sure you are seeing an authentic version of his website:
|
||||||
|
|
||||||
|
[SimonSarasova.eth.limo](https://simonsarasova.eth.limo)
|
||||||
|
|
||||||
|
[SimonSarasova.eth.link](https://simonsarasova.eth.link)
|
||||||
|
|
||||||
|
|
312
documentation/Contributing.md
Normal file
312
documentation/Contributing.md
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to Seekia!
|
||||||
|
|
||||||
|
Together, we can cure racial loneliness, facilitate eugenic breeding, and help people mate in a race and genetics aware manner.
|
||||||
|
|
||||||
|
## Ways To Help
|
||||||
|
|
||||||
|
### 1. Spread the Seekia philosophy.
|
||||||
|
|
||||||
|
Ideas are unstoppable. Awaken the world to the value of race and genetics aware mate discovery technology.
|
||||||
|
|
||||||
|
### 2. Buy Domains
|
||||||
|
|
||||||
|
If you support the goals of Seekia, buy Seekia domains on ICANN and blockchain DNS providers.
|
||||||
|
|
||||||
|
This will prevent bad actors from using these domains for nefarious purposes.
|
||||||
|
|
||||||
|
### 3. Contribute to the codebase.
|
||||||
|
|
||||||
|
There are many TODOs throughout the code that describe things that need to be fixed and built.
|
||||||
|
|
||||||
|
Auditing the codebase and sharing insights is valued.
|
||||||
|
|
||||||
|
See `/documentation/Future Plans.md` for ideas on things to build.
|
||||||
|
|
||||||
|
# Code contribution guidelines
|
||||||
|
|
||||||
|
## Warning!
|
||||||
|
|
||||||
|
**Due to the controversy associated with race, genetics, and the problems that Seekia attempts to solve, it is recommended to contribute anonymously.**
|
||||||
|
|
||||||
|
You should access the code repository, any Seekia websites, and perform all of your Seekia related research through the Tor anonymity network.
|
||||||
|
|
||||||
|
**It is recommended to begin doing this right now, even if you don't currently plan on ever contributing to the codebase.**
|
||||||
|
|
||||||
|
Use the Tor browser to access websites anonymously. Download the Tor Browser Bundle at [torproject.org/download](https://www.torproject.org/download/)
|
||||||
|
|
||||||
|
You should route all traffic on your machine through Tor to shield all network traffic, including connecting to a code repository via command line tools like `git`.
|
||||||
|
|
||||||
|
Performing all of your development activities within a [Whonix](https://whonix.org) virtual machine is an easy way to route all of your traffic through Tor, and to protect yourself against other fingerprinting deanonymization attacks.
|
||||||
|
|
||||||
|
The Whonix Wiki ([whonix.org/wiki](https://www.whonix.org/wiki/About)) is a good resource to learn about Whonix, and how to protect your online anonymity.
|
||||||
|
|
||||||
|
You should deeply consider this advice, and spend some time thinking about your decision to be anonymous or not. The risks facing contributors to Seekia are unpredictable and potentially significant, especially in the early stages of the world becoming aware of Seekia.
|
||||||
|
|
||||||
|
Seekia's legal status will take time to be decided, and could even be criminalized in some countries. For example, Tinder was banned in Pakistan.
|
||||||
|
|
||||||
|
Learn about the limitations of VPNs here: [whonix.org/wiki/Whonix_versus_VPN](https://www.whonix.org/wiki/Whonix_versus_VPN)
|
||||||
|
|
||||||
|
If you have decided to not be anonymous, you are an asset. Having some non-anonymous contributors is useful because they can generally be trusted more than anonymous people. Non-anonymous contributors can be more trusted to be administrators, run trusted servers and operate other Seekia-related infrastructure.
|
||||||
|
|
||||||
|
## Beware Of Advanced Adversaries
|
||||||
|
|
||||||
|
Beware that advanced adversaries may attempt to introduce vulnerabilities into the code, cause social conflicts, or compromise Seekia in other ways. They may use social engineering and other sophisticated methods. Be careful who you trust, and use signing keys as often as possible when communicating. You can use Seekia Memos to create signed messages that people can verify were authored by you.
|
||||||
|
|
||||||
|
## Development Practices
|
||||||
|
|
||||||
|
Developers are expected to work in their own trees and submit pull requests when they feel their feature or bug fix is ready for integration into the master branch.
|
||||||
|
|
||||||
|
No contribution is too trivial. Even a one character typo fix is welcome. We want the code to be perfect.
|
||||||
|
|
||||||
|
## Share Early, Share Often
|
||||||
|
|
||||||
|
We firmly believe in the share early, share often approach. The basic premise of the approach is to announce your plans **before** you start work, and once you have started working, craft your changes into a stream of small and easily reviewable commits.
|
||||||
|
|
||||||
|
This approach has several benefits:
|
||||||
|
|
||||||
|
- Announcing your plans to work on a feature **before** you begin work avoids duplicate work
|
||||||
|
- It permits discussions which can help you achieve your goals in a way that is consistent with the existing architecture
|
||||||
|
- It minimizes the chances of you spending time and energy on a change that might not fit with the consensus of the community or existing architecture and potentially be rejected as a result
|
||||||
|
- The quicker your changes are merged to master, the less time you will need to spend rebasing and otherwise trying to keep up with the main code base
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
One of the design goals of Seekia is to aim for complete test coverage.
|
||||||
|
|
||||||
|
Unless a new feature you submit is completely trivial, it should be accompanied by adequate test coverage for both positive and negative conditions.
|
||||||
|
|
||||||
|
That is to say, the tests must ensure your code works correctly when it is fed correct data as well as incorrect data.
|
||||||
|
|
||||||
|
Go provides an excellent test framework that makes writing test code and checking coverage statistics straight forward.
|
||||||
|
|
||||||
|
Before submitting your pull request, test your changes with the `go test ./...` command.
|
||||||
|
|
||||||
|
This command will run all existing tests to ensure that your changes have not broken anything.
|
||||||
|
|
||||||
|
All code should be accompanied by tests that ensure the code behaves correctly when given expected values and that it handles errors gracefully.
|
||||||
|
|
||||||
|
When you fix a bug, it should be accompanied by tests which exercise the bug to both prove it has been resolved and to prevent future regressions.
|
||||||
|
|
||||||
|
Seekia still has many packages which need tests to be written.
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
Comments are encouraged.
|
||||||
|
|
||||||
|
Functions should be commented with their outputs described like so:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
//Outputs:
|
||||||
|
// -bool: My identity hash exists for provided profileType
|
||||||
|
// -[16]byte: My identity hash
|
||||||
|
// -error
|
||||||
|
func GetMyIdentityHash(myProfileType string) (bool, [16]byte, error){
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Approval Process
|
||||||
|
|
||||||
|
All code which is submitted will need to be reviewed before inclusion into the master branch.
|
||||||
|
|
||||||
|
The code must be approved by the project maintainer(s) to be included.
|
||||||
|
|
||||||
|
After the code is reviewed, the change will be accepted immediately if no issues are found.
|
||||||
|
|
||||||
|
If there are any concerns or questions, you will be provided with feedback along with the next steps needed to get your contribution merged with master.
|
||||||
|
|
||||||
|
Either the code reviewer(s) or interested committers may help you rework the code, or you will simply be given feedback for you to make the necessary changes.
|
||||||
|
|
||||||
|
This process will continue until the code is finally accepted.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
Once your code is accepted, it will be integrated with the master branch.
|
||||||
|
Typically it will be rebased and fast-forward merged to master as we prefer to
|
||||||
|
keep a clean commit history over a tangled weave of merge commits. However,
|
||||||
|
regardless of the specific merge method used, the code will be integrated with
|
||||||
|
the master branch and the pull request will be closed.
|
||||||
|
|
||||||
|
Rejoice as you will now be listed as a contributor!
|
||||||
|
|
||||||
|
## Licensing of Contributions
|
||||||
|
|
||||||
|
All contributions must be released into the public domain. See `Unlicence.md`
|
||||||
|
|
||||||
|
The exception is for code that is taken from elsewhere, in which case, you must provide the license for the copied code.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
Below describes the coding style that contributors should use.
|
||||||
|
|
||||||
|
Simplicity and readability are more important than speed and brevity.
|
||||||
|
|
||||||
|
The exception is for code that would be noticeable to the end user if it were made faster.
|
||||||
|
|
||||||
|
Below are some style guidelines:
|
||||||
|
|
||||||
|
### 1. Use Camel Case
|
||||||
|
|
||||||
|
Use camel case for variables and function names.
|
||||||
|
|
||||||
|
`thisIsAnExampleOfCamelCase`
|
||||||
|
|
||||||
|
### 2. Avoid abbreviations and acronyms.
|
||||||
|
|
||||||
|
Use descriptive variable names. This may make lines and variable names seem excessively long, but I prefer it that way. When variable names are getting too long, it can be a sign that a simplification of the code is possible.
|
||||||
|
|
||||||
|
#### Bad:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
passed := now - profBTime
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Good:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
timePassed := currentTime - profileBroadcastTime
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Use Parentheses around if statements
|
||||||
|
|
||||||
|
#### Bad:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
if i > 1{
|
||||||
|
// Bad
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Good:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
if (i > 1){
|
||||||
|
// Good
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Use == true and == false for comparisons
|
||||||
|
|
||||||
|
#### Bad:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
if (foo && !bar){
|
||||||
|
// Bad
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Good:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
if (foo == true && bar == false){
|
||||||
|
// Good
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Break down statements line by line
|
||||||
|
|
||||||
|
Break down statements into their component parts, line by line.
|
||||||
|
|
||||||
|
#### Bad:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
if (CheckIfSkipped(GetHash(ConvertToInt(identifierString)))){
|
||||||
|
// Bad
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Good:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
identifierInt := ConvertToInt(identifierString)
|
||||||
|
|
||||||
|
hash := GetHash(identifierInt)
|
||||||
|
|
||||||
|
isSkipped := CheckIfSkipped(hash)
|
||||||
|
|
||||||
|
if (isSkipped == true){
|
||||||
|
// Good
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Always Use value, exists := mapObject[key]
|
||||||
|
|
||||||
|
We must always get `exists`, even if we have no reason to believe the value does not exist.
|
||||||
|
|
||||||
|
This is because cosmic rays and faulty hardware can cause bits to flip, so we should at least try to catch these kinds of errors.
|
||||||
|
|
||||||
|
This practice also helps to detect bugs in the code.
|
||||||
|
|
||||||
|
#### Bad:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
val := map[i]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Good:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
val, exists := map[i]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Indenting Style
|
||||||
|
|
||||||
|
If you are checking if `err != nil` and returning err, you don't need to indent.
|
||||||
|
|
||||||
|
If you are returning `errors.New("Something")`, you must indent.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
if (err != nil){ return err }
|
||||||
|
|
||||||
|
if (name != "Seekia"){
|
||||||
|
result := errors.New("Invalid name: " + name)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are defining a function, use standard indenting.
|
||||||
|
|
||||||
|
If you are defining a nested function, you must indent the lines containing the function.
|
||||||
|
|
||||||
|
```Go
|
||||||
|
newFunction := func(){
|
||||||
|
log.Println("Be Race Aware")
|
||||||
|
}
|
||||||
|
|
||||||
|
newButton := widget.NewButton("Select Me", func(){
|
||||||
|
log.Println("Be Race Aware")
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Return variables, not results
|
||||||
|
|
||||||
|
Store the result from a function into a variable before returning it.
|
||||||
|
|
||||||
|
#### Bad:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
|
||||||
|
func badFunction(){
|
||||||
|
|
||||||
|
return foo()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Good:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
|
||||||
|
func goodFunction(){
|
||||||
|
|
||||||
|
result := foo()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Document Attribution
|
||||||
|
|
||||||
|
Much of this is document is taken from btcd: [github.com/btcsuite/btcd](https://github.com/btcsuite/btcd)
|
||||||
|
|
||||||
|
The data comes from this file: `/btcd/docs/code_contribution_guidelines.md` (ISC License)
|
||||||
|
|
944
documentation/Documentation.md
Normal file
944
documentation/Documentation.md
Normal file
|
@ -0,0 +1,944 @@
|
||||||
|
|
||||||
|
# Seekia Documentation
|
||||||
|
|
||||||
|
![Seekia Banner](../resources/markdownImages/seekiaLogoWithSubtitle.jpg)
|
||||||
|
|
||||||
|
### Welcome to the Seekia documentation!
|
||||||
|
|
||||||
|
Thank you for being interested in race and genetics aware mate discovery technology.
|
||||||
|
|
||||||
|
This document is a technical description of how Seekia works.
|
||||||
|
|
||||||
|
Seekia will continue to evolve along with this document.
|
||||||
|
|
||||||
|
Read the whitepaper `Whitepaper.pdf` to understand more about the philosophy and motivations for Seekia.
|
||||||
|
|
||||||
|
## What is Seekia?
|
||||||
|
|
||||||
|
Seekia is a mate discovery network where users can find a mate while having a deep awareness of each potential partner's race.
|
||||||
|
|
||||||
|
Users can share racial information in their profiles such as their eye, skin, and hair color; hair texture; genetic ancestry; haplogroups; and the alleles of their genes which effect physical traits.
|
||||||
|
|
||||||
|
Seekia enables users to browse and filter potential mates by their racial attributes. Seekia can also calculate the racial characteristics for prospective offspring between users. Seekia allows for users to predict and control the race of their offspring by selecting a mate who is the most capable and likely to produce offspring of their desired race.
|
||||||
|
|
||||||
|
Seekia aims to cure racial loneliness by helping people to find members of their own race to mate with.
|
||||||
|
|
||||||
|
Users can also filter and sort users based on their genetic disease and trait information. Users can view information about the health and physical traits of their prospective offspring for each user.
|
||||||
|
|
||||||
|
Seekia aims to improve the genetic quality of humanity by making humans healthier, more beautiful, and more intelligent. Seekia aims to facilitate eugenic breeding by helping to create mate pairings which are the most likely to produce healthy, beautiful, and intelligent offspring. Seekia aims to help members of the world's most beautiful races to meet and have children.
|
||||||
|
|
||||||
|
Users can analyze their genomes using the Seekia app to learn about monogenic disease probabilities, polygenic disease risk scores, and traits. Users can share this information in their profiles. Seekia enables users to choose their mate in such a way to prevent their offspring from having monogenic diseases, reduce the probability of their offspring having polygenic diseases, and increase the probability of their offspring having certain traits.
|
||||||
|
|
||||||
|
The goal of Seekia is to accelerate the world's adoption of race and genetics aware mate discovery technology, and to help the world mate in a race and genetics aware manner.
|
||||||
|
|
||||||
|
## User Identities
|
||||||
|
|
||||||
|
### Identity Type
|
||||||
|
|
||||||
|
There are three identity types: Mate, Host, and Moderator.
|
||||||
|
|
||||||
|
### Identity Key
|
||||||
|
|
||||||
|
Each Mate, Host, and Moderator identity has an associated ed25519 public/private key pair.
|
||||||
|
|
||||||
|
### Identity Hash
|
||||||
|
|
||||||
|
The public identity key is hashed to create an identity hash.
|
||||||
|
|
||||||
|
An identity hash is 16 bytes long: (15 byte hash of the identity key, 1 identity type byte)
|
||||||
|
|
||||||
|
The first 15 bytes are encoded in Base32, and the final byte is encoded with a single letter representing the identity type.
|
||||||
|
|
||||||
|
Users must always be referred to by their identity hash, because two identity keys can be created that generate the same identity hash.
|
||||||
|
|
||||||
|
## Network
|
||||||
|
|
||||||
|
The Seekia network is comprised of hosts and clients.
|
||||||
|
|
||||||
|
Hosts can be run anonymously by anyone, and can leave and rejoin at will.
|
||||||
|
|
||||||
|
Hosts are responsible for serving profiles, messages, reviews, reports, and parameters.
|
||||||
|
|
||||||
|
Hosts can host as much or as little content as they want.
|
||||||
|
|
||||||
|
Users connect to hosts to download profiles and messages to browse.
|
||||||
|
|
||||||
|
### Network Type
|
||||||
|
|
||||||
|
Seekia currently has 2 network types: Mainnet and Testnet 1.
|
||||||
|
|
||||||
|
Each network type is described by a single byte. Mainnet == 1, Testnet1 == 2.
|
||||||
|
|
||||||
|
Multiple networks allow for the testing of new features on a test network before deploying them on the main network.
|
||||||
|
|
||||||
|
Each network has its own account credit database, account credit interface servers, network entry seeds, and parameters. Profiles, messages, reviews, reports, and parameters all contain a network type byte.
|
||||||
|
|
||||||
|
Users can switch their app's network type. Upon switching network types, the Seekia client will interface with the new network and delete downloaded database content from different networks. User data such as messages and chat keys are retained, so users can switch between networks without losing sensitive data.
|
||||||
|
|
||||||
|
Users can maintain a presence on multiple networks at the same time. For example, a user could use the same Seekia identity to host from two different machines, each on a seperate network.
|
||||||
|
|
||||||
|
### Ranges
|
||||||
|
|
||||||
|
Hosts describe the data they are hosting by sharing their ranges. Ranges are also used in network requests to requests portions of the network.
|
||||||
|
|
||||||
|
There are two types of ranges: Identity and Inbox.
|
||||||
|
|
||||||
|
A range is comprised of two bounds: Start and End.
|
||||||
|
|
||||||
|
Identity ranges bounds are of the type `[16]byte`, and inbox ranges are of the type `[10]byte`.
|
||||||
|
|
||||||
|
To determine if an identity or inbox is within a range, the bytes of the identity/inbox are compared with the Start and End bounds to see if they fall within both bounds.
|
||||||
|
|
||||||
|
The code which implements ranges can be found here: `internal/byteRange/byteRange.go`
|
||||||
|
|
||||||
|
A host will host all of the messages which were sent to an inbox within the host's Inbox bound, and a host will host all profiles whose author is within the host's Identity bound. The host may have other restrictions as well, such as only choosing to host Viewable profiles. A host has a different range for each Identity Type.
|
||||||
|
|
||||||
|
### Required Downloads
|
||||||
|
|
||||||
|
To interface with the Seekia network, all users must download the network parameters.
|
||||||
|
|
||||||
|
All hosts and moderators must download all moderator identity reviews and moderator profiles. Downloading all moderator identity reviews and moderator profiles is necessary to determine which moderators are banned. Knowing which moderators are banned is necessary to determine identity, profile, attribute and message verdicts. Moderator profiles must be downloaded to determine which moderators are disabled.
|
||||||
|
|
||||||
|
Hosts must download and host all reviews and reports for the identities/profiles/attributes/messages they are hosting. For example, if a host is hosting identities a-b, they must download all reviews and reports reviewing/reporting those identities, and all reviews/reports reviewing/reporting profiles and attributes authored by those identities.
|
||||||
|
|
||||||
|
All host profiles are downloaded by all hosts of Seekia.
|
||||||
|
|
||||||
|
If hosts are hosting Host/Moderator identities, they must host all of them and their profiles/reviews/reports, rather than a subrange. This is because all Host/Moderator profiles, reviews, and reports should always be a small enough size that breaking them up between hosts is unnecessary.
|
||||||
|
|
||||||
|
### Host Identity Balance
|
||||||
|
|
||||||
|
Each host identity must be funded with a minimum amount of credit to participate in the network.
|
||||||
|
|
||||||
|
The gold rate/day in cost is defined by the network parameters.
|
||||||
|
|
||||||
|
Hosts can be banned by moderators, so the funding requirement deters spam and unruleful behavior.
|
||||||
|
|
||||||
|
### Host Profiles
|
||||||
|
|
||||||
|
Each Seekia host has a profile which includes the following:
|
||||||
|
|
||||||
|
* Their IP address/Tor hidden service address
|
||||||
|
* Information about what content they are hosting, such as which types (messages/profiles/...) and the ranges of inboxes/identities they are hosting
|
||||||
|
* The size of the content they are hosting
|
||||||
|
|
||||||
|
Host profiles expire after an inactivity period provided in the parameters, so hosts must update their profiles at least that often. Host profile updates are performed automatically by the application for users in Host mode.
|
||||||
|
|
||||||
|
### Host Options
|
||||||
|
|
||||||
|
#### Clearnet/Tor Mode
|
||||||
|
|
||||||
|
Hosts can disable either Tor or Clearnet, or allow both.
|
||||||
|
|
||||||
|
Having clearnet-only hosts is necessary in the event that many host's Tor hidden services are DDOS attacked.
|
||||||
|
|
||||||
|
Clearnet addresses are more resilient against DDOS attacks because they can utilize anti-DDOS services.
|
||||||
|
|
||||||
|
Clearnet-only hosts are also needed to be the entry nodes to the network.
|
||||||
|
|
||||||
|
Clearnet hosts are also able to download data from other hosts over clearnet, which is much faster than Tor.
|
||||||
|
|
||||||
|
### Download Privacy
|
||||||
|
|
||||||
|
The Seekia application attempts to query data from hosts in a privacy-preserving manner.
|
||||||
|
|
||||||
|
For example, when downloaing sensitive content such as a user's messages, a new Tor circuit is created for each inbox to prevent hosts from linking user inboxes together.
|
||||||
|
|
||||||
|
#### Requestor Fingerprint
|
||||||
|
|
||||||
|
Each request contains information about a requestor, which could be used to link a requestor to a Seekia user and/or understand their behavior.
|
||||||
|
|
||||||
|
A simple example is a host, who requests profiles within a specific range, and broadcasts that range on their public host profile. If that host is the only host using that range, any download requests made for that range will be trivially linkable to that host. This is why requests must be broken up by type, sent to different hosts, and crafted in such a way that hosts learn as little as possible about the requestor.
|
||||||
|
|
||||||
|
Imagine the example of a user who wants to check for new profiles for 100 downloaded mate profile authors. If they send those 100 authors in a single request, that host now knows that requestor has 100 mate profiles created by those authors downloaded. After offering a list of profiles, hosts will also be able to tell which profiles the user has already downloaded by seeing which profiles the user skips downloading.
|
||||||
|
|
||||||
|
The host can use the location of the profiles and their attributes to try to match the requestor to a specific Seekia user. This is easier to accomplish if Seekia is used by a small number of people in a specific region.
|
||||||
|
|
||||||
|
The most private way to make this request would be to request each author's profiles individually over a new tor circuit from a different host each time, but it would take a much longer time. In this one-by-one strategy, requestor privacy is improved, but download time is increased.
|
||||||
|
|
||||||
|
#### Mate Downloads Criteria
|
||||||
|
|
||||||
|
A Seekia Mate user can select their Download Desires within the Seekia app. A user's download desires are used to create their Criteria.
|
||||||
|
|
||||||
|
A user's Download Desires are the desires the user is willing to share with hosts when they request Mate profiles to browse. The more attributes they select, the less undesired profiles they will download, saving time and bandwidth.
|
||||||
|
|
||||||
|
This feature exists because users may not want to share desires that are embarrassing or too personal. The app initializes with Age, Sex, and Distance selected. These are attributes for which most users would not care if their desires were publicly known. In the worst case, a malicious host could link a requestor's criteria to their Seekia identity and share their criteria somewhere.
|
||||||
|
|
||||||
|
If a user does not select any download desires, their client will download all Seekia mate profiles to their machine. All Mate profiles could eventually take up terabytes of data, which would be too large for most users, so the client should warn users who do not select any download desires.
|
||||||
|
|
||||||
|
An advantage of using Downloads Criteria is that it allows the application to make requests to hosts regarding all users who fulfill a user's criteria. For example, one kind of request involves checking the funded status of all profiles which fulfill a user's criteria. The user does not care if the requestor learns that they have those profiles downloaded, because the host would only be able to learn their criteria, not their private desires.
|
||||||
|
|
||||||
|
Mate users who do not fulfill a user's criteria but still need to have their profiles downloaded are called Outliers. Examples of Outliers include users that have messaged a user, a user's contacts, and a user's Liked users.
|
||||||
|
|
||||||
|
#### Desires Pruning Mode
|
||||||
|
|
||||||
|
In normal Mate mode operation, Seekia will prune profiles that do not fulfill a user's downloads criteria.
|
||||||
|
|
||||||
|
If the application instead deleted all profiles that do not fulfill a Mate user's private desires, then hosts could trivially learn their private desires by matching commonalities between the profiles that the user requests information about.
|
||||||
|
|
||||||
|
If the Mate user's machine has run out of space, and pruning profiles based on criteria is not enough, Seekia will enable **Desires Pruning Mode**.
|
||||||
|
|
||||||
|
In this mode, the application will prune profiles which do not fulfill the user's private desires to save storage space.
|
||||||
|
|
||||||
|
The user will still make requests based on their downloads criteria, but must download **all** of the profiles that they are offered, rather than being able to skip downloading the profiles they already have downloaded. If the requestor rejected profiles they already have downloaded, then the host would learn all of the user's private desires by matching commonalities between the profiles that the user does not reject.
|
||||||
|
|
||||||
|
In Desires Pruning Mode, the application can never stop downloading and discarding the same profiles over and over again. When Desires Pruning Mode is disabled, the application can eventually download most of the profiles it needs, and from then on only download new profiles as they are broadcast to the network.
|
||||||
|
|
||||||
|
In Desires Pruning Mode, the requestor only downloads a small random sample of profiles from each host, and then skips to another host. Otherwise, they would get stuck downloading from a small number of hosts.
|
||||||
|
|
||||||
|
In Desires Pruning Mode, requests that require a user's stored profiles/identities to be shared must be instead request information about each profile/identity one-by-one. For example, when getting the funded and viewable statuses for a user's stored profiles, the status for each profile must be requested on its own. This will be slower, but only profiles which fulfill a user's private desires, and whose author's newest profile fulfills our desires, will need their status to be downloaded, which will reduce the number of requests which need to be made.
|
||||||
|
|
||||||
|
Desires Pruning Mode will only be activated if a user does not have enough storage space to store all of the profiles which fulfill their downloads criteria. This would be more likely if they are not sharing many desires, or if they live in an area with a large number of Seekia users whom fulfill their downloads criteria. Most users should never need to enter Desires Pruning Mode, because their downloads criteria should limit the total profiles to download to a small enough size.
|
||||||
|
|
||||||
|
Desires Pruning Mode must also be disabled carefully. If the mode is suddenly disabled, then the requestor will leak their locally stored desire-pruned profiles to the first few hosts they request from. The requestor must first download enough profiles without rejecting any, and then they can start to reject profiles which they have already downloaded.
|
||||||
|
|
||||||
|
#### Invalid Data Fingerprinting
|
||||||
|
|
||||||
|
Here is another fingerprinting attack:
|
||||||
|
|
||||||
|
1. Requestor requests profiles which fulfill a provided Mate criteria.
|
||||||
|
2. Malicious host responds with some profiles that do not fulfill the provided criteria.
|
||||||
|
3. Requestor requests to download the profiles the requestor is missing, and declines to download the profiles the requestor already has.
|
||||||
|
|
||||||
|
The host now knows some profiles that the requestor has that are not within the requestor's downloads criteria. These profiles could be profiles that the requestor is hosting, or contacts that do not fulfill the requestor's downloads criteria. The host could then use this information to identity the requestor's identity, whom the requestor is contacting, and the requestor's host identity.
|
||||||
|
|
||||||
|
To avoid this from happening:
|
||||||
|
|
||||||
|
1. Upon receiving the response, the requestor will check if any profiles that the host is offering are already downloaded and do not fulfill the request's criteria.
|
||||||
|
2. The requestor downloads the criteria non-fulfilling profiles, and declines to download any criteria-fulfilling profiles the requestor already has.
|
||||||
|
3. After the entire download exchange is completed, the user adds the host to their malicious hosts list.
|
||||||
|
|
||||||
|
The requestor should only add the host to their malicious hosts list after the entire request has completed. If they instead rejected the host immediately, the host would know that they had some of these profiles stored. Otherwise, the requestor would have no way of knowing that these profiles did not fulfill their request criteria.
|
||||||
|
|
||||||
|
This way, the requestor leaks no unintended information about which profiles they have and the requestor still is able to download whatever valid profiles the malicious host was offering.
|
||||||
|
|
||||||
|
## Network Parameters
|
||||||
|
|
||||||
|
The Seekia network relies on a set of files called the Network Parameters.
|
||||||
|
|
||||||
|
These files contain various data, some of which is necessary for the functioning of the Seekia network.
|
||||||
|
|
||||||
|
Some of the parameters exist to help the network adapt to changing conditions, such as currency exchange rates and moderation parameters.
|
||||||
|
|
||||||
|
The files are signed and controlled by the network admin(s).
|
||||||
|
|
||||||
|
The **AdminPermissions** parameters file defines which admins have permission to sign each network parameters type. The AdminPermissions file can only be authored by the master admin(s), whose public key(s) are encoded into the code of the Seekia application.
|
||||||
|
|
||||||
|
All Seekia clients must download the network parameters to be able to host, moderate, or chat.
|
||||||
|
|
||||||
|
See `Specification.md` to see each network parameter type and its purpose.
|
||||||
|
|
||||||
|
## Blockchain Data
|
||||||
|
|
||||||
|
Seekia hosts can choose to provide blockchain data.
|
||||||
|
|
||||||
|
A Seekia host who is providing a blockchain's data is offering to share information about deposits made to addresses.
|
||||||
|
|
||||||
|
These deposits are only used to calculate moderator identity scores.
|
||||||
|
|
||||||
|
Blockchain data is requested from multiple hosts to defend against malicious hosts who share invalid deposit data.
|
||||||
|
|
||||||
|
### Relying on a local blockchain node
|
||||||
|
|
||||||
|
Seekia users and hosts can host their own cryptocurrency blockchain to eliminate the need to query other hosts for address deposits.
|
||||||
|
|
||||||
|
This will make identity scores appear and update faster, and will make the user's Seekia usage more private.
|
||||||
|
|
||||||
|
This is useful for any moderators who are already running a node for any blockchain used by Seekia.
|
||||||
|
|
||||||
|
Once more cryptocurrencies are added, most blockchain-storing users will probably only have 1 local blockchain node, relying on network hosts for the other blockchain deposits.
|
||||||
|
|
||||||
|
### Banning Malicious Hosts
|
||||||
|
|
||||||
|
Moderators who are running their own blockchain node can audit the information hosts are providing by making blockchain requests to hosts and comparing their responses to their own node's ledger. These moderators can ban hosts who provide invalid deposit information. The app should be able to do this automatically.
|
||||||
|
|
||||||
|
## Funding Content
|
||||||
|
|
||||||
|
All Seekia messages, mate profiles, and reports must be funded to be hosted by the network.
|
||||||
|
|
||||||
|
Identities must be funded to have their profiles hosted by the network.
|
||||||
|
|
||||||
|
This is required to prevent network spam and discourage bad behavior.
|
||||||
|
|
||||||
|
Without a financial cost to broadcasting content, a single actor could spam the network with billions of fake profiles/messages, rendering the network useless. By requiring funds, broadcasting spam costs an attacker money.
|
||||||
|
|
||||||
|
Seekia users can be banned if they engage in malicious behavior, so being a malicious user will cost money.
|
||||||
|
|
||||||
|
Users spend enough credit to have their profile hosted for as long as they initially desire. For example, if a Mate user wants to try out Seekia for 60 days, they fund their Mate identity for 60 days. They can extend their identity's balance any time they want.
|
||||||
|
|
||||||
|
Users must fund their mate/host identity for a minimum number of days. This only needs to be done once per each identity. The account credit servers will not allow a funding below a minimum number of days, if the mate/host identity has not already been funded in the past. Moderator identities are funded via Moderator Scores, which are described later in this document. Anyone can fund another user's identity, which is useful if that user's identity is close to expiring.
|
||||||
|
|
||||||
|
Each mate profile must be funded individually for a flat fee. Without this, an attacker could replace their identity's mate profile thousands of times, which would spam the moderators with profiles to review. Host and moderator profiles do not have this issue, because these profiles do not need to be approved by the moderators. Host and Moderator profiles can be banned or approved, but they do not need to be approved before being downloaded or viewed by users.
|
||||||
|
|
||||||
|
Reports and messages must each be funded individually. Reports use a flat fee, whereas messages are funded based on their size. Larger messages are more expensive.
|
||||||
|
|
||||||
|
The costs to fund identities/profiles/messages/reports are defined in the network parameters. All of the parameter costs must be updated in a way that allows a time period for all clients to update their parameters. Otherwise, some user clients will overpay/underpay because they have outdated costs.
|
||||||
|
|
||||||
|
If the spam on Seekia started to increase, the network admins would increase the costs. A perfect balance must be achieved which reduces the amount of spam and unrulefulness but keeps the cost low for users to participate.
|
||||||
|
|
||||||
|
To determine the funded status of an identity/profile/message/report, hosts and users request the information from the account credit servers.
|
||||||
|
|
||||||
|
## Account Credit
|
||||||
|
|
||||||
|
Seekia is not a fully decentralized network.
|
||||||
|
|
||||||
|
Seekia uses Credit rather than cryptocurrency to fund host/mate identities, reports, and messages on the network. Moderator identity scores do not use Credit, and instead use cryptocurrency.
|
||||||
|
|
||||||
|
Account credit is used instead of cryptocurrency for 2 reasons: Privacy and Scalability.
|
||||||
|
|
||||||
|
In a fully decentralized model, the funding of messages, reports, mate profiles, and identities would be accomplished with private blockchain transactions. An example of this is a zero knowledge accumulator, where each transaction is unlinkable.
|
||||||
|
|
||||||
|
Supporting 10,000 messages per second would require a blockchain that can support 10,000 private transactions per second, along with a built-in wallet within the application.
|
||||||
|
|
||||||
|
Due to the scaling limitations of privacy-preserving blockchains, the network relies on a central account credit database to perform accounting privately.
|
||||||
|
|
||||||
|
Credit is represented as milligrams (change?) of gold within the account credit database.
|
||||||
|
|
||||||
|
Credit can be purchased with cryptocurrency by destroying funds on the blockchain. The account credit interface servers can check an account's quantity of purchased credit by checking the balance of its associated blockchain address.
|
||||||
|
|
||||||
|
An advantage of using Credit is that it enables some users to join for free. The administrator of the account credit database can create credit by will. The administrator can send credit to trusted entities whose job is to distribute credit to people to onboard them to Seekia for free. For example, credit could be distributed by a faucet that requires a unique phone number, because phone numbers are costly to attain for spammers. Other examples include sending credit manually to users who have proof of personhood, sending credit to people who have verified social media accounts, etc..
|
||||||
|
|
||||||
|
Credit can be transferred between users. A user shares their Account Identifier to another user, who can send credit to that identifier.
|
||||||
|
|
||||||
|
### Account Credit Servers
|
||||||
|
|
||||||
|
There is a single central account credit database, along with many account credit interface servers.
|
||||||
|
|
||||||
|
The account credit interface servers are used to load-balance all of the operations and bandwidth that do not need to be centrally performed. They also provide protection against hacks, because some account credit servers are read-only.
|
||||||
|
|
||||||
|
There are 2 types of interface servers: Read Only and Writeable. The read only servers are only able to read from the database. This will suffice for most requests. This will reduce the number of servers that, if compromised, would be able to corrupt the master database with false information.
|
||||||
|
|
||||||
|
The central database keeps track of each account's credit balance and each funded identity/profile/message/report's expiration time.
|
||||||
|
|
||||||
|
The database server is a single point of failure. It can be regularly backed up.
|
||||||
|
|
||||||
|
Each account is an ed25519 public/private key pair.
|
||||||
|
|
||||||
|
Each account public key is used to derive cryptocurrency addresses and an account identifier. See `/internal/network/accountKeys.go` for the implementation.
|
||||||
|
|
||||||
|
Each user can create as many accounts as they need. Cryptocurrency address and account identifier reuse is discouraged because of the privacy implications.
|
||||||
|
|
||||||
|
An account's public key is used to query the servers on the balance of the account. Without the private key, a requestor cannot determine the account credit balance. They must perform a handshake and sign something provided by the account server to verify they are the owner of the account.
|
||||||
|
|
||||||
|
Users can send funds from one account to another by using the account's identifier.
|
||||||
|
|
||||||
|
The interface servers communicate with the account credit database, deducting from the account's balance for each profile/message/report/identity funding transaction they make.
|
||||||
|
|
||||||
|
To buy credit using cryptocurrency, the user sends crypto to the address associated with their public key.
|
||||||
|
|
||||||
|
Any funds sent are destroyed. This is done for multiple reasons:
|
||||||
|
1. Technical: It is easiest to create a different address for each account this way.
|
||||||
|
2. Legal: to avoid any claims that Seekia is generating profit or acting as a for-profit entity.
|
||||||
|
3. Ideological: to keep Seekia as decentralized as possible. No single entity should profit from the users.
|
||||||
|
|
||||||
|
To spend funds, the user contacts an account credit interface server with their public key, their intended amount of credit to spend (in gold), and the message/profile/identity/report to fund. The account credit interface server derives the account public key's crypto addresses and looks up the deposits made to its addresses. The server multiplies the amount of crypto sent in each deposit by the gold exchange rate at deposit time described in the parameters to determine the total amount of credit in gold purchased for the account. The server then tells the database the crypto balance and the amount being spent and the item being funded.
|
||||||
|
|
||||||
|
An account's balance is the total amount of credit received via its identifier minus the total amount of credit spent. A balance can be negative if the user has only received credit to the account by purchasing via crypto.
|
||||||
|
|
||||||
|
The database checks if the amount deducted from the account is greater than or equal to the amount of credit purchased with crypto. If so, the transaction being made is rejected. If not, the amount being spent is subtracted from the account entry in the database, and the message/profile/identity/report funded status is updated within the database.
|
||||||
|
|
||||||
|
Each interface server must have access to:
|
||||||
|
|
||||||
|
1. The blockchain address deposits of each cryptocurrency
|
||||||
|
* They must be able to get the balance of any address, as well as the time and amount of each transaction
|
||||||
|
* All deposits in each block are combined into a single deposit for the block
|
||||||
|
* The servers can retrieve these deposits from different servers
|
||||||
|
2. The network parameters that determine the amount of gold to fund a message/profile/report/identity
|
||||||
|
3. The network parameters that determine the exchange rate for each cryptocurrency to gold.
|
||||||
|
* These rates must be historical and go back to the date of Seekia's launch.
|
||||||
|
|
||||||
|
Moderator identity scores do not rely on the account server. Moderators use crypto addresses derived from their identity hash. This prevents their balances from being lost if the server data is lost, which is much worse for moderators because the amount of money spent is much greater. Moderators should use blockchain privacy tools to fund their identity scores to avoid linking their crypto wallets with their moderator identity.
|
||||||
|
|
||||||
|
All communication between the database, the interface servers, hosts, and clients must be encrypted with Nacl and Kyber.
|
||||||
|
|
||||||
|
Another use of the servers could be timestamping of messages. The servers could be a source of truth for when a message was sent. If the sender-alleged sent time conflicts with the account credit server by more than 1 minute, a warning could be shown. Otherwise, sent times could be relied upon.
|
||||||
|
|
||||||
|
Hosts get their hosted message/profile/report funded statuses from the account credit servers. Mate/Moderator users get the message funded statuses from the credit servers, and the profile funded statuses from hosts. This is done to reduce the load on the account credit interface servers, and to enable the network to maintain more functionality if the account credit servers go offline.
|
||||||
|
|
||||||
|
After a profile/message/report is funded, its funded status is static, and its expiration time cannot be increased.
|
||||||
|
|
||||||
|
### Privacy Risk
|
||||||
|
|
||||||
|
The servers pose a necessary privacy risk. The servers must be trusted to not keep track or log which account funded each profile/identity/message/report. If the servers were compromised over a long period of time, they could be used to log the profiles/identities/messages/reports funded by each account. This would negate the privacy advantages of secret inboxes, making it trivial to tell which users are talking to each other.
|
||||||
|
|
||||||
|
If an attacker only obtained a snapshot of the servers, they would only learn the balances of each account. If the attacker could link the accounts to the Seekia users whom they belong to, and a user received all of their credit by purchasing with cryptocurrency, the attacker could tell how much credit the user has spent, and guess roughly how many messages the user sent. This is not possible if the user received credit for free or from another user, in which case, their credit balance would be subtracted from the database as it is spent. The amount of credit which belonged to an account in the past should not be saved or logged by the database, thus, the spent credit would disappear without a trace.
|
||||||
|
|
||||||
|
An attacker could potentially determine which exact messages were sent by a user. If the attacker linked a user's identity to their credit account(s) and balance(s), they could subtract the user's known identity/profile funding transaction amounts and determine which sent message/report costs add up exactly to the amount of funds spent. They could use information about the recipients of the messages to better guess that they had been sent by the suspected user. This becomes more difficult as more users join Seekia. Image messages should often cost the same amount, so this strategy should become impossible with enough users.
|
||||||
|
|
||||||
|
In any server-compromise scenario, the message contents would still be encrypted.
|
||||||
|
|
||||||
|
#### Account Crypto Address Linking
|
||||||
|
|
||||||
|
Another privacy consideration is the ability to link a user's identity hash to their account crypto address(es).
|
||||||
|
|
||||||
|
Account addresses will never withdraw funds, and will likely receive funds in the small amounts recommended by the Seekia client, making them easier to identify.
|
||||||
|
|
||||||
|
If a user funds their moderator score and credit account with the same Ethereum/Cardano wallet, then linking these addresses together is trivial.
|
||||||
|
|
||||||
|
Another easy way to link identities to addresses is to correlate the funding of account addresses on the blockchain with the appearance of new users profiles on the Seekia network. This issue is mitigated by telling users in the GUI to wait a while after purchasing credit before broadcasting their profile for the first time. This breaks the link between the identity being funded and their account crypto address.
|
||||||
|
|
||||||
|
Even if they are careful to prevent any links between their account crypto address and their Seekia identity, observers will still be able to guess that the funds belong to some user of Seekia. Using blockchain analytics and user profile metadata, they could learn the wallet owner's real world identity.
|
||||||
|
|
||||||
|
If user identities are linked to account crypto addresses, users who send from crypto wallets with large amounts of money could have their crypto wallet balances revealed to the world. This could cause them to become the victim of crime or be pursued by gold diggers. Users with large amounts of crypto should use privacy preserving technologies such as zero knowlege accumulators when purchasing Seekia credit. This warning is shown within the GUI.
|
||||||
|
|
||||||
|
If a user funds the same account crypto address more than once, an observer can assume that the user has funded enough identities/profiles/messages/reports to drain at least the majority of their credit balance after their first deposit. This would allow the observer to guess that a specific user had sent a certain number of messages, which could be used to aid in other network analysis attacks. This issue is mitigated by discouraging address reuse and presenting the user with fresh crypto addresses whenever they want to purchase more credit.
|
||||||
|
|
||||||
|
As more people use Seekia and the number of Seekia transactions increase, these privacy risks are reduced.
|
||||||
|
|
||||||
|
### Account Credit Database Corruption
|
||||||
|
|
||||||
|
In the event of the database crashing or being hacked, the data could be corrupted.
|
||||||
|
|
||||||
|
The database server should be backed up regularly, but some data will likely be lost.
|
||||||
|
|
||||||
|
If the database is reset to an earlier state:
|
||||||
|
|
||||||
|
1. Any accounts funded with cryptocurrency will have their balances increase or stay the same.
|
||||||
|
2. Accounts funded via account identifiers will lose any money that was sent after the backup was made
|
||||||
|
* The account which sent the funds will have its balance restored
|
||||||
|
|
||||||
|
#### Account Credit Database Schema:
|
||||||
|
|
||||||
|
* Account Identifier `[14]byte` -> Balance (in milligrams of gold)
|
||||||
|
* This amount can be negative, if the account has purchased funds with cryptocurrency
|
||||||
|
* Sending from 1 account to another requires subtracting from the sender and adding to the recipient
|
||||||
|
* Message Hash `[26]byte` + MessageSize `int` -> ExpirationTime `int64` (unix)
|
||||||
|
* We need message size because requestor could lie about size, so each alleged size corresponds to its own entry
|
||||||
|
* Thus, the size of the message is required to get the isFunded status of a message
|
||||||
|
* The alternative requires uploading a message to the interface servers to fund it, which increases bandwidth dramatically
|
||||||
|
* Mate Profile Hash `[28]byte` -> ExpirationTime `int64` (unix)
|
||||||
|
* Identity Hash `[16]byte` -> ExpirationTime `int64` (unix)
|
||||||
|
* Identity Hash `[16]byte` -> Initial fund amount has been made `bool`
|
||||||
|
* This is needed to keep track of which identities have had their initial minimum fund amount satisfied
|
||||||
|
* Report Hash `[30]byte` -> ExpirationTime `int64` (unix)
|
||||||
|
|
||||||
|
#### Interface Servers Schema:
|
||||||
|
|
||||||
|
* MessageHash `[26]byte` + MessageSize `int` -> ExpirationTime `int64`
|
||||||
|
* Server only has to retrieve this once after it is funded, because time cannot be increased
|
||||||
|
* Mate Profile Hash `[28]byte` -> ExpirationTime `int64`
|
||||||
|
* Server only has to retrieve this once after it is funded, because time cannot be increased
|
||||||
|
* Report Hash `[30]byte` -> ExpirationTime `int64`
|
||||||
|
* Server only has to retrieve this once after it is funded, because time cannot be increased
|
||||||
|
* Identity Hash `[16]byte` -> ExpirationTime `int64`
|
||||||
|
* This is only needed for mate/host identities
|
||||||
|
* It must be updated with a background job, because a user may increase their identity expiration time using a different interface server
|
||||||
|
|
||||||
|
### A future without the servers
|
||||||
|
|
||||||
|
Once private cryptocurrency solutions can scale to our needed speed, the Seekia client can have its own crypto wallet that pays for each message/mate profile/report with a private transaction, and the account credit servers can be retired. Each message/mate profile/report/identity hash would have crypto addresses that are derived from its hash. These addresses would be used to burn coins, similarly to how moderator scores are funded.
|
||||||
|
|
||||||
|
It is possible that a more centralized high throughput blockchain could exist sooner that could support the necessary number of private transactions. It would be worth using this kind of system instead of the single-database option because it would be more decentralized.
|
||||||
|
|
||||||
|
Assuming each private transaction is 3KB, and there were 10,000 Seekia transactions per second, 30 MB would be added every second, or ~2.5 terabytes a day. At least some blockchain nodes would also have to verify the zero knowledge proofs, which would be resource intensive.
|
||||||
|
|
||||||
|
The blockchain could have a smart contract such as Tornado Cash Nova or Zcash Orchard that allows users to withdraw arbitrary amounts to addresses privately. Each transaction from the contract would be unlinkable.
|
||||||
|
|
||||||
|
In order for users to be able to create transactions, they would have to download the necessary information required to construct a zero knowledge proof that their coins came from some coin in a shielded pool. This would eventually become an enormous amount of data. There are several ways to reduce the burden of data to download:
|
||||||
|
|
||||||
|
1. Use many shielded pools. This would reduce the anonymity set, but it would be large enough for that to not matter. The Seekia application should choose one randomly for each user, so the user would only have to download changes to a single note tree.
|
||||||
|
2. Use a single shielded pool, but allow the user to only download a random portion of the shielded pool note tree, and construct a proof from this smaller anonymity set. I'm not sure if this is possible.
|
||||||
|
3. For a faster but more trusted method, there could be a way for the user to trust the blockchain provider to construct their transaction proof, without allowing the blockchain provider to steal their funds. I'm also not sure if this is possible. This would negate the need to download large amounts of data, but would require the blockchain provider to be trusted to not track which coins the user is spending, as this would reveal the messaging patterns of the user. This level of trust is already required for the account credit interface servers, which are operated by trusted entities.
|
||||||
|
|
||||||
|
To get funded statuses for identities/messages/profiles, hosts would connect to nodes which were hosting the balances of all transparent addresses, get the deposit information for the addresses that belong to the identities/messages/profiles, and use this deposit information and the network parameters to calculate the funded statuses.
|
||||||
|
|
||||||
|
If there were multiple cryptocurrencies, then multiple blockchains wallets would have to be supported within the app.
|
||||||
|
|
||||||
|
To maintain the advantage of onboarding people for free, a token would have to be created, which would require an admin to be able to mint tokens at will. This would create a marketplace for speculation, would be less decentralized, would make coins more difficult to purchase, and would introduce legal risks. I think it would be better to require all coins to be burned in the blockchain's native token. This could also be harmful by encouraging people to purchase a cryptocurrency which is centralized, so a warning must exist to discourage people from investing in the currency.
|
||||||
|
|
||||||
|
### Multiple Cryptocurrencies
|
||||||
|
|
||||||
|
Seekia is designed to support multiple cryptocurrencies.
|
||||||
|
|
||||||
|
Ethereum and Cardano are chosen as the first cryptocurrencies to power the Seekia network.
|
||||||
|
|
||||||
|
The implementing of a currency will undoubtedly encourage some users to buy into and adopt that currency.
|
||||||
|
|
||||||
|
We should only support cryptocurrencies that are well established, have a fair coin distribution, and have robust and actively maintained node implementation(s). We should avoid supporting currencies that are scammy and unprincipled.
|
||||||
|
|
||||||
|
We should try to support as few currencies as is necessary, because each supported currency is another moving part that the Seekia network must rely upon.
|
||||||
|
|
||||||
|
I want to avoid supporting proof of work currencies because they use more energy, rely on more hardware, have less consistent block times, and are arguably less resistant to attacks.
|
||||||
|
|
||||||
|
We should try to add currencies that have privacy tools built for them.
|
||||||
|
|
||||||
|
Future currencies/networks to support:
|
||||||
|
|
||||||
|
1. Ethereum Layer 2s/Sidechains
|
||||||
|
2. Cardano Layer 2s/Sidechains
|
||||||
|
|
||||||
|
### Why not Monero?
|
||||||
|
|
||||||
|
Monero is a very useful cryptocurrency, but it is not suitable for Seekia's use case of publicly destroying coins.
|
||||||
|
|
||||||
|
Outputs would have to be publicly burned, which would create many useless decoys for other transactions, reducing privacy for other Monero users.
|
||||||
|
|
||||||
|
Using Monero in this way would also reduce the privacy of Seekia users. Each burned output's input decoys could more easily be traced to a user's real world identity, aided by the user's profile metadata.
|
||||||
|
|
||||||
|
Linking two consecutively burned outputs together would also be quite easy due to the limited number of decoys. An example would be if a user funds their credit account after funding their moderator identity. This is obviously an even greater problem on transparent blockchains like Ethereum, but Monero has an expectation of being private which we do not want to degrade.
|
||||||
|
|
||||||
|
The blockchain servers would also have to parse all the outputs with a public view key, which would be slower.
|
||||||
|
|
||||||
|
### Coin Burn Address Type
|
||||||
|
|
||||||
|
Cryptocurrencies could implement a special address type for burning coins, so that Seekia transactions do not bloat the UTXO set/state which nodes have to keep track of, and so that wallets can warn users when burning funds.
|
||||||
|
|
||||||
|
## Profiles
|
||||||
|
|
||||||
|
There are three kinds of profiles: Mate, Host, and Moderator profiles.
|
||||||
|
|
||||||
|
### Disabled Profiles
|
||||||
|
|
||||||
|
To disable a profile, a user broadcasts a new profile with an attribute "Disabled" set to "Yes".
|
||||||
|
|
||||||
|
Profiles of all identity types can be disabled.
|
||||||
|
|
||||||
|
Disabled Mate/Host profiles will be retained by the network until the identity type's network profile inactivity duration has passed.
|
||||||
|
|
||||||
|
Moderator disabled profiles must be kept in the network forever, because moderator profiles never expire. This only applies to Moderators who have funded their identity.
|
||||||
|
|
||||||
|
## Race
|
||||||
|
|
||||||
|
Seekia aims to help cure racial loneliness by helping users to find mates who are racially similar to them.
|
||||||
|
|
||||||
|
Seekia allows users to filter mates by traits such as eye color, skin color, hair color, and hair texture.
|
||||||
|
|
||||||
|
Seekia profiles can also contain a user's genetic ancestry and trait genetics, and users can filter other users based on these attributes.
|
||||||
|
|
||||||
|
### Racial Similarity
|
||||||
|
|
||||||
|
Users are able to sort other users based on their racial similarity.
|
||||||
|
|
||||||
|
Seekia aims to help people find the most racially similar person, but one who is not similar enough that their offspring would have health issues. This is a tool that is useful in Seekia's goal to cure racial loneliness.
|
||||||
|
|
||||||
|
Racial similarity aims to help match people who look alike and have similar genetic sequences for physical traits. These matches are more likely to breed children who look similar to them.
|
||||||
|
|
||||||
|
Racial similarity is calculated by comparing trait similarity, trait genetic similarity, ancestral similarity, and haplogroup similarity.
|
||||||
|
|
||||||
|
#### Trait Similarity
|
||||||
|
|
||||||
|
To calculate trait similarity, each user's eye color, skin color, hair color, and hair texture are compared.
|
||||||
|
|
||||||
|
For example, if both users have blue eyes, their Eye Color similarity is 100%.
|
||||||
|
|
||||||
|
Facial similarity detection technology is a planned feature to help cure racial loneliness. The Seekia app could scan user profile photos to help users to find potential mates whom have similar physical traits.
|
||||||
|
|
||||||
|
#### Trait Genetic Similarity
|
||||||
|
|
||||||
|
Each user can choose to share the genes which effect eye color, skin color, hair color, hair texture, and facial structure.
|
||||||
|
|
||||||
|
Seekia compares the percentage of these genes which are similar between two people to calculate genetic similarity for each trait.
|
||||||
|
|
||||||
|
#### Ancestral Similarity
|
||||||
|
|
||||||
|
Ancestral Similarity is a percentage value representing how closely related the ancestral categories of 2 users are.
|
||||||
|
|
||||||
|
It relies on the ancestral composition provided by companies such as 23andMe.
|
||||||
|
|
||||||
|
A different ancestral analysis method could be created that has many more categories, but the categories have no location names. Each name would instead be a category identifier, which could be a 4 byte value.
|
||||||
|
|
||||||
|
## Genetics
|
||||||
|
|
||||||
|
Seekia offers the ability to analyze a user's genome, and share information about a user's genetics on their profile.
|
||||||
|
|
||||||
|
This allows for users to reduce the probability of their offspring having genetic diseases, and to increase the probability of their offspring having certain traits.
|
||||||
|
|
||||||
|
*TODO: Describe these features in more detail.*
|
||||||
|
|
||||||
|
## Chat
|
||||||
|
|
||||||
|
Seekia allows users to chat.
|
||||||
|
|
||||||
|
Messages can contain an image, text, an emoji, or a questionnaire response.
|
||||||
|
|
||||||
|
Messages are encrypted with Nacl and Kyber using the keys broadcasted in the recipient's profile.
|
||||||
|
|
||||||
|
Each message has an Inbox, which is used by the recipient when downloading their messages from hosts.
|
||||||
|
|
||||||
|
Only Mate/Moderator users can send messages. Hosts are excluded, because I see no need at this moment for hosts to communicate over the network. Hosts can always share some other communication method such as an email address on their profile.
|
||||||
|
|
||||||
|
Inter-identityType communication is forbidden. Mate users can only contact Mate users, and moderators can only contact other moderators. This rule exists to prevent Mate users from being manipulated by malicious Moderators.
|
||||||
|
|
||||||
|
### Funding Messages
|
||||||
|
|
||||||
|
Each message must be funded with credit. The cost depends on the size and duration of the message.
|
||||||
|
|
||||||
|
Each message only has 2 options for duration: 2 days and 2 weeks. This makes all messages look more similar, reducing the possibility of linking a message fund duration to a particular sender.
|
||||||
|
|
||||||
|
Once a message is funded, its duration cannot be extended. This allows hosts and users to not have to make any more queries to the credit servers after they have confirmed that a message has been funded and retrieved its expiration time.
|
||||||
|
|
||||||
|
### Message Encryption Keys
|
||||||
|
|
||||||
|
Each Mate and Moderator profile has 2 public encryption keys: Nacl and Kyber.
|
||||||
|
|
||||||
|
These are used to encrypt all messages sent to that identity.
|
||||||
|
|
||||||
|
They are amnesic, meaning new keys are periodically broadcast in a user's profile and old keys are eventually deleted.
|
||||||
|
|
||||||
|
The same chat key set is used for all of an identity's conversations when they are in use.
|
||||||
|
|
||||||
|
Old keys are deleted from the client machine after the client has received and decrypted the last messages still existing for a particular set of keys. The client will wait to make sure any messages that have been encrypted with those keys have propagated throughout the network and been downloaded.
|
||||||
|
|
||||||
|
A user's profile and sent messages contain a **ChatKeysLatestUpdateTime** attribute to alert users when they have updated their keys on their profile. Users will not send messages to another user unless they have downloaded their current active chat keys. A grace period exists which allows old keys to still be used even after new keys have been broadcast.
|
||||||
|
|
||||||
|
Chat keys are generated from scratch on a user's machine and must be exported from the app to be able to decrypt old conversations upon signing in on a new client. This is done so that if someone were to compromise a user's seed phrase, they could not decrypt any of their chat messages.
|
||||||
|
|
||||||
|
If a user's computer is compromised, any amnesic keys on the user's computer could be used to decrypt all messages sent to that user. Messages which were deleted along with their keys would still remain encrypted.
|
||||||
|
|
||||||
|
### Device Identifier
|
||||||
|
|
||||||
|
Upon startup, the Seekia application creates a random device seed.
|
||||||
|
|
||||||
|
Unique device identifiers are generated from this seed for a user's Mate/Host identities.
|
||||||
|
|
||||||
|
These identifiers are broadcasted on a user's profile and in their sent messages.
|
||||||
|
|
||||||
|
If Bob's device identifier changes, others know that they must discard Bob's old chat keys, and delete any secret inboxes they have saved for Bob. Chat keys and secret inboxes must be imported from an old device, and users will assume that Bob did not import them.
|
||||||
|
|
||||||
|
Device identifiers are also useful when a user restores their identity to a new device. The client will attempt to download any existing profiles authored by the user's identity from the network. The client compares the downloaded profile's device identifier with the client's device identifier, and is able to determine that the user's existing profile was created on a different device. This information is shown to the user when the client offers to import information from the profile.
|
||||||
|
|
||||||
|
### Public Inbox
|
||||||
|
|
||||||
|
Each message is sent to an inbox. Users download messages by querying hosts for messages sent to their inboxes.
|
||||||
|
|
||||||
|
All users have a public inbox, which is a hash of their identity hash. Everyone can see how many messages each user has received in their public inbox. Public inboxes are a tradeoff which increase the speed of downloading messages but are detrimental to privacy. A way to avoid having public inboxes is to require users to download many messages that were not sent to them in order to scan and determine which messages were sent to them, similar to how stealth addresses in blockchains work (see Monero view tags).
|
||||||
|
|
||||||
|
It is possible for anyone to see statistics about message recipients. An example would be a chart that shows Wealth on the X axis and Number Of Public Inbox Messages on the Y axis. This could be built into the Seekia app on the Message Statistics page.
|
||||||
|
|
||||||
|
A way to reduce this privacy flaw is to set up services which send many fake messages to inboxes with fewer messages. This would equalize the number of messages in all inboxes, making it much more difficult to tell who has received more messages. These messages need to be crafted and broadcast in a way that looks fully authentic. It may not be worth it, because it would increase the number of messages on the Seekia network substantially. A better option is for the service to not try to make all inbox quantities equal, but rather equalize only the lower tail end of public inbox quantities.
|
||||||
|
|
||||||
|
### Secret Inboxes
|
||||||
|
|
||||||
|
If users only had public inboxes, it would be possible to analyze the message sent times and other metadata about users to determine which users were communicating. For example, if 2 inboxes had increased activity around the same time periods, and their owners lived near each other, it would be possible to guess that those 2 users were communicating. This kind of analysis becomes more difficult as more users join Seekia.
|
||||||
|
|
||||||
|
To increase the privacy of chat messages, Seekia uses secret inboxes. Each message contains a Current and Next secret inbox seed. These are used to generate the inboxes that the recipient should send future messages to. A message sender's Current and Next secret inboxes should be sent to during the Current and Next secret inbox epochs. Each secret inbox is unique to each conversation recipient.
|
||||||
|
|
||||||
|
The secret inbox epoch is a time period that is defined by the secret inbox epoch duration, a variable provided within the network parameters. Each epoch start and end time is agreed upon by all users of Seekia.
|
||||||
|
|
||||||
|
Using secret inbox epochs allows for all secret inbox conversation pairs to change across the network at the same time. This facilitates a mixing effect and improves privacy. Without a global secret inbox epoch, a secret inbox's true recipient could be revealed by analyzing when a secret inbox stops receiving messages and a public inbox starts receiving messages.
|
||||||
|
|
||||||
|
A sender should not stop sending to a recipient's secret inbox until the epoch which the secret inbox belongs to has passed. If a secret inbox epoch is 3 days long, the recipient will send to the sender's 2 secret inboxes for a minimum of 3 days, or a maximum of 6 days. This depends on if the most recent message was sent towards the beginning or the end of the current secret inbox epoch.
|
||||||
|
|
||||||
|
Bob knows to send to Alice's current and next secret inboxes whenever Alice sends a new message. Only 2 secret inboxes are needed, because if Alice has not responded to Bob's message during these 2 epochs, it is safe for Bob to send messages to Alice's public inbox. The risk of timing analysis attacks becomes greatly reduced. A shorter secret inbox epoch reduces the length of time that Alice's client has to check for new messages from her secret inboxes.
|
||||||
|
|
||||||
|
As more users join Seekia, the secret inbox epoch duration can be reduced, as timing attacks will become increasingly difficult.
|
||||||
|
|
||||||
|
### Message Encryption Scheme
|
||||||
|
|
||||||
|
Messages are encrypted using Nacl and Kyber.
|
||||||
|
The ChaCha20Poly1305 (ChaPoly) cipher is used.
|
||||||
|
|
||||||
|
Two randomly generated 32 Byte keys are created.
|
||||||
|
|
||||||
|
These keys are XORed to derive a Basaldata decryption key.
|
||||||
|
|
||||||
|
The Basaldata decryption key is hashed to create a Message Cipher Key.
|
||||||
|
|
||||||
|
Both keys are encrypted: One with Nacl, and the other with Kyber.
|
||||||
|
|
||||||
|
The basaldata decryption key cannot be derived from the message cipher key. This is done because users share the message cipher key when reporting a message. Reporters are able to reveal the contents of the message communication and the message sender without revealing the Basaldata, which contains the message recipient (generally the person making the report) and other sensitive information.
|
||||||
|
|
||||||
|
In cases where the recipient of the message received the message to their secret inbox, revealing the cipher key would not reveal the message recipient's identity hash. For messages sent to the recipient's public inbox, the basaldata only prevents some of the sender's metadata from being revealed.
|
||||||
|
|
||||||
|
#### SealedKeys Encryption
|
||||||
|
|
||||||
|
The Nacl and Kyber encrypted key pieces are known as the SealedKeys.
|
||||||
|
|
||||||
|
The sealed keys are encrypted by a ChaPoly cipher using a SealedKeysSealerKey as the key.
|
||||||
|
|
||||||
|
For messages sent to a user's public inbox, the SealedKeysSealerKey is a hash of the recipient's identity hash.
|
||||||
|
|
||||||
|
For messages sent to a user's secret inboxes, the SealedKeysSealerKey is derived from the SecretInboxSeed.
|
||||||
|
|
||||||
|
The sealer key is used because revealing the SealedKeys increases the cryptographic attack surface. It is easier to determine who the messages were sent to if we have their public Nacl/Kyber keys, and the keys encrypted with those public keys. This may already be possible, or may become possible by some future cryptoanalytic breakthrough.
|
||||||
|
|
||||||
|
The SealedKeysSealerKey only increases privacy for messages sent to a recipient's secret inboxes. The encryption of the SealedKeys is not needed for public inboxes, because the recipient's identity is already knowable by their public inbox. It is done anyway to make all messages look more similar. An observer would not know that the message was sent to a public inbox unless they had the recipient's identity hash, which is needed to derive their public inbox.
|
||||||
|
|
||||||
|
### Message Cipher Key Hash
|
||||||
|
|
||||||
|
All messages include a Message Cipher Key Hash, which is a hash of the message cipher key.
|
||||||
|
|
||||||
|
This is useful as a method for moderators to prove that they have seen the contents of the message.
|
||||||
|
|
||||||
|
Moderators share the message's cipher key in their reviews, and if the cipher key hashes to the message's cipher key hash, we know that the moderator has seen the contents of the message (or the message is malformed and the moderator is malicious).
|
||||||
|
|
||||||
|
Message cipher key hashes are stored in the database as metadata, so hosts and moderators can delete messages, keep their metadata, and still be able to verify that review authors have actually seen the contents of the message. This saves space by not requiring messages to be stored while still being able to verify message reviews.
|
||||||
|
|
||||||
|
### Reporting Messages
|
||||||
|
|
||||||
|
If a user wants to report messages, they will do so publicly. Reports are created anonymously. Each reported message's recipient is knowable if the message was sent to their public inbox, and is hidden if the message was sent to their secret inbox.
|
||||||
|
|
||||||
|
A message report contains the message hash and message cipher key. The cipher key is used by the moderators to decrypt the message.
|
||||||
|
|
||||||
|
The moderators provide the message cipher key with their reviews, which acts as a proof that the moderator has seen the message, and another source for moderators to retrieve the message's cipher key. Reviews provide a way for moderators to get the message's cipher key after the original message report has expired from the network.
|
||||||
|
|
||||||
|
### Downloading Messages
|
||||||
|
|
||||||
|
Users download their messages by downloading the messages within their public inboxes and active secret inboxes.
|
||||||
|
|
||||||
|
The client keeps a list of active secret inboxes, and checks them one-by-one from different hosts over new tor circuits to prevent hosts from linking the inboxes together.
|
||||||
|
|
||||||
|
If a user contacts 100 users during a particular secret inbox epoch, this will add 100 secret inboxes to check for the current secret inbox epoch, and 100 to check for the next epoch.
|
||||||
|
|
||||||
|
Once a client has synced up from a certain unix time, it stops downloading messages from old secret inboxes. After an inbox expires, no new messages will be sent to it. After a secret inbox has expired and the user has checked it sufficiently for new messages, the user's client deletes the secret inbox.
|
||||||
|
|
||||||
|
This system may be too slow to download messages one-by-one if a user has hundreds of inboxes. An easy solution is to have trusted hosts, which would be listed in the parameters, who promise to not log requests. Users can request to download messages from all of their inboxes in the same request to these hosts, drastically increasing the speed at which they receive their messages. Another option is to download message inboxes 2-at-a-time, which only slightly reduces privacy. The same inbox pair should be provided in each request, to prevent hosts from learning more inboxes by linking the same inboxes from different pairs together.
|
||||||
|
|
||||||
|
It is important to note that sent messages will not appear as quickly in the recipient's client as they do for centralized messaging providers. A broadcasted message must propagate throughout the network hosts before being downloaded by the recipient, which increases latency.
|
||||||
|
|
||||||
|
### Using Different Devices
|
||||||
|
|
||||||
|
Users can only use Seekia one device at a time. Upon signing in to a new device, a user's profile broadcasts a device identifier, letting others know that they should discard their secret inboxes and chat keys, and download their new profile.
|
||||||
|
|
||||||
|
To transfer all conversation history and user data to a new device, a user can export and import their Seekia data.
|
||||||
|
|
||||||
|
Implementing a multi-device scheme would add a lot of complexity. Users would have to download the messages they sent from their other devices, which would be encrypted with keys that all of their devices had downloaded. Some form of cloud storage would be necessary, which could be updated whenever the user performed actions such as adding a user to their contacts or likes. Observers could identify when the user updated their encrypted cloud container and learn when they are online and potentially what they are doing. Adding these features is not worth the hassle.
|
||||||
|
|
||||||
|
Users who want to use multiple devices simultaneously could instead remotely access the device that their Seekia identity is signed in to. This feature should be built into the application, so the usage experience would be near-identical. They would have to leave their main device running.
|
||||||
|
|
||||||
|
### Resisting Network Analysis
|
||||||
|
|
||||||
|
All conversations are initiated to a user public inboxes.
|
||||||
|
|
||||||
|
Responses that are sent within the message sent time's current or next secret inbox epoch will be sent to user secret inboxes.
|
||||||
|
|
||||||
|
If a secret inbox received a new message immediately after Bob's public inbox receives one, an observer could guess that that message was a response from Bob. This becomes much more difficult to guess as more users join Seekia.
|
||||||
|
|
||||||
|
An observer could match up pairs of secret inboxes by correlating message sent times. The observer should still be unable to tell who is using each secret inbox.
|
||||||
|
|
||||||
|
Secret inboxes are retired for both conversation parties at the end of the epoch, which gives observers less time to correlate two secret inboxes together.
|
||||||
|
|
||||||
|
All secret inboxes are updated at the same time across all users, which makes it harder to link a previous secret inbox pair to a second secret inbox pair.
|
||||||
|
|
||||||
|
If all users refreshed their secret inboxes independently from the rest of the network, it would be easier to link a newly created secret inbox to an existing secret inbox through frequency analysis. An observer could see when one secret inbox stops receiving messages, and the next one starts receiving messages, and guess that those inboxes were owned by the same user.
|
||||||
|
|
||||||
|
Message metadata can greatly reduce privacy. The more unique a user's messaging patterns are, the easier it is to identify the messages they send. For example, if a user sends 14 image messages to many different users in quick succession, each group of 14 images could be identified and linked to the same sender.
|
||||||
|
|
||||||
|
As the number of Seekia users increases, the anonymity set grows larger, reducing the ability for observers to analyze the network.
|
||||||
|
|
||||||
|
## Moderation
|
||||||
|
|
||||||
|
Seekia has an open and decentralized moderation system.
|
||||||
|
|
||||||
|
Moderators create identity, profile, attribute, and message reviews.
|
||||||
|
|
||||||
|
Each identity, profile, and message has a **Verdict**. A verdict is calculated by collecting and analyzing all of the reviews for an identity, profile, or message. Users can be Banned or Not Banned. Messages/Profiles can be Approved or Banned.
|
||||||
|
|
||||||
|
Anyone can participate as a moderator. They must first fund their identity with cryptocurrency.
|
||||||
|
|
||||||
|
Anyone can report users, profiles, attributes, and messages. Reports are intended to only be created by non-moderators. Moderators should instead create a ban review to achieve the same effect. Reporting a message involves revealing the decryption key so moderators can view the message.
|
||||||
|
|
||||||
|
### Identity Score
|
||||||
|
|
||||||
|
Moderators must have a sufficient identity score to participate.
|
||||||
|
|
||||||
|
An identity score is sum of the gold value at time sent of cryptocurrency deposits sent to a moderator's identity score crypto addresses.
|
||||||
|
|
||||||
|
A moderator's identity score crypto addresses are derived from their identity hash. Any funds sent to them are destroyed forever.
|
||||||
|
|
||||||
|
There are two gold values defined in the network parameters: The minimum amount required to be a moderator, and the minimum amount required for the moderator to be able to ban other moderators.
|
||||||
|
|
||||||
|
Moderators can always send more money to their addresses to increase their score, and can do the same for other moderators whom they trust.
|
||||||
|
|
||||||
|
All moderators can be sorted based on their identity scores. A moderator's rank is defined by this list.
|
||||||
|
|
||||||
|
Moderators can ban moderators who are below them in rank.
|
||||||
|
|
||||||
|
The identity score exists as a bad behavior deterrent. If a moderator begins banning ruleful content and moderators, a higher ranked moderator can ban them. Thus, it costs money to be a malicious moderator. Once a moderator's identity is banned, they can try to convince the moderator(s) to un-ban them, try to convince higher ranked moderators to ban the moderator(s) who banned them, or spend more money to outrank and ban the moderator(s) who banned them. This system encourages cooperation, because moderators will not want to play leapfrog by spending more and more money to ban each other.
|
||||||
|
|
||||||
|
If there are enough good-natured moderators, the system should be able to root out bad moderators while still being decentralized.
|
||||||
|
|
||||||
|
A moderator's score also determines how many reviews they can upload to the network. This limit exists to prevent malicious moderators from spamming the network with reviews.
|
||||||
|
|
||||||
|
### Supermoderators
|
||||||
|
|
||||||
|
Supermoderators are moderators whom can ban all moderators below them in rank, without necessarily having a higher identity score. They are given this power by the admin(s), and the list of supermoderators is provided within the network parameters. Supermoderators are described in a ranked order, and can ban each other.
|
||||||
|
|
||||||
|
Supermoderators should ideally only need to use their power in extreme circumstances. Supermoderators are a tool to salvage the moderation system if many malicious moderators join and become highly ranked.
|
||||||
|
|
||||||
|
Without supermoderators, if a single moderator spend enough funds to become the highest ranked moderator, they could ban all other moderators and cripple the network. This malicious moderator could only be defeated by at least 1 good moderator who burns more money than the malicious one to be able to ban them. With supermoderators, a supermoderator can ban this malicious moderator to rescue the network from the control of the malicious moderator, without having to spend any money.
|
||||||
|
|
||||||
|
Supermoderators currently only have the absolute authority to ban other moderators. Their profile/message/attribute reviews are counted the same as other moderators. This is designed this way because it simplifies the protocol and code, and increases decentralization by reducing the power of supermoderators. If a profile is wrongfully approved/banned, banning the moderators who created those reviews will undo the wrongful verdicts.
|
||||||
|
|
||||||
|
### Verdicts
|
||||||
|
|
||||||
|
There are two kinds of verdicts. Each review contains a verdict, and each identity/profile/message has a network consensus verdict.
|
||||||
|
|
||||||
|
*TODO: Change the name of Verdict to something else for reviews. Perhaps "Judgement"?*
|
||||||
|
|
||||||
|
Below are the types of review verdicts:
|
||||||
|
|
||||||
|
* Identity: Ban/None
|
||||||
|
* Profile: Approve/Ban/None
|
||||||
|
* Attribute: Approve/Ban/None
|
||||||
|
* Message: Approve/Ban/None
|
||||||
|
|
||||||
|
Below are the types of network consensus verdicts:
|
||||||
|
|
||||||
|
* Identity: Banned/Not Banned
|
||||||
|
* Profile: Approved/Banned/Undecided
|
||||||
|
* Message: Approved/Banned/Undecided
|
||||||
|
|
||||||
|
User identities cannot be approved, they can only be banned. This is because a user can always change their behavior to become unruleful. Only profiles and messages can be approved, because their content is static.
|
||||||
|
|
||||||
|
The process to calculate the verdict of an identity/profile/message is complex. It involves using the identity scores of moderators to weight their verdicts.
|
||||||
|
|
||||||
|
See `internal/moderation/verifiedVerdict/verifiedVerdict.go` to see how verdicts are calculated.
|
||||||
|
|
||||||
|
### Viewable Statuses
|
||||||
|
|
||||||
|
Each identity/profile/message has a Viewable status.
|
||||||
|
|
||||||
|
A viewable profile/identity/message is one that can be displayed to users. Hosts can choose not to host unviewable profiles and messages if they want to avoid hosting unruleful and illegal content. Unviewable profiles and messages should eventually be deleted from the network.
|
||||||
|
|
||||||
|
When downloading Mate profiles to browse, users use the `GetViewableProfilesOnly` parameter to only download viewable profiles. Mate users also download the viewable statuses for identities and profiles they have downloaded. The GUI will only show matches whom are viewable, and will warn users when trying to view unviewable profiles.
|
||||||
|
|
||||||
|
#### Sticky Viewable Status
|
||||||
|
|
||||||
|
Calculating viewable statuses requires first calculating sticky viewable statuses.
|
||||||
|
|
||||||
|
Sticky viewable statuses are a kind of consensus status that requires a verdict to be present for a minimum defined period of time. To calculate a viewable status, its verdict history is needed.
|
||||||
|
|
||||||
|
Sticky statuses are needed to defend against malicious moderators.
|
||||||
|
|
||||||
|
Imagine this scenario: A malicious moderator bans all other moderators and all content on the network. All identities, profiles, and messages on the network are now **Banned** (except for the malicious moderator). Other moderators need to ban this moderator to undo the damage.
|
||||||
|
|
||||||
|
Without sticky consensus, all the hosts would treat all network profiles as being banned, and would stop seeding these profiles to users. This single malicious moderator could cripple the network for as long as it would take to ban that moderator. Banning this moderator could take hours, and is more difficult the more highly ranked they are.
|
||||||
|
|
||||||
|
Sticky statuses attempt to solve this problem.
|
||||||
|
|
||||||
|
With sticky consensus, as long as a profile/message/identity has been viewable for a certain period of time, its sticky viewable status becomes stuck.
|
||||||
|
|
||||||
|
For the sticky status to be switched to Unviewable, the profile/message/identity's status would have to be Unviewable for a certain period of time.
|
||||||
|
|
||||||
|
Hosts will serve content to users based on each identity/profile/message's sticky viewable status, not its real-time consensus verdict.
|
||||||
|
|
||||||
|
#### Sticky Status Establishing Time
|
||||||
|
|
||||||
|
Hosts must be online for long enough to determine, or establish, the sticky status for content within their ranges. Each sticky status can only be considered established if the user's client has been downloading the content's reviews for long enough.
|
||||||
|
|
||||||
|
This establishing time is needed for several reasons:
|
||||||
|
1. When adding a new range, the host needs time to initially download the reviews for content within the range.
|
||||||
|
2. Hosts may initially get an inaccurate view of the sticky status due to malicious hosts or hosts that are not caught up with the rest of the network
|
||||||
|
3. The status may have only recently been flipped by many malicious moderators, and will be flipped back to the "true" status after those malicious moderators are banned. Without waiting, the host would only see a small portion of the true verdict history.
|
||||||
|
* For example, During the last 5 minutes, a profile was viewable 100% of the time
|
||||||
|
* But within the last 50 minutes, it has been viewable for only 10% of the time
|
||||||
|
|
||||||
|
Hosts will only share an identity/profile/message's viewable status to requesting peers once the status is established.
|
||||||
|
|
||||||
|
#### Sticky Viewable Status versus Viewable Status
|
||||||
|
|
||||||
|
Each identity/profile/message has a viewable status and a sticky viewable status.
|
||||||
|
|
||||||
|
Throughout the code, you will see **Viewable Status** and **Sticky Status** to describe each.
|
||||||
|
|
||||||
|
An Identity's sticky viewable status is always identical to its viewable status.
|
||||||
|
|
||||||
|
For Messages/Profiles, sticky viewable statuses and viewable statuses are identical, except that viewable statuses take into account the sticky viewable status of their author.
|
||||||
|
|
||||||
|
For example, let's say a profile is approved, but was created by an author who is banned.
|
||||||
|
|
||||||
|
* The profile's viewable status is False.
|
||||||
|
* The profile's sticky viewable status is True.
|
||||||
|
|
||||||
|
*This is rather confusing. Maybe use Seeable and Unseeable for sticky statuses.*
|
||||||
|
|
||||||
|
#### Calculating Sticky Viewable Status
|
||||||
|
|
||||||
|
Calculating a sticky status involves checking what the real-time consensus verdict was for the identity/profile/message for the past period, and calculating what percentage of those verdicts were viewable.
|
||||||
|
|
||||||
|
Below describes the verdicts that define whether an identity, profile, or message is viewable:
|
||||||
|
|
||||||
|
Type | Viewable | Unviewable
|
||||||
|
--- | --- | ---
|
||||||
|
**Identity** | Not Banned | Banned
|
||||||
|
**Mate Profile** | Approved | Undecided/Banned
|
||||||
|
**Host/Moderator Profile** | Approved/Undecided | Banned
|
||||||
|
**Message** | Approved/Undecided | Banned
|
||||||
|
|
||||||
|
To calculate a sticky viewable status, there exist 3 parameters that are provided in the network parameters.
|
||||||
|
|
||||||
|
Identities, Profiles, and Messages each have their own 3 variables for calculating sticky status:
|
||||||
|
|
||||||
|
1. StatusEstablishingTime
|
||||||
|
* This describes, in seconds, the amount of time that a host must be downloading the reviews for an identity/profile/message to be able to determine its sticky status.
|
||||||
|
2. VerdictExpirationTime
|
||||||
|
* This describes, in seconds, the amount of time that consensus verdicts should be included in a sticky status's verdict history.
|
||||||
|
* For example, if the verdict expiration time is 10000, then any verdicts that occurred more than 10000 seconds ago will not be included in the calculation.
|
||||||
|
3. MinimumViewablePercentage
|
||||||
|
* The minimum percentage of verdicts that must be Viewable for the sticky status to be Viewable
|
||||||
|
* For example, if the value is set to 60% for identities, then an identity's verdict must be **Not Banned** for at least 60% of all verdicts younger than the VerdictExpirationTime
|
||||||
|
|
||||||
|
See `/internal/moderation/verifiedStickyStatus/verifiedStickyStatus.go` for the full implementation.
|
||||||
|
|
||||||
|
### Trusted Viewable Statuses
|
||||||
|
|
||||||
|
Normal users download viewable statues from hosts for content they download.
|
||||||
|
|
||||||
|
Users must download the viewable statuses from multiple hosts to be sure that the statuses are accurate. This requirement makes it unlikely that a user's client will mistakenly believe an unviewable profile is viewable.
|
||||||
|
|
||||||
|
### Banned Message Authors
|
||||||
|
|
||||||
|
The Seekia application will download the viewable status of all message authors for a user. If a message's author is banned, the application will hide messages sent by that user, unless the user decides to show messages from banned users. This will be very useful if identities are created which spam the network with advertisements and junk. The Seekia application does not have a spam message filter, so banning these users will hide their messages for all users.
|
||||||
|
|
||||||
|
Users may be wrongfully banned, or users may still want to communicate with the person, even if they were banned from the network. Assuming the user is not unbanned, their profile will disappear from the network, but they should still be able to chat with users who choose to show Banned users in their conversations.
|
||||||
|
|
||||||
|
The banned user's chat keys should still work, allowing the user to migrate to different communication channels.
|
||||||
|
|
||||||
|
One interesting possible feature is to ban a user's public inbox after they are banned. This would not allow messages to be sent to banned user public inboxes anymore. If this was implemented, a grace period where the user's inbox would be allowed to exist would be necessary. This grace period would prevent a user's inbox from being deleted if they are wrongfully banned and then unbanned shortly after. Secret inboxes would be immune from inbox bans, so the banned user would still be able to chat by initiating conversations with users. Users would not be able to initiate conversations with banned users, but they would be able to respond and continue the conversation. The Seekia app would also have to warn users that their messages will not be delivered if the user's client believes that the message's recipient is banned, and the message is being sent to a public inbox.
|
||||||
|
|
||||||
|
### Profile Attribute Reviews
|
||||||
|
|
||||||
|
When reviewing profiles, moderators can submit Profile reviews or Attribute reviews.
|
||||||
|
|
||||||
|
An Attribute review is a review of a specific attribute within a profile.
|
||||||
|
|
||||||
|
Attribute reviews have several advantages:
|
||||||
|
|
||||||
|
1. A moderator can specify the attribute that caused them to ban a profile.
|
||||||
|
* Example: Ban profile because Description was unruleful.
|
||||||
|
2. Moderators do not have to approve all attributes of a profile again if the user resubmits their profile with 1 attribute changed.
|
||||||
|
* The moderators only have to approve the single changed attribute, because the moderators could have already approved all of the profile's other attributes.
|
||||||
|
3. Moderators can choose the kinds of attributes they want to review.
|
||||||
|
* A moderator can choose to only review images, and they can still contribute to the network and reduce the amount of work other moderators have to do.
|
||||||
|
4. Moderators can review 1 kind of attribute at a time.
|
||||||
|
* Within the GUI, moderators choose the attribute they want to review, and cycle through the attribute value for each user profile. This reduces the cognitive load of context switching and increases moderator efficiency.
|
||||||
|
|
||||||
|
Full profile reviews still exist because moderators sometimes have to ban profiles which have no specific unruleful attribute, but are still unruleful. For example, if a profile's photo is of a young man but their age is 100. In that case, if a moderator banned the Photos attribute, it would be unclear for other moderators why the photo was banned if the photo itself was ruleful. The moderators would have to read the moderator's reason for banning the photo to understand. The GUI should prominently show how many moderators have banned/approved an attribute, and the reasons should be easily accessible to avoid misunderstandings.
|
||||||
|
|
||||||
|
### Undoing Reviews
|
||||||
|
|
||||||
|
There is a third type of verdict called a **None** verdict. These verdicts are used to undo previous verdicts. A review with a newer BroadcastTime is created for the same reviewedHash, and the old review is eventually discarded. The old review may be kept on the network if the review was used as a reason for banning the moderator in any identity ban reviews.
|
||||||
|
|
||||||
|
Attribute reviews add complexity to how a profile's reviews are undone. If a moderator bans an attribute, and later approves the full profile, then the attribute review is disregarded. If a moderator approves a full profile, and later bans an attribute from that profile, the full profile approval is disregarded.
|
||||||
|
|
||||||
|
### Content And Review Pruning
|
||||||
|
|
||||||
|
Once content becomes banned, it is still hosted by the network for some time. This is because if it was wrongfully banned, ruleful moderators need to be able to see what the content contained to determine if those who banned it were wrong for doing so.
|
||||||
|
|
||||||
|
Once content and/or its author have been banned for long enough, hosts will delete the content. They will maintain its metadata so they can continue to verify reviews and be aware if the content is within their range (see `contentMetadata.go`)
|
||||||
|
|
||||||
|
Reviews of banned identities/content will be kept until the identity/content expires from the network. It may be possible to delete reviews for content before it expires from the network if the content's author has been banned, but this should only be done if the author has been banned by a substantial enough number of moderators. The application should also wait a certain amount of time, because we don't want to lose the historical reviews from wrongfully banned moderators if a malicious moderator bans all moderators.
|
||||||
|
|
||||||
|
Each moderator's Seekia application should keep all of the locally authored and broadcasted reviews until the reviewed identity/content has expired. This way, if the reviews are dropped from the network prematurely, the moderator's client will be able to rebroadcast those reviews.
|
||||||
|
|
||||||
|
### Content Controversy
|
||||||
|
|
||||||
|
Each piece of content has a **Controversy** rating. Controversial content is content which has a large amount of disagreement around its verdict.
|
||||||
|
|
||||||
|
This can be used by other moderators to find and root out bad moderators. Controversial content may also be used to foster conversations between moderators to define the rules of the network.
|
||||||
|
|
||||||
|
### Moderator Controversy
|
||||||
|
|
||||||
|
Each moderator has a **Controversy** rating. Controversial moderators are moderators who disagree with other moderators the most often. Users can view and sort moderators by their controversy to aid in rooting out unruleful moderators.
|
||||||
|
|
||||||
|
### Automatic Banning
|
||||||
|
|
||||||
|
Moderator clients will automatically ban other moderators who create invalid reviews. These reviews cannot be created by the Seekia application, so any such reviews must have been authored by a custom piece of software.
|
||||||
|
|
||||||
|
Moderators could also automatically ban malicious hosts. Malicious hosts are hosts who provide invalid data in their network responses. The moderator would be completely sure if a host was malicious, because they would have downloaded the malicious response themselves, and the response must have been signed by the host's identity key.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Thank you for reading the Seekia documentation.
|
||||||
|
|
||||||
|
Much remains to be described in this document. Read the code to fully understand the innerworkings of Seekia.
|
475
documentation/Future Plans.md
Normal file
475
documentation/Future Plans.md
Normal file
|
@ -0,0 +1,475 @@
|
||||||
|
|
||||||
|
# Future Plans
|
||||||
|
|
||||||
|
The future of Seekia is full of possibilities.
|
||||||
|
|
||||||
|
This document describes the currently planned and theorized features and changes to be made.
|
||||||
|
|
||||||
|
## Before Launch
|
||||||
|
|
||||||
|
There are features and changes to be made before Seekia is ready for launch.
|
||||||
|
|
||||||
|
Many tasks are not included here, but are instead annotated within the code with the **TODO** keyword.
|
||||||
|
|
||||||
|
### Account Credit Database and Servers
|
||||||
|
|
||||||
|
See `Documentation.md` for the description of how this system should work.
|
||||||
|
|
||||||
|
### Importing Profiles
|
||||||
|
|
||||||
|
If a user restores their identity on a new device, they will have the option of exporting/importing their user data folder to retain their data.
|
||||||
|
|
||||||
|
Their client should also try to download their broadcasted profile from the network.
|
||||||
|
|
||||||
|
If a profile exists, a GUI wizard should help them to import this profile, navigating any conflicts that exist between their local and downloaded profile.
|
||||||
|
|
||||||
|
A similar GUI wizard should also be used for the Import Data process.
|
||||||
|
|
||||||
|
### Whitespace Characters
|
||||||
|
|
||||||
|
Users should be prohibited from providing only-whitespace values in their profile attributes/ban reasons/etc. It is confusing for other users. We already restrict tabs/newlines for some attributes.
|
||||||
|
|
||||||
|
An example of only-whitespace is " ", or " "
|
||||||
|
|
||||||
|
There are multiple only-whitespace Unicode code points we need to detect.
|
||||||
|
|
||||||
|
We could instead use the GUI to replace all whitespace characters with a visible character if no non-whitespace characters are detected, without having to restrict this at the protocol level.
|
||||||
|
|
||||||
|
### True Location
|
||||||
|
|
||||||
|
Users could add a True Location to their client.
|
||||||
|
Users currently share a privacy-preserving location on their profile and in their criteria, which is near to their true location.
|
||||||
|
Adding a private true geographic location would enable mate distances to be more accurate.
|
||||||
|
This true location would never be shared, broadcast, or uploaded anywhere.
|
||||||
|
|
||||||
|
### Reject Duplicate and Excess MessagePack Keys
|
||||||
|
|
||||||
|
Currently, the msgpack decoder does not reject MessagePack that contains duplicate entries for the same key.
|
||||||
|
|
||||||
|
Here is an example in JSON to demonstrate this behavior:
|
||||||
|
|
||||||
|
`{"1":"A","2":"A","2":"B"}` would be read into a map like so: `1->A, 2->B`
|
||||||
|
|
||||||
|
The msgpack decoder also does not reject excess, ignored keys.
|
||||||
|
For example, if we unmarshal a MessagePack map with 5 keys into a struct with only 1 item, the 4 items will be ignored.
|
||||||
|
|
||||||
|
Someone could encode unruleful content inside of a piece of content without it being detected.
|
||||||
|
|
||||||
|
This would be fine for users who would never see this data, but could have legal consequences for hosts if the injected content was illegal.
|
||||||
|
|
||||||
|
We must detect this, by either using a different package, or seeing if the total bytes and matches the size of all unmarshalled values + the msgpack serialization overhead.
|
||||||
|
|
||||||
|
### Make Content Smaller
|
||||||
|
|
||||||
|
We can replace many Review/Report/Profile/Parameter attributes with a bytes encoding to make them smaller.
|
||||||
|
|
||||||
|
An example is replacing Verdicts with numbers during encoding to decrease size:
|
||||||
|
|
||||||
|
"Approve"/"Ban"/"None" -> 1/2/3
|
||||||
|
|
||||||
|
This is a task that is underway and partially completed.
|
||||||
|
|
||||||
|
### More Encryption Methods
|
||||||
|
|
||||||
|
Add FrodoKEM, CSIDH, or both to encrypt network connections.
|
||||||
|
|
||||||
|
We don't need to add more encryption to messages, because that will increase their size, and we want to keep messages as small as possible.
|
||||||
|
|
||||||
|
We should add more encryption to network connections because doing so adds a very small amount of bandwidth when compared to the total request/response. The initial key handshake is only needed to establish the connection key, after which there is no speed/bandwidth difference.
|
||||||
|
|
||||||
|
The main consideration is the computational load of the encryption method on Hosts and servers, which may be too heavy for some kinds of post-quantum key encapsulation methods.
|
||||||
|
|
||||||
|
Someone should test FrodoKEM, CSIDH, Kyber, and Nacl to document and compare their speed and key sizes, and underlying cryptography. After this, we can make our decision on which method(s) to add. It is probably better to use 2 methods that use different cryptographic techniques (Isogeny/Lattice) rather than similar ones (Lattice1/Lattice2).
|
||||||
|
|
||||||
|
### Connection Pool
|
||||||
|
|
||||||
|
See `internal/network/connectionPool.go`
|
||||||
|
|
||||||
|
### Trusted Hosts
|
||||||
|
|
||||||
|
The network parameters could share a list of trusted hosts.
|
||||||
|
|
||||||
|
These are hosts that are run by trusted members of the community, that will claim to not track users.
|
||||||
|
|
||||||
|
These hosts could be used to download data in bulk, where sharing information with the host becomes less risky.
|
||||||
|
|
||||||
|
An example is downloading message inboxes in bulk, allowing for much faster message downloading than downloading from each inbox over a fresh tor circuit.
|
||||||
|
|
||||||
|
### Moderate Over Clearnet Mode
|
||||||
|
|
||||||
|
Moderator over Clearnet mode would allow moderators to download content over clearnet rather than Tor.
|
||||||
|
|
||||||
|
This is similar to Host Over Clearnet mode. Many moderators will probably use this mode, especially moderators who are using VPNs.
|
||||||
|
|
||||||
|
### QR Codes
|
||||||
|
|
||||||
|
We need a way to generate Ethereum and Cardano address QR codes.
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
We must add the `translate()` function everywhere it is missing within the GUI.
|
||||||
|
|
||||||
|
We must create a utility to find all calls to translation functions and build a list of terms that are needed.
|
||||||
|
|
||||||
|
We must create a list of translated terms for each supported language.
|
||||||
|
|
||||||
|
### Command Line Interface
|
||||||
|
|
||||||
|
We need a command line interface so users can deploy Seekia hosts on devices without using a GUI. This is essential for virtual private servers.
|
||||||
|
|
||||||
|
The command line should have an accompanied configuration file that is used to set the application settings.
|
||||||
|
|
||||||
|
### Ability for Admins to Gift Identity Score
|
||||||
|
|
||||||
|
The network parameters could allow admins to gift Identity Score to specific moderators.
|
||||||
|
|
||||||
|
The parameters would contain a map of Identity Hash -> Amount to gift
|
||||||
|
|
||||||
|
This would function as a more decentralized version of Supermoderators, where moderators that the admins trust are given score based on how trusted they are.
|
||||||
|
|
||||||
|
Another use would be to help moderators recover their scores if their identity keys get compromised or lost. This would require moderators to prove their new identity, either via their real-world identity or an older key that was signed by the moderator's key before it was compromised/lost.
|
||||||
|
|
||||||
|
This feature might not be worth adding.
|
||||||
|
|
||||||
|
### Public Desires
|
||||||
|
|
||||||
|
Mate profiles should have a Public Desires attribute, which can be used to describe what they desire in a mate. This would be a text attribute.
|
||||||
|
|
||||||
|
Another feature would be allowing users to share numerical desires such as minimum/maximum values. Other users could filter users with the desire "I Fulfill Their Desires". After much consideration, I am generally against adding this feature, because user desires are flexible and constantly changing, and most desires are probably embarrassing to share publicly.
|
||||||
|
|
||||||
|
Additionally, many users will be able to infer that another user will probably not be interested in them and will not bother to pursue them. We should add more written desire attributes to accomplish this, without having to define quantitative values such as minimum/maximum age.
|
||||||
|
|
||||||
|
### New Logo
|
||||||
|
|
||||||
|
The current logo needs a redesign.
|
||||||
|
|
||||||
|
The new logo should be more symmetric and visually appealing. A more intricate design would look nice.
|
||||||
|
|
||||||
|
If someone could come up with a novel design, that could also be adopted.
|
||||||
|
|
||||||
|
### Mate Mode
|
||||||
|
|
||||||
|
We need a way to disable/enable Mate mode.
|
||||||
|
|
||||||
|
When Mate mode is disabled, a user's client will not download profiles to browse. Mate mode should be disabled when the application is first started.
|
||||||
|
|
||||||
|
We also need the user to choose some basic desires and download desires before we start downloading profiles. Otherwise, the application will attempt to download all viewable Mate profiles. Age, Sex, and Distance should be enabled as download desires when a user is first created.
|
||||||
|
|
||||||
|
### Adjust Allowed Space On First Startup
|
||||||
|
|
||||||
|
Users should choose the amount of space Seekia can use on first startup.
|
||||||
|
|
||||||
|
Another option would be to set Seekia to use 10% of the free disk space upon first startup.
|
||||||
|
|
||||||
|
We need to make an easy to see warning when a user's allowed disk space has been used up.
|
||||||
|
|
||||||
|
We should add an Alerts section on the home page, which can be used for these kinds of messages.
|
||||||
|
|
||||||
|
### Desires Pruning Mode
|
||||||
|
|
||||||
|
If no space is left, and user is in Mate mode, we should enter desires pruning mode.
|
||||||
|
|
||||||
|
When this mode is enabled, we will delete profiles that do not fulfill our desires in an effort to save space.
|
||||||
|
|
||||||
|
When the mode is disabled, we only delete profiles that do not fulfill our criteria.
|
||||||
|
|
||||||
|
We need to create jobs to check for this condition and prune profiles which do not fulfill our desires.
|
||||||
|
|
||||||
|
### Rename BroadcastTime to CreationTime
|
||||||
|
|
||||||
|
Profiles, reviews, reports, and parameters currently have a BroadcastTime attribute.
|
||||||
|
|
||||||
|
This really should be called CreationTime.
|
||||||
|
|
||||||
|
Broadcasting is the process of uploading content to the network, which is typically done multiple times.
|
||||||
|
|
||||||
|
### Suspicious Hosts
|
||||||
|
|
||||||
|
The app should maintain a list of Malicious hosts and Suspicious hosts.
|
||||||
|
|
||||||
|
A suspicious host is a host that is probably acting in a malicious manner, but might not be.
|
||||||
|
|
||||||
|
An example is a host who serves different Viewable statuses than other hosts. If they have a track record of doing this, they are probably lying. They could also have a slow network connection.
|
||||||
|
|
||||||
|
Users should avoid suspicious hosts, but maybe still contact them occasionally.
|
||||||
|
|
||||||
|
### Busy Hosts
|
||||||
|
|
||||||
|
Hosts can respond to requests with a Busy response.
|
||||||
|
|
||||||
|
A package called `busyHosts` should be created to keep track of busy hosts.
|
||||||
|
|
||||||
|
User clients will avoid these hosts until they are automatically removed from the busyHosts list after a defined wait time duration.
|
||||||
|
|
||||||
|
### Statistics Range
|
||||||
|
|
||||||
|
The current limitation of the Seekia app's statistics is that they will only represent the statistics for profiles that the user has downloaded. Most users will download a subset of total profiles which are already filtered by their downloads desires. Desire statistics are still a valuable tool for users, but they will not usually reflect an accurate view of the statistics of all users.
|
||||||
|
|
||||||
|
The solution is for the Seekia app to download a set of random profiles to use for generating statistics about all network users. Each user's app should have a statistics range, which is a randomly-selected identity hash range. The user's app will make requests to download all profiles for users within this range. The range should expand and contract to contain a certain number of profiles, such as 500 or 1000.
|
||||||
|
|
||||||
|
Each user's statistics range will create an identifiable fingerprint when they make certain requests. This fingerprint must be hidden properly to preserve each user's privacy. Requests to download and update the profiles within a statistics range should be sent on their own and over unique Tor circuits.
|
||||||
|
|
||||||
|
### Tor Client
|
||||||
|
|
||||||
|
Someone should build a Tor client in Golang. We define a Tor client as a piece of software which can query the Tor network for node information, choose a path of nodes for a request, craft requests, and send requests. The initial Tor client implementation only needs to support making requests over exit nodes onto the clearnet. Building a Tor client should not be nearly as difficult as building a full Tor node, which would require implementing request routing logic, node information broadcasting, hidden service support, and various other functions.
|
||||||
|
|
||||||
|
A Tor client written in Golang would likely be used by many other projects. The Tor client I described could replace the main Tor implementation for applications and use cases which only rely on Tor to anonymize traffic over exit nodes.
|
||||||
|
|
||||||
|
## After Launch
|
||||||
|
|
||||||
|
The following describes ideas that can be implemented after the Seekia network is launched.
|
||||||
|
|
||||||
|
These ideas are a work in progress.
|
||||||
|
|
||||||
|
### Banning Malicious Hosts
|
||||||
|
|
||||||
|
Moderators should automatically ban any hosts who seed them invalid information.
|
||||||
|
|
||||||
|
An example of misbehavior is providing identities/inboxes outside of the requested range.
|
||||||
|
|
||||||
|
Moderators could make requests to hosts to audit them.
|
||||||
|
|
||||||
|
If the hosts provide invalid information, the moderators can automatically ban them.
|
||||||
|
|
||||||
|
An example: Moderators can get the funded status of random profiles from the account credit servers and from hosts.
|
||||||
|
|
||||||
|
We can do this with other kinds of requests to catch and ban bad hosts automatically.
|
||||||
|
|
||||||
|
We should avoid doing this for information where the host could be accidentally incorrect. Examples of this kind of information are viewable statuses, which could be wrong if the host does not have an accurate view of the network.
|
||||||
|
|
||||||
|
### Add Blood Type
|
||||||
|
|
||||||
|
Seekia could include blood types in user profiles. People could use this information to choose a mate whose blood type would have the highest likelihood of a healthy offspring. An example is to convey the risk of Rh Disease. There are probably other ailments whose risk could be reduced by preventing couples with certain blood types from having children with each other.
|
||||||
|
|
||||||
|
Blood type is a very complex topic, as there are hundreds of different antigens that determine someone's blood type. More research is needed on this topic.
|
||||||
|
|
||||||
|
### Other Genetic Compatibility
|
||||||
|
|
||||||
|
Other genetic based dating services like GenePartner claim to be able to match people based on their HLA genes.
|
||||||
|
|
||||||
|
The reasoning is that pairing people based on their HLA genes will result in children who have healthier immune systems. The human body naturally does this by being attracted to the scents of people who have immunity genes that are advantageous to the offspring.
|
||||||
|
|
||||||
|
Once we use neural networks to predict offspring polygenic scores, we should be able to detect many complex gene interactions, including HLA genes. If we train a neural net to predict a person's general immune system health, we should include HLA genes in the input genes.
|
||||||
|
|
||||||
|
There is a lot of potential for genetic compatibility testing that extends beyond monogenic and polygenic disease and trait analysis.
|
||||||
|
|
||||||
|
There is still value in investigating other kinds of genetic compatibility that we would not be able to detect in the predicted offspring's genome. For example, more research could be done on human scents. There might be a way to detect what kinds of scents someone will be attracted to and produce from their genome. We could add this functionality into the application, so users can see whose scents they are more likely to be attracted to.
|
||||||
|
|
||||||
|
### Kinship Analysis
|
||||||
|
|
||||||
|
Seekia needs to be able to calculate kinship between two people's genomes. This will be useful to prevent accidental inbreeding.
|
||||||
|
|
||||||
|
Many open source software packages exist that can calculate kinship. Use these as your guide to build the feature.
|
||||||
|
|
||||||
|
If it is possible, users should upload portions of their genome to their profiles which are sufficient enough to determine how related they are to other users, so users can screen out too-related users before even meeting them. If we want to keep user profiles below a few megabytes, this might not be possible. Many users may also be wary of sharing large portions of their genome in their profile.
|
||||||
|
|
||||||
|
At the very least, users should be able to calculate kinship by using the Seekia application offline. They could even send their genome files over the internet to perform the analysis before meeting in person.
|
||||||
|
|
||||||
|
### Inbreeding (Parent Relatedness)
|
||||||
|
|
||||||
|
The Seekia app should also be able to calculate how inbred a person is from their genome. The user should be able to display this value in their profile.
|
||||||
|
|
||||||
|
### Ancestral Analysis
|
||||||
|
|
||||||
|
The Seekia application should be able to perform its own ancestral analysis.
|
||||||
|
|
||||||
|
There could be several analysis methods. These analysis methods will serve as an alternative to company-provided analyses.
|
||||||
|
|
||||||
|
Providing an open source ancestral analysis method is essential for race aware mate discovery technology to be credibly neutral. There already exist multiple open source ancestral analysis packages.
|
||||||
|
|
||||||
|
### Add Custom Type Illnesses
|
||||||
|
|
||||||
|
Many genetic illnesses are not able to be detected using the methods implemented in the `monogenicDiseases` or the `polygenicDiseases` packages.
|
||||||
|
|
||||||
|
Examples include diseases such as Fragile X and Turner's Syndrome.
|
||||||
|
|
||||||
|
A new format called `complexDiseases` could be created.
|
||||||
|
|
||||||
|
Each disease can have a function that takes in a genome map and returns a diagnosis.
|
||||||
|
|
||||||
|
Many of these diseases may require additional data from the raw genome files that is not included in the genome map.
|
||||||
|
|
||||||
|
The `ReadRawGenomeFile` function should be able to read this relevant data.
|
||||||
|
|
||||||
|
The GUI would also have an accompanying set of pages to display these Custom illnesses.
|
||||||
|
|
||||||
|
### Add Polygenic Disease Probability Risk
|
||||||
|
|
||||||
|
We want to calculate an adjusted probability that the person will become victim to a polygenic disease.
|
||||||
|
|
||||||
|
Meaning, we want to tell the user the estimated probability that they will get a particular polygenic disease, for each age period of their life.
|
||||||
|
|
||||||
|
Example: Normal risk = 5%, Your risk = 10%
|
||||||
|
|
||||||
|
We should be able to calculate this risk. We know the polygenic disease odds ratio of a base pair `(odds of disease with base pair)/(odds of disease with standard (common) base pair)`. We know the average probability of disease for the general population for each age period. We know the probability of each base pair for the general population.
|
||||||
|
|
||||||
|
This will be the most useful statistic for users trying to understand their polygenic disease risk.
|
||||||
|
|
||||||
|
Knowing that the probability of a particular type of cancer has increased by 10x is very different depending on the probability of getting the cancer.
|
||||||
|
|
||||||
|
If the general population probability of getting cancer X is 5%, and the user's adjusted risk is 50%, that is a significant increase. However, if the general population risk is 0.1%, and the user's adjusted risk is 1%, then the user does not need to change their behavior or worry much.
|
||||||
|
|
||||||
|
### Add Neural Network Genetic Predictions
|
||||||
|
|
||||||
|
The current method for predicting polygenic disease risks and traits is not as informative and accurate as using neural nets.
|
||||||
|
|
||||||
|
Our current model adds and subtracts the likelihood values of various SNPs that are reported to have an effect on polygenic diseases and traits.
|
||||||
|
|
||||||
|
A much better is to train a neural net to predict traits and polygenic diseases on a large number of genes. There are methods that exist to find the list of genes that have an effect on each trait/disease. For example, height is said to be effected by ~10,000 SNPs. These are the genes to feed into the neural net for each trait/disease. These are also the genes that users will share in their profiles. See `createGeneticAnalysis.go` for information on how offspring predictions would work.
|
||||||
|
|
||||||
|
This method requires training data, which is largely unavailable for public use. We need fully open training data, not data that requires registration or permission to download.
|
||||||
|
|
||||||
|
OpenSNP.org is a free genomic data repository. OpenSNP relies on user submitted data, which can be falsified. OpenSNP should add a verification system so data provided by trustworthy people can be prioritized.
|
||||||
|
|
||||||
|
More people should create public domain genome banks. If they had multiple locations surveying and sequencing people every day, they could sequence tens of thousands of people a year. This data would quickly be sufficient to train the neural nets to predict attributes with some accuracy. Each participant would have to sign an agreement to release their response and genome into the public domain.
|
||||||
|
|
||||||
|
Whoever collects the data needs to choose what data to collect from each person. The more information collected, the more genetic prediction tests can be made.
|
||||||
|
|
||||||
|
Some examples of data to collect:
|
||||||
|
|
||||||
|
* Collecting polygenic disease information would enable prediction of polygenic disease risk.
|
||||||
|
* Pictures and scans of participants faces would enable a genetic test for facial structure
|
||||||
|
* Personality tests would enable prediction of personality
|
||||||
|
* Measuring height would enable prediction of height
|
||||||
|
|
||||||
|
These kinds of genetic tests would allow parents to choose what their offspring will look like, their personality, and their intelligence.
|
||||||
|
|
||||||
|
Another option is for a wealthy entity to purchase an existing biobank and release the data into the public. They would have to send consent forms to the people whose genomes have already been analyzed, even offering to pay them to release their genome and response data into the public domain. Participants could opt to withhold some of the information, and also augment their data by taking more tests. This approach would avoid the need to sequence large numbers of genomes again.
|
||||||
|
|
||||||
|
Many organizations and individuals would be willing to fund this endeavor. Open source genome prediction technology would enable genetic embryo selection on a much larger scale, reducing global rates of disease, increasing human intelligence, and improving all aspects of people's lives. It would be a tremendously altruistic technology to help humanity. The increase in human intelligence alone would significantly reduce poverty, crime, irrationality, and many other challenges facing humanity.
|
||||||
|
|
||||||
|
There are many benefits to having widely available open source genetic prediction technology. People will be able to predict what diseases they are likely to have and prepare accordingly. People will be able to satisfy their own curiosity about how tall they could have been if they had sufficient nutrition and sleep. Parents who are adopting children whom are already born could choose to adopt children whom will have a similar personality and intelligence to themselves. This could increase people's willingness to adopt. People with lower disease risk will have a higher sexual market value, and can convey that risk in their Seekia profiles.
|
||||||
|
|
||||||
|
All of this is already possible, but will become easier with the proliferation of more advanced open source genetic prediction models. Many advanced genetic prediction methods already exist, but many of them are closed source. Even using a public model trained on closed data would not be sufficient for Seekia's use case, because we need users to be confident that the models are reproducible and created from accurate data. We want the genetic future of the human species to be steered by open source technology.
|
||||||
|
|
||||||
|
### Add more diseases and traits
|
||||||
|
|
||||||
|
This task entails entering disease/trait SNP data from SNPedia.com and other sources. The bases have to be flipped if the orientation on SNPedia is minus. This requires flipping G/C and A/T. At least 3 people should check any added disease SNPs to ensure accuracy.
|
||||||
|
|
||||||
|
This is a tedious data entry process with negative consequences if mistakes are made. Many users could falsely believe they have monogenic diseases, which could trigger mental health crises.
|
||||||
|
|
||||||
|
### Interactive Map
|
||||||
|
|
||||||
|
Seekia should have an interactive world map. It would be similar to OpenStreetMaps, but with much less detail. It would only need to contain borders of countries and states as lines. It would be able to display latitude/longitude coordinates on the map as points.
|
||||||
|
|
||||||
|
The map would be useful for many reasons. Users could view other Seekia users on a map. Users whose city is not included in the cities dataset could use the map to visually choose a coordinate point close to their own, rather than having to use a website such as Google Maps. Some statistics could be viewed in map form by coloring in different areas of the map based on the geographic distribution of certain attributes.
|
||||||
|
|
||||||
|
The map would contain all of its needed data offline. The graphics outlining the world's continents, countries, and states would be small enough to be included with every Seekia download. We will be able to overlay the city names in the already included cities dataset to make the map even more useful.
|
||||||
|
|
||||||
|
Someone has already built an Open Street Maps explorer for Fyne. That could be useful for someone who builds this. It is located here: [github.com/fyne-io/fyne-x](https://github.com/fyne-io/fyne-x)
|
||||||
|
|
||||||
|
There exist many free vector world maps that can be used for this.
|
||||||
|
|
||||||
|
### Private Information Retrieval
|
||||||
|
|
||||||
|
Seekia could use a private information retrieval scheme when users query hosts.
|
||||||
|
|
||||||
|
This would enable users to only download the profiles that fulfill their desires, without hosts learning their private desires or which profiles they downloaded. This would enable users to download fewer profiles, by using their private desires when downloading profiles. Users could also request messages from multiple inboxes in the same request, greatly reducing the number of hosts they must request from.
|
||||||
|
|
||||||
|
I'm not sure if this is theoretically possible, or if the required data to download would be so large that it would be slower than using our current methods.
|
||||||
|
|
||||||
|
A simple example of private information retrieval:
|
||||||
|
|
||||||
|
1. The user wants to download a single profile privately.
|
||||||
|
2. They request that profile, and 100 decoy profiles from host A.
|
||||||
|
3. Host A XORs the profiles together and sends the result to the user.
|
||||||
|
4. The user then requests from host B the 100 decoy profiles.
|
||||||
|
5. Host B XORs the 100 decoy profiles and sends the result to the user.
|
||||||
|
6. The user XORs the responses from Host A and Host B and is left with the profile they desire.
|
||||||
|
|
||||||
|
Each profile has to be padded to the same size for this to work. The advantage to this method is that only 2 profiles worth of bandwidth have to be downloaded, without either host knowing which profile the user requested. This only provides privacy if host A and B are not colluding.
|
||||||
|
|
||||||
|
A better method may be possible with homomorphic encryption. The Spiral PIR scheme is already enabling this sort of querying of address balances within the bitcoin blockchain. I'm still not sure if this could actually work to increase privacy, reduce download speeds, and save bandwidth within the context of Seekia.
|
||||||
|
|
||||||
|
### Neural Network Detection Of Unruleful Content
|
||||||
|
|
||||||
|
Seekia could have a neural network built in to alert recipients of unruleful images within messages.
|
||||||
|
|
||||||
|
The neural network could give warning to the recipient before viewing content. Bumble has created the Private Detector, a neural network which detects images of a man's privates.
|
||||||
|
|
||||||
|
Seekia already uses a slow reveal in the GUI, which slowly depixelates the image, so this kind of detection may not be desired by recipients.
|
||||||
|
|
||||||
|
Neural network detection would be more useful for moderators. We could eventually rely more heavily on neural network detection for moderation.
|
||||||
|
|
||||||
|
This might be easily defeatable by editing individual pixels in the offending image, or individual characters in text, until they pass the neural net. The neural nets should not be relied upon to approve or ban content.
|
||||||
|
|
||||||
|
#### Zero Knowledge Proof Of Ruleful Content
|
||||||
|
|
||||||
|
Messages could contain a zero knowledge proof that the contents of the message have passed a neural network that detects for unruleful content.
|
||||||
|
|
||||||
|
This is difficult because the messages are encrypted. The zero knowledge proof would have to prove that the encrypted content has passed the neural network(s).
|
||||||
|
|
||||||
|
This would give more peace of mind for hosts, who would have a much higher level of confidence that that are not hosting unruleful content.
|
||||||
|
|
||||||
|
### Web Explorer
|
||||||
|
|
||||||
|
Someone should build a web explorer for the Seekia network.
|
||||||
|
|
||||||
|
Anyone could use this website to browse users without having to download the Seekia application.
|
||||||
|
|
||||||
|
If they want to create an identity, they should download the Seekia application. I am generally against creating a website that allows users to create an identity and communicate with users. Users would have to trust that the website operator is not tracking their activity. At least with the Seekia application, users can compile from source or trust reproducible builds which are signed by multiple people.
|
||||||
|
|
||||||
|
### Facial Similarity Analysis
|
||||||
|
|
||||||
|
Seekia should have the ability to analyze user photos and calculate facial similarity.
|
||||||
|
|
||||||
|
This ability will be used in the racial similarity analysis for facial structure.
|
||||||
|
|
||||||
|
#### Reverse Person Search
|
||||||
|
|
||||||
|
Users should be able to import photos, genomes, and physical characteristics for people they are attracted to and sort users by their Racial Similarity to these people. For example, if someone becomes infatuated with a celebrity, they can import the celebrity's genome, eye color, skin color, hair color, and hair texture into the Seekia app and sort their matches by their racial similarity to the celebrity.
|
||||||
|
|
||||||
|
Users should be able to include this information in their criteria to hosts.
|
||||||
|
|
||||||
|
Someone should create a service that allows users to perform this search without having to download the Seekia app. There are already websites that allow people to find their doppelganger with photos. The Seekia network would enable a more accurate search by relying on genetic data.
|
||||||
|
|
||||||
|
### Video and Voice Communication
|
||||||
|
|
||||||
|
Users should be able to communicate via video and voice chat.
|
||||||
|
|
||||||
|
Many video and voice chat apps work by creating a direct connection between two user machines. The problem with this approach is that both users will see each other's IP addresses.
|
||||||
|
|
||||||
|
Using the Tor network to shield each user's IP address is too slow. Session messenger offers onion-routed video communication over Lokinet, but onion routing voice calls is probably still too slow and unreliable for use in Seekia.
|
||||||
|
|
||||||
|
My solution is to use trusted call coordination servers. The only purpose of these servers should be to connect calls between two users and pass packets back and forth. Each user's Seekia identity does not need to be shared with the servers. The servers should only learn which IP addresses are communicating and for how long. Users using a VPN will be able to protect their true IP address from the coordination servers.
|
||||||
|
|
||||||
|
All communication should be encrypted using multiple technologies such as Kyber and Nacl. The packets should be signed with ephemeral identity keys which the users exchange in their messages, so man-in-the-middle attacks are not possible. The key handshake would occur between the conversation parties, so the coordination server cannot decrypt the conversation packets.
|
||||||
|
|
||||||
|
These servers should be run by trusted entities, and their domain names and signing keys should be listed in the network parameters.
|
||||||
|
|
||||||
|
Users should be able to send files through calls. This way, users can send their genomes through a secure channel without having to meet in person.
|
||||||
|
|
||||||
|
### Offspring Appearance Prediction
|
||||||
|
|
||||||
|
Users of Seekia should be able view a predicted appearance image of their offspring with each user. This is useful when deciding who to mate with.
|
||||||
|
|
||||||
|
Seekia should be able to download images of people who are racially similar to each user pair's calculated offspring using the same racial similarity calculation method already present in Seekia. This calculation includes comparing genes which influence traits such as skin color, eye color, hair color, and facial structure. The calculation also incorporates ancestral similarity.
|
||||||
|
|
||||||
|
Seekia could also generate a prospective offspring image by using both user's photos, ancestry, and trait information. A service called BabyAC exists which creates a prospective baby image from images of both parents (see [Baby-AC.com](https://baby-ac.com/en)).
|
||||||
|
|
||||||
|
To create this feature, we need many people to upload their genomes, ancestry composition results, and images of themselves to a database. Each participant must digitally consent to releasing this information into the public domain. The ancestral analyses can be provided by various companies and eventually Seekia.
|
||||||
|
|
||||||
|
Maybe OpenSNP.org could suffice as the database to use for this. User verification would be needed to prevent spam and fraudulent data from being used.
|
||||||
|
|
||||||
|
### WEBP Encoder
|
||||||
|
|
||||||
|
Someone should write a webp encoder in Golang.
|
||||||
|
|
||||||
|
This will allow us to stop relying on the libwebp C binding to encode WEBP images.
|
||||||
|
|
||||||
|
There are other people who desire this encoder as well.
|
||||||
|
|
||||||
|
Someone could also build a JPEG-XL encoder/decoder and Seekia could switch to that instead.
|
||||||
|
|
||||||
|
Other image encoding methods which utilize AI could be even more competent at compressing images.
|
||||||
|
|
||||||
|
### Compromised Keys List
|
||||||
|
|
||||||
|
Some user identity private keys will be hacked. Once a private key is hacked, it can be used to impersonate a user.
|
||||||
|
|
||||||
|
Users should be able to share their compromised private key to the network, allowing those identities to be banned from the network. Once shared, these identities would not be able to participate in the network, send messages, etc. The announcement that contains the compromised private key should be hosted by the network until the identity expires. For funded moderator identities, they must be hosted forever.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Thank you for reading this document. Please share your ideas on ways that we can further improve Seekia.
|
||||||
|
|
2034
documentation/Specification.md
Normal file
2034
documentation/Specification.md
Normal file
File diff suppressed because it is too large
Load diff
152
documentation/User Guide.md
Normal file
152
documentation/User Guide.md
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
|
||||||
|
# Seekia User Guide
|
||||||
|
|
||||||
|
This document is a guide describing how to use Seekia.
|
||||||
|
|
||||||
|
*This document is under construction.*
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
Users of Seekia should be aware of the following risks:
|
||||||
|
|
||||||
|
### Legal Liability
|
||||||
|
|
||||||
|
Users of Seekia must accept all legal liability and risk in their use of the software.
|
||||||
|
|
||||||
|
### Rulebreaking Content
|
||||||
|
|
||||||
|
All users of Seekia may download content that is against the rules.
|
||||||
|
|
||||||
|
Hosts can opt-in to host unapproved content, which may include profiles and messages that are unruleful. Message contents are encrypted, so they cannot be reviewed until they are reported by users. Moderators will connect to these hosts to download and review content. New profiles must be broadcast to these hosts.
|
||||||
|
|
||||||
|
Your client will connect to nodes on the network, who may send you unruleful and illegal content.
|
||||||
|
|
||||||
|
Once unruleful content is banned by a sufficient number of moderators, it will be deleted, and all hosts will stop hosting the content. Some content may be accidentally approved, and the system will never be perfect. Some content may also follow the Seekia rules but still be illegal in your country.
|
||||||
|
|
||||||
|
If you are not in Host/Moderator mode, the client will avoid downloading unruleful content. Seekia will attempt to only download moderator-approved content for Mate-only users.
|
||||||
|
|
||||||
|
### Personal Risks
|
||||||
|
|
||||||
|
All users should be aware of the general risks of using the internet, social networking, and interacting with people in real life or online.
|
||||||
|
|
||||||
|
People should be cautious when interacting with people they have met online, especially without knowing their true identity.
|
||||||
|
|
||||||
|
Sharing information in your profile and in messages could be used by bad actors to cause harm to you.
|
||||||
|
|
||||||
|
You must be proud to be a Seekia user, and comfortable with everything in your profile being shared with your employer, family, friends, and worst enemies.
|
||||||
|
|
||||||
|
Sharing less in your profile will make you more mysterious and possibly more likely to be matched with other users. People may substitute what they don't know about you with their fantasies of a perfect mate.
|
||||||
|
|
||||||
|
### Operating System
|
||||||
|
|
||||||
|
A user machine's operating system, if compromised, could be used to learn all of their Seekia behavior.
|
||||||
|
|
||||||
|
Closed source operating systems such as Windows or Mac could surveil your Seekia activity or block your ability to use Seekia.
|
||||||
|
|
||||||
|
Using an open source operating system is recommended.
|
||||||
|
|
||||||
|
### Vulnerable Software
|
||||||
|
|
||||||
|
The Seekia software may be vulnerable to hacks and exploits.
|
||||||
|
|
||||||
|
The admin(s) can limit the use of the software by enabling the "Update Required" flag, which disables the use of Seekia until an update is performed.
|
||||||
|
|
||||||
|
This method cannot be guaranteed to protect users against vulnerabilities.
|
||||||
|
|
||||||
|
Users who are concerned should run Seekia inside of a sandboxed environment such as a virtual machine. Using a Whonix workstation is a good option: [whonix.org](https://whonix.org).
|
||||||
|
|
||||||
|
### Seekia Website
|
||||||
|
|
||||||
|
Seekia's official website is typically accessed through the clearnet.
|
||||||
|
|
||||||
|
The IP addresses accessing the website could be logged by the server host or other surveilling entities.
|
||||||
|
|
||||||
|
Concerned users should access and download the Seekia client with an IP shielding technology such as a VPN or Tor.
|
||||||
|
|
||||||
|
The most private method is to access the Tor hidden service Seekia website, which requires using the Tor browser.
|
||||||
|
|
||||||
|
Users can also access the `seekia.eth` website via IPFS, which can be done in a private way using Tor or VPNs.
|
||||||
|
|
||||||
|
### Cryptography Threat
|
||||||
|
|
||||||
|
A user's messages and network traffic are encrypted with Nacl and Kyber.
|
||||||
|
|
||||||
|
If both of these encryption methods are broken, all Seekia messages will be decryptable and publicly viewable.
|
||||||
|
|
||||||
|
Users should be aware that their messages and Seekia behavior may be revealed in the future.
|
||||||
|
|
||||||
|
Every Seekia message you ever sent would be decryptable and shared publicly in this scenario.
|
||||||
|
|
||||||
|
### Tor Risks
|
||||||
|
|
||||||
|
The privacy provided by Tor can be degraded in several ways:
|
||||||
|
|
||||||
|
#### Quantum Threat
|
||||||
|
|
||||||
|
In the future, quantum computers could break the encryption used by Tor.
|
||||||
|
|
||||||
|
Many Tor network packets may be currently collected and stored by at least one surveillance entity.
|
||||||
|
|
||||||
|
Estimates for when breaking this decryption will be achievable on quantum hardware range between several years to never.
|
||||||
|
|
||||||
|
If this encryption is broken, the privacy-preserving properties of the Tor network will be degraded.
|
||||||
|
|
||||||
|
Seekia messages and network communications are encrypted with Kyber, which is believed to be resistant to quantum attacks, reducing the risk of future decryption.
|
||||||
|
|
||||||
|
#### Network Level Adversary
|
||||||
|
|
||||||
|
Tor network traffic can be analyzed and deanonymized by adversaries who control many Tor nodes.
|
||||||
|
|
||||||
|
#### Outcomes
|
||||||
|
|
||||||
|
Decrypting a user's Tor traffic would allow an adversary to know which origin IP address had used Seekia, which nodes they had connected to, when they made those connections, and possibly what content they broadcasted.
|
||||||
|
|
||||||
|
#### Mitigating Risk
|
||||||
|
|
||||||
|
Concerned users should operate under the assumption that Tor provides no privacy.
|
||||||
|
|
||||||
|
Concerned users should use Seekia from IP addresses, locations, and devices not associated with their identity.
|
||||||
|
|
||||||
|
Concerned users should also only access Seekia from one identity per location to avoid linking different identities together, while also not accessing any information connected to a user's real-world identity at the same time.
|
||||||
|
|
||||||
|
### Node Surveillance Risks
|
||||||
|
|
||||||
|
Any Seekia hosts can monitor traffic. This may include which profiles are downloaded and which messages are sent by connecting clients.
|
||||||
|
|
||||||
|
Hosts could analyze these requests and try to learn more about users. Hosts can collude to increase their ability to trace user behavior.
|
||||||
|
|
||||||
|
Each requestor has a fingerprint. Examples of information that may be provided in a request include a user's criteria and moderation ranges.
|
||||||
|
|
||||||
|
Malicious hosts can track a requestor's fingerprint across multiple requests, learning more about their behavior and the Tor exit nodes they are requesting from.
|
||||||
|
|
||||||
|
This could enable adversaries to know which profiles and messages a user is downloading, and which messages a user is sending.
|
||||||
|
|
||||||
|
Seekia attempts to guard against these attacks.
|
||||||
|
|
||||||
|
Assuming that Tor connections provide perfect privacy, requestor IP addresses should not provide useful metadata to aid in surveillance. Seekia clients split requests between many hosts, reducing the ability to link different requests together, and reducing the ability for any single node to deanonymize a user's behavior.
|
||||||
|
|
||||||
|
### Risks Summary
|
||||||
|
|
||||||
|
Users should be aware that at some of their Seekia behavior is likely trackable by any motivated attackers.
|
||||||
|
|
||||||
|
If an adversary can control enough Seekia and Tor nodes, they can learn a lot about the behavior of network participants.
|
||||||
|
|
||||||
|
Users are encouraged to not engage in illegal, unruleful, and embarrasing behavior.
|
||||||
|
|
||||||
|
## Public Chat Inbox
|
||||||
|
|
||||||
|
Everybody can see how many messages you have received to your public inbox.
|
||||||
|
|
||||||
|
They can also tell how large the messages are, and which messages contained images.
|
||||||
|
|
||||||
|
For example, everyone could tell that you received 100 public inbox messages, and that 20 of those messages were images.
|
||||||
|
|
||||||
|
The world will not know how many messages you have received in total, because messages sent by users you have responded to will usually be sent to your secret inboxes.
|
||||||
|
|
||||||
|
Seekia is designed so that the public cannot determine any information about which messages you send, including whom you are messaging.
|
||||||
|
|
||||||
|
Your client tells the account credit servers which messages you are sending, but those servers promise not to store or track this information. If the account credit servers were compromised, they could be used to monitor user behavior.
|
||||||
|
|
||||||
|
The contents of all messages are encrypted.
|
||||||
|
|
||||||
|
*TODO: Add more*
|
391
documentation/Whitepaper.md
Normal file
391
documentation/Whitepaper.md
Normal file
|
@ -0,0 +1,391 @@
|
||||||
|
# Whitepaper
|
||||||
|
|
||||||
|
This document is used to edit the whitepaper.
|
||||||
|
|
||||||
|
New releases will created from this document and exported as PDFs.
|
||||||
|
|
||||||
|
## Seekia
|
||||||
|
|
||||||
|
Be Race Aware
|
||||||
|
|
||||||
|
A race aware mate discovery network.
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
The human species is a fascinating biological phenomenon with a complex and mysterious origin story. Geographically isolated groups of humans evolved separately over long periods of time, each genetically adapting to their unique social and physical environments. Many instances of interbreeding between members of different population groups occurred, infusing each group with new genetic variation.
|
||||||
|
|
||||||
|
What has resulted is a species which possesses much beauty and biodiversity.
|
||||||
|
|
||||||
|
Humans can be classified by their geographic ancestry by describing the locations where each person's ancestors lived at different times in history. Ancestry can be measured by analyzing the percentage of DNA that a person shares with past human populations. Geographic distance was a significant impediment to gene flow between population groups for most of humanity's history. The humans within each isolated population group bred among themselves, resulting in the loss of genetic variation through the process of genetic drift. Consequently, members of these populations tend to possess similar genetic patterns and physical traits.
|
||||||
|
|
||||||
|
We can classify humans into different races. Races are defined by grouping humans by genetic attributes such as skin, eye and hair color; skin and hair texture; facial structure, and genetic ancestry. If a human is sufficiently different from any other human, they are considered the only member of a unique race. Classifications of racial groups are fuzzy and not fully discrete, because every human is genetically and physically unique.
|
||||||
|
|
||||||
|
## Racial Loneliness
|
||||||
|
|
||||||
|
Primarily over the past several centuries, advances in transportation technologies have enabled all of the world's races to spread throughout the earth. As a result of a larger global population and the increased prevalence of interracial breeding, I posit that there now exists a higher quantity of human races than ever before.
|
||||||
|
|
||||||
|
These circumstances have drastically increased the prevalence of racial loneliness.
|
||||||
|
|
||||||
|
Racial loneliness is the condition of being unable to find members of one's own race to mate with and befriend. A sufferer may be a member of a rare race or may live in an area where members of their own race are rare. Even if they are able to meet someone belonging to their own race, they must also be socially compatible with them, further decreasing the odds of a successful relationship.
|
||||||
|
|
||||||
|
Racial loneliness has become an epidemic. It is the cause of significant despair, and is a contributing factor to global fertility collapse.
|
||||||
|
|
||||||
|
Global fertility collapse can be partially explained by the trend of marriage and reproduction delayment, a phenomenon which has been worsened by racial loneliness. Many humans have a biological and psychological desire to breed with humans who resemble them and to produce offspring who resemble them. People who desire for their mate and children to look like them are spending more time searching and waiting for a mate who resembles them. The goal of finding a mate who belongs to a person's own race is a practical impossibility for many.
|
||||||
|
|
||||||
|
## Beauty
|
||||||
|
|
||||||
|
Human beauty is defined as the ability of a person's physical appearance to evoke feelings of sexual attraction, arousal, and pleasure in other humans. Beauty is subjective for each individual, but trends and patterns emerge when surveying large quantities of people. Human beauty ranking is calculated by comparing the sentiments expressed by large populations of humans. Some people are more beautiful than other people, and some races are more beautiful than other races. Human beauty inequality is an inevitable consequence of human appearance diversity.
|
||||||
|
|
||||||
|
## The Beauty Crisis
|
||||||
|
|
||||||
|
The beauty crisis, also known as the beauty scarcity crisis, the beauty shortage, and the ugliness crisis, is one of the most dire and widespread issues plaguing humanity.
|
||||||
|
|
||||||
|
The beauty crisis is defined as the modern scarcity of beautiful humans and the scarcity's negative effects on humanity's happiness and flourishing. The modern human species has collectively become much less beautiful than it was throughout most of human history.
|
||||||
|
|
||||||
|
There are many causes to the modern beauty crisis. One primary cause is the increase in humanity's collective beauty standards. Humanity's modern exposure to the world's most beautiful people and races has caused our beauty standards to increase. I posit that modern humans collectively rate their peers as being much less beautiful than ancient humans rated their own peers. When humans were hunter gatherers living in small tribes, we typically had visual exposure to a much lower quantity of races. Ancient human beauty standards were typically formed by a person's exposure to members of their own race and races which were more similar to their own. Modern humans now have visual exposure to the world's most rare and beautiful people and races via photos, videos, and in-person interactions. Digital beautification technologies and artificially generated humans have also exposed us to gorgeous humans that are more beautiful than any real-life human beings. Frequent exposure to highly beautiful human specimens has desensitized the parts of our brains which respond to beauty, causing us to perceive people as being uglier. Humanity's modern widened exposure to a larger diversity of humans has also expanded our collective beauty ranking bounds to include all of the world's ugliest and most beautiful races and people.
|
||||||
|
|
||||||
|
Beauty exposure's effect of increasing humanity's collective beauty standards is very difficult to undo. The best treatment for people living in modern times is to cease consumption of media which depicts beautiful people to attempt to reset one's beauty standards. Unfortunately, this will never reset humanity's beauty standards to the state they existed in for ancient humans. Firstly, most humans will still be exposed to a larger diversity of races in their daily lives than ancient humans were exposed to due to advances in transportation technology, which have enabled the spread all of the world's races to all regions of the world. Secondly, it is very difficult to unsee and forget beautiful people and races. Forgetting someone's face can take decades, and it is usually impossible to forget the beauty of a race or particular anatomical structures. People should avoid consuming content which depicts beautiful people as early in life as possible, especially pornography. One possible way to decrease a person's beauty standards is a novel technology which could involve showing a person photos of ugly people while giving them some form of therapeutic stimulation. Some ideas include a cocktail of drugs, electricity to the brain, brain-machine interfaces, and magnetism to the brain.
|
||||||
|
|
||||||
|
The second major cause of the modern beauty crisis is the obesity crisis. Modern technology has caused and enabled people to become more obese than ever before. Rates of obesity have skyrocketed over the past century, causing humans to become much uglier. Some of the heaviest humans of all time are alive today. We are living in an age of unprecedented ugliness.
|
||||||
|
|
||||||
|
We are also living in an age of unprecedented beauty. Teeth straightening braces, supplements, skin creams, and various other technologies have helped to produce some of the most beautiful humans of all time. Modern beautification technologies have unfortunately not been powerful enough to offset the uglifying effects of our modern world.
|
||||||
|
|
||||||
|
The beauty crisis is one of the most significant issues plaguing humankind. Humans have become less attracted to each other, exacerbating many societal ills.
|
||||||
|
|
||||||
|
The beauty crisis is contributing to the global fertility crisis. Many people lament that they cannot find a mate who they feel is beautiful enough to engage in a relationship with who also feels the same way about them. Many people are choosing not to have children because they view themselves as ugly and do not want to create ugly offspring. These factors are increasing the prevalence of singledom and childlessness, which are both increasing the global rates of loneliness, depression, meaninglessness, and lassitude.
|
||||||
|
|
||||||
|
Modern unprecedented beauty exposure, beautification technologies, and modern society's uglifying effects on our species have all contributed to the emergence of the beauty inequality crisis. The beauty inequality crisis is defined as the modern widening of the gap between the beautifulest and the ugliest humans and the gap's negative effects on humanity. The size disparity between the most and least sexually desirable portions of humanity has grown substantially. A larger portion of humanity now wants to mate and breed with a smaller portion of humanity. Competition for access to the increasingly-scarce resource of beautiful people has grown fiercer than ever. The beauty disparity crisis has increased the prevalence of people who are deeply envious of beautiful people and their superior social, romantic, and career success.
|
||||||
|
|
||||||
|
Human beauty is also becoming more important for human happiness and wellbeing. If humanity's story is like a video game, we are running out of quests to complete. Robots are replacing humans for labor. Artificial intelligence will soon provide the answers to most solvable questions. Humanity will eventually solve most of the world's problems such as curing most diseases and giving most people access to food and shelter. We are reaching the end of philosophy where most ideas have been thought of and most quandaries have been solved. As a result of these factors, modern humans have fewer sources of meaning. Humanity's modern shortage of meaning is known as the meaning crisis. The creation and worship of beauty are becoming some of the only sources of meaning in people's lives. Humans are now pursuing indulgent lives of hedonism, romance with other humans, and immersion into artificial and natural beauty. Human beauty worship and sexual relationships have become even more important to a happy human life. Robots cannot solve the beauty crisis because sexual and emotional human contact are some of the only things robots will not be able to replace or replicate.
|
||||||
|
|
||||||
|
The optimal solution to the beauty crisis is to beautify the human species. The ideal way to make humans more beautiful is by improving their genetics.
|
||||||
|
|
||||||
|
## Seekia
|
||||||
|
|
||||||
|
*App home page image*
|
||||||
|
|
||||||
|
To help cure racial loneliness and beautify the human species, I present Seekia: a race aware mate discovery network.
|
||||||
|
|
||||||
|
Seekia is a mate discovery network where users can find a mate while having a deep awareness of each potential partner's race. Users can share racial information in their profiles such as their eye, skin, and hair color; hair texture; genetic ancestry; haplogroups; and the alleles in their genome which effect physical traits.
|
||||||
|
|
||||||
|
Seekia enables users to browse and filter potential mates by their racial attributes. Seekia can also calculate the racial characteristics for prospective offspring between users. Seekia allows for users to predict and choose the race of their offspring by selecting a mate who is the most capable and likely to produce offspring of their desired race.
|
||||||
|
|
||||||
|
Seekia helps users to find members of their own race to mate with, curing racial loneliness. Seekia aims to beautify the human species by enabling people to predict what their offspring will look like with each potential mate, helping to encourage breeding between people who will produce the most beautiful offspring who belong to the most beautiful races and possess the most beautiful traits. Seekia aims to help members of the most beautiful races to meet and have offspring, helping to increase the populations of the world's most beautiful races.
|
||||||
|
|
||||||
|
I will now describe an overview of the features and advantages of Seekia. The technicals of Seekia are described in greater detail in the Seekia documentation and code implementation.
|
||||||
|
|
||||||
|
## Eugenics
|
||||||
|
|
||||||
|
Eugenics is the practice of improving humanity's genetic quality. Genetic quality is defined by three main attributes: beauty, health, and intelligence. Seekia aims to improve humanity's genetic quality by making humans more beautiful, healthy, and intelligent.
|
||||||
|
|
||||||
|
Seekia aims to improve humanity's genetics by facilitating the eugenic technique of selective breeding. Selective breeding is the practice of breeding specific human pairs to produce humans of a higher genetic quality. By encouraging breeding between certain people, it is possible to increase humanity's overall beauty, health, and intelligence. This technique is akin to combining the same set of foods together to create either 5 delicious meals or 5 revolting meals.
|
||||||
|
|
||||||
|
Seekia provides users with the ability to mate in a genetics aware manner. Users can choose their mate in such a way to reduce the probability of their offspring having genetic diseases and increase the probability of their offspring having certain traits.
|
||||||
|
|
||||||
|
### Beauty
|
||||||
|
|
||||||
|
Seekia aims to beautify the human species by encouraging human mate pairings which will create the most beautiful offspring and increase the proportion of beautiful people and races on Earth. Seekia users will be able to choose their mate with a greater knowledge of what their offspring will look like, helping them to produce the most beautiful offspring belonging to the most beautiful races and possessing the most beautiful traits.
|
||||||
|
|
||||||
|
### Health
|
||||||
|
|
||||||
|
Seekia aims to make humanity more healthy by encouraging relationships between people whose offspring will have a lower likelihood of having genetic diseases. Users can sort their matches by their offspring's total monogenic disease probability and total polygenic disease risk score. Seekia measures a prospective offspring's disease risk by combining both user's genetic information to predict the offspring's genome alleles and disease risks.
|
||||||
|
|
||||||
|
### Intelligence
|
||||||
|
|
||||||
|
Seekia aims to make humanity more intelligent by encouraging breeding between specific human pairs who are more likely to produce intelligent offspring. Seekia users will be able to sort their potential mates by the average intelligence of their prospective offspring. Seekia users will choose mates with whom their offspring is likely to have a higher intelligence. This process will cause humanity's intelligence to increase because humans who possess a more compatible set of genes for intelligence will be more likely to breed with each other. Seekia aims to produce an average offspring intelligence prediction for each potential mate pairing by combining both user's genome alleles to create many potential offspring genomes and measuring each offspring's predicted intelligence.
|
||||||
|
|
||||||
|
## Open Source
|
||||||
|
|
||||||
|
The genetic future of the human species should be steered by open source technologies. Freely available source code will help race and genetics aware mate discovery technologies to be impartial, auditable, decentralized, and rapidly improvable.
|
||||||
|
|
||||||
|
The Seekia application is open source software. It is released into the public domain under the Unlicense. It is written in Golang, an open source programming language.
|
||||||
|
|
||||||
|
I encourage others to replicate and improve upon Seekia's technology. I want alternative mate discovery services to incorporate race and genetics aware features, even if they are closed-source and for-profit. No attribution is necessary.
|
||||||
|
|
||||||
|
Seekia is not reliant on proprietary mobile app stores. The Seekia application can be compiled for use on mobile platforms, but users are recommended to use the desktop app on an open source operating system.
|
||||||
|
|
||||||
|
## Decentralization
|
||||||
|
|
||||||
|
The genetic destiny of the human species should not be controlled by a small number of entities. Centralized mate discovery services can attempt to encourage certain kinds of relationships to form. For example, a nefarious mate discovery service could try to increase the prevalence of genetic disorders by encouraging relationships between people who have a higher probability of producing diseased offspring.
|
||||||
|
|
||||||
|
The Seekia network strives to be open and decentralized. The Seekia network aims to be resilient in the event that any host suddenly stops participating or is compromised by bad actors. Seekia is not fully decentralized, because it relies on a central credit database to perform scalable private accounting. Seekia still reaps many benefits from its decentralized architecture.
|
||||||
|
|
||||||
|
Anyone can participate as a network host, which involves serving profiles and messages to other network peers. It is impossible for a single host to prevent specific profiles and messages from reaching the rest of the network. Users broadcast and download content to and from multiple network hosts.
|
||||||
|
|
||||||
|
The decentralized architecture of Seekia helps to sustain network reliability. User data exists on many computers around the world, so events such as solar flares and hosting provider bans are less likely to result in a loss of user data or network downtime. Each user's application periodically rebroadcasts content to help prevent user data from disappearing from the network.
|
||||||
|
|
||||||
|
## Dark Web
|
||||||
|
|
||||||
|
Seekia utilizes the Tor mixnet anonymity network to provide users with privacy.
|
||||||
|
|
||||||
|
User requests are sent through the Tor network to prevent sensitive data such as user mate desires and conversation partners from being linked to a user's identity. Hosts can choose to host over the Tor network to shield their IP address and protect themselves against potential risks. Hosts and moderators can choose to host or moderate over clearnet for a faster experience.
|
||||||
|
|
||||||
|
The Seekia website is also hosted on the dark web, enabling the Seekia application to be distributed in a private and uncensorable manner.
|
||||||
|
|
||||||
|
## Cryptographic Identity
|
||||||
|
|
||||||
|
Each user has an Identity Key, which is a cryptographic signing key. A hash of this key is called a user's Identity Hash, which is their unique identifier on the Seekia network. User profiles and messages are digitally signed with the author's identity key.
|
||||||
|
|
||||||
|
Centralized mate discovery services can sabotage their user's mating efforts by editing profiles and messages. Content on the Seekia network is cryptographically signed, making it impossible to impersonate users without their private keys.
|
||||||
|
|
||||||
|
A user's identity key is derived from a 15 word mnemonic seed phrase. A seed phrase can be used to recover a user's identity on any device.
|
||||||
|
|
||||||
|
There are three identity types: Mate, Host, and Moderator.
|
||||||
|
|
||||||
|
## Profiles
|
||||||
|
|
||||||
|
Each Seekia user has a profile. Users must broadcast a profile to be able to chat with other users. Profiles which are broadcasted to the network are viewable by anyone.
|
||||||
|
|
||||||
|
User profiles can contain information about a variety of topics such as age, location, biological sex, gender identity, sexuality, genetics, race, height, body type, language, fame, wealth, infectious diseases, drug use, hobbies, job, beliefs, diet, and pets.
|
||||||
|
|
||||||
|
Users can browse the network and find matches for free without creating an identity or broadcasting a profile. This freedom allows many more people to search for matches, which should significantly increase the quantity of users who eventually broadcast a profile. A web explorer should be built that allows anyone to view user profiles without having to download the Seekia app.
|
||||||
|
|
||||||
|
Users should only share information in their profiles which they are comfortable being fully public and searchable. Sharing less will possibly result in more matches and messages for a user, because others will fill the gaps of knowledge about the user with their fantasies of a perfect mate.
|
||||||
|
|
||||||
|
## Questionnaires
|
||||||
|
|
||||||
|
A questionnaire is a set of questions that users can create and share on their profile. There are 2 kinds of questions: Choice and Entry. Choice questions offer a selection of predefined options. Entry questions allow users to respond with any text, and can also be constrained to only allow numerical responses.
|
||||||
|
|
||||||
|
Users can create questionnaire responses and send them to other users in encrypted messages. Users can filter and sort their matches and conversators by the responses that they provided to their questionnaire. For example, a user could create a numeric Entry question asking how many countries other users have visited. The user could then sort their matches by their responses to this question.
|
||||||
|
|
||||||
|
## Desires
|
||||||
|
|
||||||
|
Desires represent a user's mate preferences, and are used to generate a user's matches. Users can choose their desires within the Seekia app. Seekia aims to give users total control of the algorithmic curation of their matches.
|
||||||
|
|
||||||
|
### Match Scores
|
||||||
|
|
||||||
|
Each desire has an Importance, which is a number the user can adjust. Each desire's Importance is added to a user's Match Score if the user fulfills the desire. Users can sort their matches by their Match Score.
|
||||||
|
|
||||||
|
### Desire Options
|
||||||
|
|
||||||
|
Each desire has 1 or 2 options: Filter All and Require Response.
|
||||||
|
|
||||||
|
Filter All, when enabled, will filter all users who do not fulfill the desire. Without this enabled, a desire only represents a preference rather than a requirement. The desire will still influence user match scores.
|
||||||
|
|
||||||
|
Require Response, when enabled, requires users to have provided a response to the attribute. For example, if a user enables Require Response for Age, then only users who have provided their Age will qualify as a match.
|
||||||
|
|
||||||
|
A user's desires are stored locally on their machine. A user's desires do not need to be uploaded anywhere or shared to the network.
|
||||||
|
|
||||||
|
### Download Desires
|
||||||
|
|
||||||
|
Users can choose their Download Desires, which are the desires that users are comfortable sharing with hosts. The more desires they share, the fewer profiles they will need to download. Most users should share desires such as Age and Distance, because these are usually not too private or embarrassing to risk being publicly revealed. If a user does not select any download desires, the Seekia app will download all of the newest mate profiles on the network, allowing the user to privately generate their matches without having to share any of their desires to hosts.
|
||||||
|
|
||||||
|
## Greet, Reject, Like, and Ignore
|
||||||
|
|
||||||
|
Users can send Greet and Reject messages to other users. Greet messages signal interest, and Reject messages signal disinterest. Users can filter their matches and conversations to only show users who have greeted them, and to hide users who have rejected them.
|
||||||
|
|
||||||
|
Users can also designate other users as being Liked or Ignored. A user's Liked and Ignored users are stored on their machine and are never shared or uploaded anywhere. Users can filter their matches and conversations to only show users who they have liked, and hide users who they have ignored.
|
||||||
|
|
||||||
|
## Race
|
||||||
|
|
||||||
|
Seekia is a race aware mate discovery network. Users can browse potential mates while having a deep awareness of each user's race and the predicted race of their offspring. Seekia aims to allow users to choose a mate who is the most likely and capable of producing offspring who belong to their desired race.
|
||||||
|
|
||||||
|
The racial description of users should be highly precise and detailed. The more accurately a user's profile can describe their race, the more effective Seekia will be at helping users to meet and mate with members of their desired races.
|
||||||
|
|
||||||
|
Users can share detailed information about their race such as their physical traits, trait genes, genetic ancestry, parent haplogroups, and neanderthal variant count. Users can filter and sort other users by these attributes and the calculated attributes of their offspring.
|
||||||
|
|
||||||
|
Users profiles can contain a user's skin, eye and hair color; hair texture; and genome allele values for genes which effect these traits. Users can also share the allele values for genes which effect facial structure.
|
||||||
|
|
||||||
|
User profiles can include ancestral analyses from multiple providers and computational methods. The Seekia app should also eventually provide the ability to perform ancestral analyses from raw genome data files.
|
||||||
|
|
||||||
|
### Racial Similarity
|
||||||
|
|
||||||
|
For a person to have offspring who look as similar as possible to them, they should breed with someone who is the most racially similar to them, without being so similar that the negative effects of inbreeding occur. Users are able to sort other users based on their Racial Similarity, a calculation which measures trait similarity, trait gene similarity, ancestral similarity, and haplogroup similarity.
|
||||||
|
|
||||||
|
Trait similarity compares user traits such as eye color, skin color, hair color, and hair texture. Trait gene similarity compares the alleles responsible for physical traits from each user's genome. Ancestral similarity compares the geographic distance between each user's ancestry composition locations.
|
||||||
|
|
||||||
|
Facial similarity detection technology is another planned feature for Seekia. The Seekia app could compare user profile photos to help users to find potential mates whom have similar facial structures, helping to cure racial loneliness. Users could also import photos of people they are strongly attracted to for the purpose of finding a mate who looks similar to them.
|
||||||
|
|
||||||
|
## Genetics Aware
|
||||||
|
|
||||||
|
Seekia is also a genetics aware mate discovery network. Seekia gives users the ability to choose their mate in a way that maximizes the health of their offspring and increases the probability of their offspring possessing their desired traits.
|
||||||
|
|
||||||
|
The Seekia application is capable of producing genetic analyses on raw genome files. Users and couples can perform offline analyses of their genomes within the app. Genetic analyses are computed privately on user machines without uploading any data anywhere.
|
||||||
|
|
||||||
|
There are two analysis types: Person and Couple. A person analysis contains a person's monogenic disease probabilities, polygenic disease risk scores, and traits scores. A Couple analysis is performed for two people, and contains the monogenic disease probabilities, polygenic disease risk scores, and trait scores for offspring produced from both people.
|
||||||
|
|
||||||
|
Seekia plans to add more genetic attribute analyses and genetic compatibility testing features. Kinship analysis technology should be built into Seekia to help users avoid accidental inbreeding.
|
||||||
|
|
||||||
|
### Raw Genome Files
|
||||||
|
|
||||||
|
Users must first import their raw genome file(s) from sequencing companies. The sequences obtained from these companies usually contain some inaccurate reported gene values. To remedy these errors, users can import as many raw genome files as they desire to find and root out conflicts. Seekia will combine any number of raw genomes into two genomes to be analyzed: Only Include Shared and Only Exclude Conflicts.
|
||||||
|
|
||||||
|
The Only Include Shared genome is the most accurate, and will only include genome locations where at least 2 files have agreed on the locus value. If conflicts are found, then the most attested value is chosen. If a tie exists between all files, then the location is not included.
|
||||||
|
|
||||||
|
The Only Exclude Conflicts genome is less accurate, but includes more data. It is created identically to Only Include Shared, except it will include locations which only 1 genome file has recorded.
|
||||||
|
|
||||||
|
Each analysis reports the results from each of the component genomes and the combined genomes, so users can see where conflicts exist and how those conflicts effect the analysis results.
|
||||||
|
|
||||||
|
### Monogenic Diseases
|
||||||
|
|
||||||
|
Genetic disorders are a significant issue for humanity, causing severe suffering for millions of people. The prevalence of genetic disorders among humans is increasing due to the weakening of the natural selection pressures for health. As a result of lifesaving medical technologies, people with genetic disorders are living longer and healthier lives, and are more likely to produce diseased offspring who possess their defective genes.
|
||||||
|
|
||||||
|
Seekia aims to drastically reduce the prevalence of recessive monogenic diseases within the human species. There are thousands of genes which, if defective, cause recessive monogenic disease in humans. All humans have 2 copies of these genes. A recessive monogenic disease is a disease which only causes symptoms if both copies of a person's gene are defective. Most people are carriers for many recessive monogenic diseases. A carrier has 1 defective and 1 healthy copy of the disease causing gene. Few people have defects in both copies of the same gene, which is required to cause disease symptoms.
|
||||||
|
|
||||||
|
If two people who have the same recessive monogenic disease breed, their offspring has a ~100% probability of having the disease. If someone with a recessive monogenic disease breeds with a carrier, their offspring has a ~50% probability of having the disease. If two carriers of a recessive monogenic disease breed, their offspring has a ~25% probability of having the disease. If neither people are carriers, or only one person is a carrier and the other is not, or only one person has the disease and the other is not a carrier, their offspring has a ~0% probability of having the disease.
|
||||||
|
|
||||||
|
In order to prevent people with recessive monogenic diseases from being conceived, we must prevent people who have any defects in genes for the same diseases from breeding with each other. This practice only requires reducing each person's pool of potential mates by a small amount (~5% in 2024), but will result in a drastic reduction in the prevalence of recessive monogenic disorders within the human species.
|
||||||
|
|
||||||
|
A Person analysis describes a person’s probability of having each monogenic disease and their probability of passing a disease variant for each disease. A Couple analysis will report on the offspring's probability of having each monogenic disease. Users can share their monogenic disease probabilities on their profiles, and users can filter and sort users based on their offspring's probability of having a monogenic disease.
|
||||||
|
|
||||||
|
Users have 2 options for filtering their offspring's monogenic disease probability: 0% and <100%.
|
||||||
|
|
||||||
|
Selecting 0% will only show the user potential mates with whom the user's offspring has a 0% probability of having any monogenic diseases. This option will filter all potential mates who have defects in the same recessive monogenic disease-causing genes as the user. The 0% option will also filter all users with dominant monogenic diseases, because those users always have a ~50% or greater probability of passing their dominant monogenic disease to their offspring. The 0% option should be selected by users who do not want to use embryo screening for reproduction.
|
||||||
|
|
||||||
|
Selecting <100% will only show the user potential mates with whom the user's offspring has a <100% probability of having any monogenic diseases. This option will filter potential mates who have the same recessive monogenic diseases as the user. This option will also filter any users who have a double dominant monogenic disease, because all offspring produced by these individuals have a ~100% probability of being diseased. The <100% filter could be useful for users who plan to use embryo screening, and only need to have the capability of producing disease-free offspring with their mate. It is still better to avoid these kinds of relationships, because both people could accidentally conceive diseased offspring without using embryo screening.
|
||||||
|
|
||||||
|
### Polygenic Diseases
|
||||||
|
|
||||||
|
Polygenic diseases are diseases whose risk is influenced by many genes.
|
||||||
|
|
||||||
|
A Person analysis describes a person's risk score for each polygenic disease. A Couple analysis describes a prospective offspring's average risk score for each polygenic disease. Users can share their allele values for genes which influence each disease's risk on their profile. The Seekia app is able to calculate genetic outcome probabilities for each user's offspring. Users can sort potential mates by their offspring's disease risk scores. Seekia enables users to mate with other users with whom their offspring has a lower probability of having polygenic diseases.
|
||||||
|
|
||||||
|
Seekia allows for a user's polygenic disease risk to influence their sexual market value. For users who share their polygenic disease allele values, their disease risk is calculable from their profile. Users can sort their matches by each match's total polygenic disease risk score. Users who are more likely to be healthy will be more sought after. Users with a higher risk of dying from various diseases may choose to mate with each other. Users with a higher risk of cognitive decline in their old age may choose to mate with users who do not have an elevated risk, increasing the probability that the user's mate will be able to care for them in their old age.
|
||||||
|
|
||||||
|
### Traits
|
||||||
|
|
||||||
|
A Person analysis contains a person's trait outcome scores, and a Couple analysis contains the offspring's trait outcome scores. Users can share the allele values for locations in their genome which influence each trait in their profiles. The Seekia app can calculate the offspring outcomes for each user. Seekia allows users to filter and sort other users based on their offspring's trait outcome probabilities.
|
||||||
|
|
||||||
|
A user could sort users based on the probability of their offspring being able to tolerate lactose. A user who enjoys cooking lactose-based meals could use this technology to maximize the probability that their offspring will be able to tolerate those foods in adulthood. A user could also try to maximize the probability of their offspring having a certain hair texture or eye color.
|
||||||
|
|
||||||
|
In summary, the genetic matchmaking technology within Seekia is a major improvement to the human mating experience. These features can be used in conjunction with the genetic screening of embryos to maximize each user's ability to increase the health of their offspring and to choose the traits of their offspring which they desire.
|
||||||
|
|
||||||
|
## Non-Profit
|
||||||
|
|
||||||
|
A common criticism of for-profit mate discovery services is that they have a perverse incentive to extract money from their users. Critics claim that a profit incentive may motivate these services to keep users as customers by preventing them from finding a long term mate and instead encouraging users to go on many fruitless dates with incompatible people.
|
||||||
|
|
||||||
|
No entities profit directly from Seekia users. The only reason why participating in the Seekia network costs money is to prevent spam and bad behavior. All spent cryptocurrency funds are destroyed. Requiring spent funds to be burned discourages bad actors from attacking the network, because it is impossible for them to recover any funds used in their attacks. Burning funds also reduces any incentive to keep costs high because there are no direct financial beneficiaries.
|
||||||
|
|
||||||
|
Hosts and moderators are not financially rewarded by the network protocol. Most hosts will hopefully be volunteers and non-profit institutions who are altruistically motivated. Moderators could be funded by donations, companies, non-profit institutions, or decentralized autonomous organizations.
|
||||||
|
|
||||||
|
## Spam Prevention
|
||||||
|
|
||||||
|
Without any form of spam prevention, a single malicious actor could spam the Seekia network with billions of fake profiles and messages, rendering the network inoperative.
|
||||||
|
|
||||||
|
Seekia requires users to fund their identities before broadcasting content to the network. Users must also fund each message, report, and mate profile.
|
||||||
|
|
||||||
|
In a fully decentralized model, users would use a cryptocurrency to fund each identity and piece of content. Cryptocurrency addresses would be derived from identity and content hashes. This approach requires using a decentralized cryptocurrency which can support tens of thousands of privacy-preserving transactions per second. I am not aware of any cryptocurrency which can support the necessary throughput, so a centralized accounting model is used instead.
|
||||||
|
|
||||||
|
### Account Credit
|
||||||
|
|
||||||
|
A centralized account credit database is used to facilitate the funding of content and identities on the Seekia network.
|
||||||
|
|
||||||
|
Each account has a credit balance. An account is represented by a public/private key pair. Users must possess an account's private key to view and spend its balance. Credit can be purchased with cryptocurrency. Users can send credit from one account to another.
|
||||||
|
|
||||||
|
The database is trusted to not log user behavior. If a snapshot of the database were ever leaked, sensitive information such as the senders of messages would not be revealed.
|
||||||
|
|
||||||
|
Using a central database allows for admins to freely create and distribute credit. Admins are able to onboard users to Seekia for free by sending them credit. A website could be created that allows users to receive credit by verifying ownership of a phone number. Phone numbers are costly for attackers to obtain.
|
||||||
|
|
||||||
|
The account credit database is a single point of failure which the network relies upon. Creating backups of the database is prudent. If the database ever goes offline, hosts will continue to serve any content which has already been funded until the content expires from the network.
|
||||||
|
|
||||||
|
## Messaging
|
||||||
|
|
||||||
|
Seekia provides a messaging system for users to communicate privately.
|
||||||
|
|
||||||
|
Each message must be funded. The cost of funding a message is determined by its size and desired duration.
|
||||||
|
|
||||||
|
Users can filter and sort the users who they are communicating with. For example, a user can filter their conversations to only show users who fulfill their desires, and sort their conversations by the distance of the conversator.
|
||||||
|
|
||||||
|
### Message Inboxes
|
||||||
|
|
||||||
|
Every Seekia message contains a publicly viewable 10 byte value called an Inbox. All other sensitive elements of each message such as the sender, recipient, and communication are encrypted.
|
||||||
|
|
||||||
|
There are two types of message inboxes: Public and Secret.
|
||||||
|
|
||||||
|
A user's public inbox is created by hashing their identity hash. Everyone can see the quantity and size of messages in each user's public inbox. Public inboxes sacrifice privacy but increase sync speed. A more private method would require users to download information about messages which were not sent to them, increasing bandwidth and message latency. A way to mitigate the public inbox privacy flaw is to create services which send fake messages to users in an attempt to equalize the quantity of messages in each user's public inbox.
|
||||||
|
|
||||||
|
If Seekia only used public inboxes, it would be possible to determine which users were chatting with each other by analyzing message sent times and metadata about users. For example, if two users who live near each other are both receiving messages around the same time, it is possible to guess that they are communicating.
|
||||||
|
|
||||||
|
Secret Inboxes are used to resist this kind of analysis. Each message contains 2 secret inboxes which belong to the sender: Current and Next. The recipient sends future messages to the sender's secret inboxes rather than their public inbox. Each secret inbox corresponds to a secret inbox epoch. Users send each message to the secret inbox which corresponds to the epoch of the message's sent time. Identical epoch start and end times are used by all message senders. Even if an attacker were able to correlate pairs of communicating secret inboxes, all active secret inboxes change at the same time, facilitating a mixing effect for all secret inbox pairs.
|
||||||
|
|
||||||
|
## Encryption
|
||||||
|
|
||||||
|
All chat messages and network communications are encrypted with Nacl and Kyber. Kyber is a quantum-resistant encryption method. Seekia plans to utilize more encryption methods in the future. Using many encryption methods protects against future data decryption in the event that utilized cryptography is broken. If all encryption methods used to seal messages were broken, previously broadcasted messages would be decryptable. Expired messages may be stored by malicious entities who are preparing for this possibility.
|
||||||
|
|
||||||
|
Seekia users share their chat encryption keys on their profiles. A user's chat keys are generated locally on their machine, and are periodically replaced with new keys. Old keys are automatically deleted, and users can choose to delete old messages. After deleting a broadcasted message's private chat keys and unencrypted contents, the message cannot be decrypted if the sender or recipient's machines or seed phrases are compromised.
|
||||||
|
|
||||||
|
## Statistics
|
||||||
|
|
||||||
|
Seekia provides users with the ability to view their Desire Statistics. Desire statistics describe the number and percentage of users who are being filtered by a user’s desires. For example, if a user’s total match percentage is 5%, it means that 5% of the newest Mate user profiles they have downloaded pass all of their desires.
|
||||||
|
|
||||||
|
Desire statistics also describe each desire’s filtration statistics. For each desire, the user can see how many users the desire is filtering, what percentage of users pass the desire, the number of matches a user would have if they disabled the desire, and what percentage of users would be a match if they disabled the desire.
|
||||||
|
|
||||||
|
Seekia provides graphing functionality which gives everyone the ability to view statistics about users. For example, the app can display a chart plotting Age on the X-axis and Average Wealth on the Y-axis. User statistics graphing enables anyone to learn more about the demographics of Seekia users, and can inform users about how they should alter their desires to increase their match percentage.
|
||||||
|
|
||||||
|
The Seekia network can act as a public census resource. Seekia's userbase can help humanity to track the geographic locations of the world's races, helping to inform people about where they should move to be closer to members of their favorite races. User profiles will be available to all researchers around the world, enabling interesting and valuable new insights about humanity to be discovered. Harvesting and analyzing user data is not the primary purpose of Seekia. People are more likely to lie on a mating profile than a research survey. Researchers should conduct their own studies to collect higher quality data.
|
||||||
|
|
||||||
|
## Moderation
|
||||||
|
|
||||||
|
Seekia has a transparent and decentralized moderation system.
|
||||||
|
|
||||||
|
Fair moderation systems are vital to ensure that mate discovery technologies are genetically impartial. Mate discovery services have historically banned users to harm the happiness and reproductive capability of their ideological enemies.
|
||||||
|
|
||||||
|
Anyone can participate as a moderator. Moderators create reviews of identities, profiles, and messages.
|
||||||
|
|
||||||
|
Each identity, profile, and message has a consensus verdict. Profiles and messages can be Approved, Banned, or Undecided. Identities can be Banned or Not Banned. Identities cannot be approved, because their behavior can always become unruleful.
|
||||||
|
|
||||||
|
The moderators who have approved or banned each identity, profile, and message are publicly viewable, along with their reasons for doing so.
|
||||||
|
|
||||||
|
### Identity Scores
|
||||||
|
|
||||||
|
Each moderator has an identity score which determines their power. An identity score is the sum of all cryptocurrency sent to a moderator's identity score cryptocurrency addresses, valued in gold at time sent. Each address is derived from the moderator's identity hash and thus has no known private key. Funds sent to these addresses are destroyed forever. Anyone can destroy cryptocurrency to increase the identity score of moderators who they trust.
|
||||||
|
|
||||||
|
A moderator's rank is calculated by sorting all moderators in the order of their identity scores. Moderators can ban moderators who are below them in rank. The verdict of a piece of content is calculated by summing and comparing the identity scores of the content's approve and ban advocates.
|
||||||
|
|
||||||
|
Identity scores provide many advantages. All moderators must spend funds to participate, increasing the barrier to entry for malicious moderators. Moderators are less able to increase their power by creating many moderator identities. Malicious moderators will suffer financially because they must fund new identities after being banned.
|
||||||
|
|
||||||
|
Relying on identity scores to settle disputes encourages cooperation between moderators. When moderators disagree, rather than leapfrogging each other by funding their own identity scores and banning each other, they are incentivized to resolve their differences or to recruit other higher ranked moderators to ban their opponents.
|
||||||
|
|
||||||
|
### Supermoderators
|
||||||
|
|
||||||
|
Supermoderators are a set of moderators chosen by the network admins. Supermoderators have the absolute authority to ban non-supermoderators. Supermoderators are ranked and possess the ability to ban supermoderators below them in rank.
|
||||||
|
|
||||||
|
Supermoderators are a safeguard against attacks on the moderation system by malicious moderators. In the scenario where a moderator funded their identity enough to become the top ranked moderator and banned all other moderators, another moderator would have to fund their identity enough to gain the ability to ban this malicious moderator. Supermoderators are able to ban these malicious moderators without spending any funds.
|
||||||
|
|
||||||
|
### Sticky Viewable Statuses
|
||||||
|
|
||||||
|
Each identity, profile, and message consensus verdict is always able to change. A verdict may be unjust for a period of time. For example, a malicious moderator could fund their identity to a high rank, ban all moderators below them, and ban all content on the network. This moderator could cause ruleful content on the network to have a Banned verdict. To undo the damage, a higher-ranked moderator or supermoderator must ban this malicious moderator to restore order to the network. This could take a while, depending on how highly ranked the malicious moderator is.
|
||||||
|
|
||||||
|
Sticky viewable statuses are used to solve the problem of verdict variability. Each identity, profile, and message has a sticky viewable status which is determined by the percentage of its viewable verdicts for a defined time period. A viewable verdict is a verdict which an identity or piece of content must possess to be visible to regular users. To be considered viewable, Mate profiles must be Approved, whereas Host and Moderator profiles can be Undecided or Approved.
|
||||||
|
|
||||||
|
Sticky viewable statuses become stuck and impervious to temporary changes to real-time verdicts. Sticky statuses allow content to remain viewable and served to users during attacks on the network by malicious moderators.
|
||||||
|
|
||||||
|
### Attribute Reviews
|
||||||
|
|
||||||
|
When reviewing profiles, moderators can submit Profile reviews or Attribute reviews. An attribute review is a review of a specific attribute within a profile.
|
||||||
|
|
||||||
|
Attribute reviews provide several advantages. Moderators can specify the attribute which motivated them to ban a profile. Moderators do not have to approve all attributes of a profile again if the user resubmits their profile with 1 attribute changed. Moderators would only have to approve the single changed attribute if they had already approved all of the profile's other attributes.
|
||||||
|
|
||||||
|
Moderators have the flexibility to choose which attributes they want to review. A moderator can choose to only review images and still contribute to the network. Within the Seekia app, moderators choose the attribute they want to review and cycle through the attribute value for each user profile. This functionality reduces the cognitive load of context switching and increases moderator efficiency.
|
||||||
|
|
||||||
|
### Reporting Content
|
||||||
|
|
||||||
|
Users can report identities, profiles, and messages. Reports are created anonymously and have no public author. Each report must be funded to prevent spam.
|
||||||
|
|
||||||
|
To report a message, a user includes the decryption key for the message in the report. Moderators use this key to decrypt and view the message. The decryption key is included in each message review, which functions as a proof that the reviewer has seen the contents of the message.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The goal of Seekia is to accelerate humanity's adoption of race and genetics aware mate discovery technology.
|
||||||
|
|
||||||
|
Informing human mating choices with racial and genetic information will have a major positive impact on the human species. Seekia aims to usher in a new era of human breeding strategies. Seekia aims to bring genetic order to humanity's breeding patterns.
|
||||||
|
|
||||||
|
Seekia aims to cure racial loneliness, beautify the human species, reduce the prevalence of genetic diseases, increase humanity's intelligence, and boost global fertility rates.
|
||||||
|
|
||||||
|
Seekia has the potential to create families, facilitate the conception of beautiful and healthy offspring, and increase the amount of love and happiness in the world.
|
||||||
|
|
||||||
|
Join me in my effort to change the world and build a better future for all.
|
||||||
|
|
||||||
|
The genetic future of our species is at stake.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
Access Seekia's website from these domains: **Seekia.eth** or **Seekia.net**
|
||||||
|
|
||||||
|
Access Simon Sarasova's website: SimonSarasova.eth
|
||||||
|
|
||||||
|
Research online for instructions on how to access .eth IPFS websites.
|
||||||
|
|
||||||
|
These domains may have been seized or lost by the time you are reading this. You can only trust that content is authored by me if it contains my digital signature. You can verify that Seekia memos are signed with my identity hash by using the Seekia application.
|
||||||
|
|
||||||
|
Simon's Sarasova's identity hash is: simonx5yudleks5jhwhnck5s28m
|
BIN
documentation/Whitepaper.pdf
Normal file
BIN
documentation/Whitepaper.pdf
Normal file
Binary file not shown.
80
go.mod
Normal file
80
go.mod
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
module seekia
|
||||||
|
|
||||||
|
replace seekia => ./
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require (
|
||||||
|
fyne.io/fyne/v2 v2.4.4
|
||||||
|
github.com/chai2010/webp v1.1.1
|
||||||
|
github.com/cloudflare/circl v1.3.7
|
||||||
|
github.com/dgraph-io/badger/v4 v4.2.0
|
||||||
|
github.com/disintegration/gift v1.2.1
|
||||||
|
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
|
||||||
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||||
|
github.com/wcharczuk/go-chart/v2 v2.1.1
|
||||||
|
github.com/zeebo/blake3 v0.2.3
|
||||||
|
golang.org/x/crypto v0.21.0
|
||||||
|
golang.org/x/image v0.15.0
|
||||||
|
gorgonia.org/gorgonia v0.9.18
|
||||||
|
gorgonia.org/tensor v0.9.24
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e // indirect
|
||||||
|
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect
|
||||||
|
github.com/awalterschulze/gographviz v2.0.3+incompatible // indirect
|
||||||
|
github.com/blend/go-sdk v1.20220411.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/chewxy/hm v1.0.0 // indirect
|
||||||
|
github.com/chewxy/math32 v1.10.1 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
|
github.com/fredbi/uri v1.0.0 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
|
github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect
|
||||||
|
github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 // indirect
|
||||||
|
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect
|
||||||
|
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
|
||||||
|
github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 // indirect
|
||||||
|
github.com/go-text/typesetting v0.1.0 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
github.com/golang/glog v1.0.0 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/golang/snappy v0.0.3 // indirect
|
||||||
|
github.com/google/flatbuffers v23.5.26+incompatible // indirect
|
||||||
|
github.com/google/uuid v1.5.0 // indirect
|
||||||
|
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||||
|
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
|
||||||
|
github.com/klauspost/compress v1.13.1 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
|
||||||
|
github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
|
github.com/tevino/abool v1.2.0 // indirect
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
|
github.com/xtgo/set v1.0.0 // indirect
|
||||||
|
github.com/yuin/goldmark v1.5.5 // indirect
|
||||||
|
go.opencensus.io v0.23.0 // indirect
|
||||||
|
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
|
||||||
|
golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda // indirect
|
||||||
|
golang.org/x/net v0.21.0 // indirect
|
||||||
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||||
|
gonum.org/v1/gonum v0.14.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.32.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gorgonia.org/cu v0.9.4 // indirect
|
||||||
|
gorgonia.org/dawson v1.2.0 // indirect
|
||||||
|
gorgonia.org/vecf32 v0.9.0 // indirect
|
||||||
|
gorgonia.org/vecf64 v0.9.0 // indirect
|
||||||
|
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
|
||||||
|
)
|
925
go.sum
Normal file
925
go.sum
Normal file
|
@ -0,0 +1,925 @@
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
|
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||||
|
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||||
|
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||||
|
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||||
|
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||||
|
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||||
|
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||||
|
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||||
|
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||||
|
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||||
|
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||||
|
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||||
|
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||||
|
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||||
|
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
|
||||||
|
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
|
||||||
|
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||||
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
|
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||||
|
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||||
|
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||||
|
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||||
|
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||||
|
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||||
|
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||||
|
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||||
|
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||||
|
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||||
|
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||||
|
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||||
|
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
|
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||||
|
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||||
|
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||||
|
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||||
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
fyne.io/fyne/v2 v2.4.4 h1:4efSRpoikcGbqQN83yzC9WmF8UNq9olsaJQ/Ejme6Z8=
|
||||||
|
fyne.io/fyne/v2 v2.4.4/go.mod h1:VyrxAOZ3NRZRWBvNIJbfqoKOG4DdbewoPk7ozqJKNPY=
|
||||||
|
fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e h1:Hvs+kW2VwCzNToF3FmnIAzmivNgrclwPgoUdVSrjkP8=
|
||||||
|
fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE=
|
||||||
|
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
|
||||||
|
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
|
||||||
|
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
|
||||||
|
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||||
|
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||||
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
|
github.com/apache/arrow/go/arrow v0.0.0-20201229220542-30ce2eb5d4dc/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ=
|
||||||
|
github.com/apache/arrow/go/arrow v0.0.0-20210105145422-88aaea5262db/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ=
|
||||||
|
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ=
|
||||||
|
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs=
|
||||||
|
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||||
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/awalterschulze/gographviz v0.0.0-20190221210632-1e9ccb565bca/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=
|
||||||
|
github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E=
|
||||||
|
github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=
|
||||||
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
|
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||||
|
github.com/blend/go-sdk v1.20220411.3 h1:GFV4/FQX5UzXLPwWV03gP811pj7B8J2sbuq+GJQofXc=
|
||||||
|
github.com/blend/go-sdk v1.20220411.3/go.mod h1:7lnH8fTi6U4i1fArEXRyOIY2E1X4MALg09qsQqY1+ak=
|
||||||
|
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
|
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
|
||||||
|
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
||||||
|
github.com/chewxy/hm v1.0.0 h1:zy/TSv3LV2nD3dwUEQL2VhXeoXbb9QkpmdRAVUFiA6k=
|
||||||
|
github.com/chewxy/hm v1.0.0/go.mod h1:qg9YI4q6Fkj/whwHR1D+bOGeF7SniIP40VweVepLjg0=
|
||||||
|
github.com/chewxy/math32 v1.0.0/go.mod h1:Miac6hA1ohdDUTagnvJy/q+aNnEk16qWUdb8ZVhvCN0=
|
||||||
|
github.com/chewxy/math32 v1.0.6/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=
|
||||||
|
github.com/chewxy/math32 v1.0.7-0.20210223031236-a3549c8cb6a9/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=
|
||||||
|
github.com/chewxy/math32 v1.0.8/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=
|
||||||
|
github.com/chewxy/math32 v1.10.1 h1:LFpeY0SLJXeaiej/eIp2L40VYfscTvKh/FSEZ68uMkU=
|
||||||
|
github.com/chewxy/math32 v1.10.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs=
|
||||||
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||||
|
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||||
|
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
|
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/cznic/cc v0.0.0-20181122101902-d673e9b70d4d/go.mod h1:m3fD/V+XTB35Kh9zw6dzjMY+We0Q7PMf6LLIC4vuG9k=
|
||||||
|
github.com/cznic/golex v0.0.0-20181122101858-9c343928389c/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=
|
||||||
|
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
||||||
|
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
|
||||||
|
github.com/cznic/xc v0.0.0-20181122101856-45b06973881e/go.mod h1:3oFoiOvCDBYH+swwf5+k/woVmWy7h1Fcyu8Qig/jjX0=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs=
|
||||||
|
github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak=
|
||||||
|
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||||
|
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||||
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
|
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
|
||||||
|
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||||
|
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||||
|
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
|
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
|
github.com/fredbi/uri v1.0.0 h1:s4QwUAZ8fz+mbTsukND+4V5f+mJ/wjaTokwstGUAemg=
|
||||||
|
github.com/fredbi/uri v1.0.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
|
github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe h1:A/wiwvQ0CAjPkuJytaD+SsXkPU0asQ+guQEIg1BJGX4=
|
||||||
|
github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg=
|
||||||
|
github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 h1:+31CdF/okdokeFNoy9L/2PccG3JFidQT3ev64/r4pYU=
|
||||||
|
github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504/go.mod h1:gLRWYfYnMA9TONeppRSikMdXlHQ97xVsPojddUv3b/E=
|
||||||
|
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 h1:hnLq+55b7Zh7/2IRzWCpiTcAvjv/P8ERF+N7+xXbZhk=
|
||||||
|
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2/go.mod h1:eO7W361vmlPOrykIg+Rsh1SZ3tQBaOsfzZhsIOb/Lm0=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
|
||||||
|
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
|
||||||
|
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
|
||||||
|
github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
|
||||||
|
github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
|
||||||
|
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk=
|
||||||
|
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||||
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-gota/gota v0.12.0/go.mod h1:UT+NsWpZC/FhaOyWb9Hui0jXg0Iq8e/YugZHTbyW/34=
|
||||||
|
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
|
||||||
|
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
|
||||||
|
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||||
|
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||||
|
github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 h1:VkKnvzbvHqgEfm351rfr8Uclu5fnwq8HP2ximUzJsBM=
|
||||||
|
github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8/go.mod h1:h29xCucjNsDcYb7+0rJokxVwYAq+9kQ19WiFuBKkYtc=
|
||||||
|
github.com/go-text/typesetting v0.1.0 h1:vioSaLPYcHwPEPLT7gsjCGDCoYSbljxoHJzMnKwVvHw=
|
||||||
|
github.com/go-text/typesetting v0.1.0/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
|
||||||
|
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
|
||||||
|
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
|
||||||
|
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||||
|
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
|
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
|
||||||
|
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/flatbuffers v1.10.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
|
github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
|
github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
|
github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
|
github.com/google/flatbuffers v2.0.6+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
|
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
|
||||||
|
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||||
|
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
|
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20211219123610-ec9572f70e60/go.mod h1:cz9oNYuRUWGdHmLF2IodMLkAhcPtXeULvcBNagUrxTI=
|
||||||
|
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||||
|
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||||
|
github.com/gorgonia/bindgen v0.0.0-20180812032444-09626750019e/go.mod h1:YzKk63P9jQHkwAo2rXHBv02yPxDzoQT2cBV0x5bGV/8=
|
||||||
|
github.com/gorgonia/bindgen v0.0.0-20210223094355-432cd89e7765/go.mod h1:BLHSe436vhQKRfm6wxJgebeK4fDY+ER/8jV3vVH9yYU=
|
||||||
|
github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a/go.mod h1:dy/f2gjY09hwVfIyATps4G2ai7/hLwLkc5TrPqONuXY=
|
||||||
|
github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||||
|
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||||
|
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||||
|
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||||
|
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||||
|
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||||
|
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||||
|
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
|
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e h1:LvL4XsI70QxOGHed6yhQtAU34Kx3Qq2wwBzGFKY8zKk=
|
||||||
|
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||||
|
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
|
||||||
|
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||||
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/leesper/go_rng v0.0.0-20171009123644-5344a9259b21/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U=
|
||||||
|
github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353 h1:X/79QL0b4YJVO5+OsPH9rF2u428CIrGL/jLmPsoOQQ4=
|
||||||
|
github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U=
|
||||||
|
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
|
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||||
|
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||||
|
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||||
|
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||||
|
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||||
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
|
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
|
||||||
|
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||||
|
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||||
|
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
|
||||||
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
|
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||||
|
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||||
|
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
|
||||||
|
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
|
||||||
|
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||||
|
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||||
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||||
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
|
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
|
||||||
|
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||||
|
github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE=
|
||||||
|
github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14=
|
||||||
|
github.com/xtgo/set v1.0.0 h1:6BCNBRv3ORNDQ7fyoJXRv+tstJz3m1JVFQErfeZz2pY=
|
||||||
|
github.com/xtgo/set v1.0.0/go.mod h1:d3NHzGzSa0NmB2NhFyECA+QdRp29oEn2xbT+TpeFoM8=
|
||||||
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU=
|
||||||
|
github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||||
|
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
|
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
|
||||||
|
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
|
||||||
|
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||||
|
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||||
|
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||||
|
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||||
|
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
||||||
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
|
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
|
||||||
|
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||||
|
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||||
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
|
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||||
|
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||||
|
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||||
|
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4=
|
||||||
|
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||||
|
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||||
|
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||||
|
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
|
||||||
|
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||||
|
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
|
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||||
|
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||||
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||||
|
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||||
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||||
|
golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||||
|
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
|
||||||
|
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||||
|
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||||
|
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
|
||||||
|
golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda h1:O+EUvnBNPwI4eLthn8W5K+cS8zQZfgTABPLNm6Bna34=
|
||||||
|
golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||||
|
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190226215855-775f8194d0f9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
|
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||||
|
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
|
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||||
|
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||||
|
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||||
|
gonum.org/v1/gonum v0.0.0-20190226202314-149afe6ec0b6/go.mod h1:jevfED4GnIEnJrWW55YmY9DMhajHcnkqVnEXmEtMyNI=
|
||||||
|
gonum.org/v1/gonum v0.0.0-20190902003836-43865b531bee/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
|
||||||
|
gonum.org/v1/gonum v0.8.1-0.20200930085651-eea0b5cb5cc9/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||||
|
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||||
|
gonum.org/v1/gonum v0.9.1/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
|
||||||
|
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
|
||||||
|
gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
|
||||||
|
gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0=
|
||||||
|
gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU=
|
||||||
|
gonum.org/v1/netlib v0.0.0-20190221094214-0632e2ebbd2d/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||||
|
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||||
|
gonum.org/v1/netlib v0.0.0-20201012070519-2390d26c3658/go.mod h1:zQa7n16lh3Z6FbSTYgjG+KNhz1bA/b9t3plFEaGMp+A=
|
||||||
|
gonum.org/v1/netlib v0.0.0-20220323200511-14de99971b2d/go.mod h1:ObwMamC//3VQXZ2+uTOuOfnJNnZPdwBUibkUGgltkQA=
|
||||||
|
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||||
|
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
|
||||||
|
gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo=
|
||||||
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
|
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
|
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||||
|
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||||
|
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||||
|
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||||
|
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||||
|
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
|
||||||
|
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
|
||||||
|
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||||
|
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||||
|
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||||
|
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||||
|
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||||
|
google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||||
|
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||||
|
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||||
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
|
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||||
|
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
|
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
|
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
|
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||||
|
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||||
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||||
|
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||||
|
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorgonia.org/cu v0.9.0-beta/go.mod h1:RPEPIfaxxqUmeRe7T1T8a0NER+KxBI2McoLEXhP1Vd8=
|
||||||
|
gorgonia.org/cu v0.9.3/go.mod h1:LgyAYDkN7HWhh8orGnCY2R8pP9PYbO44ivEbLMatkVU=
|
||||||
|
gorgonia.org/cu v0.9.4 h1:XTnzfusx/0caMCfG3oJse+LW8SBmReA/613Mo7ZSVQI=
|
||||||
|
gorgonia.org/cu v0.9.4/go.mod h1:nR6RAm64n9htu6Orv1NVbsMJXHjnsC3SHPfgcxI08e4=
|
||||||
|
gorgonia.org/dawson v1.1.0/go.mod h1:Px1mcziba8YUBIDsbzGwbKJ11uIblv/zkln4jNrZ9Ws=
|
||||||
|
gorgonia.org/dawson v1.2.0 h1:hJ/aofhfkReSnJdSMDzypRZ/oWDL1TmeYOauBnXKdFw=
|
||||||
|
gorgonia.org/dawson v1.2.0/go.mod h1:Px1mcziba8YUBIDsbzGwbKJ11uIblv/zkln4jNrZ9Ws=
|
||||||
|
gorgonia.org/gorgonia v0.9.2/go.mod h1:ZtOb9f/wM2OMta1ISGspQ4roGDgz9d9dKOaPNvGR+ec=
|
||||||
|
gorgonia.org/gorgonia v0.9.17/go.mod h1:g66b5Z6ATUdhVqYl2ZAAwblv5hnGW08vNinGLcnrceI=
|
||||||
|
gorgonia.org/gorgonia v0.9.18 h1:LlEhqMjPwyKlLdy3iuWHI2k1znxormNedKayQaLgbm0=
|
||||||
|
gorgonia.org/gorgonia v0.9.18/go.mod h1:kYe25GPmZ+1ycLqfKDQx+50UIhklCU7lSDXiotON/f4=
|
||||||
|
gorgonia.org/tensor v0.9.0-beta/go.mod h1:05Y4laKuVlj4qFoZIZW1q/9n1jZkgDBOLmKXZdBLG1w=
|
||||||
|
gorgonia.org/tensor v0.9.17/go.mod h1:75SMdLLhZ+2oB0/EE8lFEIt1Caoykdd4bz1mAe59deg=
|
||||||
|
gorgonia.org/tensor v0.9.20/go.mod h1:75SMdLLhZ+2oB0/EE8lFEIt1Caoykdd4bz1mAe59deg=
|
||||||
|
gorgonia.org/tensor v0.9.23/go.mod h1:ZaFaLqBTKTzTbTzfnfbW8gDxFP2mXScMzjffUkSsK5Y=
|
||||||
|
gorgonia.org/tensor v0.9.24 h1:8ahrfwO4iby+1ILObIqfjJa+wyA2RoCfJSS3LVERSRE=
|
||||||
|
gorgonia.org/tensor v0.9.24/go.mod h1:1dsOegMm2n1obs69YnVJdp2oPSKx9Q9Tco5i7GEaXRg=
|
||||||
|
gorgonia.org/vecf32 v0.7.0/go.mod h1:iHG+kvTMqGYA0SgahfO2k62WRnxmHsqAREGbayRDzy8=
|
||||||
|
gorgonia.org/vecf32 v0.9.0 h1:PClazic1r+JVJ1dEzRXgeiVl4g1/Hf/w+wUSqnco1Xg=
|
||||||
|
gorgonia.org/vecf32 v0.9.0/go.mod h1:NCc+5D2oxddRL11hd+pCB1PEyXWOyiQxfZ/1wwhOXCA=
|
||||||
|
gorgonia.org/vecf64 v0.7.0/go.mod h1:1y4pmcSd+wh3phG+InwWQjYrqwyrtN9h27WLFVQfV1Q=
|
||||||
|
gorgonia.org/vecf64 v0.9.0 h1:bgZDP5x0OzBF64PjMGC3EvTdOoMEcmfAh1VCUnZFm1A=
|
||||||
|
gorgonia.org/vecf64 v0.9.0/go.mod h1:hp7IOWCnRiVQKON73kkC/AUMtEXyf9kGlVrtPQ9ccVA=
|
||||||
|
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 h1:oomkgU6VaQDsV6qZby2uz1Lap0eXmku8+2em3A/l700=
|
||||||
|
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2/go.mod h1:sUMDUKNB2ZcVjt92UnLy3cdGs+wDAcrPdV3JP6sVgA4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
|
||||||
|
modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
|
||||||
|
modernc.org/cc v1.0.1/go.mod h1:uj1/YV+GYVdtSfGOgOtY62Jz8YIiEC0EzZNq481HIQs=
|
||||||
|
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
|
||||||
|
modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
|
||||||
|
modernc.org/golex v1.0.1/go.mod h1:QCA53QtsT1NdGkaZZkF5ezFwk4IXh4BGNafAARTC254=
|
||||||
|
modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM=
|
||||||
|
modernc.org/ir v1.0.0/go.mod h1:wxK1nK3PS04CASoUY+HJr+FQywv4+D38y2sRrd71y7s=
|
||||||
|
modernc.org/lex v1.0.0/go.mod h1:G6rxMTy3cH2iA0iXL/HRRv4Znu8MK4higxph/lE7ypk=
|
||||||
|
modernc.org/lexer v1.0.0/go.mod h1:F/Dld0YKYdZCLQ7bD0USbWL4YKCyTDRDHiDTOs0q0vk=
|
||||||
|
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
|
||||||
|
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
|
modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
|
||||||
|
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
|
||||||
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
1092
gui/accountCreditGui.go
Normal file
1092
gui/accountCreditGui.go
Normal file
File diff suppressed because it is too large
Load diff
41
gui/adminGui.go
Normal file
41
gui/adminGui.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// adminGui.go provides tools for Seekia admins to view/change/broadcast network parameters
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
|
|
||||||
|
func setAdminToolsPage(window fyne.Window){
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Admin Tools")
|
||||||
|
|
||||||
|
description := getLabelCentered("Manage admin duties.")
|
||||||
|
|
||||||
|
viewNetworkParametersButton := widget.NewButton("View Network Parameters", func(){
|
||||||
|
//TODO
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
|
||||||
|
changeNetworkParametersButton := widget.NewButton("Change Network Parameters", func(){
|
||||||
|
//TODO
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
|
||||||
|
manageMyAdminIdentityButton := widget.NewButton("Manage My Admin Identity", func(){
|
||||||
|
//TODO
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, viewNetworkParametersButton, changeNetworkParametersButton, manageMyAdminIdentityButton))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, widget.NewSeparator(), description, buttonsGrid)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
1827
gui/broadcastGui.go
Normal file
1827
gui/broadcastGui.go
Normal file
File diff suppressed because it is too large
Load diff
3622
gui/buildProfileGui_General.go
Normal file
3622
gui/buildProfileGui_General.go
Normal file
File diff suppressed because it is too large
Load diff
931
gui/buildProfileGui_Lifestyle.go
Normal file
931
gui/buildProfileGui_Lifestyle.go
Normal file
|
@ -0,0 +1,931 @@
|
||||||
|
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// buildProfileGui_Lifestyle.go implements the pages to build the lifestyle portion of a mate profile
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/dialog"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
|
||||||
|
import "seekia/resources/currencies"
|
||||||
|
|
||||||
|
import "seekia/internal/allowedText"
|
||||||
|
import "seekia/internal/globalSettings"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/profiles/myLocalProfiles"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func setBuildMateProfileCategoryPage_Lifestyle(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setBuildMateProfileCategoryPage_Lifestyle(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered(translate("Build Mate Profile - Lifestyle"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
hobbiesButton := widget.NewButton(translate("Hobbies"), func(){
|
||||||
|
setBuildMateProfilePage_Hobbies(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
wealthButton := widget.NewButton(translate("Wealth"), func(){
|
||||||
|
setBuildMateProfilePage_Wealth(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
jobButton := widget.NewButton(translate("Job"), func(){
|
||||||
|
setBuildMateProfilePage_Job(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
dietButton := widget.NewButton(translate("Diet"), func(){
|
||||||
|
setBuildMateProfilePage_Diet(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
fameButton := widget.NewButton(translate("Fame"), func(){
|
||||||
|
setBuildMateProfilePage_Fame(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
drugsButton := widget.NewButton(translate("Drugs"), func(){
|
||||||
|
setBuildMateProfilePage_Drugs(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsGrid := container.NewGridWithColumns(1, hobbiesButton, wealthButton, jobButton, dietButton, fameButton, drugsButton)
|
||||||
|
|
||||||
|
buttonsGridCentered := getContainerCentered(buttonsGrid)
|
||||||
|
|
||||||
|
buttonsGridPadded := container.NewPadded(buttonsGridCentered)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsGridPadded)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setBuildMateProfilePage_Hobbies(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setBuildMateProfilePage_Hobbies(window, previousPage)}
|
||||||
|
|
||||||
|
pageTitle := getPageTitleCentered(translate("Build Mate Profile - Lifestyle"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
pageSubtitle := getPageSubtitleCentered(translate("Hobbies"))
|
||||||
|
|
||||||
|
description1 := getLabelCentered(translate("Enter your hobbies."))
|
||||||
|
description2 := getLabelCentered(translate("Hobbies are activities you enjoy."))
|
||||||
|
|
||||||
|
currentHobbiesExist, currentHobbies, err := myLocalProfiles.GetProfileData("Mate", "Hobbies")
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getMyCurrentHobbiesRow := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
myHobbiesLabel := widget.NewLabel("My Hobbies:")
|
||||||
|
|
||||||
|
if (currentHobbiesExist == false){
|
||||||
|
|
||||||
|
noResponseLabel := getBoldItalicLabel("No Response")
|
||||||
|
currentHobbiesRow := container.NewHBox(layout.NewSpacer(), myHobbiesLabel, noResponseLabel, layout.NewSpacer())
|
||||||
|
|
||||||
|
return currentHobbiesRow, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllowed := allowedText.VerifyStringIsAllowed(currentHobbies)
|
||||||
|
if (isAllowed == false){
|
||||||
|
return nil, errors.New("My current mate hobbies is not allowed: " + currentHobbies)
|
||||||
|
}
|
||||||
|
|
||||||
|
if( len(currentHobbies) > 1000 ){
|
||||||
|
return nil, errors.New("My current mate hobbies is too long: " + currentHobbies)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHobbiesTrimmed, _, err := helpers.TrimAndFlattenString(currentHobbies, 15)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
currentHobbiesLabel := getBoldLabel(currentHobbiesTrimmed)
|
||||||
|
|
||||||
|
viewMyHobbiesButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){
|
||||||
|
setViewTextPage(window, "Viewing Hobbies", currentHobbies, false, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
currentHobbiesRow := container.NewHBox(layout.NewSpacer(), myHobbiesLabel, currentHobbiesLabel, viewMyHobbiesButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
return currentHobbiesRow, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHobbiesRow, err := getMyCurrentHobbiesRow()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editButton := widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), func(){
|
||||||
|
setBuildMateProfilePage_EditHobbies(window, currentPage, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
noResponseButton := widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){
|
||||||
|
if (currentHobbiesExist == false){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setBuildMateProfilePage_DeleteHobbies(window, currentPage, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, editButton, noResponseButton))
|
||||||
|
|
||||||
|
page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), currentHobbiesRow, widget.NewSeparator(), buttonsGrid)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setBuildMateProfilePage_DeleteHobbies(window fyne.Window, previousPage func(), nextPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Delete Hobbies")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Delete Hobbies?")
|
||||||
|
|
||||||
|
description2 := getLabelCentered("Confirm to delete your hobbies?")
|
||||||
|
|
||||||
|
confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){
|
||||||
|
myLocalProfiles.DeleteProfileData("Mate", "Hobbies")
|
||||||
|
nextPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, confirmButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setBuildMateProfilePage_EditHobbies(window fyne.Window, previousPage func(), nextPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Edit Mate Hobbies")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
hobbiesEntry := widget.NewMultiLineEntry()
|
||||||
|
hobbiesEntry.Wrapping = 3
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Current hobbies exist
|
||||||
|
// -string: Current hobbies
|
||||||
|
// -error
|
||||||
|
getCurrentHobbies := func()(bool, string, error){
|
||||||
|
|
||||||
|
exists, currentHobbies, err := myLocalProfiles.GetProfileData("Mate", "Hobbies")
|
||||||
|
if (err != nil) { return false, "", err }
|
||||||
|
if (exists == false){
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllowed := allowedText.VerifyStringIsAllowed(currentHobbies)
|
||||||
|
if (isAllowed == false){
|
||||||
|
return false, "", errors.New("My current mate hobbies is not allowed: " + currentHobbies)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(len(currentHobbies) > 1000){
|
||||||
|
return false, "", errors.New("My current mate hobbies is too long: " + currentHobbies)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, currentHobbies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHobbiesExist, currentHobbies, err := getCurrentHobbies()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (currentHobbiesExist == false){
|
||||||
|
hobbiesEntry.SetPlaceHolder("Enter your hobbies...")
|
||||||
|
} else {
|
||||||
|
hobbiesEntry.SetText(currentHobbies)
|
||||||
|
}
|
||||||
|
|
||||||
|
submitButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Save"), theme.ConfirmIcon(), func(){
|
||||||
|
|
||||||
|
newHobbies := hobbiesEntry.Text
|
||||||
|
|
||||||
|
isAllowed := allowedText.VerifyStringIsAllowed(newHobbies)
|
||||||
|
if (isAllowed == false){
|
||||||
|
title := translate("Invalid Hobbies")
|
||||||
|
dialogMessageA := getLabelCentered(translate("Your hobbies contain an invalid character."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("It must be encoded in UTF-8."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(title, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBytesLength := len(newHobbies)
|
||||||
|
if (currentBytesLength > 1000){
|
||||||
|
|
||||||
|
currentLengthString := helpers.ConvertIntToString(currentBytesLength)
|
||||||
|
|
||||||
|
title := translate("Invalid Hobbies")
|
||||||
|
dialogMessageA := getLabelCentered(translate("Your Hobbies are too long."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("Hobbies cannot be longer than 1000 bytes."))
|
||||||
|
dialogMessageC := getLabelCentered(translate("Current Length: ") + currentLengthString)
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC)
|
||||||
|
dialog.ShowCustom(title, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newHobbies == ""){
|
||||||
|
myLocalProfiles.DeleteProfileData("Mate", "Hobbies")
|
||||||
|
} else {
|
||||||
|
myLocalProfiles.SetProfileData("Mate", "Hobbies", newHobbies)
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
emptyLabel := widget.NewLabel("")
|
||||||
|
|
||||||
|
submitButtonWithSpacer := container.NewVBox(submitButton, emptyLabel)
|
||||||
|
|
||||||
|
hobbiesEntryBoxed := getWidgetBoxed(hobbiesEntry)
|
||||||
|
|
||||||
|
hobbiesEntryWithSubmitButton := container.NewBorder(nil, submitButtonWithSpacer, nil, nil, hobbiesEntryBoxed)
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator())
|
||||||
|
|
||||||
|
page := container.NewBorder(header, nil, nil, nil, hobbiesEntryWithSubmitButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func setBuildMateProfilePage_Wealth(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setBuildMateProfilePage_Wealth(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered(translate("Build Mate Profile - Lifestyle"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered("Wealth")
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Enter your total wealth.")
|
||||||
|
description2 := getLabelCentered("Be careful about sharing any secret wealth.")
|
||||||
|
description3 := getLabelCentered("For the most safety, only share an amount that is already public and obvious.")
|
||||||
|
|
||||||
|
// Outputs:
|
||||||
|
// -bool: Current wealth exists
|
||||||
|
// -string: Current wealth
|
||||||
|
// -bool: Current wealth is a lower bound
|
||||||
|
// -error
|
||||||
|
getCurrentWealthInfo := func()(bool, string, bool, error){
|
||||||
|
|
||||||
|
wealthExists, currentWealth, err := myLocalProfiles.GetProfileData("Mate", "Wealth")
|
||||||
|
if (err != nil){ return false, "", false, err }
|
||||||
|
if (wealthExists == false){
|
||||||
|
return false, "", false, nil
|
||||||
|
}
|
||||||
|
isValid, err := helpers.VerifyStringIsIntWithinRange(currentWealth, 0, 9223372036854775807)
|
||||||
|
if (err != nil){ return false, "", false, err }
|
||||||
|
if (isValid == false){
|
||||||
|
return false, "", false, errors.New("MyLocalProfiles contains invalid Wealth: " + currentWealth)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWealthIsLowerBoundExists, currentWealthIsLowerBound, err := myLocalProfiles.GetProfileData("Mate", "WealthIsLowerBound")
|
||||||
|
if (err != nil){ return false, "", false, err }
|
||||||
|
if (currentWealthIsLowerBoundExists == false){
|
||||||
|
return false, "", false, errors.New("MyLocalProfiles contains Wealth without WealthIsLowerBound attribute.")
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWealthIsLowerBoundBool, err := helpers.ConvertYesOrNoStringToBool(currentWealthIsLowerBound)
|
||||||
|
if (err != nil){
|
||||||
|
return false, "", false, errors.New("MyLocalProfiles contains Invalid WealthIsLowerBound: " + currentWealthIsLowerBound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, currentWealth, currentWealthIsLowerBoundBool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWealthExists, currentWealth, currentWealthIsLowerBound, err := getCurrentWealthInfo()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wealthEntry := widget.NewEntry()
|
||||||
|
|
||||||
|
if (currentWealthExists == false){
|
||||||
|
wealthEntry.SetPlaceHolder(translate("Enter wealth..."))
|
||||||
|
} else {
|
||||||
|
wealthEntry.SetText(currentWealth)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentWealthCurrencyCodeFunction := func()(string, error){
|
||||||
|
currentWealthCurrencyExists, currentWealthCurrencyCode, err := myLocalProfiles.GetProfileData("Mate", "WealthCurrency")
|
||||||
|
if (err != nil){ return "", err }
|
||||||
|
if (currentWealthCurrencyExists == true){
|
||||||
|
return currentWealthCurrencyCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We will default to the user's already chosen currency
|
||||||
|
exists, currentAppCurrency, err := globalSettings.GetSetting("Currency")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (exists == false){
|
||||||
|
return "USD", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentAppCurrency, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWealthCurrencyCode, err := getCurrentWealthCurrencyCodeFunction()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, currentWealthCurrencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(currentWealthCurrencyCode)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentWealthLabel := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (currentWealthExists == false){
|
||||||
|
result := translate("No Response")
|
||||||
|
|
||||||
|
wealthLabel := getBoldItalicLabel(result)
|
||||||
|
return wealthLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentWealthFormatted := func()string{
|
||||||
|
|
||||||
|
if (currentWealthIsLowerBound == true){
|
||||||
|
|
||||||
|
currentWealthFormatted := currentWealthCurrencySymbol + " " + currentWealth + "+ " + currentWealthCurrencyCode
|
||||||
|
return currentWealthFormatted
|
||||||
|
}
|
||||||
|
currentWealthFormatted := currentWealthCurrencySymbol + " " + currentWealth + " " + currentWealthCurrencyCode
|
||||||
|
return currentWealthFormatted
|
||||||
|
}
|
||||||
|
currentWealthFormatted := getCurrentWealthFormatted()
|
||||||
|
|
||||||
|
wealthLabel := getBoldLabel(currentWealthFormatted)
|
||||||
|
return wealthLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWealthLabel := getCurrentWealthLabel()
|
||||||
|
|
||||||
|
myWealthLabel := widget.NewLabel("My Wealth:")
|
||||||
|
|
||||||
|
currentWealthRow := container.NewHBox(layout.NewSpacer(), myWealthLabel, currentWealthLabel, layout.NewSpacer())
|
||||||
|
|
||||||
|
chooseCurrencyButton := widget.NewButton(currentWealthCurrencySymbol, func(){
|
||||||
|
|
||||||
|
onSelectFunction := func(newCurrencyCode string)error{
|
||||||
|
|
||||||
|
err := myLocalProfiles.SetProfileData("Mate", "WealthCurrency", newCurrencyCode)
|
||||||
|
if (err != nil){ return err }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
setChooseCurrencyPage(window, getCurrentWealthCurrencyCodeFunction, onSelectFunction, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
chooseCurrencyButtonWithSpacer := container.NewHBox(layout.NewSpacer(), chooseCurrencyButton)
|
||||||
|
|
||||||
|
currentCurrencyCodeLabel := getBoldLabel(currentWealthCurrencyCode)
|
||||||
|
|
||||||
|
currentCurrencyCodeLabelWidened := container.NewHBox(currentCurrencyCodeLabel, widget.NewLabel(""), widget.NewLabel(""))
|
||||||
|
|
||||||
|
wealthEntryRow := getContainerCentered(container.NewGridWithRows(1, chooseCurrencyButtonWithSpacer, wealthEntry, currentCurrencyCodeLabelWidened))
|
||||||
|
|
||||||
|
isLowerBoundCheck := widget.NewCheck("Is A Lower Bound?", func(newIsLowerBoundStatus bool){
|
||||||
|
|
||||||
|
if (currentWealthExists == false){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newWealth := wealthEntry.Text
|
||||||
|
if (newWealth != currentWealth){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newIsLowerBoundString := helpers.ConvertBoolToYesOrNoString(newIsLowerBoundStatus)
|
||||||
|
|
||||||
|
myLocalProfiles.SetProfileData("Mate", "WealthIsLowerBound", newIsLowerBoundString)
|
||||||
|
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (currentWealthExists == true && currentWealthIsLowerBound == true){
|
||||||
|
isLowerBoundCheck.Checked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
isLowerBoundHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){
|
||||||
|
setWealthOrIncomeIsLowerBoundExplainerPage(window, currentPage)
|
||||||
|
})
|
||||||
|
isLowerBoundCheckRow := container.NewHBox(layout.NewSpacer(), isLowerBoundCheck, isLowerBoundHelpButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
submitButton := getWidgetCentered(widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){
|
||||||
|
|
||||||
|
newWealth := wealthEntry.Text
|
||||||
|
|
||||||
|
isValid, err := helpers.VerifyStringIsIntWithinRange(newWealth, 0, 9223372036854775807)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isValid == false){
|
||||||
|
|
||||||
|
if (currentWealthExists == false){
|
||||||
|
wealthEntry.SetText("")
|
||||||
|
wealthEntry.SetPlaceHolder(translate("Enter wealth..."))
|
||||||
|
} else {
|
||||||
|
wealthEntry.SetText(currentWealth)
|
||||||
|
}
|
||||||
|
|
||||||
|
title := translate("Invalid Wealth")
|
||||||
|
dialogMessageA := getLabelCentered(translate("Invalid wealth entered."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("The value must be a number between 0 and 9223372036854775807."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(title, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myLocalProfiles.SetProfileData("Mate", "Wealth", newWealth)
|
||||||
|
|
||||||
|
newIsLowerBoundStatus := isLowerBoundCheck.Checked
|
||||||
|
|
||||||
|
newIsLowerBoundString := helpers.ConvertBoolToYesOrNoString(newIsLowerBoundStatus)
|
||||||
|
|
||||||
|
myLocalProfiles.SetProfileData("Mate", "WealthIsLowerBound", newIsLowerBoundString)
|
||||||
|
|
||||||
|
myLocalProfiles.SetProfileData("Mate", "WealthCurrency", currentWealthCurrencyCode)
|
||||||
|
|
||||||
|
currentPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){
|
||||||
|
myLocalProfiles.DeleteProfileData("Mate", "Wealth")
|
||||||
|
myLocalProfiles.DeleteProfileData("Mate", "WealthIsLowerBound")
|
||||||
|
currentPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), currentWealthRow, widget.NewSeparator(), wealthEntryRow, isLowerBoundCheckRow, submitButton, widget.NewSeparator(), noResponseButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBuildMateProfilePage_Job(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setBuildMateProfilePage_Job(window, previousPage)}
|
||||||
|
|
||||||
|
pageTitle := getPageTitleCentered(translate("Build Mate Profile - Lifestyle"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
pageSubtitle := getPageSubtitleCentered(translate("Job"))
|
||||||
|
|
||||||
|
description1 := getLabelCentered(translate("Enter your job."))
|
||||||
|
description2 := getLabelCentered(translate("This is how you make money."))
|
||||||
|
|
||||||
|
currentJobExists, currentJob, err := myLocalProfiles.GetProfileData("Mate", "Job")
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getMyCurrentJobRow := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
myJobLabel := widget.NewLabel("My Job:")
|
||||||
|
|
||||||
|
if (currentJobExists == false){
|
||||||
|
|
||||||
|
noResponseLabel := getBoldItalicLabel("No Response")
|
||||||
|
currentJobRow := container.NewHBox(layout.NewSpacer(), myJobLabel, noResponseLabel, layout.NewSpacer())
|
||||||
|
|
||||||
|
return currentJobRow, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllowed := allowedText.VerifyStringIsAllowed(currentJob)
|
||||||
|
if (isAllowed == false){
|
||||||
|
return nil, errors.New("My current mate job is not allowed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(len(currentJob) > 100){
|
||||||
|
return nil, errors.New("My current mate job is too long.")
|
||||||
|
}
|
||||||
|
|
||||||
|
currentJobTrimmed, _, err := helpers.TrimAndFlattenString(currentJob, 15)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
currentJobLabel := getBoldLabel(currentJobTrimmed)
|
||||||
|
|
||||||
|
viewMyJobButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){
|
||||||
|
setViewTextPage(window, "Viewing Job", currentJob, false, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
currentJobRow := container.NewHBox(layout.NewSpacer(), myJobLabel, currentJobLabel, viewMyJobButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
return currentJobRow, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentJobRow, err := getMyCurrentJobRow()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editButton := widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), func(){
|
||||||
|
setBuildMateProfilePage_EditJob(window, currentPage, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
noResponseButton := widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){
|
||||||
|
|
||||||
|
if (currentJobExists == false){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setBuildMateProfilePage_DeleteJob(window, currentPage, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, editButton, noResponseButton))
|
||||||
|
|
||||||
|
page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), currentJobRow, widget.NewSeparator(), buttonsGrid)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setBuildMateProfilePage_DeleteJob(window fyne.Window, previousPage func(), nextPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Delete Job")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Delete Job?")
|
||||||
|
|
||||||
|
description2 := getLabelCentered("Confirm to delete your job?")
|
||||||
|
|
||||||
|
confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){
|
||||||
|
myLocalProfiles.DeleteProfileData("Mate", "Job")
|
||||||
|
nextPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, confirmButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setBuildMateProfilePage_EditJob(window fyne.Window, previousPage func(), nextPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Edit Job")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
jobEntry := widget.NewMultiLineEntry()
|
||||||
|
jobEntry.Wrapping = 3
|
||||||
|
|
||||||
|
getCurrentJob := func()(string, error){
|
||||||
|
|
||||||
|
exists, currentJob, err := myLocalProfiles.GetProfileData("Mate", "Job")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (exists == false){
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllowed := allowedText.VerifyStringIsAllowed(currentJob)
|
||||||
|
if (isAllowed == false){
|
||||||
|
return "", errors.New("My current mate job is not allowed: " + currentJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
if( len(currentJob) > 100 ){
|
||||||
|
return "", errors.New("My current mate job is too long: " + currentJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentJob, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentJob, err := getCurrentJob()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (currentJob == ""){
|
||||||
|
jobEntry.SetPlaceHolder("Enter your job...")
|
||||||
|
} else {
|
||||||
|
jobEntry.SetText(currentJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
submitButton := getWidgetCentered(widget.NewButtonWithIcon(translate("Save"), theme.ConfirmIcon(), func(){
|
||||||
|
|
||||||
|
newJob := jobEntry.Text
|
||||||
|
|
||||||
|
isAllowed := allowedText.VerifyStringIsAllowed(newJob)
|
||||||
|
if (isAllowed == false){
|
||||||
|
title := translate("Invalid Job")
|
||||||
|
dialogMessageA := getLabelCentered(translate("Your job contain an invalid character."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("It must be encoded in UTF-8."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(title, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBytesLength := len(newJob)
|
||||||
|
if (currentBytesLength > 100){
|
||||||
|
|
||||||
|
currentLengthString := helpers.ConvertIntToString(currentBytesLength)
|
||||||
|
|
||||||
|
title := translate("Invalid Job")
|
||||||
|
|
||||||
|
dialogMessageA := getLabelCentered(translate("Your Job is too long."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("Job cannot be longer than 100 bytes."))
|
||||||
|
dialogMessageC := getLabelCentered(translate("Current Length: ") + currentLengthString)
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC)
|
||||||
|
dialog.ShowCustom(title, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newJob == ""){
|
||||||
|
myLocalProfiles.DeleteProfileData("Mate", "Job")
|
||||||
|
} else {
|
||||||
|
myLocalProfiles.SetProfileData("Mate", "Job", newJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
emptyLabel := widget.NewLabel("")
|
||||||
|
|
||||||
|
submitButtonWithSpacer := container.NewVBox(submitButton, emptyLabel)
|
||||||
|
|
||||||
|
jobEntryBoxed := getWidgetBoxed(jobEntry)
|
||||||
|
|
||||||
|
jobEntryWithSubmitButton := container.NewBorder(nil, submitButtonWithSpacer, nil, nil, jobEntryBoxed)
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator())
|
||||||
|
|
||||||
|
page := container.NewBorder(header, nil, nil, nil, jobEntryWithSubmitButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setBuildMateProfilePage_Diet(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setBuildMateProfilePage_Diet(window, previousPage)}
|
||||||
|
|
||||||
|
pageTitle := getPageTitleCentered(translate("Build Mate Profile - Lifestyle"))
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
pageSubtitle := getPageSubtitleCentered(translate("Diet"))
|
||||||
|
|
||||||
|
description1 := getLabelCentered(translate("Rate each food."))
|
||||||
|
description2 := getLabelCentered(translate("1 = Strongly Dislike, 10 = Strongly Like"))
|
||||||
|
|
||||||
|
//TODO: Shrink the columns of this grid so they are only as wide as the widest element within them
|
||||||
|
// Also add grid lines
|
||||||
|
foodSelectorsGrid := container.NewGridWithColumns(3)
|
||||||
|
|
||||||
|
foodsList := []string{"Fruit", "Vegetables", "Nuts", "Grains", "Dairy", "Seafood", "Beef", "Pork", "Poultry", "Eggs", "Beans"}
|
||||||
|
|
||||||
|
for _, foodName := range foodsList{
|
||||||
|
|
||||||
|
foodAttributeName := foodName + "Rating"
|
||||||
|
|
||||||
|
foodNameLabel := getBoldLabelCentered(foodName)
|
||||||
|
|
||||||
|
optionsList := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}
|
||||||
|
|
||||||
|
foodRatingSelector := widget.NewSelect(optionsList, func(response string){
|
||||||
|
|
||||||
|
err := myLocalProfiles.SetProfileData("Mate", foodAttributeName, response)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
currentRatingExists, currentFoodRating, err := myLocalProfiles.GetProfileData("Mate", foodAttributeName)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRatingExists == true){
|
||||||
|
foodRatingSelector.Selected = currentFoodRating
|
||||||
|
} else {
|
||||||
|
foodRatingSelector.PlaceHolder = translate("Choose rating...")
|
||||||
|
}
|
||||||
|
|
||||||
|
noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){
|
||||||
|
|
||||||
|
err := myLocalProfiles.DeleteProfileData("Mate", foodAttributeName)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
foodSelectorsGrid.Add(foodNameLabel)
|
||||||
|
foodSelectorsGrid.Add(foodRatingSelector)
|
||||||
|
foodSelectorsGrid.Add(noResponseButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
foodsGrid := getContainerCentered(foodSelectorsGrid)
|
||||||
|
|
||||||
|
page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), foodsGrid)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setBuildMateProfilePage_Fame(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setBuildMateProfilePage_Fame(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered(translate("Build Mate Profile - Lifestyle"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered("Fame")
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Describe how famous you are on a scale of 1-10.")
|
||||||
|
description2 := getLabelCentered("1 = Least famous, 10 = Most famous.")
|
||||||
|
|
||||||
|
currentSelectionExists, currentFameSelection, err := myLocalProfiles.GetProfileData("Mate", "Fame")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myFameTitle := widget.NewLabel("My Fame:")
|
||||||
|
|
||||||
|
getMyFameLabel := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (currentSelectionExists == false){
|
||||||
|
|
||||||
|
myFameLabel := getBoldItalicLabel(translate("No Response"))
|
||||||
|
|
||||||
|
return myFameLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
labelText := currentFameSelection + "/10"
|
||||||
|
|
||||||
|
myFameLabel := getBoldLabel(labelText)
|
||||||
|
return myFameLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
myFameLabel := getMyFameLabel()
|
||||||
|
|
||||||
|
myFameRow := container.NewHBox(layout.NewSpacer(), myFameTitle, myFameLabel, layout.NewSpacer())
|
||||||
|
|
||||||
|
selectFameLabel := getItalicLabelCentered("Select your fame:")
|
||||||
|
|
||||||
|
optionsList := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}
|
||||||
|
fameSelector := widget.NewSelect(optionsList, func(newSelection string){
|
||||||
|
|
||||||
|
err := myLocalProfiles.SetProfileData("Mate", "Fame", newSelection)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (currentSelectionExists == true){
|
||||||
|
fameSelector.Selected = currentFameSelection
|
||||||
|
} else {
|
||||||
|
fameSelector.PlaceHolder = translate("Select one...")
|
||||||
|
}
|
||||||
|
|
||||||
|
fameSelectorCentered := getWidgetCentered(fameSelector)
|
||||||
|
|
||||||
|
noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){
|
||||||
|
|
||||||
|
err := myLocalProfiles.DeleteProfileData("Mate", "Fame")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), myFameRow, widget.NewSeparator(), selectFameLabel, fameSelectorCentered, noResponseButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBuildMateProfilePage_Drugs(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setBuildMateProfilePage_Drugs(window, previousPage)}
|
||||||
|
|
||||||
|
pageTitle := getPageTitleCentered(translate("Build Mate Profile - Lifestyle"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
pageSubtitle := getPageSubtitleCentered(translate("Drugs"))
|
||||||
|
|
||||||
|
description1 := getLabelCentered(translate("Describe how often you take each drug."))
|
||||||
|
description2 := getLabelCentered(translate("1 = Never, 10 = Most Often"))
|
||||||
|
description3 := getBoldLabelCentered(translate("If the drug is illegal where you live, do not respond with a non-1 response."))
|
||||||
|
|
||||||
|
drugsWarningButton := getWidgetCentered(widget.NewButtonWithIcon("Drugs Warning", theme.WarningIcon(), func(){
|
||||||
|
setDrugsWarningPage(window, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
getDrugsGrid := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
drugsGrid := container.NewGridWithColumns(3)
|
||||||
|
|
||||||
|
drugsList := []string{"Alcohol", "Tobacco", "Cannabis"}
|
||||||
|
|
||||||
|
for _, drugName := range drugsList{
|
||||||
|
|
||||||
|
drugAttributeName := drugName + "Frequency"
|
||||||
|
|
||||||
|
drugNameLabel := getBoldLabelCentered(translate(drugName))
|
||||||
|
|
||||||
|
optionsList := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}
|
||||||
|
|
||||||
|
handleSelectionFunction := func(response string){
|
||||||
|
err := myLocalProfiles.SetProfileData("Mate", drugAttributeName, response)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drugRatingSelector := widget.NewSelect(optionsList, handleSelectionFunction)
|
||||||
|
|
||||||
|
currentRatingExists, currentDrugRating, err := myLocalProfiles.GetProfileData("Mate", drugAttributeName)
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
if (currentRatingExists == true){
|
||||||
|
drugRatingSelector.Selected = currentDrugRating
|
||||||
|
} else {
|
||||||
|
drugRatingSelector.PlaceHolder = translate("Choose rating...")
|
||||||
|
}
|
||||||
|
|
||||||
|
noResponseButton := getWidgetCentered(widget.NewButtonWithIcon(translate("No Response"), theme.CancelIcon(), func(){
|
||||||
|
err := myLocalProfiles.DeleteProfileData("Mate", drugAttributeName)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
drugsGrid.Add(drugNameLabel)
|
||||||
|
drugsGrid.Add(drugRatingSelector)
|
||||||
|
drugsGrid.Add(noResponseButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
drugsGridCentered := getContainerCentered(drugsGrid)
|
||||||
|
|
||||||
|
return drugsGridCentered, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
drugsGrid, err := getDrugsGrid()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), description3, drugsWarningButton, widget.NewSeparator(), drugsGrid)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setDrugsWarningPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Drugs Warning")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered("Drugs Warning")
|
||||||
|
|
||||||
|
//TODO: Improve this.
|
||||||
|
// We have to include some kind of warning, considering the damage that drugs can cause.
|
||||||
|
// Not including any kind of warning would be normalizing drug use.
|
||||||
|
// It would be similar to asking: "How often do you self harm?" without saying self harm is bad.
|
||||||
|
// Everything harms and helps your body to some extent, including running and other forms of exercise, but drugs are harmful enough that they warrant a warning.
|
||||||
|
// Maybe information about the effects of each drug?
|
||||||
|
// -Tobacco = Cancer, etc...
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Drugs can damage your brain and body.")
|
||||||
|
description2 := getLabelCentered("Be wary when considering the use of drugs.")
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
1108
gui/buildProfileGui_Mental.go
Normal file
1108
gui/buildProfileGui_Mental.go
Normal file
File diff suppressed because it is too large
Load diff
3035
gui/buildProfileGui_Physical.go
Normal file
3035
gui/buildProfileGui_Physical.go
Normal file
File diff suppressed because it is too large
Load diff
3256
gui/chatGui.go
Normal file
3256
gui/chatGui.go
Normal file
File diff suppressed because it is too large
Load diff
1089
gui/contactsGui.go
Normal file
1089
gui/contactsGui.go
Normal file
File diff suppressed because it is too large
Load diff
921
gui/createIdentityGui.go
Normal file
921
gui/createIdentityGui.go
Normal file
|
@ -0,0 +1,921 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// createIdentityGui.go implements pages to choose/create new identity hashes
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/dialog"
|
||||||
|
import "fyne.io/fyne/v2/data/binding"
|
||||||
|
|
||||||
|
import "seekia/resources/wordLists"
|
||||||
|
|
||||||
|
import "seekia/internal/appMemory"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/identity"
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
import "seekia/internal/myIdentity"
|
||||||
|
import "seekia/internal/seedPhrase"
|
||||||
|
import "seekia/internal/mySeedPhrases"
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
import "strings"
|
||||||
|
import "math"
|
||||||
|
import "time"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func setChooseNewIdentityHashPage(window fyne.Window, myIdentityType string, previousPage func(), onceCompletePage func()){
|
||||||
|
|
||||||
|
currentPage := func(){ setChooseNewIdentityHashPage(window, myIdentityType, previousPage, onceCompletePage) }
|
||||||
|
|
||||||
|
title := getPageTitleCentered(translate("Choose " + myIdentityType + " Identity Hash"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
myIdentityExists, _, err := myIdentity.GetMyIdentityHash(myIdentityType)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (myIdentityExists == true){
|
||||||
|
// This should not occur. This page should only be called if no identity exists.
|
||||||
|
description1 := getLabelCentered("Your " + myIdentityType + " identity already exists.")
|
||||||
|
description2 := getLabelCentered("Delete your identity before creating a new one.")
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Choose your " + myIdentityType + " profile identity hash.")
|
||||||
|
description2 := getLabelCentered("Your identity hash cannot be changed once selected.")
|
||||||
|
|
||||||
|
createCustomDescription := getLabelCentered("Create a custom identity hash:")
|
||||||
|
|
||||||
|
submitPageFunction := func(newSeedPhrase string, prevPage func()){
|
||||||
|
setConfirmMyNewSeedPhrasePage(window, myIdentityType, newSeedPhrase, prevPage, onceCompletePage)
|
||||||
|
}
|
||||||
|
|
||||||
|
createCustomIdentityHashButton := getWidgetCentered(widget.NewButtonWithIcon("Create Custom", theme.SearchReplaceIcon(), func(){
|
||||||
|
setCreateCustomIdentityHashPage(window, myIdentityType, currentPage, submitPageFunction)
|
||||||
|
}))
|
||||||
|
|
||||||
|
selectRandomIdentityHashLabel := getLabelCentered("Select a random identity hash:")
|
||||||
|
|
||||||
|
identityHashLabelColumn := container.NewVBox(widget.NewSeparator())
|
||||||
|
chooseIdentityHashColumn := container.NewVBox(widget.NewSeparator())
|
||||||
|
|
||||||
|
//TODO: Fix to retrieve language from settings
|
||||||
|
myLanguage := "English"
|
||||||
|
|
||||||
|
for n := 0; n < 3; n++ {
|
||||||
|
|
||||||
|
newSeedPhrase, newSeedPhraseHash, err := seedPhrase.GetNewRandomSeedPhrase(myLanguage)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newIdentityHash, err := identity.GetIdentityHashFromSeedPhraseHash(newSeedPhraseHash, myIdentityType)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(newIdentityHash)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identityHashText := getBoldLabelCentered(newIdentityHashString)
|
||||||
|
|
||||||
|
selectButton := widget.NewButtonWithIcon("Select", theme.ConfirmIcon(), func(){
|
||||||
|
setConfirmMyNewSeedPhrasePage(window, myIdentityType, newSeedPhrase, currentPage, onceCompletePage)
|
||||||
|
})
|
||||||
|
|
||||||
|
identityHashLabelColumn.Add(identityHashText)
|
||||||
|
chooseIdentityHashColumn.Add(selectButton)
|
||||||
|
|
||||||
|
identityHashLabelColumn.Add(widget.NewSeparator())
|
||||||
|
chooseIdentityHashColumn.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
randomIdentityHashesGrid := container.NewHBox(layout.NewSpacer(), identityHashLabelColumn, chooseIdentityHashColumn, layout.NewSpacer())
|
||||||
|
|
||||||
|
refreshOptionsButton := getWidgetCentered(widget.NewButtonWithIcon("Refresh Choices", theme.ViewRefreshIcon(), currentPage))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), createCustomDescription, createCustomIdentityHashButton, widget.NewSeparator(), selectRandomIdentityHashLabel, randomIdentityHashesGrid, refreshOptionsButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConfirmMyNewSeedPhrasePage(window fyne.Window, myIdentityType string, newSeedPhrase string, previousPage func(), nextPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setConfirmMyNewSeedPhrasePage(window, myIdentityType, newSeedPhrase, previousPage, nextPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Confirm New Identity Hash")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description := getLabelCentered("Confirm new identity hash for " + myIdentityType + " profile?")
|
||||||
|
|
||||||
|
isValid := seedPhrase.VerifySeedPhrase(newSeedPhrase)
|
||||||
|
if (isValid == false){
|
||||||
|
setErrorEncounteredPage(window, errors.New("setConfirmMyNewSeedPhrasePage called with invalid seed phrase."), previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newSeedPhraseHash, err := seedPhrase.ConvertSeedPhraseToSeedPhraseHash(newSeedPhrase)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newIdentityHash, err := identity.GetIdentityHashFromSeedPhraseHash(newSeedPhraseHash, myIdentityType)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(newIdentityHash)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identityHashLabel := getContainerCentered(getWidgetBoxed(getBoldLabel(newIdentityHashString)))
|
||||||
|
|
||||||
|
confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){
|
||||||
|
|
||||||
|
exists, _, err := myIdentity.GetMyIdentityHash(myIdentityType)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (exists == true){
|
||||||
|
// This should not occur, as this page should only be shown if existing identity hash is not present.
|
||||||
|
setErrorEncounteredPage(window, errors.New("Trying to set seed phrase when existing identity is present."), previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mySeedPhrases.SetMySeedPhrase(myIdentityType, newSeedPhrase)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setEnterMyNewSeedPhrasePage(window, myIdentityType, newSeedPhrase, nextPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
seedPhraseDescription := getLabelCentered("Write down your seed phrase to backup your identity.")
|
||||||
|
|
||||||
|
seedPhraseLabel := widget.NewMultiLineEntry()
|
||||||
|
seedPhraseLabel.Wrapping = 3
|
||||||
|
seedPhraseLabel.SetText(newSeedPhrase)
|
||||||
|
seedPhraseLabel.OnChanged = func(_ string){
|
||||||
|
seedPhraseLabel.SetText(newSeedPhrase)
|
||||||
|
}
|
||||||
|
seedPhraseLabelBoxed := getWidgetBoxed(seedPhraseLabel)
|
||||||
|
widener := widget.NewLabel(" ")
|
||||||
|
seedPhraseLabelWidened := getContainerCentered(container.NewGridWithColumns(1, seedPhraseLabelBoxed, widener))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description, identityHashLabel, confirmButton, widget.NewSeparator(), seedPhraseDescription, seedPhraseLabelWidened)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setEnterMyNewSeedPhrasePage(window fyne.Window, myIdentityType string, newSeedPhrase string, nextPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Enter Seed Phrase")
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Enter your new " + myIdentityType + " seed phrase to confirm your wrote it down correctly.")
|
||||||
|
description2 := getLabelCentered("You can skip this step.")
|
||||||
|
|
||||||
|
descriptionsContainer := container.NewVBox(layout.NewSpacer(), description1, description2, layout.NewSpacer())
|
||||||
|
|
||||||
|
seedPhraseLabel := widget.NewMultiLineEntry()
|
||||||
|
seedPhraseLabel.Wrapping = 3
|
||||||
|
seedPhraseLabel.SetText(newSeedPhrase)
|
||||||
|
seedPhraseLabel.OnChanged = func(_ string){
|
||||||
|
seedPhraseLabel.SetText(newSeedPhrase)
|
||||||
|
}
|
||||||
|
seedPhraseLabelBoxed := getWidgetBoxed(seedPhraseLabel)
|
||||||
|
|
||||||
|
seedPhraseLabelWithDescriptions := getContainerCentered(container.NewGridWithColumns(1, descriptionsContainer, seedPhraseLabelBoxed))
|
||||||
|
|
||||||
|
isCorrectStatusBinding := binding.NewString()
|
||||||
|
isCorrectStatusBinding.Set("Incorrect")
|
||||||
|
|
||||||
|
isCorrectStatusLabel := widget.NewLabelWithData(isCorrectStatusBinding)
|
||||||
|
isCorrectStatusLabel.TextStyle = getFyneTextStyle_Bold()
|
||||||
|
|
||||||
|
isCorrectStatusIcon := widget.NewIcon(theme.CancelIcon())
|
||||||
|
statusLabel := widget.NewLabel("Status:")
|
||||||
|
|
||||||
|
isCorrectRow := container.NewHBox(layout.NewSpacer(), statusLabel, isCorrectStatusLabel, isCorrectStatusIcon, layout.NewSpacer())
|
||||||
|
|
||||||
|
skipOrExitButton := widget.NewButtonWithIcon("Skip", theme.MediaFastForwardIcon(), nextPage)
|
||||||
|
|
||||||
|
seedPhraseEntryOnChangedFunction := func(newText string){
|
||||||
|
if (newText != newSeedPhrase){
|
||||||
|
isCorrectStatusBinding.Set("Incorrect")
|
||||||
|
isCorrectStatusIcon.SetResource(theme.CancelIcon())
|
||||||
|
skipOrExitButton.SetText("Skip")
|
||||||
|
skipOrExitButton.SetIcon(theme.MediaFastForwardIcon())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isCorrectStatusBinding.Set("Correct")
|
||||||
|
isCorrectStatusIcon.SetResource(theme.ConfirmIcon())
|
||||||
|
skipOrExitButton.SetText("Exit")
|
||||||
|
skipOrExitButton.SetIcon(theme.ConfirmIcon())
|
||||||
|
}
|
||||||
|
|
||||||
|
seedPhraseEntry := widget.NewMultiLineEntry()
|
||||||
|
seedPhraseEntry.Wrapping = 3
|
||||||
|
seedPhraseEntry.OnChanged = seedPhraseEntryOnChangedFunction
|
||||||
|
seedPhraseEntryBoxed := getWidgetBoxed(seedPhraseEntry)
|
||||||
|
|
||||||
|
skipOrExitButtonCentered := getWidgetCentered(skipOrExitButton)
|
||||||
|
|
||||||
|
whitespace := " "
|
||||||
|
emptyLabel := widget.NewLabel("")
|
||||||
|
widener := widget.NewLabel(whitespace)
|
||||||
|
skipOrExitButtonWithWhitespace := container.NewVBox(skipOrExitButtonCentered, emptyLabel, widener)
|
||||||
|
|
||||||
|
seedPhraseEntryWithButton := getContainerCentered(container.NewGridWithColumns(1, seedPhraseEntryBoxed, skipOrExitButtonWithWhitespace))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, widget.NewSeparator(), seedPhraseLabelWithDescriptions, widget.NewSeparator(), isCorrectRow, seedPhraseEntryWithButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// submitPage function = func(newSeedPhrase string, previousPage func())
|
||||||
|
|
||||||
|
func setCreateCustomIdentityHashPage(window fyne.Window, identityType string, previousPage func(), submitPage func(string, func()) ){
|
||||||
|
|
||||||
|
setLoadingScreen(window, "Create Custom Identity Hash", "Loading...")
|
||||||
|
|
||||||
|
currentPage := func(){ setCreateCustomIdentityHashPage(window, identityType, previousPage, submitPage) }
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// -int64: Hashes per second
|
||||||
|
// -error
|
||||||
|
getIdentityHashGenerationSpeed := func()(int64, error){
|
||||||
|
|
||||||
|
//TODO: Fix to retrieve language from settings
|
||||||
|
currentLanguage := "English"
|
||||||
|
currentLanguageWordList, err := wordLists.GetWordListFromLanguage(currentLanguage)
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
|
||||||
|
// This counter will store the number of hashes we can compute in 1 second
|
||||||
|
var counterMutex sync.Mutex
|
||||||
|
counter := int64(0)
|
||||||
|
|
||||||
|
// This bool keeps track of if the identity hash benchmark is happening
|
||||||
|
var generateHashesStatusBoolMutex sync.RWMutex
|
||||||
|
generateHashesStatusBool := false
|
||||||
|
|
||||||
|
var errorEncounteredMutex sync.Mutex
|
||||||
|
var errorEncountered error
|
||||||
|
|
||||||
|
setErrorEncounteredFunction := func(inputError error){
|
||||||
|
|
||||||
|
errorEncounteredMutex.Lock()
|
||||||
|
errorEncountered = inputError
|
||||||
|
errorEncounteredMutex.Unlock()
|
||||||
|
|
||||||
|
generateHashesStatusBoolMutex.Lock()
|
||||||
|
generateHashesStatusBool = false
|
||||||
|
generateHashesStatusBoolMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
var identityHashGenerationWaitgroup sync.WaitGroup
|
||||||
|
|
||||||
|
generateIdentityHashesFunction := func(){
|
||||||
|
|
||||||
|
subCounter := int64(0)
|
||||||
|
|
||||||
|
for{
|
||||||
|
|
||||||
|
_, newSeedPhraseHash, err := seedPhrase.GetNewSeedPhraseFromWordList(currentLanguageWordList)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredFunction(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIdentityHashPrefix, err := identity.GetIdentityHash16CharacterPrefixFromSeedPhraseHash(newSeedPhraseHash)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredFunction(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have this check because we want to simulate how long it would take to check for a prefix
|
||||||
|
// The majority of the time is spent performing the hashing and ed25519 operations
|
||||||
|
strings.HasPrefix(currentIdentityHashPrefix, "seekia")
|
||||||
|
|
||||||
|
subCounter += 1
|
||||||
|
|
||||||
|
generateHashesStatusBoolMutex.RLock()
|
||||||
|
generatingStatus := generateHashesStatusBool
|
||||||
|
generateHashesStatusBoolMutex.RUnlock()
|
||||||
|
|
||||||
|
if (generatingStatus == false){
|
||||||
|
counterMutex.Lock()
|
||||||
|
counter += subCounter
|
||||||
|
counterMutex.Unlock()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
identityHashGenerationWaitgroup.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHashesStatusBool = true
|
||||||
|
|
||||||
|
identityHashGenerationWaitgroup.Add(2)
|
||||||
|
|
||||||
|
go generateIdentityHashesFunction()
|
||||||
|
go generateIdentityHashesFunction()
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
generateHashesStatusBoolMutex.Lock()
|
||||||
|
generateHashesStatusBool = false
|
||||||
|
generateHashesStatusBoolMutex.Unlock()
|
||||||
|
|
||||||
|
identityHashGenerationWaitgroup.Wait()
|
||||||
|
|
||||||
|
if (errorEncountered != nil){
|
||||||
|
return 0, errorEncountered
|
||||||
|
}
|
||||||
|
|
||||||
|
return counter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hashGenerationSpeed, err := getIdentityHashGenerationSpeed()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Create Custom Identity Hash")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description := getLabelCentered("Generate an identity hash with a custom prefix.")
|
||||||
|
description2 := getLabelCentered("The longer the prefix, the more difficult it will be to generate.")
|
||||||
|
|
||||||
|
enterDesiredPrefixText := getBoldLabelCentered(" Enter desired prefix: ")
|
||||||
|
|
||||||
|
customPrefixEntry := widget.NewEntry()
|
||||||
|
customPrefixEntry.SetPlaceHolder(translate("Enter desired prefix"))
|
||||||
|
|
||||||
|
estimatedTimeLabelBinding := binding.NewString()
|
||||||
|
estimatedTimeUnitsBinding := binding.NewString()
|
||||||
|
estimatedTimeLabel := widget.NewLabelWithData(estimatedTimeLabelBinding)
|
||||||
|
estimatedTimeLabel.TextStyle = getFyneTextStyle_Bold()
|
||||||
|
estimatedTimeLabelCentered := getWidgetCentered(estimatedTimeLabel)
|
||||||
|
estimatedTimeUnitsText := getWidgetCentered(widget.NewLabelWithData(estimatedTimeUnitsBinding))
|
||||||
|
|
||||||
|
customPrefixEntryOnChangedFunction := func(newPrefix string){
|
||||||
|
|
||||||
|
if (newPrefix == ""){
|
||||||
|
err = estimatedTimeLabelBinding.Set("")
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = estimatedTimeUnitsBinding.Set("")
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixLength := len(newPrefix)
|
||||||
|
if (prefixLength >= 13){
|
||||||
|
err = estimatedTimeLabelBinding.Set("Prefix is too long.")
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = estimatedTimeUnitsBinding.Set("")
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isBase32, invalidCharacter := encoding.VerifyStringContainsOnlyBase32Charset(newPrefix)
|
||||||
|
if (isBase32 == false){
|
||||||
|
err = estimatedTimeLabelBinding.Set("Invalid character detected: " + invalidCharacter)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = estimatedTimeUnitsBinding.Set(translate("Allowed characters") + ": abcdefghijklmnopqrstuvwxyz234567")
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfCharacters := len(newPrefix)
|
||||||
|
numberOfBits := float64(numberOfCharacters * 5)
|
||||||
|
numberOfRequiredHashes := math.Pow(2, numberOfBits)
|
||||||
|
|
||||||
|
estimatedTimeToGenerateInSeconds := numberOfRequiredHashes / float64(hashGenerationSpeed)
|
||||||
|
|
||||||
|
estimatedTimeUnitsTranslated, err := helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(int64(estimatedTimeToGenerateInSeconds), false)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = estimatedTimeLabelBinding.Set("Estimated time to generate:")
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = estimatedTimeUnitsBinding.Set(estimatedTimeUnitsTranslated)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customPrefixEntry.OnChanged = customPrefixEntryOnChangedFunction
|
||||||
|
|
||||||
|
startGeneratingHashesButton := getWidgetCentered(widget.NewButtonWithIcon("Start Generating", theme.ConfirmIcon(), func(){
|
||||||
|
prefix := customPrefixEntry.Text
|
||||||
|
if (prefix == "") {
|
||||||
|
dialogTitle := translate("No Prefix Provided")
|
||||||
|
dialogMessage := widget.NewLabel("You must enter a prefix to generate.")
|
||||||
|
dialogContent := container.NewVBox(dialogMessage)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixLength := len(prefix)
|
||||||
|
if (prefixLength >= 13){
|
||||||
|
dialogTitle := translate("Prefix Is Too Long")
|
||||||
|
dialogMessage := widget.NewLabel("Prefix is too long.")
|
||||||
|
dialogContent := container.NewVBox(dialogMessage)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid, offendingCharacter := encoding.VerifyStringContainsOnlyBase32Charset(prefix)
|
||||||
|
if (isValid == false){
|
||||||
|
dialogTitle := translate("Prefix Not Allowed")
|
||||||
|
dialogMessage := widget.NewLabel("Prefix contains unallowed character: " + offendingCharacter)
|
||||||
|
dialogContent := container.NewVBox(dialogMessage)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
estimatedTimeUnitsTranslated, err := estimatedTimeUnitsBinding.Get()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyList := make([]string, 0)
|
||||||
|
setRunCustomIdentityHashGenerationPage(window, identityType, prefix, estimatedTimeUnitsTranslated, hashGenerationSpeed, emptyList, currentPage, submitPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
customPrefixEntrySection := getContainerCentered(container.NewGridWithColumns(1, enterDesiredPrefixText, customPrefixEntry, startGeneratingHashesButton))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description, description2, widget.NewSeparator(), customPrefixEntrySection, widget.NewSeparator(), estimatedTimeLabelCentered, estimatedTimeUnitsText)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: Update the estimated time every ~10 seconds
|
||||||
|
//TODO: Figure out optimal number of goroutines to use for maximum speed
|
||||||
|
|
||||||
|
func setRunCustomIdentityHashGenerationPage(window fyne.Window, identityType string, desiredPrefix string, estimatedTimeRequired string, initialHashesPerSecond int64, seedPhrasesFoundList []string, previousPage func(), submitPage func(string, func()) ){
|
||||||
|
|
||||||
|
currentPage := func(){setRunCustomIdentityHashGenerationPage(window, identityType, desiredPrefix, estimatedTimeRequired, initialHashesPerSecond, seedPhrasesFoundList, previousPage, submitPage)}
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", "RunCustomIdentityHashGeneration")
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Generating Custom Identity Hash")
|
||||||
|
|
||||||
|
// We use this bool to keep track of the status of the identity hash generation goroutines
|
||||||
|
var generateHashesStatusBoolMutex sync.RWMutex
|
||||||
|
generateHashesStatusBool := false
|
||||||
|
|
||||||
|
setGenerateHashesStatusBool := func(newStatus bool){
|
||||||
|
|
||||||
|
generateHashesStatusBoolMutex.Lock()
|
||||||
|
generateHashesStatusBool = newStatus
|
||||||
|
generateHashesStatusBoolMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
getGenerateHashesStatusBool := func()bool{
|
||||||
|
generateHashesStatusBoolMutex.RLock()
|
||||||
|
currentStatus := generateHashesStatusBool
|
||||||
|
generateHashesStatusBoolMutex.RUnlock()
|
||||||
|
|
||||||
|
return currentStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// This waitgroup is used to manage the identity hash generation goroutines
|
||||||
|
var generateHashesWaitgroup sync.WaitGroup
|
||||||
|
|
||||||
|
numberOfFoundSeedPhrases := len(seedPhrasesFoundList)
|
||||||
|
|
||||||
|
backButtonFunction := func(){
|
||||||
|
|
||||||
|
// We stop generating hashes
|
||||||
|
setGenerateHashesStatusBool(false)
|
||||||
|
generateHashesWaitgroup.Wait()
|
||||||
|
|
||||||
|
if (numberOfFoundSeedPhrases == 0){
|
||||||
|
previousPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDialogCallbackFunction := func(response bool){
|
||||||
|
if (response == true){
|
||||||
|
|
||||||
|
previousPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (numberOfFoundSeedPhrases < 30){
|
||||||
|
// Not enough hashes found yet
|
||||||
|
// We restart hash generation
|
||||||
|
currentPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogTitle := translate("Go Back?")
|
||||||
|
dialogMessageA := getBoldLabelCentered("Confirm to go back?")
|
||||||
|
dialogMessageB := getLabelCentered("You will lose all of your generated identity hashes.")
|
||||||
|
dialogMessageC := getLabelCentered("Write down each identity hash's seed phrase to retain them.")
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC)
|
||||||
|
dialog.ShowCustomConfirm(dialogTitle, translate("Go Back"), translate("Cancel"), dialogContent, confirmDialogCallbackFunction, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(backButtonFunction)
|
||||||
|
|
||||||
|
getFoundHashesGrid := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
if (numberOfFoundSeedPhrases == 0){
|
||||||
|
emptyContainer := container.NewVBox()
|
||||||
|
return emptyContainer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
identityHashesColumn := container.NewVBox(widget.NewSeparator())
|
||||||
|
selectButtonsColumn := container.NewVBox(widget.NewSeparator())
|
||||||
|
|
||||||
|
for _, currentSeedPhrase := range seedPhrasesFoundList{
|
||||||
|
|
||||||
|
currentSeedPhraseHash, err := seedPhrase.ConvertSeedPhraseToSeedPhraseHash(currentSeedPhrase)
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
currentIdentityHash, err := identity.GetIdentityHashFromSeedPhraseHash(currentSeedPhraseHash, identityType)
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
currentIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(currentIdentityHash)
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
currentHashLabel := getBoldLabelCentered(currentIdentityHashString)
|
||||||
|
|
||||||
|
currentHashSelectButton := widget.NewButtonWithIcon("Select", theme.ConfirmIcon(), func(){
|
||||||
|
|
||||||
|
setGenerateHashesStatusBool(false)
|
||||||
|
generateHashesWaitgroup.Wait()
|
||||||
|
|
||||||
|
submitPage(currentSeedPhrase, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
identityHashesColumn.Add(currentHashLabel)
|
||||||
|
selectButtonsColumn.Add(currentHashSelectButton)
|
||||||
|
|
||||||
|
identityHashesColumn.Add(widget.NewSeparator())
|
||||||
|
selectButtonsColumn.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
foundHashesGrid := container.NewHBox(layout.NewSpacer(), identityHashesColumn, selectButtonsColumn, layout.NewSpacer())
|
||||||
|
|
||||||
|
return foundHashesGrid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
foundHashesGrid, err := getFoundHashesGrid()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numberOfFoundSeedPhrases >= 30){
|
||||||
|
|
||||||
|
// We are done generating identity hashes
|
||||||
|
|
||||||
|
doneDescription1 := getLabelCentered("Hash generation is complete.")
|
||||||
|
doneDescription2 := getBoldLabelCentered("Found 30 identity hashes.")
|
||||||
|
|
||||||
|
retryButton := getWidgetCentered(widget.NewButtonWithIcon("Retry", theme.ViewRefreshIcon(), func(){
|
||||||
|
emptyList := make([]string, 0)
|
||||||
|
setRunCustomIdentityHashGenerationPage(window, identityType, desiredPrefix, estimatedTimeRequired, initialHashesPerSecond, emptyList, previousPage, submitPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), doneDescription1, doneDescription2, widget.NewSeparator(), foundHashesGrid, retryButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getGenerationStatusString := func()string{
|
||||||
|
if (numberOfFoundSeedPhrases == 0){
|
||||||
|
return "Generating identity hash."
|
||||||
|
}
|
||||||
|
if (numberOfFoundSeedPhrases == 1){
|
||||||
|
return "Found 1 identity hash."
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfFoundSeedPhrasesString := helpers.ConvertIntToString(numberOfFoundSeedPhrases)
|
||||||
|
|
||||||
|
return "Found " + numberOfFoundSeedPhrasesString + " identity hashes."
|
||||||
|
}
|
||||||
|
|
||||||
|
generationStatusString := getGenerationStatusString()
|
||||||
|
|
||||||
|
generationStatusWithAnimationBinding := binding.NewString()
|
||||||
|
generationStatusLabel := widget.NewLabelWithData(generationStatusWithAnimationBinding)
|
||||||
|
generationStatusLabel.TextStyle = getFyneTextStyle_Bold()
|
||||||
|
generationStatusLabelCentered := getWidgetCentered(generationStatusLabel)
|
||||||
|
|
||||||
|
timeElapsedStringBinding := binding.NewString()
|
||||||
|
timeElapsedLabel := getWidgetCentered(widget.NewLabelWithData(timeElapsedStringBinding))
|
||||||
|
|
||||||
|
estimatedTimeRequiredLabelBinding := binding.NewString()
|
||||||
|
|
||||||
|
numberOfHashesPerSecondBinding := binding.NewString()
|
||||||
|
numberOfHashesPerSecondLabel := getWidgetCentered(widget.NewLabelWithData(numberOfHashesPerSecondBinding))
|
||||||
|
|
||||||
|
initializeBindingsFunction := func()error{
|
||||||
|
|
||||||
|
err = estimatedTimeRequiredLabelBinding.Set("Estimated time required: " + estimatedTimeRequired)
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = timeElapsedStringBinding.Set("Time elapsed: 0 Seconds")
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
hashesPerSecondString, err := helpers.ConvertFloat64ToRoundedStringWithTranslatedUnits(float64(initialHashesPerSecond))
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = numberOfHashesPerSecondBinding.Set("Hashes per second: " + hashesPerSecondString)
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = generationStatusWithAnimationBinding.Set(generationStatusString + "... ")
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = initializeBindingsFunction()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
estimatedTimeRequiredLabel := widget.NewLabelWithData(estimatedTimeRequiredLabelBinding)
|
||||||
|
|
||||||
|
estimatedTimeHelpDialogButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){
|
||||||
|
dialogTitle := translate("Estimated Time To Generate")
|
||||||
|
dialogMessageA := getLabelCentered("To generate a custom identity hash, many random identity hashes are created.")
|
||||||
|
dialogMessageB := getLabelCentered("Finding your desired prefix requires luck.")
|
||||||
|
dialogMessageC := getLabelCentered("The time to generate may vary significantly from the estimated time.")
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
estimatedTimeRequiredRow := container.NewHBox(layout.NewSpacer(), estimatedTimeRequiredLabel, estimatedTimeHelpDialogButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
//TODO: Retrieve language from user settings
|
||||||
|
currentLanguage := "English"
|
||||||
|
currentLanguageWordList, err := wordLists.GetWordListFromLanguage(currentLanguage)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), timeElapsedLabel, widget.NewSeparator(), estimatedTimeRequiredRow, widget.NewSeparator(), numberOfHashesPerSecondLabel, widget.NewSeparator(), generationStatusLabelCentered, foundHashesGrid)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
|
||||||
|
// Now we start hash generation
|
||||||
|
|
||||||
|
startHashGenerationFunction := func(){
|
||||||
|
|
||||||
|
// This variable stores the current number of hashes per second that are being generated
|
||||||
|
var currentHashesPerSecondMutex sync.RWMutex
|
||||||
|
currentHashesPerSecond := initialHashesPerSecond
|
||||||
|
|
||||||
|
// We use this mutex when adding new seed phrases to the list
|
||||||
|
var seedPhrasesFoundListMutex sync.Mutex
|
||||||
|
|
||||||
|
// We use this error to keep track of any errors
|
||||||
|
var encounteredErrorMutex sync.RWMutex
|
||||||
|
var encounteredError error
|
||||||
|
|
||||||
|
setErrorEncountered := func(newError error){
|
||||||
|
|
||||||
|
encounteredErrorMutex.Lock()
|
||||||
|
encounteredError = newError
|
||||||
|
encounteredErrorMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
generateIdentityHashesFunction := func(){
|
||||||
|
|
||||||
|
// We use this counter to count how many hashes we have generated
|
||||||
|
// We add this periodically to the hashesPerSecond counter
|
||||||
|
// We do this so we can avoid having to Rlock a mutex for each increment
|
||||||
|
subcounter := int64(0)
|
||||||
|
|
||||||
|
for{
|
||||||
|
generatingStatus := getGenerateHashesStatusBool()
|
||||||
|
if (generatingStatus == false){
|
||||||
|
generateHashesWaitgroup.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newSeedPhrase, newSeedPhraseHash, err := seedPhrase.GetNewSeedPhraseFromWordList(currentLanguageWordList)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncountered(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
newIdentityHashPrefix, err := identity.GetIdentityHash16CharacterPrefixFromSeedPhraseHash(newSeedPhraseHash)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncountered(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
subcounter += 1
|
||||||
|
|
||||||
|
if (subcounter >= 2000){
|
||||||
|
currentHashesPerSecondMutex.Lock()
|
||||||
|
currentHashesPerSecond += subcounter
|
||||||
|
currentHashesPerSecondMutex.Unlock()
|
||||||
|
subcounter = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPrefix := strings.HasPrefix(newIdentityHashPrefix, desiredPrefix)
|
||||||
|
if (hasPrefix == false){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We found a valid identity hash
|
||||||
|
|
||||||
|
seedPhrasesFoundListMutex.Lock()
|
||||||
|
|
||||||
|
if (len(seedPhrasesFoundList) >= 30){
|
||||||
|
// We have found enough identity hashes
|
||||||
|
seedPhrasesFoundListMutex.Unlock()
|
||||||
|
generateHashesWaitgroup.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seedPhrasesFoundList = append(seedPhrasesFoundList, newSeedPhrase)
|
||||||
|
seedPhrasesFoundListMutex.Unlock()
|
||||||
|
|
||||||
|
// We keep searching
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should only be reached if an error is encountered
|
||||||
|
generateHashesWaitgroup.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
setGenerateHashesStatusBool(true)
|
||||||
|
|
||||||
|
generateHashesWaitgroup.Add(2)
|
||||||
|
|
||||||
|
go generateIdentityHashesFunction()
|
||||||
|
go generateIdentityHashesFunction()
|
||||||
|
|
||||||
|
numberOfSecondsElapsed := 0
|
||||||
|
|
||||||
|
// This variable holds the status of the trailing dots after the generation progress text
|
||||||
|
progressAnimationString := "... "
|
||||||
|
|
||||||
|
for{
|
||||||
|
|
||||||
|
currentHashesPerSecondMutex.Lock()
|
||||||
|
// We reset the counter
|
||||||
|
currentHashesPerSecond = 0
|
||||||
|
currentHashesPerSecondMutex.Unlock()
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
generatingStatus := getGenerateHashesStatusBool()
|
||||||
|
if (generatingStatus == false){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encounteredErrorMutex.RLock()
|
||||||
|
currentEncounteredError := encounteredError
|
||||||
|
encounteredErrorMutex.RUnlock()
|
||||||
|
|
||||||
|
if (currentEncounteredError != nil){
|
||||||
|
// One of the goroutines experienced an error
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfSecondsElapsed += 1
|
||||||
|
|
||||||
|
timeElapsedUnitsString, err := helpers.ConvertUnixTimeDurationToUnitsTimeTranslated(int64(numberOfSecondsElapsed), true)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncountered(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
err = timeElapsedStringBinding.Set("Time elapsed: " + timeElapsedUnitsString)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncountered(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressAnimationString == ". "){
|
||||||
|
progressAnimationString = ".. "
|
||||||
|
} else if (progressAnimationString == ".. " ){
|
||||||
|
progressAnimationString = "... "
|
||||||
|
} else if (progressAnimationString == "... " ){
|
||||||
|
progressAnimationString = "...."
|
||||||
|
} else if (progressAnimationString == "...." ){
|
||||||
|
progressAnimationString = ". "
|
||||||
|
}
|
||||||
|
|
||||||
|
err = generationStatusWithAnimationBinding.Set(generationStatusString + progressAnimationString)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncountered(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, currentPage := appMemory.GetMemoryEntry("CurrentViewedPage")
|
||||||
|
if (exists == false || currentPage != "RunCustomIdentityHashGeneration"){
|
||||||
|
setGenerateHashesStatusBool(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seedPhrasesFoundListMutex.Lock()
|
||||||
|
newSeedPhrasesListLength := len(seedPhrasesFoundList)
|
||||||
|
seedPhrasesFoundListMutex.Unlock()
|
||||||
|
|
||||||
|
if (newSeedPhrasesListLength > numberOfFoundSeedPhrases){
|
||||||
|
// At least 1 new identity hash has been found.
|
||||||
|
// We will refresh the page.
|
||||||
|
setGenerateHashesStatusBool(false)
|
||||||
|
|
||||||
|
// We wait for all loops to exit
|
||||||
|
generateHashesWaitgroup.Wait()
|
||||||
|
|
||||||
|
setRunCustomIdentityHashGenerationPage(window, identityType, desiredPrefix, estimatedTimeRequired, currentHashesPerSecond, seedPhrasesFoundList, previousPage, submitPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHashesPerSecondMutex.Lock()
|
||||||
|
currentHashesPerSecondCopy := currentHashesPerSecond
|
||||||
|
currentHashesPerSecondMutex.Unlock()
|
||||||
|
|
||||||
|
currentHashesPerSecondString, err := helpers.ConvertFloat64ToRoundedStringWithTranslatedUnits(float64(currentHashesPerSecondCopy))
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncountered(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
err = numberOfHashesPerSecondBinding.Set("Hashes per second: " + currentHashesPerSecondString)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncountered(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should only be reached if an error is encountered
|
||||||
|
setGenerateHashesStatusBool(false)
|
||||||
|
|
||||||
|
// We wait for goroutines to exit
|
||||||
|
generateHashesWaitgroup.Wait()
|
||||||
|
|
||||||
|
setErrorEncounteredPage(window, errors.New("Something went wrong during hash generation: " + encounteredError.Error()), previousPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
go startHashGenerationFunction()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
550
gui/desireStatisticsGui.go
Normal file
550
gui/desireStatisticsGui.go
Normal file
|
@ -0,0 +1,550 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// desireStatisticsGui.go implements pages to view a user's desire statistics
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/data/binding"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
|
import "seekia/internal/appMemory"
|
||||||
|
import "seekia/internal/desires/mateDesires"
|
||||||
|
import "seekia/internal/desires/myDesireStatistics"
|
||||||
|
import "seekia/internal/desires/myLocalDesires"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/network/appNetworkType/getAppNetworkType"
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// This page is used to view the statistics of a single desire
|
||||||
|
func setViewMyMateDesireStatisticsPage(window fyne.Window, desireTitle string, desireName string, showChartsButton bool, barOrDonutChart string, attributeName string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setViewMyMateDesireStatisticsPage(window, desireTitle, desireName, showChartsButton, barOrDonutChart, attributeName, previousPage)}
|
||||||
|
|
||||||
|
// We use a page identifier to uniquely identify this page and detect if the user is still viewing the page
|
||||||
|
// If the user leaves this page before the getStatistics goroutine is complete, once it completes, the page will not refresh
|
||||||
|
|
||||||
|
pageIdentifier, err := helpers.GetNewRandomHexString(16)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", pageIdentifier)
|
||||||
|
|
||||||
|
checkIfPageHasChangedFunction := func()bool{
|
||||||
|
exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage")
|
||||||
|
if (exists == true && currentViewedPage == pageIdentifier){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("My Desire Statistics - " + desireTitle)
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
viewChartsFunction := func(){
|
||||||
|
|
||||||
|
if (showChartsButton == false){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (barOrDonutChart == "Bar"){
|
||||||
|
|
||||||
|
setViewUserAttributeStatisticsPage_BarChart(window, "Mate", attributeName, "Number Of Users", " Users", true, false, false, nil, false, nil, nil, currentPage)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (barOrDonutChart == "Donut"){
|
||||||
|
|
||||||
|
setViewUserAttributeStatisticsPage_DonutChart(window, "Mate", attributeName, false, false, nil, false, nil, nil, currentPage)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setErrorEncounteredPage(window, errors.New("setViewMyMateDesireStatisticsPage called with invalid barOrDonutChart: " + barOrDonutChart), currentPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Desire is disabled
|
||||||
|
// -bool: Filter All is disabled
|
||||||
|
// -bool: Require Response is disabled
|
||||||
|
// -error
|
||||||
|
getDesireIsDisabledStatus := func()(bool, bool, bool, error){
|
||||||
|
|
||||||
|
getFilterAllIsEnabledBool := func()(bool, error){
|
||||||
|
|
||||||
|
exists, currentFilterAllSetting, err := myLocalDesires.GetDesire(desireName + "_FilterAll")
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (exists == true && currentFilterAllSetting == "Yes"){
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filterAllIsEnabled, err := getFilterAllIsEnabledBool()
|
||||||
|
if (err != nil) { return false, false, false, err }
|
||||||
|
if (filterAllIsEnabled == true){
|
||||||
|
// Desire is enabled
|
||||||
|
return false, false, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Most desires allow the user to enable Require Response, which filters user who did not respond
|
||||||
|
allowsRequireResponse, _ := mateDesires.CheckIfDesireAllowsRequireResponse(desireName)
|
||||||
|
if (allowsRequireResponse == false){
|
||||||
|
// Desire is disabled, no users will be filtered
|
||||||
|
return true, true, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequireResponseIsEnabledBool := func()(bool, error){
|
||||||
|
|
||||||
|
exists, currentRequireResponse, err := myLocalDesires.GetDesire(desireName + "_RequireResponse")
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (exists == true && currentRequireResponse == "Yes"){
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
requireResponseIsEnabled, err := getRequireResponseIsEnabledBool()
|
||||||
|
if (err != nil) { return false, false, false, err }
|
||||||
|
if (requireResponseIsEnabled == true){
|
||||||
|
// Desire is enabled, because we require a response
|
||||||
|
return false, false, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterAll and RequireResponse are both disabled, thus the desire is disabled
|
||||||
|
|
||||||
|
return true, true, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
desireIsDisabled, filterAllIsDisabled, requireResponseIsDisabled, err := getDesireIsDisabledStatus()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (desireIsDisabled == true){
|
||||||
|
|
||||||
|
getDisabledOptionsString := func()string{
|
||||||
|
|
||||||
|
if (filterAllIsDisabled == true && requireResponseIsDisabled == true){
|
||||||
|
|
||||||
|
return "Filter All or Require Response"
|
||||||
|
}
|
||||||
|
// filterAllIsDisabled == true && requireResponseIsDisabled == false
|
||||||
|
|
||||||
|
return "Filter All"
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledOptionsString := getDisabledOptionsString()
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("You do not have " + disabledOptionsString + " enabled for this desire.")
|
||||||
|
description2 := getLabelCentered("This means that all users will pass the desire.")
|
||||||
|
|
||||||
|
description3Label := getLabelCentered("Without " + disabledOptionsString + " enabled, the desire only impacts user match scores.")
|
||||||
|
filterOptionsHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){
|
||||||
|
setDesireFilterOptionsExplainerPage(window, currentPage)
|
||||||
|
})
|
||||||
|
description3Row := container.NewHBox(layout.NewSpacer(), description3Label, filterOptionsHelpButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3Row)
|
||||||
|
|
||||||
|
if (showChartsButton == true){
|
||||||
|
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
description4 := getLabelCentered("Visualize the distribution of user responses on a chart.")
|
||||||
|
page.Add(description4)
|
||||||
|
|
||||||
|
viewChartButton := getWidgetCentered(widget.NewButtonWithIcon("View Chart", theme.InfoIcon(), viewChartsFunction))
|
||||||
|
page.Add(viewChartButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appNetworkType, err := getAppNetworkType.GetAppNetworkType()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allIdentitiesStatisticsBinding := binding.NewString()
|
||||||
|
matchStatisticsBinding := binding.NewString()
|
||||||
|
|
||||||
|
updateBindingsFunction := func(){
|
||||||
|
|
||||||
|
statisticsFound := false
|
||||||
|
|
||||||
|
updateBindingsWithProgressEllipsesFunction := func(){
|
||||||
|
|
||||||
|
secondsElapsed := 0
|
||||||
|
for {
|
||||||
|
if (statisticsFound == true){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pageHasChanged := checkIfPageHasChangedFunction()
|
||||||
|
if (pageHasChanged == true){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secondsElapsed % 3 == 0){
|
||||||
|
allIdentitiesStatisticsBinding.Set("Loading.")
|
||||||
|
matchStatisticsBinding.Set("Loading.")
|
||||||
|
} else if (secondsElapsed %3 == 1){
|
||||||
|
allIdentitiesStatisticsBinding.Set("Loading..")
|
||||||
|
matchStatisticsBinding.Set("Loading..")
|
||||||
|
} else {
|
||||||
|
allIdentitiesStatisticsBinding.Set("Loading...")
|
||||||
|
matchStatisticsBinding.Set("Loading...")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
secondsElapsed += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go updateBindingsWithProgressEllipsesFunction()
|
||||||
|
|
||||||
|
totalNumberOfMateIdentities, numberOfMateIdentitiesWhoPassDesire, percentageOfMateIdentitiesWhoPassDesire, numberOfMatches, numberOfMatchesWhoPassDesire, percentageOfMatchesWhoPassDesire, err := myDesireStatistics.GetMyDesireStatistics(desireName, appNetworkType)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statisticsFound = true
|
||||||
|
|
||||||
|
totalNumberOfMateIdentitiesString := helpers.ConvertInt64ToString(totalNumberOfMateIdentities)
|
||||||
|
numberOfMateIdentitiesWhoPassDesireString := helpers.ConvertInt64ToString(numberOfMateIdentitiesWhoPassDesire)
|
||||||
|
percentageOfMateIdentiesWhoPassDesireString := helpers.ConvertFloat64ToStringRounded(percentageOfMateIdentitiesWhoPassDesire, 1)
|
||||||
|
|
||||||
|
numberOfMatchesString := helpers.ConvertInt64ToString(numberOfMatches)
|
||||||
|
numberOfMatchesWhoPassDesireString := helpers.ConvertInt64ToString(numberOfMatchesWhoPassDesire)
|
||||||
|
percentageOfMatchesWhoPassDesireString := helpers.ConvertFloat64ToStringRounded(percentageOfMatchesWhoPassDesire, 1)
|
||||||
|
|
||||||
|
allIdentitiesStatisticsString := numberOfMateIdentitiesWhoPassDesireString + "/" + totalNumberOfMateIdentitiesString + " = " + percentageOfMateIdentiesWhoPassDesireString + "%"
|
||||||
|
|
||||||
|
matchesStatisticsString := numberOfMatchesWhoPassDesireString + "/" + numberOfMatchesString + " = " + percentageOfMatchesWhoPassDesireString + "%"
|
||||||
|
|
||||||
|
allIdentitiesStatisticsBinding.Set(allIdentitiesStatisticsString)
|
||||||
|
matchStatisticsBinding.Set(matchesStatisticsString)
|
||||||
|
}
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Below are your " + desireTitle + " desire statistics.")
|
||||||
|
description2 := getLabelCentered("They are the statistics of the users who pass your " + desireTitle + " desires.")
|
||||||
|
description3 := getLabelCentered("My Matches divides by the number of matches you would have without the desire.")
|
||||||
|
|
||||||
|
allUsersLabel := getBoldLabelCentered("All Users:")
|
||||||
|
|
||||||
|
allIdentitiesStatisticsLabel := getWidgetCentered(widget.NewLabelWithData(allIdentitiesStatisticsBinding))
|
||||||
|
|
||||||
|
myMatchesLabel := getBoldLabelCentered("My Matches:")
|
||||||
|
|
||||||
|
matchIdentitiesStatisticsLabel := getWidgetCentered(widget.NewLabelWithData(matchStatisticsBinding))
|
||||||
|
|
||||||
|
accuracyWarningButton := getWidgetCentered(widget.NewButtonWithIcon("Statistics Warning", theme.WarningIcon(), func(){
|
||||||
|
setMateDesireStatisticsWarningPage(window, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), allUsersLabel, allIdentitiesStatisticsLabel, myMatchesLabel, matchIdentitiesStatisticsLabel, widget.NewSeparator(), accuracyWarningButton)
|
||||||
|
|
||||||
|
if (showChartsButton == true){
|
||||||
|
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
description4 := getLabelCentered("Visualize the distribution of users on a chart.")
|
||||||
|
page.Add(description4)
|
||||||
|
|
||||||
|
viewChartButton := getWidgetCentered(widget.NewButtonWithIcon("View Chart", theme.InfoIcon(), viewChartsFunction))
|
||||||
|
page.Add(viewChartButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
|
||||||
|
go updateBindingsFunction()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func setViewAllMyDesireStatisticsPage(window fyne.Window, statisticsReady bool, numberOfMateIdentities int64, numberOfMatches int64, statisticsItemsList []myDesireStatistics.DesireStatisticsItem, previousPage func()){
|
||||||
|
|
||||||
|
pageIdentifier, err := helpers.GetNewRandomHexString(16)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", pageIdentifier)
|
||||||
|
|
||||||
|
checkIfPageHasChangedFunction := func()bool{
|
||||||
|
exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage")
|
||||||
|
if (exists == true && currentViewedPage == pageIdentifier){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage := func(){setViewAllMyDesireStatisticsPage(window, statisticsReady, numberOfMateIdentities, numberOfMatches, statisticsItemsList, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("My Mate Desire Statistics")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
if (statisticsReady == false){
|
||||||
|
|
||||||
|
appNetworkType, err := getAppNetworkType.GetAppNetworkType()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingTextBinding := binding.NewString()
|
||||||
|
loadingTextBinding.Set("Loading Statistics...")
|
||||||
|
|
||||||
|
loadingProgressBinding := binding.NewString()
|
||||||
|
|
||||||
|
loadingTextLabel := widget.NewLabelWithData(loadingTextBinding)
|
||||||
|
loadingTextLabel.TextStyle = getFyneTextStyle_Bold()
|
||||||
|
loadingTextLabelCentered := getWidgetCentered(loadingTextLabel)
|
||||||
|
|
||||||
|
loadingProgressLabel := getWidgetCentered(widget.NewLabelWithData(loadingProgressBinding))
|
||||||
|
|
||||||
|
calculateStatisticsAndRefreshPageFunction := func(){
|
||||||
|
|
||||||
|
progressIdentifier, _ := helpers.GetNewRandomHexString(16)
|
||||||
|
|
||||||
|
statisticsComplete := false
|
||||||
|
|
||||||
|
updateLoadingBindingFunction := func(){
|
||||||
|
|
||||||
|
startTime := time.Now().Unix()
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
secondsElapsed := currentTime - startTime
|
||||||
|
|
||||||
|
if (secondsElapsed % 3 == 0){
|
||||||
|
loadingTextBinding.Set("Loading Statistics.")
|
||||||
|
} else if (secondsElapsed % 3 == 1){
|
||||||
|
loadingTextBinding.Set("Loading Statistics..")
|
||||||
|
} else {
|
||||||
|
loadingTextBinding.Set("Loading Statistics...")
|
||||||
|
}
|
||||||
|
|
||||||
|
statusExists, newProgressStatus := appMemory.GetMemoryEntry(progressIdentifier)
|
||||||
|
if (statusExists == true){
|
||||||
|
loadingProgressBinding.Set(newProgressStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
pageHasChanged := checkIfPageHasChangedFunction()
|
||||||
|
if (pageHasChanged == true){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statisticsComplete == true){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go updateLoadingBindingFunction()
|
||||||
|
|
||||||
|
numberOfMateIdentities, numberOfMatches, myDesireStatisticsItemsList, err := myDesireStatistics.GetAllMyDesireStatistics(progressIdentifier, appNetworkType)
|
||||||
|
if (err != nil) {
|
||||||
|
statisticsComplete = true
|
||||||
|
pageHasChanged := checkIfPageHasChangedFunction()
|
||||||
|
if (pageHasChanged == false){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statisticsComplete = true
|
||||||
|
|
||||||
|
pageHasChanged := checkIfPageHasChangedFunction()
|
||||||
|
if (pageHasChanged == false){
|
||||||
|
setViewAllMyDesireStatisticsPage(window, true, numberOfMateIdentities, numberOfMatches, myDesireStatisticsItemsList, previousPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), loadingTextLabelCentered, loadingProgressLabel)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
|
||||||
|
go calculateStatisticsAndRefreshPageFunction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingScreen(window, "My Mate Desire Statistics", "Loading desire statistics...")
|
||||||
|
|
||||||
|
getStatisticsSection := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
// These are the desires for which FilterAll and Require Response is disabled
|
||||||
|
// These desires will not filter any matches. They only effect match score
|
||||||
|
disabledDesiresList := make([]string, 0)
|
||||||
|
|
||||||
|
desireNameLabel := getItalicLabelCentered("Desire Name")
|
||||||
|
allUsersLabel := getItalicLabelCentered("All Users")
|
||||||
|
myMatchesLabel := getItalicLabelCentered("My Matches")
|
||||||
|
|
||||||
|
desireTitlesColumn := container.NewVBox(desireNameLabel, widget.NewSeparator())
|
||||||
|
allUsersColumn := container.NewVBox(allUsersLabel, widget.NewSeparator())
|
||||||
|
myMatchesColumn := container.NewVBox(myMatchesLabel, widget.NewSeparator())
|
||||||
|
|
||||||
|
numberOfMateIdentitiesString := helpers.ConvertInt64ToString(numberOfMateIdentities)
|
||||||
|
numberOfMatchesString := helpers.ConvertInt64ToString(numberOfMatches)
|
||||||
|
|
||||||
|
for _, desireObject := range statisticsItemsList{
|
||||||
|
|
||||||
|
desireName := desireObject.DesireName
|
||||||
|
numberOfUsersWhoPassDesire := desireObject.NumberOfUsersWhoPassDesire
|
||||||
|
percentageOfUsersWhoPassDesire := desireObject.PercentageOfUsersWhoPassDesire
|
||||||
|
numberOfDesireExcludedMatches := desireObject.NumberOfDesireExcludedMatches
|
||||||
|
percentageOfDesireExcludedMatchesWhoPassDesire := desireObject.PercentageOfDesireExcludedMatchesWhoPassDesire
|
||||||
|
|
||||||
|
desireTitle, err := mateDesires.GetDesireTitleFromDesireName(desireName)
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
desireTitleTranslated := translate(desireTitle)
|
||||||
|
|
||||||
|
checkIfDesireIsDisabled := func()(bool, error){
|
||||||
|
|
||||||
|
exists, filterAllStatus, err := myLocalDesires.GetDesire(desireName + "_FilterAll")
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (exists == true && filterAllStatus == "Yes"){
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
requireResponseAllowed, _ := mateDesires.CheckIfDesireAllowsRequireResponse(desireName)
|
||||||
|
if (requireResponseAllowed == true){
|
||||||
|
|
||||||
|
exists, requireResponseStatus, err := myLocalDesires.GetDesire(desireName + "_RequireResponse")
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (exists == true && requireResponseStatus == "Yes"){
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desire is disabled
|
||||||
|
// We use 99.9 because floating point is not exact, we really mean 100%.
|
||||||
|
if (percentageOfUsersWhoPassDesire < 99.9){
|
||||||
|
return false, errors.New("Disabled desire does not have a 100% pass rate: " + desireName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
desireIsDisabled, err := checkIfDesireIsDisabled()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (desireIsDisabled == true){
|
||||||
|
disabledDesiresList = append(disabledDesiresList, desireTitleTranslated)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
desireTitleLabel := getBoldLabelCentered(desireTitleTranslated)
|
||||||
|
|
||||||
|
numberOfUsersWhoPassDesireString := helpers.ConvertInt64ToString(numberOfUsersWhoPassDesire)
|
||||||
|
percentageOfUsersWhoPassDesireString := helpers.ConvertFloat64ToStringRounded(percentageOfUsersWhoPassDesire, 1)
|
||||||
|
numberOfDesireExcludedMatchesString := helpers.ConvertInt64ToString(numberOfDesireExcludedMatches)
|
||||||
|
percentageOfDesireExcludedMatchesWhoPassDesireString := helpers.ConvertFloat64ToStringRounded(percentageOfDesireExcludedMatchesWhoPassDesire, 1)
|
||||||
|
|
||||||
|
allUsersLabel := getBoldLabelCentered(numberOfUsersWhoPassDesireString + "/" + numberOfMateIdentitiesString + " = " + percentageOfUsersWhoPassDesireString + "%")
|
||||||
|
|
||||||
|
matchesLabel := getBoldLabelCentered(numberOfMatchesString + "/" + numberOfDesireExcludedMatchesString + " = " + percentageOfDesireExcludedMatchesWhoPassDesireString + "%")
|
||||||
|
|
||||||
|
desireTitlesColumn.Add(desireTitleLabel)
|
||||||
|
allUsersColumn.Add(allUsersLabel)
|
||||||
|
myMatchesColumn.Add(matchesLabel)
|
||||||
|
|
||||||
|
desireTitlesColumn.Add(widget.NewSeparator())
|
||||||
|
allUsersColumn.Add(widget.NewSeparator())
|
||||||
|
myMatchesColumn.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfDisabledDesires := len(disabledDesiresList)
|
||||||
|
|
||||||
|
if (numberOfDisabledDesires == len(statisticsItemsList)){
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("All of your desires are disabled.")
|
||||||
|
description2 := getLabelCentered("None of your desires have Filter All or Require Response enabled.")
|
||||||
|
description3 := getLabelCentered("All users are your matches.")
|
||||||
|
|
||||||
|
viewDisabledDesiresButton := getWidgetCentered(widget.NewButtonWithIcon("View Disabled Desires", theme.VisibilityIcon(), func(){
|
||||||
|
setViewAllMyDisabledDesiresPage(window, disabledDesiresList, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
statisticsSection := container.NewVBox(description1, description2, description3, viewDisabledDesiresButton)
|
||||||
|
|
||||||
|
return statisticsSection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Below are the percentages of users who pass each of your desires.")
|
||||||
|
description2 := getLabelCentered("My Matches divides by the number of matches you would have without the desire.")
|
||||||
|
|
||||||
|
accuracyWarningButton := getWidgetCentered(widget.NewButtonWithIcon("Statistics Warning", theme.WarningIcon(), func(){
|
||||||
|
setMateDesireStatisticsWarningPage(window, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
statisticsGrid := container.NewHBox(layout.NewSpacer(), desireTitlesColumn, allUsersColumn, myMatchesColumn, layout.NewSpacer())
|
||||||
|
|
||||||
|
numberOfDisabledDesiresString := helpers.ConvertIntToString(numberOfDisabledDesires)
|
||||||
|
|
||||||
|
disabledDesiresDescription1 := getBoldLabelCentered("There are " + numberOfDisabledDesiresString + " desires with Filter All and Require Response disabled.")
|
||||||
|
disabledDesiresDescription2 := getLabelCentered("These desires do not filter any matches.")
|
||||||
|
|
||||||
|
viewDisabledDesiresButton := getWidgetCentered(widget.NewButtonWithIcon("View Disabled Desires", theme.VisibilityIcon(), func(){
|
||||||
|
setViewAllMyDisabledDesiresPage(window, disabledDesiresList, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
statisticsSection := container.NewVBox(description1, description2, widget.NewSeparator(), accuracyWarningButton, widget.NewSeparator(), statisticsGrid, widget.NewSeparator(), disabledDesiresDescription1, disabledDesiresDescription2, viewDisabledDesiresButton)
|
||||||
|
|
||||||
|
return statisticsSection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
statisticsSection, err := getStatisticsSection()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), statisticsSection)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setViewAllMyDisabledDesiresPage(window fyne.Window, disabledDesiresList []string, previousPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("My Mate Desire Statistics")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered("Disabled Desires")
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Below are your desires with Filter All and Require Response disabled.")
|
||||||
|
description2 := getLabelCentered("They will not filter any matches.")
|
||||||
|
description3 := getLabelCentered("They will only effect match scores.")
|
||||||
|
|
||||||
|
disabledDesiresGrid := container.NewGridWithColumns(2)
|
||||||
|
|
||||||
|
for _, desireTitle := range disabledDesiresList{
|
||||||
|
|
||||||
|
desireTitleLabel := getBoldLabelCentered(desireTitle)
|
||||||
|
|
||||||
|
disabledDesiresGrid.Add(desireTitleLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledDesiresGridCentered := getContainerCentered(disabledDesiresGrid)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), disabledDesiresGridCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
2267
gui/desiresGui_General.go
Normal file
2267
gui/desiresGui_General.go
Normal file
File diff suppressed because it is too large
Load diff
333
gui/desiresGui_Lifestyle.go
Normal file
333
gui/desiresGui_Lifestyle.go
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// desiresGui_Lifestyle.go implements pages to manage a user's lifestyle desires
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
|
||||||
|
import "seekia/resources/currencies"
|
||||||
|
|
||||||
|
import "seekia/internal/desires/myLocalDesires"
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseDesiresCategoryPage_Lifestyle(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresCategoryPage_Lifestyle(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered(translate("My Mate Desires - Lifestyle"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
wealthButton := widget.NewButton(translate("Wealth"), func(){
|
||||||
|
setChooseDesiresPage_Wealth(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
dietButton := widget.NewButton(translate("Diet"), func(){
|
||||||
|
setChooseDesiresPage_Diet(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
fameButton := widget.NewButton(translate("Fame"), func(){
|
||||||
|
setChooseDesiresPage_Fame(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
drugsButton := widget.NewButton(translate("Drugs"), func(){
|
||||||
|
setChooseDesiresPage_Drugs(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, wealthButton, dietButton, fameButton, drugsButton))
|
||||||
|
|
||||||
|
buttonsGridPadded := container.NewPadded(buttonsGrid)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsGridPadded)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseDesiresPage_Wealth(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_Wealth(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("My Mate Desires - Lifestyle")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered("Wealth")
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Choose your wealth desires.")
|
||||||
|
|
||||||
|
currentCurrencyLabel := getItalicLabel("Current Currency:")
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -string: Current currency code (Example: "USD")
|
||||||
|
// -error
|
||||||
|
getCurrentCurrencyCodeFunction := func()(string, error){
|
||||||
|
|
||||||
|
currentCurrencyExists, currencyCode, err := myLocalDesires.GetDesire("WealthCurrency")
|
||||||
|
if (err != nil){ return "", err }
|
||||||
|
|
||||||
|
if (currentCurrencyExists == false){
|
||||||
|
return "USD", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return currencyCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currencyCode, err := getCurrentCurrencyCodeFunction()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, currencySymbol, err := currencies.GetCurrencyInfoFromCurrencyCode(currencyCode)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
changeCurrencyButtonText := currencySymbol + currencyCode
|
||||||
|
changeCurrencyButton := widget.NewButton(changeCurrencyButtonText, func(){
|
||||||
|
|
||||||
|
onSelectFunction := func(newCurrencyCode string)error{
|
||||||
|
|
||||||
|
err := myLocalDesires.SetDesire("WealthCurrency", newCurrencyCode)
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
setChooseCurrencyPage(window, getCurrentCurrencyCodeFunction, onSelectFunction, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
currentCurrencyRow := container.NewHBox(layout.NewSpacer(), currentCurrencyLabel, changeCurrencyButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
desireEditor, err := getDesireEditor_Numeric(window, currentPage, "Wealth", 0, 9223372036854775807, currencyCode, 0, false, nil, nil, false, "", "", nil)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){
|
||||||
|
setViewMyMateDesireStatisticsPage(window, "Wealth", "Wealth", true, "Bar", "WealthInGold", currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, widget.NewSeparator(), currentCurrencyRow, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseDesiresPage_Diet(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_Diet(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("My Mate Desires - Lifestyle")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered("Diet")
|
||||||
|
|
||||||
|
description := getLabelCentered("Choose your diet desires.")
|
||||||
|
|
||||||
|
foodNamesList := []string{"Fruit", "Vegetables", "Nuts", "Grains", "Dairy", "Seafood", "Beef", "Pork", "Poultry", "Eggs", "Beans"}
|
||||||
|
|
||||||
|
buttonsGrid := container.NewGridWithColumns(2)
|
||||||
|
|
||||||
|
for _, foodName := range foodNamesList{
|
||||||
|
|
||||||
|
foodButton := widget.NewButton(foodName, func(){
|
||||||
|
setChooseDesiresPage_FoodRating(window, foodName, currentPage)
|
||||||
|
})
|
||||||
|
buttonsGrid.Add(foodButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonsGridCentered := getContainerCentered(buttonsGrid)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), buttonsGridCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setChooseDesiresPage_FoodRating(window fyne.Window, foodName string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_FoodRating(window, foodName, previousPage)}
|
||||||
|
|
||||||
|
pageTitle := getPageTitleCentered(translate("My Mate Desires - Lifestyle"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
pageSubtitle := getPageSubtitleCentered(translate(foodName))
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Choose your " + foodName + " rating desires.")
|
||||||
|
description2 := getLabelCentered("Users provide a food rating between 1 and 10.")
|
||||||
|
description3 := getLabelCentered("Choose the responses that you desire.")
|
||||||
|
description4 := getLabelCentered("1/10 = Strongly dislike, 10/10 = Strongly like")
|
||||||
|
|
||||||
|
optionTitlesList := []string{"1/10", "2/10", "3/10", "4/10", "5/10", "6/10", "7/10", "8/10", "9/10", "10/10"}
|
||||||
|
|
||||||
|
optionNamesMap := map[string][]string{
|
||||||
|
"1/10": []string{"1"},
|
||||||
|
"2/10": []string{"2"},
|
||||||
|
"3/10": []string{"3"},
|
||||||
|
"4/10": []string{"4"},
|
||||||
|
"5/10": []string{"5"},
|
||||||
|
"6/10": []string{"6"},
|
||||||
|
"7/10": []string{"7"},
|
||||||
|
"8/10": []string{"8"},
|
||||||
|
"9/10": []string{"9"},
|
||||||
|
"10/10": []string{"10"},
|
||||||
|
}
|
||||||
|
|
||||||
|
desireName := foodName + "Rating"
|
||||||
|
|
||||||
|
desireEditor, err := getDesireEditor_Choice(window, currentPage, desireName, optionTitlesList, optionNamesMap, false, true, 5)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
desireTitle := foodName + " Rating"
|
||||||
|
|
||||||
|
viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){
|
||||||
|
setViewMyMateDesireStatisticsPage(window, desireTitle, desireName, true, "Bar", desireName, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseDesiresPage_Fame(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_Fame(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered(translate("My Mate Desires - Lifestyle"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered(translate("Fame"))
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Choose your fame desires.")
|
||||||
|
description2 := getLabelCentered("Users describe their fame from 1 to 10.")
|
||||||
|
description3 := getLabelCentered("Choose the fame responses that you desire.")
|
||||||
|
description4 := getLabelCentered("1/10 = No fame, 10/10 = Most fame.")
|
||||||
|
|
||||||
|
optionTitlesList := []string{"1/10", "2/10", "3/10", "4/10", "5/10", "6/10", "7/10", "8/10", "9/10", "10/10"}
|
||||||
|
|
||||||
|
optionNamesMap := map[string][]string{
|
||||||
|
"1/10": []string{"1"},
|
||||||
|
"2/10": []string{"2"},
|
||||||
|
"3/10": []string{"3"},
|
||||||
|
"4/10": []string{"4"},
|
||||||
|
"5/10": []string{"5"},
|
||||||
|
"6/10": []string{"6"},
|
||||||
|
"7/10": []string{"7"},
|
||||||
|
"8/10": []string{"8"},
|
||||||
|
"9/10": []string{"9"},
|
||||||
|
"10/10": []string{"10"},
|
||||||
|
}
|
||||||
|
|
||||||
|
desireEditor, err := getDesireEditor_Choice(window, currentPage, "Fame", optionTitlesList, optionNamesMap, false, true, 5)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){
|
||||||
|
setViewMyMateDesireStatisticsPage(window, "Fame", "Fame", true, "Bar", "Fame", currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseDesiresPage_Drugs(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_Drugs(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("My Mate Desires - Lifestyle")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered("Drugs")
|
||||||
|
|
||||||
|
description := getLabelCentered("Choose your drug desires.")
|
||||||
|
|
||||||
|
drugNamesList := []string{"Alcohol", "Tobacco", "Cannabis"}
|
||||||
|
|
||||||
|
buttonsGrid := container.NewGridWithColumns(1)
|
||||||
|
|
||||||
|
for _, drugName := range drugNamesList{
|
||||||
|
|
||||||
|
drugButton := widget.NewButton(drugName, func(){
|
||||||
|
setChooseDesiresPage_DrugFrequency(window, drugName, currentPage)
|
||||||
|
})
|
||||||
|
buttonsGrid.Add(drugButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonsGridCentered := getContainerCentered(buttonsGrid)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), buttonsGridCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setChooseDesiresPage_DrugFrequency(window fyne.Window, drugName string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_DrugFrequency(window, drugName, previousPage)}
|
||||||
|
|
||||||
|
pageTitle := getPageTitleCentered(translate("My Mate Desires - Lifestyle"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
pageSubtitle := getPageSubtitleCentered(translate(drugName))
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Choose your " + drugName + " frequency desires.")
|
||||||
|
description2 := getLabelCentered("Users describe their drug use frequency from 1 to 10.")
|
||||||
|
description3 := getLabelCentered("Choose the user responses that you desire.")
|
||||||
|
description4 := getLabelCentered("1/10 = Never, 10/10 = Constantly")
|
||||||
|
|
||||||
|
desireName := drugName + "Frequency"
|
||||||
|
|
||||||
|
optionTitlesList := []string{"1/10", "2/10", "3/10", "4/10", "5/10", "6/10", "7/10", "8/10", "9/10", "10/10"}
|
||||||
|
|
||||||
|
optionNamesMap := map[string][]string{
|
||||||
|
"1/10": []string{"1"},
|
||||||
|
"2/10": []string{"2"},
|
||||||
|
"3/10": []string{"3"},
|
||||||
|
"4/10": []string{"4"},
|
||||||
|
"5/10": []string{"5"},
|
||||||
|
"6/10": []string{"6"},
|
||||||
|
"7/10": []string{"7"},
|
||||||
|
"8/10": []string{"8"},
|
||||||
|
"9/10": []string{"9"},
|
||||||
|
"10/10": []string{"10"},
|
||||||
|
}
|
||||||
|
|
||||||
|
desireEditor, err := getDesireEditor_Choice(window, currentPage, desireName, optionTitlesList, optionNamesMap, false, true, 5)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
desireTitle := drugName + " Frequency"
|
||||||
|
|
||||||
|
viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){
|
||||||
|
setViewMyMateDesireStatisticsPage(window, desireTitle, desireName, true, "Bar", desireName, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(pageTitle, backButton, widget.NewSeparator(), pageSubtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
544
gui/desiresGui_Mental.go
Normal file
544
gui/desiresGui_Mental.go
Normal file
|
@ -0,0 +1,544 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// desiresGui_Mental.go implements pages to manage a user's Mental mate desires
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
|
import "seekia/resources/worldLanguages"
|
||||||
|
|
||||||
|
import "seekia/internal/desires/myLocalDesires"
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
import "errors"
|
||||||
|
import "slices"
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseDesiresCategoryPage_Mental(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresCategoryPage_Mental(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered(translate("My Mate Desires - Mental"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
languageButton := widget.NewButton(translate("Language"), func(){
|
||||||
|
setChooseDesiresPage_Language(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
genderIdentityButton := widget.NewButton(translate("Gender Identity"), func(){
|
||||||
|
setChooseDesiresPage_GenderIdentity(window, currentPage)
|
||||||
|
})
|
||||||
|
petsButton := widget.NewButton(translate("Pets"), func(){
|
||||||
|
setChooseDesiresPage_Pets(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, languageButton, genderIdentityButton, petsButton))
|
||||||
|
|
||||||
|
buttonsGridPadded := container.NewPadded(buttonsGrid)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsGridPadded)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseDesiresPage_Language(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_Language(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("My Mate Desires - Mental")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered(translate("Language"))
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Choose the languages that you desire.")
|
||||||
|
description2 := getLabelCentered("This refers to the languages that a user can speak.")
|
||||||
|
description3 := getLabelCentered("For example, add English if you desire someone who can speak it.")
|
||||||
|
|
||||||
|
getSelectedLanguagesSection := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
getCurrentDesiredChoicesList := func()([]string, error){
|
||||||
|
|
||||||
|
currentChoicesListExists, currentChoicesList, err := myLocalDesires.GetDesire("Language")
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (currentChoicesListExists == false){
|
||||||
|
|
||||||
|
emptyList := make([]string, 0)
|
||||||
|
return emptyList, nil
|
||||||
|
}
|
||||||
|
//currentChoicesList is a "+" separated list of choices
|
||||||
|
// Each choice option is encoded in base64 (except for "Other")
|
||||||
|
currentDesiredChoicesList := strings.Split(currentChoicesList, "+")
|
||||||
|
|
||||||
|
return currentDesiredChoicesList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDesiredChoicesList, err := getCurrentDesiredChoicesList()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
addLanguageButton := getWidgetCentered(widget.NewButtonWithIcon("Add Language", theme.ContentAddIcon(), func(){
|
||||||
|
|
||||||
|
onSubmitFunction := func(_ int, newLanguagePrimaryName string)error{
|
||||||
|
|
||||||
|
newLanguageNameBase64 := encoding.EncodeBytesToBase64String([]byte(newLanguagePrimaryName))
|
||||||
|
|
||||||
|
newLanguagesList := helpers.AddItemToStringListAndAvoidDuplicate(currentDesiredChoicesList, newLanguageNameBase64)
|
||||||
|
|
||||||
|
newDesireString := strings.Join(newLanguagesList, "+")
|
||||||
|
|
||||||
|
err = myLocalDesires.SetDesire("Language", newDesireString)
|
||||||
|
if (err != nil){ return err }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
setChooseDesiresPage_AddLanguage(window, onSubmitFunction, currentPage, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
allowOtherCheck := widget.NewCheck("Allow Other", func(response bool){
|
||||||
|
|
||||||
|
getNewChoicesList := func()[]string{
|
||||||
|
|
||||||
|
if (response == false){
|
||||||
|
|
||||||
|
newList, _ := helpers.DeleteAllMatchingItemsFromStringList(currentDesiredChoicesList, "Other")
|
||||||
|
return newList
|
||||||
|
}
|
||||||
|
|
||||||
|
newList := helpers.AddItemToStringListAndAvoidDuplicate(currentDesiredChoicesList, "Other")
|
||||||
|
|
||||||
|
return newList
|
||||||
|
}
|
||||||
|
|
||||||
|
newList := getNewChoicesList()
|
||||||
|
|
||||||
|
if (len(newList) == 0){
|
||||||
|
err := myLocalDesires.DeleteDesire("Language")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newDesireValueString := strings.Join(newList, "+")
|
||||||
|
|
||||||
|
err := myLocalDesires.SetDesire("Language", newDesireValueString)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
allowOtherIsSelected := slices.Contains(currentDesiredChoicesList, "Other")
|
||||||
|
if (allowOtherIsSelected == true){
|
||||||
|
allowOtherCheck.Checked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
allowOtherHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){
|
||||||
|
setAllowOtherExplainerPage(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
allowOtherRow := container.NewHBox(layout.NewSpacer(), allowOtherCheck, allowOtherHelpButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
getAnyLanguageIsSelectedBool := func()bool{
|
||||||
|
|
||||||
|
if (len(currentDesiredChoicesList) == 0){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (len(currentDesiredChoicesList) == 1){
|
||||||
|
onlyValue := currentDesiredChoicesList[0]
|
||||||
|
if (onlyValue == "Other"){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
anyLanguageIsSelected := getAnyLanguageIsSelectedBool()
|
||||||
|
if (anyLanguageIsSelected == false){
|
||||||
|
|
||||||
|
noLanguagesLabel := getBoldLabelCentered("No languages selected.")
|
||||||
|
|
||||||
|
selectedLanguagesSection := container.NewVBox(noLanguagesLabel, addLanguageButton, widget.NewSeparator(), allowOtherRow)
|
||||||
|
|
||||||
|
return selectedLanguagesSection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
myDesiredLanguagesLabel := getItalicLabelCentered("My Desired Languages:")
|
||||||
|
|
||||||
|
languageNameColumn := container.NewVBox(widget.NewSeparator())
|
||||||
|
|
||||||
|
deleteButtonsColumn := container.NewVBox(widget.NewSeparator())
|
||||||
|
|
||||||
|
for _, languageNameBase64 := range currentDesiredChoicesList{
|
||||||
|
|
||||||
|
if (languageNameBase64 == "Other"){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
languageName, err := encoding.DecodeBase64StringToUnicodeString(languageNameBase64)
|
||||||
|
if (err != nil){
|
||||||
|
return nil, errors.New("My current language desire is malformed: Contains invalid language: " + languageNameBase64)
|
||||||
|
}
|
||||||
|
|
||||||
|
languageNameLabel := getBoldLabelCentered(translate(languageName))
|
||||||
|
|
||||||
|
deleteLanguageButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func(){
|
||||||
|
newDesiredLanguagesList, _ := helpers.DeleteAllMatchingItemsFromStringList(currentDesiredChoicesList, languageNameBase64)
|
||||||
|
|
||||||
|
if (len(newDesiredLanguagesList) == 0){
|
||||||
|
|
||||||
|
err := myLocalDesires.DeleteDesire("Language")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newDesireValueString := strings.Join(newDesiredLanguagesList, "+")
|
||||||
|
|
||||||
|
err := myLocalDesires.SetDesire("Language", newDesireValueString)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
languageNameColumn.Add(languageNameLabel)
|
||||||
|
deleteButtonsColumn.Add(deleteLanguageButton)
|
||||||
|
|
||||||
|
languageNameColumn.Add(widget.NewSeparator())
|
||||||
|
deleteButtonsColumn.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
languagesGrid := container.NewHBox(layout.NewSpacer(), languageNameColumn, deleteButtonsColumn, layout.NewSpacer())
|
||||||
|
|
||||||
|
//TODO: Add option for requiring a user to fulfill all of your chosen languages
|
||||||
|
// For example, a user could only search for people who speak both English and French
|
||||||
|
|
||||||
|
selectedLanguagesSection := container.NewVBox(addLanguageButton, widget.NewSeparator(), myDesiredLanguagesLabel, languagesGrid, widget.NewSeparator(), allowOtherRow)
|
||||||
|
|
||||||
|
return selectedLanguagesSection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedLanguagesSection, err := getSelectedLanguagesSection()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filterOptionsSection, err := getDesireEditorFilterOptionsSection(window, currentPage, "Language", true)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){
|
||||||
|
setViewMyMateDesireStatisticsPage(window, "Language", "Language", false, "", "", currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), selectedLanguagesSection, widget.NewSeparator(), filterOptionsSection, widget.NewSeparator(), viewStatisticsButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Inputs:
|
||||||
|
// -fyne.Window
|
||||||
|
// -func(int, string)error: The function to be executed when a choice is selected
|
||||||
|
// -Int: Language identifier
|
||||||
|
// -string: Language primary name
|
||||||
|
// -func(): Previous Page
|
||||||
|
// -func(): Page to go to after selection
|
||||||
|
func setChooseDesiresPage_AddLanguage(window fyne.Window, submitFunction func(int, string)error, previousPage func(), nextPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_AddLanguage(window, submitFunction, previousPage, nextPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("My Mate Desires - Mental")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered("Choose Language")
|
||||||
|
|
||||||
|
worldLanguageObjectsList, err := worldLanguages.GetWorldLanguageObjectsList()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// This list stores the translated language names
|
||||||
|
worldLanguageDescriptionsList := make([]string, 0, len(worldLanguageObjectsList))
|
||||||
|
|
||||||
|
// This map will store all of the language names
|
||||||
|
// If a language has multiple names, the first name is used
|
||||||
|
//Map Structure: Language Description -> Language Object
|
||||||
|
worldLanguageObjectsMap := make(map[string]worldLanguages.LanguageObject)
|
||||||
|
|
||||||
|
for _, languageObject := range worldLanguageObjectsList{
|
||||||
|
|
||||||
|
languageNamesList := languageObject.NamesList
|
||||||
|
|
||||||
|
languageDescription := helpers.TranslateAndJoinStringListItems(languageNamesList, "/")
|
||||||
|
|
||||||
|
worldLanguageDescriptionsList = append(worldLanguageDescriptionsList, languageDescription)
|
||||||
|
|
||||||
|
worldLanguageObjectsMap[languageDescription] = languageObject
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.SortStringListToUnicodeOrder(worldLanguageDescriptionsList)
|
||||||
|
|
||||||
|
selectFunction := func(itemIndex int){
|
||||||
|
|
||||||
|
languageDescription := worldLanguageDescriptionsList[itemIndex]
|
||||||
|
|
||||||
|
languageObject, exists := worldLanguageObjectsMap[languageDescription]
|
||||||
|
if (exists == false){
|
||||||
|
setErrorEncounteredPage(window, errors.New("worldLanguageNamesMap missing languageDescription"), currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
languageIdentifier := languageObject.Identifier
|
||||||
|
languagePrimaryName := languageObject.NamesList[0]
|
||||||
|
|
||||||
|
err := submitFunction(languageIdentifier, languagePrimaryName)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
languagesWidgetList, err := getFyneWidgetListFromStringList(worldLanguageDescriptionsList, selectFunction)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator())
|
||||||
|
|
||||||
|
page := container.NewBorder(header, nil, nil, nil, languagesWidgetList)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseDesiresPage_GenderIdentity(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_GenderIdentity(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("My Mate Desires - Mental")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered(translate("Gender Identity"))
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Choose the gender identities that you desire.")
|
||||||
|
description2 := getLabelCentered("This refers to the gender a user identifies as.")
|
||||||
|
description3 := getLabelCentered("For example, if you desire people who identify as Men, choose Man.")
|
||||||
|
|
||||||
|
optionTitlesList := []string{translate("Man"), translate("Woman")}
|
||||||
|
|
||||||
|
optionNamesMap := make(map[string][]string)
|
||||||
|
|
||||||
|
optionNamesMap[translate("Man")] = []string{"Man"}
|
||||||
|
optionNamesMap[translate("Woman")] = []string{"Woman"}
|
||||||
|
|
||||||
|
desireEditor, err := getDesireEditor_Choice(window, currentPage, "GenderIdentity", optionTitlesList, optionNamesMap, true, true, 1)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){
|
||||||
|
setViewMyMateDesireStatisticsPage(window, "Gender Identity", "GenderIdentity", true, "Donut", "GenderIdentity", currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseDesiresPage_Pets(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_Pets(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("My Mate Desires - Mental")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered(translate("Pets"))
|
||||||
|
|
||||||
|
description := getLabelCentered("Choose your pet desires.")
|
||||||
|
|
||||||
|
allPetsButton := widget.NewButton("All Pets", func(){
|
||||||
|
setChooseDesiresPage_AllPets(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
dogsButton := widget.NewButton("Dogs", func(){
|
||||||
|
setChooseDesiresPage_Dogs(window, currentPage)
|
||||||
|
})
|
||||||
|
catsButton := widget.NewButton("Cats", func(){
|
||||||
|
setChooseDesiresPage_Cats(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, allPetsButton, dogsButton, catsButton))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description, widget.NewSeparator(), buttonsGrid)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseDesiresPage_AllPets(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_AllPets(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered(translate("My Mate Desires - Mental"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered(translate("All Pets"))
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Choose your pets rating desires.")
|
||||||
|
description2 := getLabelCentered("Users describe how much they enjoy having pets from 1 to 10.")
|
||||||
|
description3 := getLabelCentered("Choose the user responses that you desire.")
|
||||||
|
description4 := getLabelCentered("1/10 = Strongly Dislike, 10/10 = Strongly Like.")
|
||||||
|
|
||||||
|
optionTitlesList := []string{"1/10", "2/10", "3/10", "4/10", "5/10", "6/10", "7/10", "8/10", "9/10", "10/10"}
|
||||||
|
|
||||||
|
optionNamesMap := map[string][]string{
|
||||||
|
"1/10": []string{"1"},
|
||||||
|
"2/10": []string{"2"},
|
||||||
|
"3/10": []string{"3"},
|
||||||
|
"4/10": []string{"4"},
|
||||||
|
"5/10": []string{"5"},
|
||||||
|
"6/10": []string{"6"},
|
||||||
|
"7/10": []string{"7"},
|
||||||
|
"8/10": []string{"8"},
|
||||||
|
"9/10": []string{"9"},
|
||||||
|
"10/10": []string{"10"},
|
||||||
|
}
|
||||||
|
|
||||||
|
desireEditor, err := getDesireEditor_Choice(window, currentPage, "PetsRating", optionTitlesList, optionNamesMap, false, true, 5)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){
|
||||||
|
setViewMyMateDesireStatisticsPage(window, "Pets Rating", "PetsRating", true, "Bar", "PetsRating", currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setChooseDesiresPage_Dogs(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_Dogs(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered(translate("My Mate Desires - Mental"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered(translate("Dogs"))
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Choose your dogs rating desires.")
|
||||||
|
description2 := getLabelCentered("Users describe how much they enjoy having dogs from 1 to 10.")
|
||||||
|
description3 := getLabelCentered("Choose the user responses that you desire.")
|
||||||
|
description4 := getLabelCentered("1/10 = Strongly Dislike, 10/10 = Strongly Like.")
|
||||||
|
|
||||||
|
optionTitlesList := []string{"1/10", "2/10", "3/10", "4/10", "5/10", "6/10", "7/10", "8/10", "9/10", "10/10"}
|
||||||
|
|
||||||
|
optionNamesMap := map[string][]string{
|
||||||
|
"1/10": []string{"1"},
|
||||||
|
"2/10": []string{"2"},
|
||||||
|
"3/10": []string{"3"},
|
||||||
|
"4/10": []string{"4"},
|
||||||
|
"5/10": []string{"5"},
|
||||||
|
"6/10": []string{"6"},
|
||||||
|
"7/10": []string{"7"},
|
||||||
|
"8/10": []string{"8"},
|
||||||
|
"9/10": []string{"9"},
|
||||||
|
"10/10": []string{"10"},
|
||||||
|
}
|
||||||
|
|
||||||
|
desireEditor, err := getDesireEditor_Choice(window, currentPage, "DogsRating", optionTitlesList, optionNamesMap, false, true, 5)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){
|
||||||
|
setViewMyMateDesireStatisticsPage(window, "Dogs Rating", "DogsRating", true, "Bar", "DogsRating", currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseDesiresPage_Cats(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseDesiresPage_Cats(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered(translate("My Mate Desires - Mental"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
subtitle := getPageSubtitleCentered(translate("Cats"))
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Choose your cats rating desires.")
|
||||||
|
description2 := getLabelCentered("Users describe how much they enjoy having cats from 1 to 10.")
|
||||||
|
description3 := getLabelCentered("Choose the user responses that you desire.")
|
||||||
|
description4 := getLabelCentered("1/10 = Strongly Dislike, 10/10 = Strongly Like.")
|
||||||
|
|
||||||
|
optionTitlesList := []string{"1/10", "2/10", "3/10", "4/10", "5/10", "6/10", "7/10", "8/10", "9/10", "10/10"}
|
||||||
|
|
||||||
|
optionNamesMap := make(map[string][]string)
|
||||||
|
optionNamesMap["1/10"] = []string{"1"}
|
||||||
|
optionNamesMap["2/10"] = []string{"2"}
|
||||||
|
optionNamesMap["3/10"] = []string{"3"}
|
||||||
|
optionNamesMap["4/10"] = []string{"4"}
|
||||||
|
optionNamesMap["5/10"] = []string{"5"}
|
||||||
|
optionNamesMap["6/10"] = []string{"6"}
|
||||||
|
optionNamesMap["7/10"] = []string{"7"}
|
||||||
|
optionNamesMap["8/10"] = []string{"8"}
|
||||||
|
optionNamesMap["9/10"] = []string{"9"}
|
||||||
|
optionNamesMap["10/10"] = []string{"10"}
|
||||||
|
|
||||||
|
desireEditor, err := getDesireEditor_Choice(window, currentPage, "CatsRating", optionTitlesList, optionNamesMap, false, true, 5)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewStatisticsButton := getWidgetCentered(widget.NewButtonWithIcon("View Statistics", theme.InfoIcon(), func(){
|
||||||
|
setViewMyMateDesireStatisticsPage(window, "Cats Rating", "CatsRating", true, "Bar", "CatsRating", currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), desireEditor, widget.NewSeparator(), viewStatisticsButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
2325
gui/desiresGui_Physical.go
Normal file
2325
gui/desiresGui_Physical.go
Normal file
File diff suppressed because it is too large
Load diff
438
gui/downloadGui.go
Normal file
438
gui/downloadGui.go
Normal file
|
@ -0,0 +1,438 @@
|
||||||
|
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// downloadGui.go implements pages to monitor manual downloads
|
||||||
|
// These are downloads whose status and progress the user is able to monitor
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/data/binding"
|
||||||
|
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/network/appNetworkType/getAppNetworkType"
|
||||||
|
import "seekia/internal/network/manualDownloads"
|
||||||
|
import "seekia/internal/appMemory"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
|
||||||
|
func setDownloadMissingUserProfilePage(window fyne.Window, profileAuthorIdentityHash [16]byte, getViewableOnly bool, stopDownloadOnPageExit bool, previousPage func(), nextPage func(), exitPage func()){
|
||||||
|
|
||||||
|
pageTitleText := "Download User Profile"
|
||||||
|
description1Text := "The user's profile is missing."
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Any hosts found
|
||||||
|
// -[][23]byte: Download identifiers list
|
||||||
|
// -error
|
||||||
|
startNewDownloadFunction := func()(bool, [][23]byte, error){
|
||||||
|
|
||||||
|
appNetworkType, err := getAppNetworkType.GetAppNetworkType()
|
||||||
|
if (err != nil) { return false, nil, err }
|
||||||
|
|
||||||
|
anyHostsFound, processIdentifier, err := manualDownloads.StartNewestUserProfileDownload(profileAuthorIdentityHash, appNetworkType, getViewableOnly, 1, 10)
|
||||||
|
if (err != nil) { return false, nil, err }
|
||||||
|
if (anyHostsFound == false){
|
||||||
|
emptyList := make([][23]byte, 0)
|
||||||
|
return false, emptyList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processIdentifiersList := [][23]byte{processIdentifier}
|
||||||
|
|
||||||
|
return true, processIdentifiersList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
anyHostsFound, processIdentifiersList, err := startNewDownloadFunction()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
noHostsFound := !anyHostsFound
|
||||||
|
|
||||||
|
setMonitorManualDownloadsPage(window, pageTitleText, "Profile", description1Text, noHostsFound, processIdentifiersList, startNewDownloadFunction, stopDownloadOnPageExit, 1, previousPage, nextPage, exitPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Inputs:
|
||||||
|
// -fyne.Window:
|
||||||
|
// -string
|
||||||
|
// -string
|
||||||
|
// -string
|
||||||
|
// -bool: True if no hosts are found
|
||||||
|
// -[][23]byte: List of process identifiers to monitor
|
||||||
|
// -func()(bool, [][23]byte, error): Function to retry all downloads
|
||||||
|
// -bool: Any hosts found
|
||||||
|
// -[][23]byte: New process identifiers of new download
|
||||||
|
// -error
|
||||||
|
// -bool: Stop download on page exit
|
||||||
|
// -int: expectedSuccessfulDownloadsPerProcess
|
||||||
|
// -func(): The page to visit if back button is pressed.
|
||||||
|
// -func(): The page to visit after a successful download
|
||||||
|
// -func()
|
||||||
|
func setMonitorManualDownloadsPage(window fyne.Window,
|
||||||
|
pageTitleText string,
|
||||||
|
downloadType string,
|
||||||
|
description1Text string,
|
||||||
|
noHostsFound bool,
|
||||||
|
processIdentifiersList [][23]byte,
|
||||||
|
startNewDownloadFunction func()(bool, [][23]byte, error),
|
||||||
|
stopDownloadOnPageExit bool,
|
||||||
|
expectedSuccessfulDownloadsPerProcess int,
|
||||||
|
previousPage func(),
|
||||||
|
afterCompletionPage func(),
|
||||||
|
exitPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setMonitorManualDownloadsPage(window, pageTitleText, downloadType, description1Text, noHostsFound, processIdentifiersList, startNewDownloadFunction, stopDownloadOnPageExit, expectedSuccessfulDownloadsPerProcess, previousPage, afterCompletionPage, exitPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered(pageTitleText)
|
||||||
|
|
||||||
|
previousPageWithCancel := func(){
|
||||||
|
if (stopDownloadOnPageExit == true && noHostsFound == false){
|
||||||
|
for _, processIdentifier := range processIdentifiersList{
|
||||||
|
manualDownloads.EndProcess(processIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appMemory.DeleteMemoryEntry("CurrentViewedPage")
|
||||||
|
previousPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadType != "Profile" && downloadType != "Message"){
|
||||||
|
//TODO: Replace downloadType with custom descriptions for missing hosts, download in progress, and content may not exist
|
||||||
|
setErrorEncounteredPage(window, errors.New("setMonitorProfileDownloadPage called with invalid downloadType: " + downloadType), previousPageWithCancel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPageWithCancel)
|
||||||
|
|
||||||
|
pageIdentifier, err := helpers.GetNewRandomHexString(16)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPageWithCancel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", pageIdentifier)
|
||||||
|
|
||||||
|
checkIfPageHasChangedFunction := func()bool{
|
||||||
|
exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage")
|
||||||
|
if (exists == true && currentViewedPage == pageIdentifier){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
description1 := getLabelCentered(description1Text)
|
||||||
|
|
||||||
|
retryFunction := func(){
|
||||||
|
newDownloadAnyHostsFound, newProcessIdentifiersList, err := startNewDownloadFunction()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPageWithCancel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newDownloadNoHostsFound := !newDownloadAnyHostsFound
|
||||||
|
|
||||||
|
setMonitorManualDownloadsPage(window, pageTitleText, downloadType, description1Text, newDownloadNoHostsFound, newProcessIdentifiersList, startNewDownloadFunction, stopDownloadOnPageExit, expectedSuccessfulDownloadsPerProcess, previousPage, afterCompletionPage, exitPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noHostsFound == true){
|
||||||
|
|
||||||
|
description2 := getLabelCentered(downloadType + " download failed because no available hosts were found.")
|
||||||
|
description3 := getLabelCentered("Please wait for Seekia to find more hosts.")
|
||||||
|
description4 := getLabelCentered("This should take less than 1 minute.")
|
||||||
|
|
||||||
|
retryingInSecondsBinding := binding.NewString()
|
||||||
|
|
||||||
|
startRetryCountdownFunction := func(){
|
||||||
|
|
||||||
|
secondsRemaining := 30
|
||||||
|
for {
|
||||||
|
|
||||||
|
secondsRemainingString := helpers.ConvertIntToString(secondsRemaining)
|
||||||
|
|
||||||
|
if (secondsRemaining != 1){
|
||||||
|
retryingInSecondsBinding.Set("Retrying in " + secondsRemainingString + " seconds...")
|
||||||
|
} else {
|
||||||
|
retryingInSecondsBinding.Set("Retrying in " + secondsRemainingString + " second...")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
secondsRemaining -= 1
|
||||||
|
|
||||||
|
if (secondsRemaining <= 0){
|
||||||
|
|
||||||
|
pageHasChanged := checkIfPageHasChangedFunction()
|
||||||
|
if (pageHasChanged == true){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
retryFunction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
retryingInLabel := widget.NewLabelWithData(retryingInSecondsBinding)
|
||||||
|
retryingInLabel.TextStyle = getFyneTextStyle_Bold()
|
||||||
|
retryingInLabelCentered := getWidgetCentered(retryingInLabel)
|
||||||
|
|
||||||
|
retryButton := getWidgetCentered(widget.NewButtonWithIcon("Retry", theme.ViewRefreshIcon(), retryFunction))
|
||||||
|
exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), exitPage))
|
||||||
|
|
||||||
|
manageConnectionDescription := getLabelCentered("Check if your internet connection is working below.")
|
||||||
|
|
||||||
|
manageConnectionButton := getWidgetCentered(widget.NewButtonWithIcon("Manage Connection", theme.DownloadIcon(), func(){
|
||||||
|
setManageNetworkConnectionPage(window, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), retryingInLabelCentered, widget.NewSeparator(), retryButton, exitButton, widget.NewSeparator(), manageConnectionDescription, manageConnectionButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
|
||||||
|
go startRetryCountdownFunction()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadProgressStatusBinding := binding.NewString()
|
||||||
|
downloadNumHostsContactedBinding := binding.NewString()
|
||||||
|
downloadProgressDetailsBinding := binding.NewString()
|
||||||
|
|
||||||
|
updateBindingsFunction := func(){
|
||||||
|
|
||||||
|
startTime := time.Now().Unix()
|
||||||
|
|
||||||
|
setDownloadProgressStatus := func(processComplete bool, newStatus string){
|
||||||
|
|
||||||
|
getProgressEllipsis := func()string{
|
||||||
|
if (processComplete == true){
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
secondsElapsed := currentTime - startTime
|
||||||
|
if (secondsElapsed % 3 == 0){
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
if (secondsElapsed % 3 == 1){
|
||||||
|
return ".."
|
||||||
|
}
|
||||||
|
return "..."
|
||||||
|
}
|
||||||
|
progressEllipsis := getProgressEllipsis()
|
||||||
|
downloadProgressStatusBinding.Set(newStatus + progressEllipsis)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map Structure: Process identifier -> Progress details
|
||||||
|
processLatestProgressDetailsMap := make(map[[23]byte]string)
|
||||||
|
// Map Structure: Process identifier -> Progress details last update time
|
||||||
|
processLatestUpdatedTimesMap := make(map[[23]byte]int64)
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
// We use the below function to combine the stats from all processes, if multiple processes exist.
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Download is complete
|
||||||
|
// -bool: Download ran out of hosts
|
||||||
|
// -int: Number of successful downloads
|
||||||
|
// -int: Number of hosts missing content
|
||||||
|
// -string: Download progress details
|
||||||
|
// -error
|
||||||
|
getDownloadInfo := func()(bool, bool, int, int, string, error){
|
||||||
|
|
||||||
|
allProcessesAreComplete := false
|
||||||
|
|
||||||
|
// This will sum all the successful downloads for all processes
|
||||||
|
numberOfSuccessfulDownloads := 0
|
||||||
|
|
||||||
|
// This will sum all of the number of hosts missing content for all processes
|
||||||
|
numberOfHostsMissingContent := 0
|
||||||
|
|
||||||
|
for _, processIdentifier := range processIdentifiersList{
|
||||||
|
|
||||||
|
processFound, processIsComplete, processEncounteredError, processError, processNumberOfSuccessfulDownloads, processNumberOfHostsMissingContent, processProgressDetails := manualDownloads.GetProcessInfo(processIdentifier)
|
||||||
|
if (processFound == false){
|
||||||
|
// This should not happen
|
||||||
|
return false, false, 0, 0, "", errors.New("Download process not found.")
|
||||||
|
}
|
||||||
|
if (processIsComplete == true && processEncounteredError == true){
|
||||||
|
return true, false, 0, 0, "", processError
|
||||||
|
}
|
||||||
|
if (processIsComplete == true && processNumberOfSuccessfulDownloads == 0){
|
||||||
|
// Process failed and ran out of hosts
|
||||||
|
return true, true, 0, 0, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfSuccessfulDownloads += processNumberOfSuccessfulDownloads
|
||||||
|
numberOfHostsMissingContent += processNumberOfHostsMissingContent
|
||||||
|
|
||||||
|
if (processIsComplete == false){
|
||||||
|
allProcessesAreComplete = false
|
||||||
|
}
|
||||||
|
|
||||||
|
latestDetails, exists := processLatestProgressDetailsMap[processIdentifier]
|
||||||
|
if (exists == true && latestDetails == processProgressDetails){
|
||||||
|
// This process has not had a new details update
|
||||||
|
// We can skip it
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// This process has new details
|
||||||
|
|
||||||
|
processLatestProgressDetailsMap[processIdentifier] = processProgressDetails
|
||||||
|
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
|
||||||
|
processLatestUpdatedTimesMap[processIdentifier] = currentTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we find the newest status to show to the user
|
||||||
|
|
||||||
|
newestProgressDetails := ""
|
||||||
|
newestDetailsUpdatedTime := int64(0)
|
||||||
|
|
||||||
|
for index, processIdentifier := range processIdentifiersList{
|
||||||
|
|
||||||
|
latestDetailsUpdateTime, exists := processLatestUpdatedTimesMap[processIdentifier]
|
||||||
|
if (exists == false){
|
||||||
|
// This should not happen
|
||||||
|
// All processes should be added to this map during our first iteration through processes details
|
||||||
|
return false, false, 0, 0, "", errors.New("processLatestUpdatedTimesMap missing process latest updated time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == 0 || latestDetailsUpdateTime > newestDetailsUpdatedTime){
|
||||||
|
|
||||||
|
latestProcessDetails, exists := processLatestProgressDetailsMap[processIdentifier]
|
||||||
|
if (exists == false){
|
||||||
|
// This should not happen
|
||||||
|
// All processes should be added to this map during our first iteration through processes details
|
||||||
|
return false, false, 0, 0, "", errors.New("processLatestProgressDetailsMap missing process details")
|
||||||
|
}
|
||||||
|
|
||||||
|
newestProgressDetails = latestProcessDetails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allProcessesAreComplete, false, numberOfSuccessfulDownloads, numberOfHostsMissingContent, newestProgressDetails, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadIsComplete, downloadRanOutOfHosts, numberOfSuccessfulDownloads, numberOfHostsMissingContent, downloadProgressDetails, err := getDownloadInfo()
|
||||||
|
if (err != nil){
|
||||||
|
setDownloadProgressStatus(true, "ERROR: " + err.Error())
|
||||||
|
downloadProgressDetailsBinding.Set("Report this error to the Seekia developers.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfHostsContacted := numberOfSuccessfulDownloads + numberOfHostsMissingContent
|
||||||
|
numberOfHostsContactedString := helpers.ConvertIntToString(numberOfHostsContacted)
|
||||||
|
|
||||||
|
if (downloadIsComplete == true){
|
||||||
|
|
||||||
|
// Download is complete.
|
||||||
|
|
||||||
|
pageHasChanged := checkIfPageHasChangedFunction()
|
||||||
|
if (pageHasChanged == true){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedSuccessfulDownloads := len(processIdentifiersList) * expectedSuccessfulDownloadsPerProcess
|
||||||
|
|
||||||
|
if (numberOfSuccessfulDownloads >= expectedSuccessfulDownloads){
|
||||||
|
// We downloaded the content we wanted. Nothing left to do.
|
||||||
|
afterCompletionPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download did not get required content.
|
||||||
|
// We will show user option to retry.
|
||||||
|
|
||||||
|
retryButton := getWidgetCentered(widget.NewButtonWithIcon("Retry", theme.ViewRefreshIcon(), retryFunction))
|
||||||
|
|
||||||
|
exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), exitPage))
|
||||||
|
|
||||||
|
checkConnectionDescription := getLabelCentered("Check if your internet connection is working below.")
|
||||||
|
|
||||||
|
manageConnectionButton := getWidgetCentered(widget.NewButtonWithIcon("Manage Connection", theme.DownloadIcon(), func(){
|
||||||
|
setManageNetworkConnectionPage(window, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (downloadRanOutOfHosts == true){
|
||||||
|
|
||||||
|
description2 := getLabelCentered("The download was unsuccessful.")
|
||||||
|
description3 := getLabelCentered("All the hosts we contacted failed to respond.")
|
||||||
|
description4 := getLabelCentered("You can exit or wait for more hosts to be found and retry.")
|
||||||
|
|
||||||
|
page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, description4, retryButton, exitButton, widget.NewSeparator(), checkConnectionDescription, manageConnectionButton)
|
||||||
|
setPageContent(page, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
description2 := getLabelCentered("The download was unsuccessful.")
|
||||||
|
description3 := getLabelCentered("We contacted " + numberOfHostsContactedString + " hosts.")
|
||||||
|
description4 := getLabelCentered("The " + downloadType + " may not exist on the network.")
|
||||||
|
description5 := getLabelCentered("Retry the download?")
|
||||||
|
|
||||||
|
page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, description4, description5, retryButton, exitButton, widget.NewSeparator(), checkConnectionDescription, manageConnectionButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download is not complete
|
||||||
|
|
||||||
|
numberOfSuccessfulDownloadsString := helpers.ConvertIntToString(numberOfSuccessfulDownloads)
|
||||||
|
|
||||||
|
progressProgressStatusString := "Downloaded from " + numberOfSuccessfulDownloadsString + " hosts."
|
||||||
|
setDownloadProgressStatus(downloadIsComplete, progressProgressStatusString)
|
||||||
|
downloadProgressDetailsBinding.Set(downloadProgressDetails)
|
||||||
|
|
||||||
|
downloadNumHostsContactedBinding.Set("Contacted " + numberOfHostsContactedString + " hosts.")
|
||||||
|
|
||||||
|
pageHasChanged := checkIfPageHasChangedFunction()
|
||||||
|
if (pageHasChanged == true){
|
||||||
|
|
||||||
|
if (stopDownloadOnPageExit == true){
|
||||||
|
for _, processIdentifier := range processIdentifiersList{
|
||||||
|
manualDownloads.EndProcess(processIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
description2 := getBoldLabelCentered("Seekia is attempting to download the " + downloadType)
|
||||||
|
description3 := getLabelCentered("The status of the download is displayed below.")
|
||||||
|
|
||||||
|
downloadProgressStatusLabel := widget.NewLabelWithData(downloadProgressStatusBinding)
|
||||||
|
downloadProgressStatusLabel.TextStyle = getFyneTextStyle_Bold()
|
||||||
|
downloadProgressStatusLabelCentered := getWidgetCentered(downloadProgressStatusLabel)
|
||||||
|
|
||||||
|
downloadNumHostsContactedLabel := getWidgetCentered(widget.NewLabelWithData(downloadNumHostsContactedBinding))
|
||||||
|
downloadProgressDetailsLabel := getWidgetCentered(widget.NewLabelWithData(downloadProgressDetailsBinding))
|
||||||
|
|
||||||
|
exitPageButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.MediaSkipNextIcon(), func(){
|
||||||
|
|
||||||
|
for _, processIdentifier := range processIdentifiersList{
|
||||||
|
|
||||||
|
manualDownloads.EndProcess(processIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
appMemory.DeleteMemoryEntry("CurrentViewedPage")
|
||||||
|
exitPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), downloadProgressStatusLabelCentered, downloadNumHostsContactedLabel, downloadProgressDetailsLabel, widget.NewSeparator(), exitPageButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
|
||||||
|
go updateBindingsFunction()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
1259
gui/gui.go
Normal file
1259
gui/gui.go
Normal file
File diff suppressed because it is too large
Load diff
1101
gui/helpGui.go
Normal file
1101
gui/helpGui.go
Normal file
File diff suppressed because it is too large
Load diff
780
gui/hostGui.go
Normal file
780
gui/hostGui.go
Normal file
|
@ -0,0 +1,780 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// hostGui.go implements pages for hosts to manage their server, view logs, view other hosts, and more.
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
|
||||||
|
import "seekia/internal/network/myIdentityBalance"
|
||||||
|
import "seekia/internal/myIdentity"
|
||||||
|
import "seekia/internal/mySettings"
|
||||||
|
import "seekia/internal/network/appNetworkType/getAppNetworkType"
|
||||||
|
import "seekia/internal/appMemory"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func setHostPage(window fyne.Window, previousPageExists bool, previousPage func()){
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", "Host")
|
||||||
|
|
||||||
|
currentPage := func(){setHostPage(window, previousPageExists, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Host")
|
||||||
|
|
||||||
|
page := container.NewVBox(title)
|
||||||
|
|
||||||
|
if (previousPageExists == true){
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
page.Add(backButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
description := getLabelCentered("Support the Seekia network by seeding content.")
|
||||||
|
page.Add(description)
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
settingsIcon, err := getFyneImageIcon("Settings")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, func(){setHomePage(window)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profileIcon, err := getFyneImageIcon("Profile")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, func(){setHomePage(window)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statsIcon, err := getFyneImageIcon("Stats")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, func(){setHomePage(window)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logsIcon, err := getFyneImageIcon("Choice")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, func(){setHomePage(window)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helpIcon, err := getFyneImageIcon("Info")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, func(){setHomePage(window)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hostsIcon, err := getFyneImageIcon("Host")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, func(){setHomePage(window)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helpButton := widget.NewButton("Help", func(){
|
||||||
|
setHostingHelpPage(window, currentPage)
|
||||||
|
})
|
||||||
|
helpButtonWithIcon := container.NewGridWithColumns(1, helpIcon, helpButton)
|
||||||
|
|
||||||
|
settingsButton := widget.NewButton("Settings", func(){
|
||||||
|
setHostSettingsPage(window, currentPage)
|
||||||
|
})
|
||||||
|
settingsButtonWithIcon := container.NewGridWithColumns(1, settingsIcon, settingsButton)
|
||||||
|
|
||||||
|
statsButton := widget.NewButton("Stats", func(){
|
||||||
|
setHostStatsPage(window, currentPage)
|
||||||
|
})
|
||||||
|
statsButtonWithIcon := container.NewGridWithColumns(1, statsIcon, statsButton)
|
||||||
|
|
||||||
|
logsButton := widget.NewButton("Logs", func(){
|
||||||
|
//TODO: Logs about hosting
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
logsButtonWithIcon := container.NewGridWithColumns(1, logsIcon, logsButton)
|
||||||
|
|
||||||
|
profileButton := widget.NewButton("Profile", func(){
|
||||||
|
setProfilePage(window, true, "Host", true, currentPage)
|
||||||
|
})
|
||||||
|
profileButtonWithIcon := container.NewGridWithColumns(1, profileIcon, profileButton)
|
||||||
|
|
||||||
|
hostsButton := widget.NewButton("Hosts", func(){
|
||||||
|
setViewHostsPage(window, currentPage)
|
||||||
|
})
|
||||||
|
hostsButtonWithIcon := container.NewGridWithColumns(1, hostsIcon, hostsButton)
|
||||||
|
|
||||||
|
buttonsRow := getContainerCentered(container.NewGridWithRows(1, helpButtonWithIcon, profileButtonWithIcon, settingsButtonWithIcon, hostsButtonWithIcon, statsButtonWithIcon, logsButtonWithIcon))
|
||||||
|
|
||||||
|
page.Add(buttonsRow)
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
getHostStatusContainer := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
exists, hostModeStatus, err := mySettings.GetSetting("HostModeOnOffStatus")
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
if (exists == false || hostModeStatus == "Off"){
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("You must enable host mode.")
|
||||||
|
description2 := getLabelCentered("Enable it on the Settings page.")
|
||||||
|
|
||||||
|
content := container.NewVBox(description1, description2)
|
||||||
|
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
identityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Host")
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (identityExists == false){
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Your Host identity does not exist.")
|
||||||
|
description2 := getLabelCentered("You must create it to be a host.")
|
||||||
|
|
||||||
|
createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create Identity", theme.NavigateNextIcon(), func(){
|
||||||
|
setChooseNewIdentityHashPage(window, "Host", currentPage, currentPage)
|
||||||
|
}))
|
||||||
|
content := container.NewVBox(description1, description2, createIdentityButton)
|
||||||
|
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appNetworkType, err := getAppNetworkType.GetAppNetworkType()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
identityExists, identityIsActivated, identityIsFunded, _, _, err := myIdentityBalance.GetMyIdentityBalanceStatus(myIdentityHash, appNetworkType)
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
if (identityExists == false){
|
||||||
|
return nil, errors.New("My host identity not found after being found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identityIsActivated == false || identityIsFunded == false){
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Your host identity is not funded.")
|
||||||
|
description2 := getLabelCentered("Fund your Host identity below.")
|
||||||
|
|
||||||
|
fundIdentityButton := getWidgetCentered(widget.NewButton("Fund Identity", func(){
|
||||||
|
setIncreaseMyIdentityBalancePage(window, "Host", 30, currentPage)
|
||||||
|
}))
|
||||||
|
content := container.NewVBox(description1, description2, fundIdentityButton)
|
||||||
|
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Under Construction")
|
||||||
|
description2 := getLabelCentered("Seekia is not able to host content yet.")
|
||||||
|
|
||||||
|
/*
|
||||||
|
hostServerStatus := peerServer.GetPeerServerOnOffStatus()
|
||||||
|
hostServerStatusLabel := widget.NewLabel("Host Server Status:")
|
||||||
|
hostServerStatusText := getBoldLabel(hostServerStatus)
|
||||||
|
|
||||||
|
hostServerStatusRow := getContainerCentered(container.NewHBox(hostServerStatusLabel, hostServerStatusText))
|
||||||
|
|
||||||
|
getServerStartStopButton := func() fyne.Widget{
|
||||||
|
if (hostServerStatus == "Off"){
|
||||||
|
startButton := widget.NewButtonWithIcon(translate("Start Server"), theme.MediaPlayIcon(), func(){
|
||||||
|
err := peerServer.StartPeerServer()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, func(){setHomePage(window)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
return startButton
|
||||||
|
}
|
||||||
|
|
||||||
|
stopButton := widget.NewButtonWithIcon(translate("Stop Server"), theme.MediaStopIcon(), func(){
|
||||||
|
peerServer.StopPeerServer()
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return stopButton
|
||||||
|
}
|
||||||
|
|
||||||
|
startStopButton := getServerStartStopButton()
|
||||||
|
*/
|
||||||
|
|
||||||
|
result := container.NewVBox(description1, description2)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hostStatusContainer, err := getHostStatusContainer()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, func(){setHomePage(window)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page.Add(hostStatusContainer)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHostStatsPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
setLoadingScreen(window, "Host Stats", "Loading Host Stats...")
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Host Server Stats")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
//TODO: Retrieve all variables below from new package to keep track of hosting history
|
||||||
|
|
||||||
|
totalSeededAmountTitle := widget.NewLabel("Total Seeded Amount:")
|
||||||
|
totalSeededAmountLabel := getBoldLabel("0 Gigabytes")
|
||||||
|
totalSeededAmountRow := container.NewHBox(layout.NewSpacer(), totalSeededAmountTitle, totalSeededAmountLabel, layout.NewSpacer())
|
||||||
|
|
||||||
|
numberOfSeededProfilesTitle := widget.NewLabel("Seeded Profiles:")
|
||||||
|
numberOfSeededProfilesLabel := getBoldLabel("0")
|
||||||
|
numberOfSeededProfilesRow := container.NewHBox(layout.NewSpacer(), numberOfSeededProfilesTitle, numberOfSeededProfilesLabel, layout.NewSpacer())
|
||||||
|
|
||||||
|
numberOfSeededMessagesTitle := widget.NewLabel("Seeded Messages:")
|
||||||
|
numberOfSeededMessagesLabel := getBoldLabel("0")
|
||||||
|
numberOfSeededMessagesRow := container.NewHBox(layout.NewSpacer(), numberOfSeededMessagesTitle, numberOfSeededMessagesLabel, layout.NewSpacer())
|
||||||
|
|
||||||
|
numberOfSeededReviewsTitle := widget.NewLabel("Seeded Reviews:")
|
||||||
|
numberOfSeededReviewsLabel := getBoldLabel("0")
|
||||||
|
numberOfSeededReviewsRow := container.NewHBox(layout.NewSpacer(), numberOfSeededReviewsTitle, numberOfSeededReviewsLabel, layout.NewSpacer())
|
||||||
|
|
||||||
|
numberOfSeededReportsTitle := widget.NewLabel("Seeded Reports:")
|
||||||
|
numberOfSeededReportsLabel := getBoldLabel("0")
|
||||||
|
numberOfSeededReportsRow := container.NewHBox(layout.NewSpacer(), numberOfSeededReportsTitle, numberOfSeededReportsLabel, layout.NewSpacer())
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), totalSeededAmountRow, widget.NewSeparator(), numberOfSeededProfilesRow, numberOfSeededMessagesRow, numberOfSeededReviewsRow, numberOfSeededReportsRow)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHostSettingsPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setHostSettingsPage(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Host Settings")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description := getLabelCentered("Manage your host settings.")
|
||||||
|
|
||||||
|
getSettingsGrid := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
settingTitleColumn := container.NewVBox()
|
||||||
|
settingStatusColumn := container.NewVBox()
|
||||||
|
manageSettingButtonsColumn := container.NewVBox()
|
||||||
|
|
||||||
|
addSettingRow := func(addSeparator bool, settingTitle string, settingName string, manageSettingPage func())error{
|
||||||
|
|
||||||
|
settingTitleLabel := getBoldLabelCentered(settingTitle)
|
||||||
|
|
||||||
|
getSettingOnOffStatus := func()(string, error){
|
||||||
|
|
||||||
|
exists, hostModeStatus, err := mySettings.GetSetting(settingName)
|
||||||
|
if (err != nil){ return "", err }
|
||||||
|
if (exists == false){
|
||||||
|
return "Off", nil
|
||||||
|
}
|
||||||
|
return hostModeStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
settingStatus, err := getSettingOnOffStatus()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
settingStatusLabel := getBoldLabelCentered(settingStatus)
|
||||||
|
|
||||||
|
manageSettingButton := widget.NewButtonWithIcon("Manage", theme.SettingsIcon(), manageSettingPage)
|
||||||
|
|
||||||
|
settingTitleColumn.Add(settingTitleLabel)
|
||||||
|
settingStatusColumn.Add(settingStatusLabel)
|
||||||
|
manageSettingButtonsColumn.Add(manageSettingButton)
|
||||||
|
|
||||||
|
if (addSeparator == true){
|
||||||
|
|
||||||
|
settingTitleColumn.Add(widget.NewSeparator())
|
||||||
|
settingStatusColumn.Add(widget.NewSeparator())
|
||||||
|
manageSettingButtonsColumn.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := addSettingRow(true, "Host Mode", "HostModeOnOffStatus", func(){
|
||||||
|
setHostSettingsPage_HostMode(window, currentPage)
|
||||||
|
})
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
err = addSettingRow(true, "Host Unviewable Profiles", "HostUnviewableProfilesOnOffStatus", func(){
|
||||||
|
setHostSettingsPage_HostUnviewableProfiles(window, currentPage)
|
||||||
|
})
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
err = addSettingRow(true, "Host Messages", "HostMessagesOnOffStatus", func(){
|
||||||
|
setHostSettingsPage_HostMessages(window, currentPage)
|
||||||
|
})
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
err = addSettingRow(false, "Host Over Clearnet", "HostOverClearnetOnOffStatus", func(){
|
||||||
|
setHostSettingsPage_HostOverClearnet(window, currentPage)
|
||||||
|
})
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
settingsGrid := container.NewHBox(layout.NewSpacer(), settingTitleColumn, settingStatusColumn, manageSettingButtonsColumn, layout.NewSpacer())
|
||||||
|
|
||||||
|
return settingsGrid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsGrid, err := getSettingsGrid()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manageStorageButton := getWidgetCentered(widget.NewButtonWithIcon("Manage Storage", theme.DocumentSaveIcon(), func(){
|
||||||
|
setManageStoragePage(window, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), settingsGrid, widget.NewSeparator(), manageStorageButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setHostSettingsPage_HostMode(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setHostSettingsPage_HostMode(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Settings - Host Mode")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Enable this mode to start hosting Seekia content.")
|
||||||
|
description2 := getLabelCentered("You can choose what type of content you want to host on the Host Settings page")
|
||||||
|
description3 := getLabelCentered("You will only host over the Tor anonymity network unless you enable clearnet hosting.")
|
||||||
|
description4 := getLabelCentered("Your host identity will be tied to everything you host.")
|
||||||
|
description5 := getLabelCentered("Create and fund a new host identity if you want to start fresh.")
|
||||||
|
|
||||||
|
getCurrentHostModeStatus := func()(string, error){
|
||||||
|
|
||||||
|
exists, hostModeStatus, err := mySettings.GetSetting("HostModeOnOffStatus")
|
||||||
|
if (err != nil){ return "", err }
|
||||||
|
if (exists == false){
|
||||||
|
return "Off", nil
|
||||||
|
}
|
||||||
|
if (hostModeStatus != "On" && hostModeStatus != "Off"){
|
||||||
|
return "", errors.New("Invalid host mode status:" + hostModeStatus)
|
||||||
|
}
|
||||||
|
return hostModeStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hostModeStatus, err := getCurrentHostModeStatus()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStatusLabel := widget.NewLabel("Current Status:")
|
||||||
|
currentStatusText := getBoldLabel(hostModeStatus)
|
||||||
|
currentStatusRow := container.NewHBox(layout.NewSpacer(), currentStatusLabel, currentStatusText, layout.NewSpacer())
|
||||||
|
|
||||||
|
getEnableDisableButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (hostModeStatus == "On"){
|
||||||
|
disableButton := widget.NewButton("Disable", func(){
|
||||||
|
err := mySettings.SetSetting("HostModeOnOffStatus", "Off")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
return disableButton
|
||||||
|
}
|
||||||
|
|
||||||
|
enableButton := widget.NewButton("Enable", func(){
|
||||||
|
setConfirmEnableHostModePage(window, currentPage, currentPage)
|
||||||
|
})
|
||||||
|
return enableButton
|
||||||
|
}
|
||||||
|
|
||||||
|
enableDisableButton := getEnableDisableButton()
|
||||||
|
enableDisableButtonCentered := getWidgetCentered(enableDisableButton)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, widget.NewSeparator(), currentStatusRow, enableDisableButtonCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConfirmEnableHostModePage(window fyne.Window, previousPage func(), nextPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Confirm Enable Host Mode")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Enable host mode?")
|
||||||
|
|
||||||
|
description2 := getLabelCentered("In this mode, you will become a host on the Seekia network.")
|
||||||
|
description3 := getLabelCentered("You may host content that is unruleful.")
|
||||||
|
description4 := getLabelCentered("The Seekia moderation system will try to remove unruleful content from the network.")
|
||||||
|
description5 := getLabelCentered("You must accept all legal risks associated with hosting this content.")
|
||||||
|
|
||||||
|
enableHostModeButton := getWidgetCentered(widget.NewButtonWithIcon("Enable", theme.ConfirmIcon(), func(){
|
||||||
|
err := mySettings.SetSetting("HostModeOnOffStatus", "On")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nextPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, enableHostModeButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHostSettingsPage_HostUnviewableProfiles(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setHostSettingsPage_HostUnviewableProfiles(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Settings - Host Unviewable Profiles")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Enable this mode if you are willing to host unviewable profiles.")
|
||||||
|
description2 := getLabelCentered("These profiles may contain rulebreaking content.")
|
||||||
|
description3 := getLabelCentered("Unruleful profiles should eventually be banned by the moderators and deleted from the network.")
|
||||||
|
description4 := getLabelCentered("You will only serve these profiles to other hosts and moderators who opt-in.")
|
||||||
|
description5 := getLabelCentered("Only enable this mode if you accept the legal liability of hosting these profiles.")
|
||||||
|
|
||||||
|
getCurrentHostUnviewableProfilesStatus := func()(string, error){
|
||||||
|
|
||||||
|
exists, hostUnviewableStatus, err := mySettings.GetSetting("HostUnviewableProfilesOnOffStatus")
|
||||||
|
if (err != nil){ return "", err }
|
||||||
|
if (exists == false){
|
||||||
|
return "Off", nil
|
||||||
|
}
|
||||||
|
if (hostUnviewableStatus != "On" && hostUnviewableStatus != "Off"){
|
||||||
|
return "", errors.New("Invalid HostUnviewableProfilesOnOffStatus: " + hostUnviewableStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostUnviewableStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hostUnviewableStatus, err := getCurrentHostUnviewableProfilesStatus()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStatusLabel := widget.NewLabel("Current Status:")
|
||||||
|
currentStatusText := getBoldLabel(hostUnviewableStatus)
|
||||||
|
currentStatusRow := container.NewHBox(layout.NewSpacer(), currentStatusLabel, currentStatusText, layout.NewSpacer())
|
||||||
|
|
||||||
|
getEnableDisableButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (hostUnviewableStatus == "On"){
|
||||||
|
disableButton := widget.NewButton("Disable", func(){
|
||||||
|
err := mySettings.SetSetting("HostUnviewableProfilesOnOffStatus", "Off")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
return disableButton
|
||||||
|
}
|
||||||
|
|
||||||
|
enableButton := widget.NewButton("Enable", func(){
|
||||||
|
setConfirmEnableHostUnviewableProfilesPage(window, currentPage, currentPage)
|
||||||
|
})
|
||||||
|
return enableButton
|
||||||
|
}
|
||||||
|
|
||||||
|
enableDisableButton := getEnableDisableButton()
|
||||||
|
enableDisableButtonCentered := getWidgetCentered(enableDisableButton)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, widget.NewSeparator(), currentStatusRow, enableDisableButtonCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setConfirmEnableHostUnviewableProfilesPage(window fyne.Window, previousPage func(), nextPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Confirm Host Unviewable Profiles")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Host Unviewable Profiles?")
|
||||||
|
|
||||||
|
description2 := getLabelCentered("In this mode, you will download and serve unviewable profiles.")
|
||||||
|
description3 := getLabelCentered("These profiles may contain illegal and rulebreaking content.")
|
||||||
|
description4 := getLabelCentered("Unruleful profiles should be banned by the moderators and deleted.")
|
||||||
|
description5 := getLabelCentered("You must accept the legal risks of hosting these profiles.")
|
||||||
|
|
||||||
|
enableButton := getWidgetCentered(widget.NewButtonWithIcon("Enable", theme.ConfirmIcon(), func(){
|
||||||
|
err := mySettings.SetSetting("HostUnviewableProfilesOnOffStatus", "On")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nextPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, enableButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHostSettingsPage_HostMessages(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setHostSettingsPage_HostMessages(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Settings - Host Messages")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Enable this mode if you are willing to host messages.")
|
||||||
|
description2 := getLabelCentered("Messsages are encrypted and their contents are unknown until they are reported.")
|
||||||
|
description3 := getLabelCentered("Messages may contain unlawful and unruleful content.")
|
||||||
|
description4 := getLabelCentered("Only enable this mode if you accept the legal liability of hosting messages.")
|
||||||
|
description5 := getLabelCentered("Moderators can ban messages after they are publicly reported.")
|
||||||
|
|
||||||
|
getCurrentHostMessagesStatus := func()(string, error){
|
||||||
|
|
||||||
|
exists, hostMessagesStatus, err := mySettings.GetSetting("HostMessagesOnOffStatus")
|
||||||
|
if (err != nil){ return "", err }
|
||||||
|
if (exists == false){
|
||||||
|
return "Off", nil
|
||||||
|
}
|
||||||
|
if (hostMessagesStatus != "On" && hostMessagesStatus != "Off"){
|
||||||
|
return "", errors.New("Invalid host messages status: " + hostMessagesStatus)
|
||||||
|
}
|
||||||
|
return hostMessagesStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hostMessagesStatus, err := getCurrentHostMessagesStatus()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStatusLabel := widget.NewLabel("Current Status:")
|
||||||
|
currentStatusText := getBoldLabel(hostMessagesStatus)
|
||||||
|
currentStatusRow := container.NewHBox(layout.NewSpacer(), currentStatusLabel, currentStatusText, layout.NewSpacer())
|
||||||
|
|
||||||
|
getEnableDisableButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (hostMessagesStatus == "On"){
|
||||||
|
disableButton := widget.NewButton("Disable", func(){
|
||||||
|
err := mySettings.SetSetting("HostMessagesOnOffStatus", "Off")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
return disableButton
|
||||||
|
}
|
||||||
|
|
||||||
|
enableButton := widget.NewButton("Enable", func(){
|
||||||
|
setConfirmEnableHostMessagesPage(window, currentPage, currentPage)
|
||||||
|
})
|
||||||
|
return enableButton
|
||||||
|
}
|
||||||
|
|
||||||
|
enableDisableButton := getEnableDisableButton()
|
||||||
|
enableDisableButtonCentered := getWidgetCentered(enableDisableButton)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, widget.NewSeparator(), currentStatusRow, enableDisableButtonCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConfirmEnableHostMessagesPage(window fyne.Window, previousPage func(), nextPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Confirm Enable Host Messages Mode")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Enable Host Messages Mode?")
|
||||||
|
|
||||||
|
description2 := getLabelCentered("In this mode, you will download and serve Seekia messages.")
|
||||||
|
description3 := getLabelCentered("Messages are encrypted and may contain illegal and rulebreaking content.")
|
||||||
|
description4 := getLabelCentered("Unruleful messages should be banned upon being reported and reviewed by moderators.")
|
||||||
|
description5 := getLabelCentered("You must accept all legal risks associated with the hosting of these messages.")
|
||||||
|
|
||||||
|
enableButton := getWidgetCentered(widget.NewButtonWithIcon("Enable", theme.ConfirmIcon(), func(){
|
||||||
|
err := mySettings.SetSetting("HostMessagesOnOffStatus", "On")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nextPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, enableButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHostSettingsPage_HostOverClearnet(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setHostSettingsPage_HostOverClearnet(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Settings - Host Over Clearnet")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Enable this mode if you are willing to host over clearnet.")
|
||||||
|
description2 := getLabelCentered("This will expose your IP address to Seekia peers.")
|
||||||
|
description3 := getLabelCentered("Clearnet is faster than Tor and is recommended for most hosts.")
|
||||||
|
description4 := getLabelCentered("In this mode, use a VPN to hide your true IP address from Seekia peers.")
|
||||||
|
|
||||||
|
getCurrentHostOverClearnetStatus := func()(string, error){
|
||||||
|
|
||||||
|
exists, hostOverClearnetStatus, err := mySettings.GetSetting("HostOverClearnetOnOffStatus")
|
||||||
|
if (err != nil){ return "", err }
|
||||||
|
if (exists == false){
|
||||||
|
return "Off", nil
|
||||||
|
}
|
||||||
|
if (hostOverClearnetStatus != "On" && hostOverClearnetStatus != "Off"){
|
||||||
|
return "", errors.New("Invalid host over clearnet status: " + hostOverClearnetStatus)
|
||||||
|
}
|
||||||
|
return hostOverClearnetStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hostOverClearnetStatus, err := getCurrentHostOverClearnetStatus()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStatusLabel := widget.NewLabel("Current Status:")
|
||||||
|
currentStatusText := getBoldLabel(hostOverClearnetStatus)
|
||||||
|
currentStatusRow := container.NewHBox(layout.NewSpacer(), currentStatusLabel, currentStatusText, layout.NewSpacer())
|
||||||
|
|
||||||
|
getEnableDisableButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (hostOverClearnetStatus == "On"){
|
||||||
|
disableButton := widget.NewButton("Disable", func(){
|
||||||
|
setConfirmDisableHostOverClearnetPage(window, currentPage, currentPage)
|
||||||
|
})
|
||||||
|
return disableButton
|
||||||
|
}
|
||||||
|
|
||||||
|
enableButton := widget.NewButton("Enable", func(){
|
||||||
|
setConfirmEnableHostOverClearnetPage(window, currentPage, currentPage)
|
||||||
|
})
|
||||||
|
return enableButton
|
||||||
|
}
|
||||||
|
|
||||||
|
enableDisableButton := getEnableDisableButton()
|
||||||
|
enableDisableButtonCentered := getWidgetCentered(enableDisableButton)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), currentStatusRow, enableDisableButtonCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setConfirmEnableHostOverClearnetPage(window fyne.Window, previousPage func(), nextPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Enable Host Over Clearnet")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Enable Host Over Clearnet?")
|
||||||
|
|
||||||
|
description2 := getLabelCentered("In this mode, you will host content over clearnet.")
|
||||||
|
description3 := getLabelCentered("This will expose your IP address.")
|
||||||
|
description4 := getLabelCentered("Use a VPN to shield your true IP address.")
|
||||||
|
description5 := getLabelCentered("Anything you already hosted could be linked to your IP address.")
|
||||||
|
description6 := getLabelCentered("Create a new host identity to cut ties with your hosting history.")
|
||||||
|
|
||||||
|
enableButton := getWidgetCentered(widget.NewButtonWithIcon("Enable", theme.ConfirmIcon(), func(){
|
||||||
|
err := mySettings.SetSetting("HostOverClearnetOnOffStatus", "On")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nextPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, enableButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConfirmDisableHostOverClearnetPage(window fyne.Window, previousPage func(), nextPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Disable Host Over Clearnet")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Disable Host Over Clearnet?")
|
||||||
|
|
||||||
|
description2 := getLabelCentered("If you disable this, you will stop hosting over clearnet.")
|
||||||
|
description3 := getLabelCentered("Your Host identity will not change.")
|
||||||
|
description4 := getLabelCentered("Your clearnet IP address is already associated with your host identity.")
|
||||||
|
description5 := getLabelCentered("Your IP address could be linked to anything you hosted or will host in the future.")
|
||||||
|
description6 := getLabelCentered("To cut all ties with your old IP address, create a new Host identity.")
|
||||||
|
|
||||||
|
disableButton := getWidgetCentered(widget.NewButtonWithIcon("Disable", theme.ConfirmIcon(), func(){
|
||||||
|
err := mySettings.SetSetting("HostOverClearnetOnOffStatus", "Off")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nextPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, disableButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setBuildMyHostProfilePage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setBuildMyHostProfilePage(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Build Host Profile")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Build your host profile.")
|
||||||
|
description2 := getLabelCentered("All information is optional.")
|
||||||
|
|
||||||
|
usernameButton := widget.NewButton("Username", func(){
|
||||||
|
setBuildProfilePage_Username(window, "Host", currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
avatarButton := widget.NewButton("Avatar", func(){
|
||||||
|
setBuildProfilePage_Avatar(window, "Host", currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
descriptionButton := widget.NewButton("Description", func(){
|
||||||
|
setBuildProfilePage_Description(window, "Host", currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
profileLanguageButton := widget.NewButton(translate("Profile Language"), func(){
|
||||||
|
setBuildProfilePage_ProfileLanguage(window, "Host", currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, usernameButton, avatarButton, descriptionButton, profileLanguageButton))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), buttonsGrid)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setViewHostDetailsPage(window fyne.Window, hostIdentityHash [16]byte, previousPage func()){
|
||||||
|
|
||||||
|
//TODO: A page to view info about a host
|
||||||
|
// This will be seperate from the host's profile. We should show a ViewProfile button on this page
|
||||||
|
// We should also show a Peer Actions button
|
||||||
|
// We need this page because a Host's profile may not exist, or is unviewable
|
||||||
|
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
676
gui/imageGui.go
Normal file
676
gui/imageGui.go
Normal file
|
@ -0,0 +1,676 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// imageGui.go implements an image editor and pages to view images
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/canvas"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
|
||||||
|
import "seekia/internal/imagery"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/imageEffects"
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
|
||||||
|
func setViewFullpageImagePage(window fyne.Window, inputImage image.Image, previousPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Viewing Image")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator())
|
||||||
|
|
||||||
|
//TODO: Add a right-click to save ability
|
||||||
|
// This is useful so users/moderators can perform reverse-image searching to detect fake profiles
|
||||||
|
|
||||||
|
fyneImage := canvas.NewImageFromImage(inputImage)
|
||||||
|
fyneImage.FillMode = canvas.ImageFillContain
|
||||||
|
|
||||||
|
page := container.NewBorder(header, nil, nil, nil, fyneImage)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setViewFullpageImagesWithNavigationPage(window fyne.Window, inputImagesList []image.Image, imageIndex int, previousPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Viewing Image")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator())
|
||||||
|
|
||||||
|
numberOfImages := len(inputImagesList)
|
||||||
|
|
||||||
|
if (numberOfImages == 0){
|
||||||
|
setErrorEncounteredPage(window, errors.New("setViewFullpageImagesWithNavigationPage called with empty images list"), previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
finalIndex := numberOfImages - 1
|
||||||
|
|
||||||
|
getCurrentImageIndex := func()int{
|
||||||
|
|
||||||
|
if (imageIndex > finalIndex){
|
||||||
|
return finalIndex
|
||||||
|
}
|
||||||
|
if (imageIndex < 0){
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex := getCurrentImageIndex()
|
||||||
|
|
||||||
|
//TODO: Add a right-click to save ability
|
||||||
|
// This is useful so users/moderators can perform reverse-image searching to detect fake profiles
|
||||||
|
|
||||||
|
currentImage := inputImagesList[currentIndex]
|
||||||
|
currentFyneImage := canvas.NewImageFromImage(currentImage)
|
||||||
|
currentFyneImage.FillMode = canvas.ImageFillContain
|
||||||
|
|
||||||
|
if (numberOfImages == 1){
|
||||||
|
|
||||||
|
page := container.NewBorder(header, nil, nil, nil, currentFyneImage)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousImageButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (currentIndex == 0){
|
||||||
|
emptyButton := widget.NewButton("", nil)
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
previousButton := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){
|
||||||
|
setViewFullpageImagesWithNavigationPage(window, inputImagesList, currentIndex-1, previousPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return previousButton
|
||||||
|
}
|
||||||
|
|
||||||
|
previousImageButton := getPreviousImageButton()
|
||||||
|
|
||||||
|
getNextImageButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (currentIndex == finalIndex){
|
||||||
|
emptyButton := widget.NewButton("", nil)
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
nextButton := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){
|
||||||
|
setViewFullpageImagesWithNavigationPage(window, inputImagesList, currentIndex+1, previousPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nextButton
|
||||||
|
}
|
||||||
|
|
||||||
|
nextImageButton := getNextImageButton()
|
||||||
|
|
||||||
|
navigationButtonsRow := getContainerCentered(container.NewGridWithRows(1, previousImageButton, nextImageButton))
|
||||||
|
|
||||||
|
page := container.NewBorder(header, navigationButtonsRow, nil, nil, currentFyneImage)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSlowlyRevealImagePage(window fyne.Window, inputImage image.Image, percentageRevealedInt int, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setSlowlyRevealImagePage(window, inputImage, percentageRevealedInt, previousPage)}
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
if (percentageRevealedInt >= 100){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Viewing Image")
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator())
|
||||||
|
|
||||||
|
fyneImageObject := canvas.NewImageFromImage(inputImage)
|
||||||
|
fyneImageObject.FillMode = canvas.ImageFillContain
|
||||||
|
|
||||||
|
page := container.NewBorder(header, nil, nil, nil, fyneImageObject)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Reveal Image")
|
||||||
|
|
||||||
|
description := widget.NewLabel("Slowly reveal the image.")
|
||||||
|
revealSettingsButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){
|
||||||
|
setManagePixelateImagesSettingPage(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
descriptionRow := container.NewHBox(layout.NewSpacer(), description, revealSettingsButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
invert0to100 := func(input0to100 int)int{
|
||||||
|
|
||||||
|
if (input0to100 < 0) {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
|
||||||
|
inverted := 100 - input0to100
|
||||||
|
return inverted
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAmountToPixelate := invert0to100(percentageRevealedInt)
|
||||||
|
|
||||||
|
currentPixelatedImage, err := imagery.PixelateGolangImage(inputImage, currentAmountToPixelate)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imageSize := getCustomFyneSize(100)
|
||||||
|
|
||||||
|
imageObject := canvas.NewImageFromImage(currentPixelatedImage)
|
||||||
|
imageObject.FillMode = canvas.ImageFillContain
|
||||||
|
imageObject.SetMinSize(imageSize)
|
||||||
|
|
||||||
|
imageCentered := getFyneImageCentered(imageObject)
|
||||||
|
|
||||||
|
percentageRevealedString := helpers.ConvertIntToString(percentageRevealedInt)
|
||||||
|
|
||||||
|
percentageRevealedLabel := getBoldLabelCentered(percentageRevealedString + "% revealed")
|
||||||
|
|
||||||
|
buttonsGrid := container.NewGridWithColumns(1)
|
||||||
|
|
||||||
|
if (percentageRevealedInt <= 90){
|
||||||
|
|
||||||
|
reveal10PercentButton := widget.NewButton("Reveal 10%", func(){
|
||||||
|
amountToPixelate := invert0to100(percentageRevealedInt + 10)
|
||||||
|
newImage, err := imagery.PixelateGolangImage(inputImage, amountToPixelate)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imageObject.Image = newImage
|
||||||
|
imageObject.Refresh()
|
||||||
|
|
||||||
|
setSlowlyRevealImagePage(window, inputImage, percentageRevealedInt + 10, previousPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsGrid.Add(reveal10PercentButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (percentageRevealedInt <= 99){
|
||||||
|
|
||||||
|
reveal1PercentButton := widget.NewButton("Reveal 1%", func(){
|
||||||
|
|
||||||
|
amountToPixelate := invert0to100(percentageRevealedInt + 1)
|
||||||
|
newImage, err := imagery.PixelateGolangImage(inputImage, amountToPixelate)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imageObject.Image = newImage
|
||||||
|
imageObject.Refresh()
|
||||||
|
|
||||||
|
setSlowlyRevealImagePage(window, inputImage, percentageRevealedInt + 1, previousPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsGrid.Add(reveal1PercentButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (percentageRevealedInt < 100){
|
||||||
|
revealButton := widget.NewButtonWithIcon("Reveal", theme.VisibilityIcon(), func(){
|
||||||
|
setSlowlyRevealImagePage(window, inputImage, 100, previousPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsGrid.Add(revealButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonsGridCentered := getContainerCentered(buttonsGrid)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionRow, widget.NewSeparator(), imageCentered, percentageRevealedLabel, buttonsGridCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// This function provides an image editor to perform image effects
|
||||||
|
// PreviousState is a function to return to the image as it was before the current filter/effect was added
|
||||||
|
func setEditImagePage(window fyne.Window, originalImage image.Image, previousStateExists bool, previousState func(), currentImage image.Image, previousPage func(), setSubmitImagePageFunction func(image.Image, func())){
|
||||||
|
|
||||||
|
if (originalImage == nil || currentImage == nil){
|
||||||
|
setErrorEncounteredPage(window, errors.New("setEditImagePage called with nil image(s)."), previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage := func(){setEditImagePage(window, originalImage, previousStateExists, previousState, currentImage, previousPage, setSubmitImagePageFunction)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Apply Image Effects")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator())
|
||||||
|
|
||||||
|
if (previousStateExists == true){
|
||||||
|
|
||||||
|
revertChangesButton := widget.NewButtonWithIcon("Revert All Changes", theme.ContentClearIcon(), func(){
|
||||||
|
//TODO: Add Are you sure dialog.
|
||||||
|
setEditImagePage(window, originalImage, false, nil, originalImage, previousPage, setSubmitImagePageFunction)
|
||||||
|
})
|
||||||
|
|
||||||
|
undoChangesButton := widget.NewButtonWithIcon("Undo", theme.ContentUndoIcon(), func(){
|
||||||
|
previousState()
|
||||||
|
})
|
||||||
|
|
||||||
|
undoRevertButtonsRow := getContainerCentered(container.NewGridWithRows(1, revertChangesButton, undoChangesButton))
|
||||||
|
page.Add(undoRevertButtonsRow)
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
currentImageFyne := canvas.NewImageFromImage(currentImage)
|
||||||
|
currentImageFyne.FillMode = canvas.ImageFillContain
|
||||||
|
currentImageFyne.SetMinSize(getCustomFyneSize(70))
|
||||||
|
|
||||||
|
currentImageCentered := container.NewHBox(layout.NewSpacer(), currentImageFyne, layout.NewSpacer())
|
||||||
|
|
||||||
|
viewFullpageButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.ZoomInIcon(), func(){
|
||||||
|
setViewFullpageImagePage(window, currentImage, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page.Add(currentImageCentered)
|
||||||
|
page.Add(viewFullpageButton)
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
submitButton := getWidgetCentered(widget.NewButtonWithIcon("Submit Image", theme.NavigateNextIcon(), func(){
|
||||||
|
setSubmitImagePageFunction(currentImage, currentPage)
|
||||||
|
}))
|
||||||
|
page.Add(submitButton)
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
effectsLabel := getBoldLabelCentered("Effects:")
|
||||||
|
page.Add(effectsLabel)
|
||||||
|
|
||||||
|
overlayEmojiFunction := func(){
|
||||||
|
nextPageFunction := func(newImage image.Image){
|
||||||
|
setEditImagePage(window, originalImage, true, currentPage, newImage, previousPage, setSubmitImagePageFunction)
|
||||||
|
}
|
||||||
|
setApplyImageEffectPage_OverlayEmoji(window, currentImage, currentImage, false, nil, 50, 50, 50, currentPage, nextPageFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
cartoonEffectFunction := func(){
|
||||||
|
nextPageFunction := func(newImage image.Image){
|
||||||
|
setEditImagePage(window, originalImage, true, currentPage, newImage, previousPage, setSubmitImagePageFunction)
|
||||||
|
}
|
||||||
|
cartoonEffectFunction := imageEffects.ApplyCartoonEffect
|
||||||
|
|
||||||
|
setApplyAnyImageEffectPage(window, "Cartoon", 10, cartoonEffectFunction, currentImage, false, nil, currentPage, nextPageFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
pencilEffectFunction := func(){
|
||||||
|
nextPageFunction := func(newImage image.Image){
|
||||||
|
setEditImagePage(window, originalImage, true, currentPage, newImage, previousPage, setSubmitImagePageFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
pencilEffectFunction := imageEffects.ApplyPencilEffect
|
||||||
|
|
||||||
|
setApplyAnyImageEffectPage(window, "Pencil", 10, pencilEffectFunction, currentImage, false, nil, currentPage, nextPageFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
oilPaintingEffectFunction := func(){
|
||||||
|
nextPageFunction := func(newImage image.Image){
|
||||||
|
setEditImagePage(window, originalImage, true, currentPage, newImage, previousPage, setSubmitImagePageFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
oilPaintingEffectFunction := imageEffects.ApplyOilPaintingEffect
|
||||||
|
|
||||||
|
setApplyAnyImageEffectPage(window, "Oil Painting", 10, oilPaintingEffectFunction, currentImage, false, nil, currentPage, nextPageFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
wireframeEffectFunction := func(){
|
||||||
|
nextPageFunction := func(newImage image.Image){
|
||||||
|
setEditImagePage(window, originalImage, true, currentPage, newImage, previousPage, setSubmitImagePageFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
wireframeEffectFunction := imageEffects.ApplyWireframeEffect
|
||||||
|
|
||||||
|
setApplyAnyImageEffectPage(window, "Wireframe", 10, wireframeEffectFunction, currentImage, false, nil, currentPage, nextPageFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
strokeEffectFunction := func(){
|
||||||
|
nextPageFunction := func(newImage image.Image){
|
||||||
|
setEditImagePage(window, originalImage, true, currentPage, newImage, previousPage, setSubmitImagePageFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
strokeEffectFunction := imageEffects.ApplyStrokeEffect
|
||||||
|
|
||||||
|
setApplyAnyImageEffectPage(window, "Stroke", 10, strokeEffectFunction, currentImage, false, nil, currentPage, nextPageFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayEmojiButton := widget.NewButton("Overlay Emoji", overlayEmojiFunction)
|
||||||
|
cartoonButton := widget.NewButton("Cartoon", cartoonEffectFunction)
|
||||||
|
pencilButton := widget.NewButton("Pencil", pencilEffectFunction)
|
||||||
|
oilPaintingButton := widget.NewButton("Oil Painting", oilPaintingEffectFunction)
|
||||||
|
wireframeButton := widget.NewButton("Wireframe", wireframeEffectFunction)
|
||||||
|
strokeButton := widget.NewButton("Stroke", strokeEffectFunction)
|
||||||
|
|
||||||
|
effectButtonsGrid := container.NewGridWithColumns(3, overlayEmojiButton, cartoonButton, pencilButton, oilPaintingButton, wireframeButton, strokeButton)
|
||||||
|
effectButtonsGridCentered := getContainerCentered(effectButtonsGrid)
|
||||||
|
page.Add(effectButtonsGridCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setApplyAnyImageEffectPage(window fyne.Window, effectTitle string, inputEffectStrength int, effectFunction func(image.Image, int)(image.Image, error), originalImage image.Image, effectedImageReady bool, effectedImage image.Image, previousPage func(), nextPage func(image.Image)){
|
||||||
|
|
||||||
|
if (originalImage == nil){
|
||||||
|
setErrorEncounteredPage(window, errors.New("setApplyAnyImageEffectPage called with nil image"), previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentEffectStrength := func()int{
|
||||||
|
if (inputEffectStrength <= 0){
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (inputEffectStrength >= 100){
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
return inputEffectStrength
|
||||||
|
}
|
||||||
|
|
||||||
|
effectStrength := getCurrentEffectStrength()
|
||||||
|
|
||||||
|
if (effectedImageReady == false){
|
||||||
|
|
||||||
|
setLoadingScreen(window, "Apply " + effectTitle + " Effect", "Applying " + effectTitle + " Effect")
|
||||||
|
|
||||||
|
getImageWithEffect := func()(image.Image, error){
|
||||||
|
|
||||||
|
if (effectStrength == 0){
|
||||||
|
return originalImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
imageWithEffect, err := effectFunction(originalImage, effectStrength)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
return imageWithEffect, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
imageWithEffect, err := getImageWithEffect()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, errors.New("Unable to apply image effect: " + err.Error()), previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setApplyAnyImageEffectPage(window, effectTitle, inputEffectStrength, effectFunction, originalImage, true, imageWithEffect, previousPage, nextPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage := func(){setApplyAnyImageEffectPage(window, effectTitle, effectStrength, effectFunction, originalImage, true, effectedImage, previousPage, nextPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Apply " + effectTitle + " Effect")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
imageSizeFyne := getCustomFyneSize(100)
|
||||||
|
currentImageFyne := canvas.NewImageFromImage(effectedImage)
|
||||||
|
currentImageFyne.FillMode = canvas.ImageFillContain
|
||||||
|
currentImageFyne.SetMinSize(imageSizeFyne)
|
||||||
|
|
||||||
|
currentImageCentered := container.NewHBox(layout.NewSpacer(), currentImageFyne, layout.NewSpacer())
|
||||||
|
|
||||||
|
viewFullpageButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.ZoomInIcon(), func(){
|
||||||
|
setViewFullpageImagePage(window, effectedImage, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
applyEffectFunction := func(newEffectStrength int){
|
||||||
|
setApplyAnyImageEffectPage(window, effectTitle, newEffectStrength, effectFunction, originalImage, false, nil, previousPage, nextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
getIncreaseDecreaseButtonsRow := func()*fyne.Container{
|
||||||
|
|
||||||
|
getIncrease1Button := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (effectStrength == 100) {
|
||||||
|
return widget.NewButton("", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
increaseEffectButton := widget.NewButtonWithIcon("+1", theme.MoveUpIcon(), func(){
|
||||||
|
applyEffectFunction(effectStrength + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
return increaseEffectButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getDecrease1Button := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (effectStrength == 0) {
|
||||||
|
return widget.NewButton("", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
decreaseEffectButton := widget.NewButtonWithIcon("-1", theme.MoveDownIcon(), func(){
|
||||||
|
|
||||||
|
applyEffectFunction(effectStrength-1)
|
||||||
|
})
|
||||||
|
|
||||||
|
return decreaseEffectButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getIncrease10Button := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (effectStrength > 90) {
|
||||||
|
return widget.NewButton("", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
increaseEffectButton := widget.NewButtonWithIcon("+10", theme.MoveUpIcon(), func(){
|
||||||
|
|
||||||
|
if (effectStrength >= 90){
|
||||||
|
applyEffectFunction(100)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
applyEffectFunction(effectStrength + 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
return increaseEffectButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getDecrease10Button := func()fyne.Widget{
|
||||||
|
if (effectStrength < 10) {
|
||||||
|
return widget.NewButton("", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
decreaseEffectButton := widget.NewButtonWithIcon("-10", theme.MoveDownIcon(), func(){
|
||||||
|
applyEffectFunction(effectStrength - 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
return decreaseEffectButton
|
||||||
|
}
|
||||||
|
|
||||||
|
increase1Button := getIncrease1Button()
|
||||||
|
decrease1Button := getDecrease1Button()
|
||||||
|
|
||||||
|
increase10Button := getIncrease10Button()
|
||||||
|
decrease10Button := getDecrease10Button()
|
||||||
|
|
||||||
|
buttonsRow := getContainerBoxed(container.NewGridWithColumns(2, increase1Button, increase10Button, decrease1Button, decrease10Button))
|
||||||
|
|
||||||
|
return buttonsRow
|
||||||
|
}
|
||||||
|
|
||||||
|
increaseDecreaseButtonsRow := getIncreaseDecreaseButtonsRow()
|
||||||
|
|
||||||
|
effectStrengthTitle := getBoldLabelCentered("Strength:")
|
||||||
|
|
||||||
|
currentEffectStrengthString := helpers.ConvertIntToString(effectStrength)
|
||||||
|
currentEffectStrengthLabel := getWidgetCentered(getBoldLabel(currentEffectStrengthString))
|
||||||
|
|
||||||
|
effectIncreaseDecreaseButtonsWithLabel := container.NewVBox(effectStrengthTitle, currentEffectStrengthLabel, increaseDecreaseButtonsRow)
|
||||||
|
|
||||||
|
effectIncreaseDecreaseSection := getContainerBoxed(getContainerCentered(effectIncreaseDecreaseButtonsWithLabel))
|
||||||
|
|
||||||
|
submitButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm Changes", theme.ConfirmIcon(), func(){
|
||||||
|
nextPage(effectedImage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), currentImageCentered, viewFullpageButton, widget.NewSeparator(), submitButton, effectIncreaseDecreaseSection)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setApplyImageEffectPage_OverlayEmoji(window fyne.Window, originalImage image.Image, currentImage image.Image, emojiIsChosen bool, emojiImage image.Image, emojiScalePercentage int, xAxisPercentage int, yAxisPercentage int, previousPage func(), nextPage func(image.Image)){
|
||||||
|
|
||||||
|
currentPage := func(){setApplyImageEffectPage_OverlayEmoji(window, originalImage, currentImage, emojiIsChosen, emojiImage, emojiScalePercentage, xAxisPercentage, yAxisPercentage, previousPage, nextPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Overlay Emoji")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
setChooseEmojiPageFunction := func(){
|
||||||
|
|
||||||
|
submitEmojiFunction := func(emojiIdentifier int){
|
||||||
|
|
||||||
|
emojiGolangImage, err := getEmojiImageObject(emojiIdentifier)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newImage, err := imageEffects.GetImageWithEmojiOverlay(originalImage, emojiGolangImage, 50, 50, 50)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setApplyImageEffectPage_OverlayEmoji(window, originalImage, newImage, true, emojiGolangImage, 50, 50, 50, previousPage, nextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
setChooseEmojiPage(window, "Choose Emoji", "Circle Face", 0, currentPage, submitEmojiFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emojiIsChosen == false){
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("This tool enables you to overlay an emoji onto your image.")
|
||||||
|
description2 := getLabelCentered("You must first choose an emoji.")
|
||||||
|
|
||||||
|
chooseEmojiButton := getWidgetCentered(widget.NewButtonWithIcon("Choose Emoji", theme.NavigateNextIcon(), setChooseEmojiPageFunction))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, chooseEmojiButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imageSizeFyne := getCustomFyneSize(100)
|
||||||
|
currentImageFyne := canvas.NewImageFromImage(currentImage)
|
||||||
|
currentImageFyne.FillMode = canvas.ImageFillContain
|
||||||
|
currentImageFyne.SetMinSize(imageSizeFyne)
|
||||||
|
|
||||||
|
currentImageCentered := container.NewHBox(layout.NewSpacer(), currentImageFyne, layout.NewSpacer())
|
||||||
|
|
||||||
|
viewFullpageButton := getWidgetCentered(widget.NewButtonWithIcon("", theme.ZoomInIcon(), func(){
|
||||||
|
setViewFullpageImagePage(window, currentImage, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
applyImageChangeFunction := func(newEmojiScalePercentage int, newXAxisPercentage int, newYAxisPercentage int){
|
||||||
|
|
||||||
|
newImage, err := imageEffects.GetImageWithEmojiOverlay(originalImage, emojiImage, newEmojiScalePercentage, newXAxisPercentage, newYAxisPercentage)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setApplyImageEffectPage_OverlayEmoji(window, originalImage, newImage, true, emojiImage, newEmojiScalePercentage, newXAxisPercentage, newYAxisPercentage, previousPage, nextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
getMoveEmojiButtons := func()*fyne.Container{
|
||||||
|
|
||||||
|
moveUpFunction := func(){
|
||||||
|
newYAxisPercentage := yAxisPercentage + 3
|
||||||
|
if (newYAxisPercentage > 100) {
|
||||||
|
newYAxisPercentage = 100
|
||||||
|
}
|
||||||
|
applyImageChangeFunction(emojiScalePercentage, xAxisPercentage, newYAxisPercentage)
|
||||||
|
}
|
||||||
|
moveDownFunction := func(){
|
||||||
|
newYAxisPercentage := yAxisPercentage - 3
|
||||||
|
if (newYAxisPercentage < 0) {
|
||||||
|
newYAxisPercentage = 0
|
||||||
|
}
|
||||||
|
applyImageChangeFunction(emojiScalePercentage, xAxisPercentage, newYAxisPercentage)
|
||||||
|
}
|
||||||
|
moveLeftFunction := func(){
|
||||||
|
newXAxisPercentage := xAxisPercentage - 3
|
||||||
|
if (newXAxisPercentage < 0) {
|
||||||
|
newXAxisPercentage = 0
|
||||||
|
}
|
||||||
|
applyImageChangeFunction(emojiScalePercentage, newXAxisPercentage, yAxisPercentage)
|
||||||
|
}
|
||||||
|
moveRightFunction := func(){
|
||||||
|
newXAxisPercentage := xAxisPercentage + 3
|
||||||
|
if (newXAxisPercentage > 100) {
|
||||||
|
newXAxisPercentage = 100
|
||||||
|
}
|
||||||
|
applyImageChangeFunction(emojiScalePercentage, newXAxisPercentage, yAxisPercentage)
|
||||||
|
}
|
||||||
|
|
||||||
|
moveUpButton := widget.NewButtonWithIcon("", theme.MoveUpIcon(), moveUpFunction)
|
||||||
|
moveDownButton := widget.NewButtonWithIcon("", theme.MoveDownIcon(), moveDownFunction)
|
||||||
|
moveLeftButton := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), moveLeftFunction)
|
||||||
|
moveRightButton := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), moveRightFunction)
|
||||||
|
|
||||||
|
buttonsGrid := container.NewGridWithColumns(3, widget.NewLabel(""), moveUpButton, widget.NewLabel(""), moveLeftButton, widget.NewLabel(""), moveRightButton, widget.NewLabel(""), moveDownButton, widget.NewLabel(""))
|
||||||
|
|
||||||
|
buttonsGridBoxed := getContainerBoxed(buttonsGrid)
|
||||||
|
|
||||||
|
return buttonsGridBoxed
|
||||||
|
}
|
||||||
|
|
||||||
|
getScaleEmojiButtons := func()*fyne.Container{
|
||||||
|
scaleUpFunction := func(){
|
||||||
|
if (emojiScalePercentage >= 97) {
|
||||||
|
applyImageChangeFunction(100, xAxisPercentage, yAxisPercentage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newScalePercentage := emojiScalePercentage + 3
|
||||||
|
applyImageChangeFunction(newScalePercentage, xAxisPercentage, yAxisPercentage)
|
||||||
|
}
|
||||||
|
scaleDownFunction := func(){
|
||||||
|
if (emojiScalePercentage <= 10) {
|
||||||
|
applyImageChangeFunction(7, xAxisPercentage, yAxisPercentage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newScalePercentage := emojiScalePercentage - 3
|
||||||
|
applyImageChangeFunction(newScalePercentage, xAxisPercentage, yAxisPercentage)
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleUpButton := widget.NewButtonWithIcon("", theme.ContentAddIcon(), scaleUpFunction)
|
||||||
|
scaleDownButton := widget.NewButtonWithIcon("", theme.ContentRemoveIcon(), scaleDownFunction)
|
||||||
|
|
||||||
|
scaleButtonsGrid := container.NewGridWithColumns(1, scaleUpButton, scaleDownButton)
|
||||||
|
|
||||||
|
scaleButtonsBoxed := getContainerBoxed(scaleButtonsGrid)
|
||||||
|
|
||||||
|
return scaleButtonsBoxed
|
||||||
|
}
|
||||||
|
|
||||||
|
moveEmojiButtons := getMoveEmojiButtons()
|
||||||
|
scaleEmojiButtons := getScaleEmojiButtons()
|
||||||
|
|
||||||
|
moveAndScaleRow := container.NewHBox(layout.NewSpacer(), moveEmojiButtons, scaleEmojiButtons, layout.NewSpacer())
|
||||||
|
|
||||||
|
changeEmojiButton := getWidgetCentered(widget.NewButtonWithIcon("Change Emoji", theme.DocumentCreateIcon(), setChooseEmojiPageFunction))
|
||||||
|
|
||||||
|
submitButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm Changes", theme.ConfirmIcon(), func(){
|
||||||
|
nextPage(currentImage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), currentImageCentered, viewFullpageButton, widget.NewSeparator(), changeEmojiButton, submitButton, widget.NewSeparator(), moveAndScaleRow)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
1821
gui/manageGeneticsGui.go
Normal file
1821
gui/manageGeneticsGui.go
Normal file
File diff suppressed because it is too large
Load diff
2118
gui/matchesGui.go
Normal file
2118
gui/matchesGui.go
Normal file
File diff suppressed because it is too large
Load diff
4855
gui/moderatorGui.go
Normal file
4855
gui/moderatorGui.go
Normal file
File diff suppressed because it is too large
Load diff
1161
gui/peerActionsGui.go
Normal file
1161
gui/peerActionsGui.go
Normal file
File diff suppressed because it is too large
Load diff
529
gui/questionnaireGui.go
Normal file
529
gui/questionnaireGui.go
Normal file
|
@ -0,0 +1,529 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// questionnaireGui.go implements pages to view and take a user's questionnaire
|
||||||
|
// Pages to build a user's questionnaire exist in buildProfileGui_General.go
|
||||||
|
// TODO: We need to add pages to view users who have taken a user's questionnaire, and statistics about those users and their responses
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
import "fyne.io/fyne/v2/dialog"
|
||||||
|
|
||||||
|
import "seekia/internal/allowedText"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/mateQuestionnaire"
|
||||||
|
import "seekia/internal/myIdentity"
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// This page is used to take a questionnaire
|
||||||
|
// Submit page is used to submit the completed questionnaire
|
||||||
|
//Inputs:
|
||||||
|
// -fyne.Window
|
||||||
|
// -[]mateQuestionnaire.QuestionObject: Input questionnaire
|
||||||
|
// -int: Current viewed question index
|
||||||
|
// -map[string]string: Current questionnaire response map to add responses to. Is submitted once questionnaire is completed.
|
||||||
|
// -Structure: Question Identifier -> Response
|
||||||
|
// -func(): Previous page
|
||||||
|
// -func(response string, previousPage func()): Submit page
|
||||||
|
func setTakeQuestionnairePage(window fyne.Window, inputQuestionnaire []mateQuestionnaire.QuestionObject, currentIndex int, myResponsesMap map[string]string, previousPage func(), submitPage func(string, func())){
|
||||||
|
|
||||||
|
currentPage := func(){setTakeQuestionnairePage(window, inputQuestionnaire, currentIndex, myResponsesMap, previousPage, submitPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Take Questionnaire")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
if (len(inputQuestionnaire) == 0){
|
||||||
|
setErrorEncounteredPage(window, errors.New("setTakeQuestionnairePage called with empty questionnaire."), previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex < 0 || currentIndex > (len(inputQuestionnaire)-1) ){
|
||||||
|
setErrorEncounteredPage(window, errors.New("setTakeQuestionnairePage called with invalid questionnaire index."), previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getNavigateQuestionnaireButtons := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
getNavigatePreviousButton := func()fyne.Widget{
|
||||||
|
if (currentIndex == 0){
|
||||||
|
emptyButton := widget.NewButton("", nil)
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
previousButton := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){
|
||||||
|
setTakeQuestionnairePage(window, inputQuestionnaire, currentIndex-1, myResponsesMap, previousPage, submitPage)
|
||||||
|
})
|
||||||
|
return previousButton
|
||||||
|
}
|
||||||
|
|
||||||
|
navigatePreviousButton := getNavigatePreviousButton()
|
||||||
|
navigateNextButton := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){
|
||||||
|
|
||||||
|
if (currentIndex == len(inputQuestionnaire)-1){
|
||||||
|
|
||||||
|
// No questions are left to view. We submit the questionnaire.
|
||||||
|
|
||||||
|
if (len(myResponsesMap) == 0){
|
||||||
|
dialogTitle := translate("No Questions Answered")
|
||||||
|
dialogMessageA := getBoldLabelCentered(translate("You have not answered any questions."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("You must answer at least 1 question."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newResponse, err := mateQuestionnaire.CreateQuestionnaireResponse(myResponsesMap)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitPage(newResponse, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTakeQuestionnairePage(window, inputQuestionnaire, currentIndex+1, myResponsesMap, previousPage, submitPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
navigateButtonsGrid := container.NewGridWithColumns(2, navigatePreviousButton, navigateNextButton)
|
||||||
|
|
||||||
|
return navigateButtonsGrid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateQuestionnaireButtons, err := getNavigateQuestionnaireButtons()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentQuestionViewIndex := helpers.ConvertIntToString(currentIndex + 1)
|
||||||
|
totalQuestionsString := helpers.ConvertIntToString(len(inputQuestionnaire))
|
||||||
|
|
||||||
|
currentIndexLabel := getBoldLabelCentered("Question " + currentQuestionViewIndex + " of " + totalQuestionsString)
|
||||||
|
|
||||||
|
navigateQuestionnaireButtonsCentered := getContainerCentered(navigateQuestionnaireButtons)
|
||||||
|
|
||||||
|
currentQuestionMap := inputQuestionnaire[currentIndex]
|
||||||
|
|
||||||
|
currentQuestionContainer, err := getViewQuestionnaireQuestionContainer(window, currentPage, currentQuestionMap, myResponsesMap)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), currentIndexLabel, navigateQuestionnaireButtonsCentered, widget.NewSeparator(), currentQuestionContainer)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setSubmitQuestionnairePage(window fyne.Window, recipientIdentityHash [16]byte, questionnaireResponse string, previousPage func(), onCompletePage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setSubmitQuestionnairePage(window, recipientIdentityHash, questionnaireResponse, previousPage, onCompletePage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Submit Questionnaire")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Mate")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (myIdentityExists == false){
|
||||||
|
description1 := getBoldLabelCentered("Your Mate identity does not exist.")
|
||||||
|
description2 := getLabelCentered("You must create it before sending your questionnaire response.")
|
||||||
|
|
||||||
|
createIdentityButton := getWidgetCentered(widget.NewButtonWithIcon("Create Identity", theme.NavigateNextIcon(), func(){
|
||||||
|
setChooseNewIdentityHashPage(window, "Mate", currentPage, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, createIdentityButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Are you sure you want to submit your questionnaire?")
|
||||||
|
description2 := getLabelCentered("You will pay for the message on the next page.")
|
||||||
|
|
||||||
|
myResponseMap, err := mateQuestionnaire.ReadQuestionnaireResponse(questionnaireResponse)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfResponses := len(myResponseMap)
|
||||||
|
numberOfResponsesString := helpers.ConvertIntToString(numberOfResponses)
|
||||||
|
|
||||||
|
numberOfResponsesLabel := widget.NewLabel("Number Of Responses:")
|
||||||
|
numberOfResponsesText := getBoldLabel(numberOfResponsesString)
|
||||||
|
|
||||||
|
numberOfResponsesRow := container.NewHBox(layout.NewSpacer(), numberOfResponsesLabel, numberOfResponsesText, layout.NewSpacer())
|
||||||
|
|
||||||
|
messageCommunication := ">!>QuestionnaireResponse=" + questionnaireResponse
|
||||||
|
|
||||||
|
confirmButton := getWidgetCentered(widget.NewButtonWithIcon("Confirm", theme.ConfirmIcon(), func(){
|
||||||
|
|
||||||
|
setFundAndSendChatMessagePage(window, myIdentityHash, recipientIdentityHash, messageCommunication, false, currentPage, onCompletePage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
//TODO: After send, add to questionnaireHistory so we can keep track of our past answers and update them with new responses
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, numberOfResponsesRow, confirmButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function returns a container containing a questionnaire choice/entry question with a submit button
|
||||||
|
// It is used on the question preview page and when a user is taking a questionnaire
|
||||||
|
func getViewQuestionnaireQuestionContainer(window fyne.Window, currentPage func(), questionObject mateQuestionnaire.QuestionObject, myResponsesMap map[string]string)(*fyne.Container, error){
|
||||||
|
|
||||||
|
questionIdentifier := questionObject.Identifier
|
||||||
|
questionType := questionObject.Type
|
||||||
|
questionContent := questionObject.Content
|
||||||
|
questionOptions := questionObject.Options
|
||||||
|
|
||||||
|
questionLabel := getBoldLabelCentered("Question:")
|
||||||
|
|
||||||
|
questionContentTrimmed, _, err := helpers.TrimAndFlattenString(questionContent, 30)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
questionContentLabel := getLabelCentered(questionContentTrimmed)
|
||||||
|
|
||||||
|
viewQuestionContentButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){
|
||||||
|
setViewTextPage(window, "Viewing Question", questionContent, false, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
questionContentRow := container.NewHBox(layout.NewSpacer(), questionContentLabel, viewQuestionContentButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
if (questionType == "Choice"){
|
||||||
|
|
||||||
|
//TODO: Deal with long choices
|
||||||
|
|
||||||
|
maximumAnswersAllowedString, choicesListString, delimiterFound := strings.Cut(questionOptions, "#")
|
||||||
|
if (delimiterFound == false){
|
||||||
|
return nil, errors.New("Malformed question object: Invalid choice question options: " + questionOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
maximumAnswersAllowedInt, err := helpers.ConvertStringToInt(maximumAnswersAllowedString)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (maximumAnswersAllowedInt < 1 || maximumAnswersAllowedInt > 6){
|
||||||
|
return nil, errors.New("Malformed question object: Invalid maximum answers allowed: " + maximumAnswersAllowedString)
|
||||||
|
}
|
||||||
|
|
||||||
|
choicesList := strings.Split(choicesListString, "$¥")
|
||||||
|
|
||||||
|
if (len(choicesList) < 2 || len(choicesList) > 6){
|
||||||
|
return nil, errors.New("Malformed question object: Invalid choices.")
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaximumAnswersAllowedAdjusted := func()string{
|
||||||
|
|
||||||
|
if (maximumAnswersAllowedInt > len(choicesList)){
|
||||||
|
maximumAnswersAllowedAdjusted := helpers.ConvertIntToString(len(choicesList))
|
||||||
|
|
||||||
|
return maximumAnswersAllowedAdjusted
|
||||||
|
}
|
||||||
|
|
||||||
|
return maximumAnswersAllowedString
|
||||||
|
}
|
||||||
|
|
||||||
|
maximumAnswersAllowedAdjusted := getMaximumAnswersAllowedAdjusted()
|
||||||
|
|
||||||
|
getChooseChoiceLabelText := func()string{
|
||||||
|
if (maximumAnswersAllowedInt == 1){
|
||||||
|
return "Select Answer:"
|
||||||
|
}
|
||||||
|
return "Select Answer(s):"
|
||||||
|
}
|
||||||
|
chooseChoiceLabelText := getChooseChoiceLabelText()
|
||||||
|
chooseChoiceLabel := getBoldLabelCentered(chooseChoiceLabelText)
|
||||||
|
maximumAnswersLabel := getWidgetCentered(getItalicLabel("Maximum: " + maximumAnswersAllowedAdjusted))
|
||||||
|
|
||||||
|
getChoiceSelectButtons := func()(fyne.Widget, error){
|
||||||
|
|
||||||
|
if (maximumAnswersAllowedInt == 1){
|
||||||
|
|
||||||
|
questionSelector := widget.NewRadioGroup(choicesList, func(newChoice string){
|
||||||
|
|
||||||
|
if (newChoice == ""){
|
||||||
|
delete(myResponsesMap, questionIdentifier)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getNewSelectedIndex := func()(int, error){
|
||||||
|
|
||||||
|
for index, element := range choicesList{
|
||||||
|
|
||||||
|
if (element == newChoice){
|
||||||
|
return index, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, errors.New("questionSelector onChanged function called with unknown choice: " + newChoice)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSelectedIndex, err := getNewSelectedIndex()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoded response is index+1
|
||||||
|
// For example, if we chose the first option in the list, our encoded response will contain "1"
|
||||||
|
|
||||||
|
newEncodedResponse := helpers.ConvertIntToString(newSelectedIndex+1)
|
||||||
|
|
||||||
|
myResponsesMap[questionIdentifier] = newEncodedResponse
|
||||||
|
})
|
||||||
|
|
||||||
|
myEncodedResponse, myResponseExists := myResponsesMap[questionIdentifier]
|
||||||
|
if (myResponseExists == true){
|
||||||
|
|
||||||
|
mySelectedOptionInt, err := helpers.ConvertStringToInt(myEncodedResponse)
|
||||||
|
if (err != nil){
|
||||||
|
return nil, errors.New("myResponsesMap is malformed: Contains invalid 1-selection-allowed choice question response: " + myEncodedResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
mySelectedOptionIndex := mySelectedOptionInt-1
|
||||||
|
|
||||||
|
if (mySelectedOptionIndex < 0 || mySelectedOptionIndex > (len(choicesList) - 1)){
|
||||||
|
return nil, errors.New("myResponsesMap is malformed: Contains invalid 1-selection-allowed choice question response: Item index is out of range: " + myEncodedResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
mySelectedOption := choicesList[mySelectedOptionIndex]
|
||||||
|
|
||||||
|
questionSelector.Selected = mySelectedOption
|
||||||
|
}
|
||||||
|
|
||||||
|
return questionSelector, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
getMySelectionsList := func()([]string, error){
|
||||||
|
|
||||||
|
myEncodedResponse, myResponseExists := myResponsesMap[questionIdentifier]
|
||||||
|
if (myResponseExists == false){
|
||||||
|
emptyList := make([]string, 0)
|
||||||
|
return emptyList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mySelectedIndexesList := strings.Split(myEncodedResponse, "$")
|
||||||
|
|
||||||
|
mySelectionsList := make([]string, 0, len(mySelectedIndexesList))
|
||||||
|
|
||||||
|
for _, mySelectionIndexString := range mySelectedIndexesList{
|
||||||
|
|
||||||
|
mySelectionIndexInt, err := helpers.ConvertStringToInt(mySelectionIndexString)
|
||||||
|
if (err != nil) {
|
||||||
|
return nil, errors.New("myResponsesMap is malformed: Choice response contains non-int item: " + mySelectionIndexString)
|
||||||
|
}
|
||||||
|
|
||||||
|
indexInt := mySelectionIndexInt - 1
|
||||||
|
|
||||||
|
if (indexInt < 0 || indexInt > (len(choicesList)-1)){
|
||||||
|
return nil, errors.New("myResponsesMap is malformed: MySelectionIndex is out of range.")
|
||||||
|
}
|
||||||
|
|
||||||
|
mySelectedItem := choicesList[indexInt]
|
||||||
|
mySelectionsList = append(mySelectionsList, mySelectedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mySelectionsList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mySelectionsList, err := getMySelectionsList()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
choicesButtons := widget.NewCheckGroup(choicesList, nil)
|
||||||
|
|
||||||
|
handleChoiceSelectionFunction := func(newSelectionsList []string){
|
||||||
|
|
||||||
|
if (len(newSelectionsList) == 0){
|
||||||
|
delete(myResponsesMap, questionIdentifier)
|
||||||
|
mySelectionsList = make([]string, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(newSelectionsList) > maximumAnswersAllowedInt){
|
||||||
|
|
||||||
|
// We undo this selection
|
||||||
|
choicesButtons.Selected = mySelectionsList
|
||||||
|
|
||||||
|
dialogTitle := translate("Too Many Selections")
|
||||||
|
dialogMessageA := getLabelCentered(translate("You have selected too many responses."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("You can only select " + maximumAnswersAllowedString + " responses."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedSelectionsList := make([]string, 0)
|
||||||
|
|
||||||
|
for _, selectedChoice := range newSelectionsList{
|
||||||
|
|
||||||
|
getSelectedChoiceIndex := func()(int, error){
|
||||||
|
|
||||||
|
for index, element := range choicesList{
|
||||||
|
if (element == selectedChoice){
|
||||||
|
return index, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, errors.New("Selected choice not found in choices list.")
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedChoiceIndex, err := getSelectedChoiceIndex()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedSelection := helpers.ConvertIntToString(selectedChoiceIndex + 1)
|
||||||
|
|
||||||
|
encodedSelectionsList = append(encodedSelectionsList, encodedSelection)
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedResponse := strings.Join(encodedSelectionsList, "$")
|
||||||
|
myResponsesMap[questionIdentifier] = encodedResponse
|
||||||
|
|
||||||
|
mySelectionsList = newSelectionsList
|
||||||
|
}
|
||||||
|
|
||||||
|
choicesButtons.OnChanged = handleChoiceSelectionFunction
|
||||||
|
|
||||||
|
choicesButtons.Selected = mySelectionsList
|
||||||
|
|
||||||
|
return choicesButtons, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
choiceSelectButtons, err := getChoiceSelectButtons()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
choicesButtonsCentered := getWidgetCentered(choiceSelectButtons)
|
||||||
|
|
||||||
|
noResponseButton := getWidgetCentered(widget.NewButtonWithIcon("No Response", theme.CancelIcon(), func(){
|
||||||
|
|
||||||
|
_, exists := myResponsesMap[questionIdentifier]
|
||||||
|
if (exists == false){
|
||||||
|
// No response exists
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(myResponsesMap, questionIdentifier)
|
||||||
|
currentPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
questionContainer := container.NewVBox(questionLabel, questionContentRow, widget.NewSeparator(), chooseChoiceLabel, maximumAnswersLabel, choicesButtonsCentered, noResponseButton)
|
||||||
|
|
||||||
|
return questionContainer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (questionType != "Entry"){
|
||||||
|
return nil, errors.New("getViewQuestionnaireQuestionContainer called with invalid questionObject: Invalid question type: " + questionType)
|
||||||
|
}
|
||||||
|
if (questionOptions != "Numeric" && questionOptions != "Any"){
|
||||||
|
return nil, errors.New("getViewQuestionnaireQuestionContainer called with invalid questionObject: Invalid Entry question options: " + questionOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
enterResponseLabel := getBoldLabelCentered("Enter " + questionOptions + " Response:")
|
||||||
|
|
||||||
|
responseEntry := widget.NewMultiLineEntry()
|
||||||
|
responseEntry.Wrapping = 3
|
||||||
|
|
||||||
|
myResponse, myResponseExists := myResponsesMap[questionIdentifier]
|
||||||
|
if (myResponseExists == true){
|
||||||
|
responseEntry.TextStyle = getFyneTextStyle_Bold()
|
||||||
|
responseEntry.SetText(myResponse)
|
||||||
|
} else {
|
||||||
|
responseEntry.SetPlaceHolder("Enter response...")
|
||||||
|
}
|
||||||
|
|
||||||
|
responseEntry.OnChanged = func(newResponse string){
|
||||||
|
|
||||||
|
if (myResponseExists == true && newResponse == myResponse){
|
||||||
|
responseEntry.TextStyle = getFyneTextStyle_Bold()
|
||||||
|
} else {
|
||||||
|
responseEntry.TextStyle = getFyneTextStyle_Standard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseEntryBoxed := getWidgetBoxed(responseEntry)
|
||||||
|
|
||||||
|
submitButton := getWidgetCentered(widget.NewButtonWithIcon("Submit", theme.ConfirmIcon(), func(){
|
||||||
|
|
||||||
|
newResponse := responseEntry.Text
|
||||||
|
|
||||||
|
if (newResponse == ""){
|
||||||
|
delete(myResponsesMap, questionIdentifier)
|
||||||
|
currentPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(newResponse) > 2000){
|
||||||
|
|
||||||
|
currentResponseCharacterCountString := helpers.ConvertIntToString(len(newResponse))
|
||||||
|
|
||||||
|
title := translate("Response Is Too Long.")
|
||||||
|
dialogMessageA := getLabelCentered("The longest response allowed is 2000 bytes.")
|
||||||
|
dialogMessageB := getLabelCentered("Your response bytes count: " + currentResponseCharacterCountString)
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(title, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (questionOptions == "Numeric"){
|
||||||
|
responseIsNumeric := helpers.VerifyStringIsFloat(newResponse)
|
||||||
|
if (responseIsNumeric == false){
|
||||||
|
title := translate("Response Is Not Numeric.")
|
||||||
|
dialogMessageA := getLabelCentered("You must respond with a number.")
|
||||||
|
dialogMessageB := getLabelCentered("Change your response to a number.")
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(title, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responseIsAllowed := allowedText.VerifyStringIsAllowed(newResponse)
|
||||||
|
if (responseIsAllowed == false){
|
||||||
|
dialogTitle := translate("Response Is Invalid.")
|
||||||
|
dialogMessageA := getLabelCentered(translate("Response contains a prohibited character."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("It must be encoded in UTF-8."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isContained := strings.Contains(newResponse, "+&")
|
||||||
|
if (isContained == true){
|
||||||
|
dialogTitle := translate("Response Is Invalid.")
|
||||||
|
dialogMessageA := getLabelCentered(translate("Question contains prohibited string: ") + "+&")
|
||||||
|
dialogMessageB := getLabelCentered(translate("Remove this string and resubmit."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myResponsesMap[questionIdentifier] = newResponse
|
||||||
|
currentPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
noResponseButton := getWidgetCentered(widget.NewButtonWithIcon("No Response", theme.CancelIcon(), func(){
|
||||||
|
delete(myResponsesMap, questionIdentifier)
|
||||||
|
currentPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
widener := widget.NewLabel(" ")
|
||||||
|
heightener := widget.NewLabel("")
|
||||||
|
|
||||||
|
buttonsWithWidener := container.NewVBox(submitButton, noResponseButton, widener, heightener)
|
||||||
|
|
||||||
|
entryWithButtons := getContainerCentered(container.NewGridWithColumns(1, responseEntryBoxed, buttonsWithWidener))
|
||||||
|
|
||||||
|
questionContainer := container.NewVBox(questionLabel, questionContentRow, widget.NewSeparator(), enterResponseLabel, entryWithButtons)
|
||||||
|
|
||||||
|
return questionContainer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
281
gui/resourcesGui.go
Normal file
281
gui/resourcesGui.go
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// resourcesGui.go provides functions to retrieve icons/emojis from the imageFiles package as fyne canvas images
|
||||||
|
// It also provides the Choose Emoji page
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/canvas"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
|
||||||
|
import "seekia/resources/imageFiles"
|
||||||
|
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/imagery"
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
import "image"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
|
||||||
|
func getIdentityTypeIcon(identityType string, iconSizeShift int)(*canvas.Image, error){
|
||||||
|
|
||||||
|
iconSize := getCustomFyneSize(iconSizeShift)
|
||||||
|
|
||||||
|
if (identityType == "Mate"){
|
||||||
|
mateIcon, err := getFyneImageIcon("Mate")
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
mateIcon.SetMinSize(iconSize)
|
||||||
|
return mateIcon, nil
|
||||||
|
}
|
||||||
|
if (identityType == "Host"){
|
||||||
|
hostIcon, err := getFyneImageIcon("Host")
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
hostIcon.SetMinSize(iconSize)
|
||||||
|
return hostIcon, nil
|
||||||
|
}
|
||||||
|
if (identityType == "Moderator"){
|
||||||
|
moderatorIcon, err := getFyneImageIcon("Moderate")
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
moderatorIcon.SetMinSize(iconSize)
|
||||||
|
return moderatorIcon, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("getIdentityTypeIcon called with invalid identityType: " + identityType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFyneImageIcon(iconName string)(*canvas.Image, error){
|
||||||
|
|
||||||
|
iconFileBytes, err := imageFiles.GetIconFileBytesFromName(iconName)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
iconImageObject, err := getFyneImageFromFileBytes(iconFileBytes, "SVG")
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
return iconImageObject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -image.Image
|
||||||
|
// -error (will return err if file cannot be read)
|
||||||
|
func getEmojiImageObject(emojiIdentifier int)(image.Image, error){
|
||||||
|
|
||||||
|
emojiFileBytes, err := imageFiles.GetEmojiFileBytesFromIdentifier(emojiIdentifier)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
golangImageObject, err := imagery.ConvertSVGImageFileBytesToGolangImage(emojiFileBytes)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
return golangImageObject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func getFyneImageFromFileBytes(imageFileBytes []byte, fileType string)(*canvas.Image, error){
|
||||||
|
|
||||||
|
if (fileType != "SVG" && fileType != "PNG"){
|
||||||
|
return nil, errors.New("getFyneImageFromFileBytes called with invalid fileType: " + fileType)
|
||||||
|
}
|
||||||
|
|
||||||
|
randomString, err := helpers.GetNewRandomHexString(16)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
fileReader := bytes.NewReader(imageFileBytes)
|
||||||
|
|
||||||
|
getFileExtention := func()string{
|
||||||
|
if (fileType == "SVG"){
|
||||||
|
return ".svg"
|
||||||
|
}
|
||||||
|
// fileType == PNG
|
||||||
|
return ".png"
|
||||||
|
}
|
||||||
|
|
||||||
|
fileExtention := getFileExtention()
|
||||||
|
|
||||||
|
randomFileName := randomString + fileExtention
|
||||||
|
iconImageObject := canvas.NewImageFromReader(fileReader, randomFileName)
|
||||||
|
iconImageObject.FillMode = canvas.ImageFillContain
|
||||||
|
|
||||||
|
return iconImageObject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getColorSquareAsFyneImage(colorCode string)(*canvas.Image, error){
|
||||||
|
|
||||||
|
colorSquareGoImage, err := imagery.GetColorSquare(colorCode)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
fyneImageObject := canvas.NewImageFromImage(colorSquareGoImage)
|
||||||
|
fyneImageObject.FillMode = canvas.ImageFillContain
|
||||||
|
|
||||||
|
return fyneImageObject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseEmojiPage(window fyne.Window, pageTitle string, currentCategory string, emojiIndex int, previousPage func(), nextPage func(int)){
|
||||||
|
|
||||||
|
setLoadingScreen(window, pageTitle, "Loading Emojis...")
|
||||||
|
|
||||||
|
title := getPageTitleCentered(pageTitle)
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
emojiCategoriesList := imageFiles.GetEmojiCategoriesList()
|
||||||
|
|
||||||
|
handleCategorySelectFunction := func(newCategory string){
|
||||||
|
if (currentCategory == newCategory){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setChooseEmojiPage(window, pageTitle, newCategory, 0, previousPage, nextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
categorySelector := widget.NewSelect(emojiCategoriesList, handleCategorySelectFunction)
|
||||||
|
categorySelector.Selected = currentCategory
|
||||||
|
|
||||||
|
categoryLabel := widget.NewLabel("Category:")
|
||||||
|
|
||||||
|
categorySelectorRow := container.NewHBox(layout.NewSpacer(), categoryLabel, categorySelector, layout.NewSpacer())
|
||||||
|
|
||||||
|
emojisInCategoryList, err := imageFiles.GetEmojisInCategoryList(currentCategory)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPageIndex := len(emojisInCategoryList) - 10
|
||||||
|
|
||||||
|
// This returns an index that represents the beginning of a page, if emoji index is out of bounds
|
||||||
|
getPageIndex := func()int{
|
||||||
|
|
||||||
|
if (emojiIndex <= 0){
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (emojiIndex >= finalPageIndex){
|
||||||
|
return finalPageIndex
|
||||||
|
}
|
||||||
|
return emojiIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
pageIndex := getPageIndex()
|
||||||
|
|
||||||
|
getFirstPageButton := func()fyne.Widget{
|
||||||
|
if (emojiIndex <= 0){
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
firstPageButton := widget.NewButtonWithIcon("", theme.MediaFastRewindIcon(), func(){
|
||||||
|
setChooseEmojiPage(window, pageTitle, currentCategory, 0, previousPage, nextPage)
|
||||||
|
})
|
||||||
|
return firstPageButton
|
||||||
|
}
|
||||||
|
|
||||||
|
firstPageButton := getFirstPageButton()
|
||||||
|
|
||||||
|
getPreviousPageButton := func()fyne.Widget{
|
||||||
|
if (emojiIndex <= 0){
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
backButton := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){
|
||||||
|
setChooseEmojiPage(window, pageTitle, currentCategory, emojiIndex-10, previousPage, nextPage)
|
||||||
|
})
|
||||||
|
return backButton
|
||||||
|
}
|
||||||
|
|
||||||
|
previousPageButton := getPreviousPageButton()
|
||||||
|
|
||||||
|
getNextPageButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (emojiIndex >= finalPageIndex){
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
nextButton := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){
|
||||||
|
setChooseEmojiPage(window, pageTitle, currentCategory, emojiIndex+10, previousPage, nextPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nextButton
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPageButton := getNextPageButton()
|
||||||
|
|
||||||
|
getFinalPageButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (emojiIndex >= finalPageIndex){
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPageButton := widget.NewButtonWithIcon("", theme.MediaFastForwardIcon(), func(){
|
||||||
|
setChooseEmojiPage(window, pageTitle, currentCategory, finalPageIndex, previousPage, nextPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
return finalPageButton
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPageButton := getFinalPageButton()
|
||||||
|
|
||||||
|
numberOfPages := len(emojisInCategoryList)/10
|
||||||
|
numberOfPagesString := helpers.ConvertIntToString(numberOfPages)
|
||||||
|
|
||||||
|
currentPage := (pageIndex/10) +1
|
||||||
|
currentPageString := helpers.ConvertIntToString(currentPage)
|
||||||
|
|
||||||
|
pageLabel := getBoldLabel("Page " + currentPageString + "/" + numberOfPagesString)
|
||||||
|
|
||||||
|
navigationRow := container.NewHBox(layout.NewSpacer(), firstPageButton, previousPageButton, pageLabel, nextPageButton, finalPageButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
getEmojisGrid := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
emojisGrid := container.NewGridWithColumns(5)
|
||||||
|
|
||||||
|
finalIndex := len(emojisInCategoryList) - 1
|
||||||
|
|
||||||
|
counter := 0
|
||||||
|
|
||||||
|
for i := pageIndex; i <= finalIndex; i++{
|
||||||
|
|
||||||
|
currentEmojiIdentifier := emojisInCategoryList[i]
|
||||||
|
|
||||||
|
emojiImage, err := getEmojiImageObject(currentEmojiIdentifier)
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
emojiFyneImage := canvas.NewImageFromImage(emojiImage)
|
||||||
|
emojiFyneImage.FillMode = canvas.ImageFillContain
|
||||||
|
|
||||||
|
chooseButton := widget.NewButtonWithIcon("Select", theme.ConfirmIcon(), func(){
|
||||||
|
nextPage(currentEmojiIdentifier)
|
||||||
|
})
|
||||||
|
|
||||||
|
emojiCell := getContainerBoxed(container.NewBorder(nil, chooseButton, nil, nil, emojiFyneImage))
|
||||||
|
|
||||||
|
emojisGrid.Add(emojiCell)
|
||||||
|
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
if (counter >= 10){
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emojisGrid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
emojisGrid, err := getEmojisGrid()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator(), categorySelectorRow, widget.NewSeparator(), navigationRow)
|
||||||
|
|
||||||
|
page := container.NewBorder(header, nil, nil, nil, emojisGrid)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
1882
gui/settingsGui.go
Normal file
1882
gui/settingsGui.go
Normal file
File diff suppressed because it is too large
Load diff
624
gui/startupGui.go
Normal file
624
gui/startupGui.go
Normal file
|
@ -0,0 +1,624 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// startupGui.go implements the app startup pages
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/app"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/canvas"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
import "fyne.io/fyne/v2/dialog"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
|
||||||
|
import "seekia/resources/imageFiles"
|
||||||
|
|
||||||
|
import "seekia/internal/appMemory"
|
||||||
|
import "seekia/internal/badgerDatabase"
|
||||||
|
import "seekia/internal/localFilesystem"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/appUsers"
|
||||||
|
import "seekia/internal/mySeedPhrases"
|
||||||
|
import "seekia/internal/globalSettings"
|
||||||
|
import "seekia/internal/translation"
|
||||||
|
import "seekia/internal/imagery"
|
||||||
|
import "seekia/internal/backgroundJobs"
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
import "errors"
|
||||||
|
import "slices"
|
||||||
|
|
||||||
|
func StartGui(){
|
||||||
|
|
||||||
|
app := app.New()
|
||||||
|
|
||||||
|
window := app.NewWindow("Seekia")
|
||||||
|
|
||||||
|
windowSize := fyne.NewSize(600, 600)
|
||||||
|
window.Resize(windowSize)
|
||||||
|
|
||||||
|
window.CenterOnScreen()
|
||||||
|
|
||||||
|
err := localFilesystem.InitializeAppDatastores()
|
||||||
|
if (err != nil){
|
||||||
|
|
||||||
|
getThemeObject := func()fyne.Theme{
|
||||||
|
|
||||||
|
themeObject, err := getCustomFyneTheme("Light")
|
||||||
|
if (err != nil){
|
||||||
|
return theme.LightTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
return themeObject
|
||||||
|
}
|
||||||
|
|
||||||
|
themeObject := getThemeObject()
|
||||||
|
|
||||||
|
app.Settings().SetTheme(themeObject)
|
||||||
|
|
||||||
|
errorToShow := errors.New("Seekia cannot access the filesystem: " + err.Error())
|
||||||
|
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, errorToShow, false, nil)
|
||||||
|
|
||||||
|
window.ShowAndRun()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getAppTheme := func()(fyne.Theme, error){
|
||||||
|
|
||||||
|
getAppThemeName := func()(string, error){
|
||||||
|
|
||||||
|
exists, currentAppTheme, err := globalSettings.GetSetting("AppTheme")
|
||||||
|
if (err != nil){
|
||||||
|
return "", errors.New("Seekia cannot access the filesystem: " + err.Error())
|
||||||
|
}
|
||||||
|
if (exists == false){
|
||||||
|
return "Light", nil
|
||||||
|
}
|
||||||
|
return currentAppTheme, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appThemeName, err := getAppThemeName()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
themeObject, err := getCustomFyneTheme(appThemeName)
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
return themeObject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appTheme, err := getAppTheme()
|
||||||
|
if (err != nil){
|
||||||
|
|
||||||
|
getThemeObject := func()fyne.Theme{
|
||||||
|
|
||||||
|
themeObject, err := getCustomFyneTheme("Light")
|
||||||
|
if (err != nil){
|
||||||
|
return theme.LightTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
return themeObject
|
||||||
|
}
|
||||||
|
|
||||||
|
themeObject := getThemeObject()
|
||||||
|
|
||||||
|
app.Settings().SetTheme(themeObject)
|
||||||
|
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, err, false, nil)
|
||||||
|
|
||||||
|
window.ShowAndRun()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Settings().SetTheme(appTheme)
|
||||||
|
|
||||||
|
window.SetCloseIntercept(func(){
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", "Shutdown")
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Shutting Down...")
|
||||||
|
|
||||||
|
progressBar := getWidgetCentered(widget.NewProgressBarInfinite())
|
||||||
|
|
||||||
|
description2 := getItalicLabelCentered("Seekia is shutting down background tasks.")
|
||||||
|
|
||||||
|
emptyLabel := widget.NewLabel("")
|
||||||
|
|
||||||
|
forceCloseButton := getWidgetCentered(widget.NewButtonWithIcon("Force Close", theme.CancelIcon(), func(){
|
||||||
|
window.Close()
|
||||||
|
}))
|
||||||
|
|
||||||
|
//TODO: Show the number of background tasks we are waiting to stop, along with their names
|
||||||
|
|
||||||
|
page := container.NewVBox(layout.NewSpacer(), description1, progressBar, description2, emptyLabel, forceCloseButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
stopJobsAndCloseWindowFunction := func(){
|
||||||
|
|
||||||
|
//TODO: Remove this time.Sleep once we actually have background tasks to close
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
backgroundJobs.StopBackgroundJobs()
|
||||||
|
|
||||||
|
badgerDatabase.StopDatabase()
|
||||||
|
|
||||||
|
//TODO: Add peer node, peer server, manual broadcasts, manual downloads
|
||||||
|
// We should also use goroutines to start the stop function for all background tasks
|
||||||
|
// This way they can all gracefully shutdown simultaneously while we wait for them to stop
|
||||||
|
|
||||||
|
window.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.SetContent(page)
|
||||||
|
|
||||||
|
go stopJobsAndCloseWindowFunction()
|
||||||
|
})
|
||||||
|
|
||||||
|
//TODO: On first startup, first show Choose Language page.
|
||||||
|
// Then show page describing legal risks of Seekia, including information about countries where Tor is illegal.
|
||||||
|
// It should not be a generic warning that users will not read.
|
||||||
|
// Users should actually stop if Seekia use is illegal and puts them at risk of prosecution.
|
||||||
|
|
||||||
|
setChooseAppUserPage(window)
|
||||||
|
|
||||||
|
window.ShowAndRun()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setChooseAppUserPage(window fyne.Window){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseAppUserPage(window)}
|
||||||
|
|
||||||
|
logoFileBytes := imageFiles.PNG_SeekiaLogo
|
||||||
|
logoGoImage, err := imagery.ConvertPNGImageFileBytesToGolangImage(logoFileBytes)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, err, true, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seekiaLogo := canvas.NewImageFromImage(logoGoImage)
|
||||||
|
seekiaLogo.FillMode = canvas.ImageFillContain
|
||||||
|
logoSize := getCustomFyneSize(30)
|
||||||
|
seekiaLogo.SetMinSize(logoSize)
|
||||||
|
|
||||||
|
beRaceAwareLabel := getItalicLabelCentered("Be Race Aware")
|
||||||
|
|
||||||
|
selectLanguageTitle := getBoldLabel("Language:")
|
||||||
|
|
||||||
|
currentLanguage := translation.GetMyLanguage()
|
||||||
|
selectLanguageButton := getWidgetCentered(widget.NewButton(currentLanguage, func(){
|
||||||
|
|
||||||
|
setSelectLanguagePage(window, false, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
selectLanguageRow := container.NewHBox(layout.NewSpacer(), selectLanguageTitle, selectLanguageButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
spacerA := widget.NewLabel("")
|
||||||
|
spacerB := widget.NewLabel("")
|
||||||
|
|
||||||
|
page := container.NewVBox(spacerA, spacerB, seekiaLogo, beRaceAwareLabel, widget.NewSeparator(), selectLanguageRow, widget.NewSeparator())
|
||||||
|
|
||||||
|
allUsersList, err := appUsers.GetAppUsersList()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, err, true, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(allUsersList) != 0){
|
||||||
|
|
||||||
|
selectUserLabel := getBoldLabelCentered("Select User:")
|
||||||
|
page.Add(selectUserLabel)
|
||||||
|
|
||||||
|
userButtonsGrid := container.NewGridWithColumns(1)
|
||||||
|
|
||||||
|
for _, userName := range allUsersList{
|
||||||
|
|
||||||
|
selectUserButton := widget.NewButtonWithIcon(userName, theme.AccountIcon(), func(){
|
||||||
|
|
||||||
|
setLoadingScreen(window, translate("Starting Up"), translate("Loading..."))
|
||||||
|
|
||||||
|
err := appUsers.SignInToAppUser(userName, true)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, err, true, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setHomePage(window)
|
||||||
|
})
|
||||||
|
|
||||||
|
selectUserButton.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
userButtonsGrid.Add(selectUserButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
userButtonsGridCentered := getContainerCentered(userButtonsGrid)
|
||||||
|
page.Add(userButtonsGridCentered)
|
||||||
|
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
createUserButton := widget.NewButtonWithIcon("Create User", theme.ContentAddIcon(), func(){
|
||||||
|
setCreateAppUserPage(window, currentPage, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (len(allUsersList) == 0){
|
||||||
|
|
||||||
|
createUserButtonCentered := getWidgetCentered(createUserButton)
|
||||||
|
|
||||||
|
page.Add(createUserButtonCentered)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
renameUserButton := widget.NewButtonWithIcon("Rename User", theme.DocumentCreateIcon(), func(){
|
||||||
|
setRenameAppUserPage(window, currentPage, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
deleteUserButton := widget.NewButtonWithIcon("Delete User", theme.DeleteIcon(), func(){
|
||||||
|
setDeleteAppUserPage(window, currentPage, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
userActionButtonsGrid := getContainerCentered(container.NewGridWithColumns(1, createUserButton, renameUserButton, deleteUserButton))
|
||||||
|
|
||||||
|
page.Add(userActionButtonsGrid)
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
page.Add(layout.NewSpacer())
|
||||||
|
|
||||||
|
pageScrollable := container.NewVScroll(page)
|
||||||
|
|
||||||
|
window.SetContent(pageScrollable)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setCreateAppUserPage(window fyne.Window, previousPage func(), afterCreatePage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setCreateAppUserPage(window, previousPage, afterCreatePage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Create User")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
userHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){
|
||||||
|
setAppUserExplainerPage(window, currentPage)
|
||||||
|
})
|
||||||
|
description1 := getBoldLabel("Create a Seekia user.")
|
||||||
|
|
||||||
|
description1Row := container.NewHBox(layout.NewSpacer(), description1, userHelpButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
description2 := widget.NewLabel("This name will not be shared publicly.")
|
||||||
|
|
||||||
|
nameEntry := widget.NewEntry()
|
||||||
|
nameEntry.PlaceHolder = "Enter name..."
|
||||||
|
|
||||||
|
nameEntryBoxed := getWidgetBoxed(nameEntry)
|
||||||
|
|
||||||
|
description2WithEntry := getContainerCentered(container.NewGridWithColumns(1, description2, nameEntryBoxed))
|
||||||
|
|
||||||
|
createUserButton := getWidgetCentered(widget.NewButtonWithIcon("Create", theme.ConfirmIcon(), func(){
|
||||||
|
|
||||||
|
newUserName := nameEntry.Text
|
||||||
|
|
||||||
|
if (newUserName == ""){
|
||||||
|
dialogTitle := translate("User Name Is Empty")
|
||||||
|
dialogMessageA := getLabelCentered(translate("The user name is empty."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("Enter a name."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(newUserName) > 30){
|
||||||
|
dialogTitle := translate("User Name Is Too Long")
|
||||||
|
dialogMessageA := getLabelCentered(translate("The user name is too long."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("It cannot exceed 30 characters."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllowed := appUsers.VerifyAppUserNameCharactersAreAllowed(newUserName)
|
||||||
|
if (isAllowed == false){
|
||||||
|
dialogTitle := translate("User Name Is Not Allowed")
|
||||||
|
dialogMessageA := getLabelCentered(translate("The user name is not allowed."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("It must contain only numbers and letters."))
|
||||||
|
dialogMessageC := getLabelCentered(translate("It cannot contain any spaces."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB, dialogMessageC)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
alreadyExists, err := appUsers.CreateAppUser(newUserName)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, err, true, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (alreadyExists == true){
|
||||||
|
dialogTitle := translate("User Name Is A Duplicate.")
|
||||||
|
dialogMessageA := getLabelCentered(translate("You already have a user with this name."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("Enter a unique name."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
afterCreatePage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1Row, description2WithEntry, createUserButton)
|
||||||
|
|
||||||
|
window.SetContent(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setAppUserExplainerPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Help - App User")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("To use Seekia, you must create an app user.")
|
||||||
|
description2 := getLabelCentered("Each app user has its own data folder.")
|
||||||
|
description3 := getLabelCentered("Each user can create their own Mate, Host, and Moderator identity.")
|
||||||
|
description4 := getLabelCentered("This allows you to create multiple identities and switch between them.")
|
||||||
|
description5 := getLabelCentered("For example, you could operate 2 moderator identities and switch between them.")
|
||||||
|
description6 := getLabelCentered("Most users will only need to create 1 user.")
|
||||||
|
description7 := getLabelCentered("Your user names are never uploaded or shared anywhere.")
|
||||||
|
description8 := getLabelCentered("You must export each user's data when transferring to a new device.")
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, description5, description6, description7, description8)
|
||||||
|
|
||||||
|
window.SetContent(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRenameAppUserPage(window fyne.Window, previousPage func(), afterRenamePage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setRenameAppUserPage(window, previousPage, afterRenamePage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Rename User")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
allUsersList, err := appUsers.GetAppUsersList()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, err, true, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(allUsersList) == 0){
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, errors.New("setRenameAppUserPage called when no users exist."), true, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectUserLabel := getBoldLabelCentered("Select User:")
|
||||||
|
|
||||||
|
userSelector := widget.NewSelect(allUsersList, nil)
|
||||||
|
userSelector.PlaceHolder = "Select user..."
|
||||||
|
|
||||||
|
userSelectorCentered := getWidgetCentered(userSelector)
|
||||||
|
|
||||||
|
enterNewNameLabel := getBoldLabel("Enter New Name:")
|
||||||
|
|
||||||
|
newNameEntry := widget.NewEntry()
|
||||||
|
newNameEntry.SetPlaceHolder("Enter new name...")
|
||||||
|
|
||||||
|
newNameEntryBoxed := getWidgetBoxed(newNameEntry)
|
||||||
|
|
||||||
|
newNameEntryWithLabel := getContainerCentered(container.NewGridWithColumns(1, enterNewNameLabel, newNameEntryBoxed))
|
||||||
|
|
||||||
|
renameButton := getWidgetCentered(widget.NewButtonWithIcon("Rename", theme.ConfirmIcon(), func(){
|
||||||
|
|
||||||
|
currentUserNameSelectedIndex := userSelector.SelectedIndex()
|
||||||
|
if (currentUserNameSelectedIndex == -1){
|
||||||
|
dialogTitle := translate("No User Selected")
|
||||||
|
dialogMessageA := getLabelCentered(translate("No user is selected."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("Select the user you want to rename."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUserName := userSelector.Selected
|
||||||
|
|
||||||
|
newUserName := newNameEntry.Text
|
||||||
|
|
||||||
|
if (newUserName == ""){
|
||||||
|
dialogTitle := translate("User Name Is Empty")
|
||||||
|
dialogMessageA := getLabelCentered(translate("The user name is empty."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("Enter a name."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(newUserName) > 30){
|
||||||
|
dialogTitle := translate("User Name Is Too Long")
|
||||||
|
dialogMessageA := getLabelCentered(translate("The user name is too long."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("It cannot exceed 30 characters."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllowed := appUsers.VerifyAppUserNameCharactersAreAllowed(newUserName)
|
||||||
|
if (isAllowed == false){
|
||||||
|
dialogTitle := translate("User Name Is Not Allowed")
|
||||||
|
dialogMessageA := getLabelCentered(translate("The user name is not allowed."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("It must contain only numbers and letters."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
alreadyExists := slices.Contains(allUsersList, newUserName)
|
||||||
|
if (alreadyExists == true){
|
||||||
|
dialogTitle := translate("User Name Is A Duplicate.")
|
||||||
|
dialogMessageA := getLabelCentered(translate("You already have a user with this name."))
|
||||||
|
dialogMessageB := getLabelCentered(translate("Enter a unique name."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingScreen(window, translate("Rename User"), translate("Renaming User..."))
|
||||||
|
|
||||||
|
userFound, err := appUsers.RenameAppUser(currentUserName, newUserName)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, err, true, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (userFound == false){
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, errors.New("App user not found after being found already."), true, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
afterRenamePage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), selectUserLabel, userSelectorCentered, newNameEntryWithLabel, renameButton)
|
||||||
|
|
||||||
|
window.SetContent(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setDeleteAppUserPage(window fyne.Window, previousPage func(), afterDeletePage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setDeleteAppUserPage(window, previousPage, afterDeletePage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Delete User")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description := getBoldLabelCentered("Choose the user to delete.")
|
||||||
|
|
||||||
|
allUsersList, err := appUsers.GetAppUsersList()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, err, true, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(allUsersList) == 0){
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, errors.New("setDeleteAppUserPage called when no users exist."), true, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userButtonsGrid := container.NewGridWithColumns(1)
|
||||||
|
|
||||||
|
for _, userName := range allUsersList{
|
||||||
|
|
||||||
|
selectUserButton := widget.NewButtonWithIcon(userName, theme.AccountIcon(), func(){
|
||||||
|
setConfirmDeleteAppUserPage(window, userName, currentPage, afterDeletePage)
|
||||||
|
})
|
||||||
|
|
||||||
|
userButtonsGrid.Add(selectUserButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
userButtonsGridCentered := getContainerCentered(userButtonsGrid)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), userButtonsGridCentered)
|
||||||
|
|
||||||
|
window.SetContent(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConfirmDeleteAppUserPage(window fyne.Window, userName string, previousPage func(), afterDeletePage func()){
|
||||||
|
|
||||||
|
setLoadingScreen(window, translate("Delete User"), translate("Loading Delete User Page..."))
|
||||||
|
|
||||||
|
currentPage := func(){setConfirmDeleteAppUserPage(window, userName, previousPage, afterDeletePage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Delete User")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
// We sign in to the user to see if there are any user identities
|
||||||
|
|
||||||
|
getNumberOfUserIdentities := func()(int, error){
|
||||||
|
|
||||||
|
err := appUsers.SignInToAppUser(userName, false)
|
||||||
|
if (err != nil){ return 0, err }
|
||||||
|
|
||||||
|
numberOfIdentities := 0
|
||||||
|
|
||||||
|
mateIdentityExists, _, err := mySeedPhrases.GetMySeedPhrase("Mate")
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
if (mateIdentityExists == true){
|
||||||
|
numberOfIdentities += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
hostIdentityExists, _, err := mySeedPhrases.GetMySeedPhrase("Host")
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
if (hostIdentityExists == true){
|
||||||
|
numberOfIdentities += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
moderatorIdentityExists, _, err := mySeedPhrases.GetMySeedPhrase("Moderator")
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
if (moderatorIdentityExists == true){
|
||||||
|
numberOfIdentities += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
err = appUsers.SignOutOfAppUser()
|
||||||
|
if (err != nil){ return 0, err }
|
||||||
|
|
||||||
|
return numberOfIdentities, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfIdentities, err := getNumberOfUserIdentities()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, err, true, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (numberOfIdentities != 0){
|
||||||
|
|
||||||
|
getNumberOfIdentitiesDescription := func()string{
|
||||||
|
if (numberOfIdentities == 1){
|
||||||
|
return "This user has 1 identity."
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfIdentitiesString := helpers.ConvertIntToString(numberOfIdentities)
|
||||||
|
|
||||||
|
return "This user has " + numberOfIdentitiesString + " identities."
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfIdentitiesDescription := getNumberOfIdentitiesDescription()
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered(numberOfIdentitiesDescription)
|
||||||
|
description2 := getLabelCentered("You must delete all user identities before deleting the user.")
|
||||||
|
description3 := getLabelCentered("Delete your identities on the Settings - My Data - Delete Identity page.")
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3)
|
||||||
|
window.SetContent(page)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
description1 := getBoldLabelCentered("Are you sure you want to delete this user?")
|
||||||
|
|
||||||
|
nameLabel := widget.NewLabel("Name:")
|
||||||
|
userNameLabel := getBoldLabel(userName)
|
||||||
|
|
||||||
|
userNameRow := container.NewHBox(layout.NewSpacer(), nameLabel, userNameLabel, layout.NewSpacer())
|
||||||
|
|
||||||
|
description2 := getLabelCentered("This will delete all of your user data.")
|
||||||
|
description3 := getLabelCentered("This includes your desires, genetic analyses, genomes, and settings.")
|
||||||
|
description4 := getLabelCentered("Export your data on the Settings - My Data - Export Data page to retain your data.")
|
||||||
|
|
||||||
|
deleteButton := getWidgetCentered(widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func(){
|
||||||
|
|
||||||
|
setLoadingScreen(window, translate("Delete User"), translate("Deleting User..."))
|
||||||
|
|
||||||
|
userExists, err := appUsers.DeleteAppUser(userName)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, err, true, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (userExists == false){
|
||||||
|
setErrorEncounteredPage_NoNavBar(window, errors.New("setConfirmDeleteAppUserPage called when user does not exist."), true, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
afterDeletePage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, userNameRow, description2, description3, description4, deleteButton)
|
||||||
|
|
||||||
|
window.SetContent(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
1150
gui/statisticsGui.go
Normal file
1150
gui/statisticsGui.go
Normal file
File diff suppressed because it is too large
Load diff
117
gui/syncGui.go
Normal file
117
gui/syncGui.go
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// syncGui.go implements pages to manage the app connection to the Seekia network
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
|
||||||
|
import "seekia/internal/appMemory"
|
||||||
|
|
||||||
|
func setSyncPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", "Sync")
|
||||||
|
|
||||||
|
currentPage := func(){setSyncPage(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Sync")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
syncDescription := getLabelCentered("Manage your connection to the Seekia network.")
|
||||||
|
|
||||||
|
settingsIcon, err := getFyneImageIcon("Settings")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settingsButton := widget.NewButton(translate("Settings"), func(){
|
||||||
|
setManageNetworkSettingsPage(window, currentPage)
|
||||||
|
})
|
||||||
|
settingsButtonWithIcon := container.NewGridWithColumns(1, settingsIcon, settingsButton)
|
||||||
|
|
||||||
|
logsIcon, err := getFyneImageIcon("Choice")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logsButton := widget.NewButton(translate("Logs"), func(){
|
||||||
|
setViewLogsPage(window, "Network", currentPage)
|
||||||
|
})
|
||||||
|
logsButtonWithIcon := container.NewGridWithColumns(1, logsIcon, logsButton)
|
||||||
|
|
||||||
|
peersIcon, err := getFyneImageIcon("Host")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peersButton := widget.NewButton(translate("Peers"), func(){
|
||||||
|
//TODO
|
||||||
|
// This should show a page where we can view all connected peers
|
||||||
|
// It should show a display similar to a torrenting client, where we can see information such as
|
||||||
|
// how much data has been uploaded/downloaded from each peer and the IP of each peer
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
peersButtonWithIcon := container.NewGridWithColumns(1, peersIcon, peersButton)
|
||||||
|
|
||||||
|
buttonsRow := getContainerCentered(container.NewGridWithRows(1, settingsButtonWithIcon, logsButtonWithIcon, peersButtonWithIcon))
|
||||||
|
|
||||||
|
/*
|
||||||
|
//TODO
|
||||||
|
syncOnOffStatus := "Off"
|
||||||
|
|
||||||
|
syncOnOffStatusTitle := widget.NewLabel("Sync Status:")
|
||||||
|
syncOnOffStatusLabel := getBoldLabel(syncOnOffStatus)
|
||||||
|
|
||||||
|
// We show a button to start and stop the client's connection to the network
|
||||||
|
// This is useful for users who want to use Seekia in offline-only mode
|
||||||
|
// This way, Seekia will not connect to any servers, and the user can use the app for offline activities such as analyzing genomes
|
||||||
|
getStartStopSyncButton := func() fyne.Widget{
|
||||||
|
if (syncOnOffStatus == "Off"){
|
||||||
|
startButton := widget.NewButtonWithIcon(translate("Start"), theme.MediaPlayIcon(), func(){
|
||||||
|
//TODO
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
return startButton
|
||||||
|
}
|
||||||
|
stopButton := widget.NewButtonWithIcon(translate("Stop"), theme.MediaStopIcon(), func(){
|
||||||
|
//TODO
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
return stopButton
|
||||||
|
}
|
||||||
|
|
||||||
|
startStopSyncButton := getStartStopSyncButton()
|
||||||
|
|
||||||
|
syncStartStopRow := container.NewHBox(layout.NewSpacer(), syncOnOffStatusTitle, syncOnOffStatusLabel, startStopSyncButton, layout.NewSpacer())
|
||||||
|
*/
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Seekia is under construction.")
|
||||||
|
description2 := getLabelCentered("The app is unable to connect to other peers.")
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), syncDescription, widget.NewSeparator(), buttonsRow, widget.NewSeparator(), description1, description2)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setManageNetworkConnectionPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Manage Network Connection")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description := getBoldLabelCentered("Under Construction")
|
||||||
|
|
||||||
|
//TODO: This page should show information about the network connection
|
||||||
|
// The user should be able to see if the app is currently able to connect to other peers over Tor and clearnet
|
||||||
|
// They should also see a realtime description of how much data is being uploaded and downloaded in megabytes per second
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
287
gui/themeGui.go
Normal file
287
gui/themeGui.go
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// themeGui.go contains code to generate a custom fyne theme
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
|
||||||
|
import "seekia/internal/imagery"
|
||||||
|
|
||||||
|
import "image/color"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
|
||||||
|
// We use this to define a custom fyne theme
|
||||||
|
type customTheme struct{
|
||||||
|
themeName string
|
||||||
|
defaultTheme fyne.Theme
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCustomFyneTheme(themeName string)(fyne.Theme, error){
|
||||||
|
|
||||||
|
getStandardThemeObject := func()(fyne.Theme, error){
|
||||||
|
|
||||||
|
if (themeName == "Light" || themeName == "Love" || themeName == "Ocean"){
|
||||||
|
result := theme.LightTheme()
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
if (themeName == "Dark"){
|
||||||
|
result := theme.DarkTheme()
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("getCustomFyneTheme called with invalid themeName: " + themeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
standardThemeObject, err := getStandardThemeObject()
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
newTheme := customTheme{
|
||||||
|
themeName: themeName,
|
||||||
|
defaultTheme: standardThemeObject,
|
||||||
|
}
|
||||||
|
|
||||||
|
return newTheme, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// This function is used to define our custom fyne themes
|
||||||
|
// It changes a few default colors, while leaving all other colors the same as the default theme
|
||||||
|
func (input customTheme)Color(colorName fyne.ThemeColorName, variant fyne.ThemeVariant)color.Color{
|
||||||
|
|
||||||
|
//TODO: Make these colors better and create more themes
|
||||||
|
|
||||||
|
themeName := input.themeName
|
||||||
|
|
||||||
|
switch colorName{
|
||||||
|
|
||||||
|
case theme.ColorNameForeground:{
|
||||||
|
|
||||||
|
switch themeName{
|
||||||
|
|
||||||
|
case "Light", "Love", "Ocean":{
|
||||||
|
|
||||||
|
newColor := color.Black
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
case "Dark":{
|
||||||
|
|
||||||
|
newColor := color.White
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case theme.ColorNameSeparator:{
|
||||||
|
|
||||||
|
// This is the color used for separators
|
||||||
|
|
||||||
|
switch themeName{
|
||||||
|
|
||||||
|
case "Light":{
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("b3b3b3")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "Ocean":{
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("646464")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:{
|
||||||
|
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("999999")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case theme.ColorNameInputBackground:{
|
||||||
|
|
||||||
|
// This color is used for the background of input elements such as text entries
|
||||||
|
|
||||||
|
switch themeName{
|
||||||
|
|
||||||
|
case "Light":{
|
||||||
|
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("b3b3b3")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "Love":{
|
||||||
|
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("ffbbbe")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "Ocean":{
|
||||||
|
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("ccd1ef")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:{
|
||||||
|
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("999999")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case theme.ColorNameBackground:{
|
||||||
|
|
||||||
|
// This is the color used for the app background
|
||||||
|
|
||||||
|
switch themeName{
|
||||||
|
|
||||||
|
case "Love":{
|
||||||
|
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("ff7a80")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "Ocean":{
|
||||||
|
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("7f6fff")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case theme.ColorNameOverlayBackground:{
|
||||||
|
|
||||||
|
// This is the color used for backgrounds of overlays like dialogs.
|
||||||
|
|
||||||
|
switch themeName{
|
||||||
|
|
||||||
|
case "Love":{
|
||||||
|
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("ffabaf")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "Ocean":{
|
||||||
|
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("a59aff")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case theme.ColorNameButton:{
|
||||||
|
|
||||||
|
// This is the color used for buttons
|
||||||
|
|
||||||
|
switch themeName{
|
||||||
|
|
||||||
|
case "Light":{
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("d8d8d8")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "Love":{
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("7664ff")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "Ocean":{
|
||||||
|
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("ccd1ef")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "Dark":{
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("4d4d4d")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case theme.ColorNamePlaceHolder:{
|
||||||
|
|
||||||
|
// This is the color used for text
|
||||||
|
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("4d4d4d")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case theme.ColorNamePrimary:{
|
||||||
|
|
||||||
|
// This color is used for high importance buttons
|
||||||
|
|
||||||
|
switch themeName{
|
||||||
|
|
||||||
|
case "Love", "Ocean":{
|
||||||
|
|
||||||
|
newColor, err := imagery.GetColorObjectFromColorCode("1300a8")
|
||||||
|
if (err == nil){
|
||||||
|
return newColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We will use the default color for this theme
|
||||||
|
return input.defaultTheme.Color(colorName, variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our custom themes change nothing about the default theme fonts
|
||||||
|
func (input customTheme)Font(style fyne.TextStyle)fyne.Resource{
|
||||||
|
|
||||||
|
themeFont := input.defaultTheme.Font(style)
|
||||||
|
|
||||||
|
return themeFont
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our custom themes change nothing about the default theme icons
|
||||||
|
func (input customTheme)Icon(iconName fyne.ThemeIconName)fyne.Resource{
|
||||||
|
|
||||||
|
themeIcon := input.defaultTheme.Icon(iconName)
|
||||||
|
|
||||||
|
return themeIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
func (input customTheme)Size(name fyne.ThemeSizeName)float32{
|
||||||
|
|
||||||
|
themeSize := input.defaultTheme.Size(name)
|
||||||
|
|
||||||
|
if (name == theme.SizeNameText){
|
||||||
|
|
||||||
|
// After fyne v2.3.0, text labels are no longer the same height as buttons
|
||||||
|
// We increase the text size so that a text label is the same height as a button
|
||||||
|
// We need to increase text size because we are creating grids by creating multiple VBoxes, and connecting them with an HBox
|
||||||
|
//
|
||||||
|
// If we could create grids in a different way, we could avoid having to do this
|
||||||
|
// Example: Create a new grid type: container.NewThinGrid?
|
||||||
|
// -The columns will only be as wide as the the widest element within them
|
||||||
|
// -We can add separators between each row (grid.ShowRowLines = true) or between columns (grid.ShowColumnLines = true)
|
||||||
|
// -We can add borders (grid.ShowTopBorder = true, grid.ShowBottomBorder = true, grid.ShowLeftBorder = true, grid.ShowRightBorder = true)
|
||||||
|
|
||||||
|
// Using a different grid type is the solution we need to eventually use
|
||||||
|
// Then, we can show the user an option to increase the text size globally, and all grids will still render correctly
|
||||||
|
|
||||||
|
result := themeSize * 1.08
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return themeSize
|
||||||
|
}
|
||||||
|
|
||||||
|
|
737
gui/toolsGui.go
Normal file
737
gui/toolsGui.go
Normal file
|
@ -0,0 +1,737 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// toolsGui.go implements pages to use Seekia tools
|
||||||
|
// Tools are various utilities to perform tasks such as generating identity hashes, verifying memos, and more
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/data/binding"
|
||||||
|
import "fyne.io/fyne/v2/dialog"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
|
import "seekia/internal/cryptocurrency/cardanoAddress"
|
||||||
|
import "seekia/internal/cryptocurrency/ethereumAddress"
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/identity"
|
||||||
|
import "seekia/internal/memos/createMemos"
|
||||||
|
import "seekia/internal/memos/readMemos"
|
||||||
|
import "seekia/internal/myIdentity"
|
||||||
|
import "seekia/internal/mySettings"
|
||||||
|
import "seekia/internal/seedPhrase"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func setToolsPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setToolsPage(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Tools")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
moderateButton := widget.NewButton("Moderate", func(){
|
||||||
|
setModeratePage(window, true, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
hostButton := widget.NewButton(translate("Host"), func(){
|
||||||
|
setHostPage(window, true, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
networkStatisticsButton := widget.NewButton(translate("Network Statistics"), func(){
|
||||||
|
setNetworkStatisticsPage(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
generateCustomIdentityHashButton := widget.NewButton("Generate Custom Identity Hash", func(){
|
||||||
|
setChooseIdentityTypeForNewIdentityHashPage(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
deriveIdentityScoreAddressButton := widget.NewButton("Derive Identity Score Addresses", func(){
|
||||||
|
setDeriveIdentityScoreAddressesPage(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
createMemoButton := widget.NewButton("Create Memo", func(){
|
||||||
|
setCreateMemoPage(window, false, "", currentPage)
|
||||||
|
})
|
||||||
|
verifyMemoButton := widget.NewButton("Verify Memo", func(){
|
||||||
|
setVerifyMemoPage(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonsGrid := container.NewGridWithColumns(1, moderateButton, hostButton, networkStatisticsButton, generateCustomIdentityHashButton, deriveIdentityScoreAddressButton, createMemoButton, verifyMemoButton)
|
||||||
|
|
||||||
|
buttonsGridCentered := getContainerCentered(buttonsGrid)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), buttonsGridCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This page is used to generate a new identity hash from the tools menu, not for the user's own identity
|
||||||
|
// The identity hash that is generated will not be saved to the disk when the user exits the utility.
|
||||||
|
func setChooseIdentityTypeForNewIdentityHashPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setChooseIdentityTypeForNewIdentityHashPage(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Create New Identity Hash")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Choose the identity type for your new identity hash.")
|
||||||
|
description2 := getLabelCentered("The last character of the identity hash will be different for each identity type.")
|
||||||
|
description3 := getLabelCentered("Mate = m, Host = h, Moderator = r")
|
||||||
|
description4 := getLabelCentered("You can change this later by importing the seed phrase as a different identity type.")
|
||||||
|
|
||||||
|
getChooseIdentityTypeButtonWithIcon := func(identityType string)(*fyne.Container, error){
|
||||||
|
|
||||||
|
identityTypeIcon, err := getIdentityTypeIcon(identityType, -2)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
submitPage := func(seedPhrase string, previousPageFunction func()){
|
||||||
|
setViewNewSeedPhrasePage(window, identityType, seedPhrase, previousPageFunction, currentPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseIdentityTypeButton := widget.NewButton(identityType, func(){
|
||||||
|
setCreateCustomIdentityHashPage(window, identityType, currentPage, submitPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
buttonWithIcon := container.NewGridWithColumns(1, identityTypeIcon, chooseIdentityTypeButton)
|
||||||
|
|
||||||
|
return buttonWithIcon, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mateButtonWithIcon, err := getChooseIdentityTypeButtonWithIcon("Mate")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hostButtonWithIcon, err := getChooseIdentityTypeButtonWithIcon("Host")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
moderatorButtonWithIcon, err := getChooseIdentityTypeButtonWithIcon("Moderator")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseIdentityTypeButtonsRow := getContainerCentered(container.NewGridWithRows(1, mateButtonWithIcon, hostButtonWithIcon, moderatorButtonWithIcon))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, description4, widget.NewSeparator(), chooseIdentityTypeButtonsRow)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This page is used to view a newly generated seed phrase from the generation tool.
|
||||||
|
// The seed phrase is not saved anywhere
|
||||||
|
func setViewNewSeedPhrasePage(window fyne.Window, identityType string, newSeedPhrase string, previousPage func(), nextPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("View New Identity Hash")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Here is your generated identity hash.")
|
||||||
|
|
||||||
|
newSeedPhraseHash, err := seedPhrase.ConvertSeedPhraseToSeedPhraseHash(newSeedPhrase)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newIdentityHash, err := identity.GetIdentityHashFromSeedPhraseHash(newSeedPhraseHash, identityType)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(newIdentityHash)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identityHashBox := getContainerCentered(getWidgetBoxed(getBoldLabel(newIdentityHashString)))
|
||||||
|
|
||||||
|
doneButton := getWidgetCentered(widget.NewButtonWithIcon("Done", theme.ConfirmIcon(), nextPage))
|
||||||
|
|
||||||
|
description2 := getBoldLabelCentered("When you press Done, this identity hash will be deleted.")
|
||||||
|
description3 := getLabelCentered("Write down this seed phrase to save this identity.")
|
||||||
|
|
||||||
|
seedPhraseLabel := widget.NewMultiLineEntry()
|
||||||
|
seedPhraseLabel.Wrapping = 3
|
||||||
|
seedPhraseLabel.SetText(newSeedPhrase)
|
||||||
|
seedPhraseLabel.OnChanged = func(_ string){
|
||||||
|
seedPhraseLabel.SetText(newSeedPhrase)
|
||||||
|
}
|
||||||
|
seedPhraseLabelBoxed := getWidgetBoxed(seedPhraseLabel)
|
||||||
|
widener := widget.NewLabel(" ")
|
||||||
|
seedPhraseLabelWidened := getContainerCentered(container.NewGridWithColumns(1, seedPhraseLabelBoxed, widener))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, identityHashBox, doneButton, widget.NewSeparator(), description2, description3, seedPhraseLabelWidened)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setDeriveIdentityScoreAddressesPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Derive Identity Score Addresses")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Convert a moderator's identity hash to their identity score cryptocurrency addresses.")
|
||||||
|
description2 := getLabelCentered("Send cryptocurrency to these addresses to increase the user's identity score.")
|
||||||
|
|
||||||
|
enterIdentityHashLabel := getBoldLabelCentered(" Enter Identity Hash: ")
|
||||||
|
|
||||||
|
identityHashEntry := widget.NewEntry()
|
||||||
|
identityHashEntry.SetPlaceHolder("Enter Identity Hash.")
|
||||||
|
identityHashEntryBoxed := getWidgetBoxed(identityHashEntry)
|
||||||
|
|
||||||
|
enterIdentityHashLabelWithEntry := getContainerCentered(container.NewGridWithColumns(1, enterIdentityHashLabel, identityHashEntryBoxed))
|
||||||
|
|
||||||
|
ethereumLabel := getBoldLabelCentered(" Ethereum: ")
|
||||||
|
|
||||||
|
cardanoLabel := getBoldLabelCentered("Cardano:")
|
||||||
|
|
||||||
|
addressBinding_Ethereum := binding.NewString()
|
||||||
|
|
||||||
|
addressBinding_Cardano := binding.NewString()
|
||||||
|
|
||||||
|
addressEntry_Ethereum := widget.NewEntry()
|
||||||
|
addressEntry_Cardano := widget.NewEntry()
|
||||||
|
|
||||||
|
addressEntryOnChangedFunction_Ethereum := func(_ string){
|
||||||
|
currentAddress, err := addressBinding_Ethereum.Get()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addressEntry_Ethereum.SetText(currentAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
addressEntryOnChangedFunction_Cardano := func(_ string){
|
||||||
|
currentAddress, err := addressBinding_Cardano.Get()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addressEntry_Cardano.SetText(currentAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
addressEntry_Ethereum.OnChanged = addressEntryOnChangedFunction_Ethereum
|
||||||
|
addressEntry_Ethereum.SetPlaceHolder("The Ethereum identity score address will display here.")
|
||||||
|
addressEntryBoxed_Ethereum := getWidgetBoxed(addressEntry_Ethereum)
|
||||||
|
|
||||||
|
addressEntry_Cardano.OnChanged = addressEntryOnChangedFunction_Cardano
|
||||||
|
addressEntry_Cardano.SetPlaceHolder("The Cardano identity score address will display here.")
|
||||||
|
addressEntryBoxed_Cardano := getWidgetBoxed(addressEntry_Cardano)
|
||||||
|
|
||||||
|
deriveButton := getWidgetCentered(widget.NewButtonWithIcon("Derive", theme.MoveDownIcon(), func(){
|
||||||
|
inputIdentityHashString := identityHashEntry.Text
|
||||||
|
if (inputIdentityHashString == "Admin"){
|
||||||
|
setAdminToolsPage(window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (inputIdentityHashString == ""){
|
||||||
|
addressBinding_Ethereum.Set("")
|
||||||
|
addressBinding_Cardano.Set("")
|
||||||
|
addressEntry_Ethereum.SetText("")
|
||||||
|
addressEntry_Cardano.SetText("")
|
||||||
|
dialogTitle := translate("No Identity Hash Provided")
|
||||||
|
dialogMessage := getLabelCentered("You must enter an identity hash.")
|
||||||
|
dialogContent := container.NewVBox(dialogMessage)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inputIdentityHash, identityType, err := identity.ReadIdentityHashString(inputIdentityHashString)
|
||||||
|
if (err != nil){
|
||||||
|
addressBinding_Ethereum.Set("")
|
||||||
|
addressBinding_Cardano.Set("")
|
||||||
|
addressEntry_Ethereum.SetText("")
|
||||||
|
addressEntry_Cardano.SetText("")
|
||||||
|
dialogTitle := translate("Identity Hash Is Invalid")
|
||||||
|
dialogMessage1 := getLabelCentered("The identity hash you have entered is invalid.")
|
||||||
|
dialogMessage2 := getLabelCentered("Identity hashes are 27 characters long.")
|
||||||
|
dialogContent := container.NewVBox(dialogMessage1, dialogMessage2)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (identityType != "Moderator"){
|
||||||
|
addressBinding_Ethereum.Set("")
|
||||||
|
addressBinding_Cardano.Set("")
|
||||||
|
addressEntry_Ethereum.SetText("")
|
||||||
|
addressEntry_Cardano.SetText("")
|
||||||
|
dialogTitle := translate("Identity Hash Is Invalid")
|
||||||
|
dialogMessage1 := getLabelCentered("The identity hash you have entered is not a moderator identity.")
|
||||||
|
dialogMessage2 := getLabelCentered("Moderator identity hashes end with an r character.")
|
||||||
|
dialogContent := container.NewVBox(dialogMessage1, dialogMessage2)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identityScoreAddress_Ethereum, err := ethereumAddress.GetIdentityScoreEthereumAddressFromIdentityHash(inputIdentityHash)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = addressBinding_Ethereum.Set(identityScoreAddress_Ethereum)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addressEntry_Ethereum.SetText(identityScoreAddress_Ethereum)
|
||||||
|
|
||||||
|
identityScoreAddress_Cardano, err := cardanoAddress.GetIdentityScoreCardanoAddressFromIdentityHash(inputIdentityHash)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = addressBinding_Cardano.Set(identityScoreAddress_Cardano)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addressEntry_Cardano.SetText(identityScoreAddress_Cardano)
|
||||||
|
|
||||||
|
}))
|
||||||
|
|
||||||
|
resultAddressesSection := getContainerCentered(container.NewGridWithColumns(1, ethereumLabel, addressEntryBoxed_Ethereum, cardanoLabel, addressEntryBoxed_Cardano))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), enterIdentityHashLabelWithEntry, deriveButton, widget.NewSeparator(), resultAddressesSection)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setCreateMemoPage(window fyne.Window, messageExists bool, messageText string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setCreateMemoPage(window, messageExists, messageText, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Create Memo")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description := widget.NewLabel("This page allows you to create a Seekia memo.")
|
||||||
|
memoHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){
|
||||||
|
setMemoExplainerPage(window, currentPage)
|
||||||
|
})
|
||||||
|
descriptionRow := container.NewHBox(layout.NewSpacer(), description, memoHelpButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
identityTypesList := []string{"Mate", "Host", "Moderator"}
|
||||||
|
|
||||||
|
myIdentityHashesList := make([]string, 0)
|
||||||
|
|
||||||
|
for _, identityType := range identityTypesList{
|
||||||
|
|
||||||
|
myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash(identityType)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (myIdentityExists == false){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
myIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(myIdentityHash)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myIdentityHashesList = append(myIdentityHashesList, myIdentityHashString)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(myIdentityHashesList) == 0){
|
||||||
|
|
||||||
|
description2 := getBoldLabelCentered("No identities exist.")
|
||||||
|
description3 := getLabelCentered("You must create a Seekia identity to create a memo.")
|
||||||
|
description4 := getLabelCentered("Create your identity on the Settings - My Data - My Identity Hashes page.")
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionRow, widget.NewSeparator(), description2, description3, description4)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseIdentityLabel := getBoldLabelCentered("Choose Identity:")
|
||||||
|
|
||||||
|
chooseIdentitySelector := widget.NewSelect(myIdentityHashesList, nil)
|
||||||
|
chooseIdentitySelector.Selected = myIdentityHashesList[0]
|
||||||
|
|
||||||
|
chooseIdentitySelectorCentered := getWidgetCentered(chooseIdentitySelector)
|
||||||
|
|
||||||
|
chooseDecorationLabel := getBoldLabelCentered("Choose Decoration:")
|
||||||
|
|
||||||
|
decorationOptionsList := []string{
|
||||||
|
"«« Seekia Memo »»",
|
||||||
|
"⁕ Seekia Memo ⁕",
|
||||||
|
"⁂ Seekia Memo ⁂",
|
||||||
|
"※ Seekia Memo ※",
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseDecorationSelector := widget.NewSelect(decorationOptionsList, func(newDecoration string){
|
||||||
|
err := mySettings.SetSetting("MemoDecoration", newDecoration)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
getCurrentMemoDecoration := func()(string, error){
|
||||||
|
|
||||||
|
exists, memoDecoration, err := mySettings.GetSetting("MemoDecoration")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (exists == false){
|
||||||
|
return "«« Seekia Memo »»", nil
|
||||||
|
}
|
||||||
|
return memoDecoration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentMemoDecoration, err := getCurrentMemoDecoration()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseDecorationSelector.Selected = currentMemoDecoration
|
||||||
|
|
||||||
|
chooseDecorationSelectorCentered := getWidgetCentered(chooseDecorationSelector)
|
||||||
|
|
||||||
|
chooseMessageOptionsGrid := getContainerCentered(container.NewGridWithColumns(2, chooseIdentityLabel, chooseDecorationLabel, chooseIdentitySelectorCentered, chooseDecorationSelectorCentered))
|
||||||
|
|
||||||
|
enterTextLabel := getBoldLabelCentered("Enter Message:")
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionRow, widget.NewSeparator(), chooseMessageOptionsGrid, widget.NewSeparator(), enterTextLabel)
|
||||||
|
|
||||||
|
enterMessageEntry := widget.NewMultiLineEntry()
|
||||||
|
if (messageExists == true){
|
||||||
|
enterMessageEntry.SetText(messageText)
|
||||||
|
} else {
|
||||||
|
enterMessageEntry.SetPlaceHolder("Enter Message...")
|
||||||
|
}
|
||||||
|
|
||||||
|
createMemoButton := getWidgetCentered(widget.NewButtonWithIcon("Create Memo", theme.ConfirmIcon(), func(){
|
||||||
|
|
||||||
|
newMemoMessage := enterMessageEntry.Text
|
||||||
|
|
||||||
|
if (newMemoMessage == ""){
|
||||||
|
title := translate("No Message Provided")
|
||||||
|
dialogMessage := getLabelCentered(translate("You must enter a message to create a memo."))
|
||||||
|
dialogContent := container.NewVBox(dialogMessage)
|
||||||
|
dialog.ShowCustom(title, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myIdentityHashString := chooseIdentitySelector.Selected
|
||||||
|
|
||||||
|
myIdentityHash, _, err := identity.ReadIdentityHashString(myIdentityHashString)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
identityFound, myPublicIdentityKey, myPrivateIdentityKey, err := myIdentity.GetMyPublicPrivateIdentityKeysFromIdentityHash(myIdentityHash)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (identityFound == false){
|
||||||
|
setErrorEncounteredPage(window, errors.New("My identity not found after being found already."), currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
myIdentityType, err := identity.GetIdentityTypeFromIdentityHash(myIdentityHash)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getNewMemoDecorations := func()(string, string, error){
|
||||||
|
|
||||||
|
exists, myMemoDecoration, err := mySettings.GetSetting("MemoDecoration")
|
||||||
|
if (err != nil) { return "", "", err }
|
||||||
|
if (exists == false){
|
||||||
|
return "««", "»»", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (myMemoDecoration == "«« Seekia Memo »»"){
|
||||||
|
return "««", "»»", nil
|
||||||
|
}
|
||||||
|
if (myMemoDecoration == "⁕ Seekia Memo ⁕"){
|
||||||
|
return "⁕", "⁕", nil
|
||||||
|
}
|
||||||
|
if (myMemoDecoration == "⁂ Seekia Memo ⁂"){
|
||||||
|
return "⁂", "⁂", nil
|
||||||
|
}
|
||||||
|
if (myMemoDecoration == "※ Seekia Memo ※"){
|
||||||
|
return "※", "※", nil
|
||||||
|
}
|
||||||
|
return "", "", errors.New("MySettings contains unknown MemoDecoration: " + myMemoDecoration)
|
||||||
|
}
|
||||||
|
|
||||||
|
newMemoDecorationPrefix, newMemoDecorationSuffix, err := getNewMemoDecorations()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newMemoString, err := createMemos.CreateMemo(myPublicIdentityKey, myPrivateIdentityKey, myIdentityType, newMemoDecorationPrefix, newMemoDecorationSuffix, newMemoMessage)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use this function so that the current memo is not lost if the user goes back
|
||||||
|
nextPagePreviousPage := func(){
|
||||||
|
setCreateMemoPage(window, true, newMemoMessage, previousPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
setViewCreatedMemoButton(window, newMemoString, nextPagePreviousPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
emptyLabel := widget.NewLabel("")
|
||||||
|
createMemoButtonWithSpacer := container.NewVBox(createMemoButton, emptyLabel)
|
||||||
|
|
||||||
|
page := container.NewBorder(header, createMemoButtonWithSpacer, nil, nil, enterMessageEntry)
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setViewCreatedMemoButton(window fyne.Window, memoString string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setViewCreatedMemoButton(window, memoString, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered(translate("View Memo"))
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Here is your new Memo.")
|
||||||
|
description2 := getLabelCentered("Send funds to the crypto addresses to timestamp your memo.")
|
||||||
|
|
||||||
|
memoIsValid, memoHash, _, _, err := readMemos.ReadMemo(memoString)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (memoIsValid == false){
|
||||||
|
setErrorEncounteredPage(window, errors.New("setViewCreatedMemoButton called with invalid memo."), previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewCryptocurrencyAddressesButton := getWidgetCentered(widget.NewButtonWithIcon("View Cryptocurrency Addresses", theme.VisibilityIcon(), func(){
|
||||||
|
setViewMemoCryptocurrencyAddressesPage(window, memoHash, "Ethereum", currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
memoHashTitle := widget.NewLabel("Memo Hash:")
|
||||||
|
|
||||||
|
memoHashHex := encoding.EncodeBytesToHexString(memoHash[:])
|
||||||
|
|
||||||
|
memoHashTrimmed, _, err := helpers.TrimAndFlattenString(memoHashHex, 15)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
memoHashLabel := getBoldLabel(memoHashTrimmed)
|
||||||
|
|
||||||
|
viewMemoHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){
|
||||||
|
setViewContentHashPage(window, "Memo", memoHash[:], currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
memoHashRow := container.NewHBox(layout.NewSpacer(), memoHashTitle, memoHashLabel, viewMemoHashButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, widget.NewSeparator(), viewCryptocurrencyAddressesButton, widget.NewSeparator(), memoHashRow, widget.NewSeparator())
|
||||||
|
|
||||||
|
memoTextBox := widget.NewEntry()
|
||||||
|
memoTextBox.SetText(memoString)
|
||||||
|
memoTextBox.OnChanged = func(_ string){
|
||||||
|
memoTextBox.SetText(memoString)
|
||||||
|
}
|
||||||
|
|
||||||
|
page := container.NewBorder(header, nil, nil, nil, memoTextBox)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setViewMemoCryptocurrencyAddressesPage(window fyne.Window, memoHash [32]byte, cryptocurrencyName string, previousPage func()){
|
||||||
|
|
||||||
|
if (cryptocurrencyName != "Ethereum" && cryptocurrencyName != "Cardano"){
|
||||||
|
setErrorEncounteredPage(window, errors.New("setViewMemoCryptocurrencyAddressesPage called with invalid cryptocurrencyName: " + cryptocurrencyName), previousPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage := func(){setViewMemoCryptocurrencyAddressesPage(window, memoHash, cryptocurrencyName, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("View Memo Cryptocurrency Addresses")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description1 := getLabelCentered("Below is the " + cryptocurrencyName + " address for this memo.")
|
||||||
|
description2 := getLabelCentered("Send funds to this address to timestamp the memo.")
|
||||||
|
description3 := getLabelCentered("Anyone can verify that the memo existed at the time of the earliest transaction to the address.")
|
||||||
|
|
||||||
|
cryptocurrencyIcon, err := getFyneImageIcon(cryptocurrencyName)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switchCryptocurrencyButton := widget.NewButton(cryptocurrencyName, func(){
|
||||||
|
|
||||||
|
if (cryptocurrencyName == "Ethereum"){
|
||||||
|
setViewMemoCryptocurrencyAddressesPage(window, memoHash, "Cardano", previousPage)
|
||||||
|
} else {
|
||||||
|
setViewMemoCryptocurrencyAddressesPage(window, memoHash, "Ethereum", previousPage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
switchCryptocurrencyButtonWithIcon := getContainerCentered(container.NewGridWithColumns(1, cryptocurrencyIcon, switchCryptocurrencyButton))
|
||||||
|
|
||||||
|
|
||||||
|
cryptocurrencyAddress, err := readMemos.GetBlockchainAddressFromMemoHash(cryptocurrencyName, memoHash)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addressWithButtonsRow, err := getCryptocurrencyAddressLabelWithCopyAndQRButtons(window, cryptocurrencyName, cryptocurrencyAddress, currentPage)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description1, description2, description3, widget.NewSeparator(), switchCryptocurrencyButtonWithIcon, widget.NewSeparator(), addressWithButtonsRow)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVerifyMemoPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setVerifyMemoPage(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Verify Memo")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description := widget.NewLabel("This page can be used to verify a Seekia memo.")
|
||||||
|
memoHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){
|
||||||
|
setMemoExplainerPage(window, currentPage)
|
||||||
|
})
|
||||||
|
descriptionRow := container.NewHBox(layout.NewSpacer(), description, memoHelpButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
memoEntry := widget.NewMultiLineEntry()
|
||||||
|
|
||||||
|
verifyButton := getWidgetCentered(widget.NewButtonWithIcon("Verify", theme.NavigateNextIcon(), func(){
|
||||||
|
|
||||||
|
memoToVerify := memoEntry.Text
|
||||||
|
|
||||||
|
if (memoToVerify == ""){
|
||||||
|
dialogTitle := translate("No Memo Provided")
|
||||||
|
dialogMessage := getLabelCentered("The must enter a memo to verify.")
|
||||||
|
dialogContent := container.NewVBox(dialogMessage)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
memoIsValid, _, _, _, err := readMemos.ReadMemo(memoToVerify)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (memoIsValid == false){
|
||||||
|
dialogTitle := translate("Memo Is Invalid")
|
||||||
|
dialogMessageA := getBoldLabelCentered("The memo you have entered is invalid.")
|
||||||
|
dialogMessageB := getLabelCentered("You cannot trust that it was written by its alleged author.")
|
||||||
|
dialogContent := container.NewVBox(dialogMessageA, dialogMessageB)
|
||||||
|
dialog.ShowCustom(dialogTitle, translate("Close"), dialogContent, window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setViewVerifiedMemoInfoPage(window, memoToVerify, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
emptyLabel := widget.NewLabel("")
|
||||||
|
|
||||||
|
verifyButtonWithSpacer := container.NewVBox(verifyButton, emptyLabel)
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionRow)
|
||||||
|
|
||||||
|
page := container.NewBorder(header, verifyButtonWithSpacer, nil, nil, memoEntry)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setViewVerifiedMemoInfoPage(window fyne.Window, memoString string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setViewVerifiedMemoInfoPage(window, memoString, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("View Memo Info")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description := getBoldLabelCentered("The memo is valid!")
|
||||||
|
|
||||||
|
memoIsValid, memoHash, authorIdentityHash, memoUnarmoredContents, err := readMemos.ReadMemo(memoString)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (memoIsValid == false){
|
||||||
|
setErrorEncounteredPage(window, errors.New("setViewVerifiedMemoInfoPage called with invalid memo: " + memoString), previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
memoHashTitle := widget.NewLabel("Memo Hash:")
|
||||||
|
|
||||||
|
memoHashHex := encoding.EncodeBytesToHexString(memoHash[:])
|
||||||
|
|
||||||
|
memoHashTrimmed, _, err := helpers.TrimAndFlattenString(memoHashHex, 15)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
memoHashLabel := getBoldLabel(memoHashTrimmed)
|
||||||
|
|
||||||
|
viewMemoHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){
|
||||||
|
setViewContentHashPage(window, "Memo", memoHash[:], currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
memoHashRow := container.NewHBox(layout.NewSpacer(), memoHashTitle, memoHashLabel, viewMemoHashButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
authorLabel := widget.NewLabel("Author:")
|
||||||
|
|
||||||
|
authorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(authorIdentityHash)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorIdentityHashTrimmed, _, err := helpers.TrimAndFlattenString(authorIdentityHashString, 15)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
authorIdentityHashLabel := getBoldLabel(authorIdentityHashTrimmed)
|
||||||
|
viewIdentityHashButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){
|
||||||
|
setViewIdentityHashPage(window, authorIdentityHash, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
authorRow := container.NewHBox(layout.NewSpacer(), authorLabel, authorIdentityHashLabel, viewIdentityHashButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
viewCryptocurrencyAddressesButton := getWidgetCentered(widget.NewButtonWithIcon("View Cryptocurrency Addresses", theme.VisibilityIcon(), func(){
|
||||||
|
setViewMemoCryptocurrencyAddressesPage(window, memoHash, "Ethereum", currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
viewUnarmoredContentsButton := getWidgetCentered(widget.NewButtonWithIcon("View Unarmored Contents", theme.VisibilityIcon(), func(){
|
||||||
|
setViewTextPage(window, "Viewing Memo Unarmored Contents", memoUnarmoredContents, true, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), memoHashRow, authorRow, viewCryptocurrencyAddressesButton, viewUnarmoredContentsButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
2494
gui/viewAnalysisGui_Couple.go
Normal file
2494
gui/viewAnalysisGui_Couple.go
Normal file
File diff suppressed because it is too large
Load diff
2444
gui/viewAnalysisGui_Person.go
Normal file
2444
gui/viewAnalysisGui_Person.go
Normal file
File diff suppressed because it is too large
Load diff
671
gui/viewContentGui.go
Normal file
671
gui/viewContentGui.go
Normal file
|
@ -0,0 +1,671 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// viewContentGui.go implements a page used by moderators to browse content
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/data/binding"
|
||||||
|
|
||||||
|
import "seekia/internal/appMemory"
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/moderation/contentControversy"
|
||||||
|
import "seekia/internal/moderation/reviewStorage"
|
||||||
|
import "seekia/internal/moderation/viewedContent"
|
||||||
|
import "seekia/internal/mySettings"
|
||||||
|
import "seekia/internal/network/appNetworkType/getAppNetworkType"
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// The viewedContent page provides a way for moderators to view, filter and sort all stored content
|
||||||
|
// One use of this is to sort content by controversy, to find moderators to ban
|
||||||
|
|
||||||
|
func setBrowseContentPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", "BrowseContent")
|
||||||
|
|
||||||
|
checkIfPageHasChangedFunction := func()bool{
|
||||||
|
exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage")
|
||||||
|
if (exists == false || currentViewedPage != "BrowseContent"){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Check if moderator/host mode is enabled
|
||||||
|
// If not, we cannot calculate many of the attributes
|
||||||
|
// We should let the user know that somehow
|
||||||
|
|
||||||
|
currentPage := func(){setBrowseContentPage(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Browse Content")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
statsIcon, err := getFyneImageIcon("Stats")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
statsButton := widget.NewButton("Stats", func(){
|
||||||
|
//TODO: A page to view statistics about all stored content
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
statsButtonWithIcon := container.NewGridWithRows(2, statsIcon, statsButton)
|
||||||
|
|
||||||
|
filtersIcon, err := getFyneImageIcon("Desires")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filtersButton := widget.NewButton("Filters", func(){
|
||||||
|
//TODO: A page to configure filters for the viewed content
|
||||||
|
// Examples:
|
||||||
|
// -Only show content created between a certain time frame
|
||||||
|
// -Only show content authored by certain identity types
|
||||||
|
// -Only show a certain contentType (Profile/Message)
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
filtersButtonWithIcon := container.NewGridWithRows(2, filtersIcon, filtersButton)
|
||||||
|
|
||||||
|
controversyIcon, err := getFyneImageIcon("Controversy")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controversyButton := widget.NewButton("Controversy", func(){
|
||||||
|
//TODO: A page to tune the controversy calculation parameters
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
controversyButtonWithIcon := container.NewGridWithRows(2, controversyIcon, controversyButton)
|
||||||
|
|
||||||
|
pageButtonsRow := getContainerCentered(container.NewGridWithRows(1, controversyButtonWithIcon, filtersButtonWithIcon, statsButtonWithIcon))
|
||||||
|
|
||||||
|
currentSortByAttribute, err := viewedContent.GetViewedContentSortByAttribute()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sortingByLabel := getBoldLabel(translate("Sorting By:"))
|
||||||
|
|
||||||
|
getSortByAttributeTitle := func()string{
|
||||||
|
|
||||||
|
if (currentSortByAttribute == "BanAdvocates"){
|
||||||
|
result := translate("Ban Advocates")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
if (currentSortByAttribute == "NumberOfReviewers"){
|
||||||
|
result := translate("Number Of Reviewers")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result := translate(currentSortByAttribute)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByAttributeTitle := getSortByAttributeTitle()
|
||||||
|
|
||||||
|
sortByAttributeButton := widget.NewButton(sortByAttributeTitle, func(){
|
||||||
|
setSelectViewedContentSortByAttributePage(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
getSortDirectionButtonWithIcon := func()(fyne.Widget, error){
|
||||||
|
|
||||||
|
currentSortDirection, err := viewedContent.GetViewedContentSortDirection()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
if (currentSortDirection == "Ascending"){
|
||||||
|
button := widget.NewButtonWithIcon(translate("Ascending"), theme.MoveUpIcon(), func(){
|
||||||
|
appMemory.SetMemoryEntry("StopBuildViewedContentYesNo", "Yes")
|
||||||
|
_ = mySettings.SetSetting("ViewedContentSortDirection", "Descending")
|
||||||
|
_ = mySettings.SetSetting("ViewedContentSortedStatus", "No")
|
||||||
|
_ = mySettings.SetSetting("ViewedContentViewIndex", "0")
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
return button, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
button := widget.NewButtonWithIcon(translate("Descending"), theme.MoveDownIcon(), func(){
|
||||||
|
appMemory.SetMemoryEntry("StopBuildViewedContentYesNo", "Yes")
|
||||||
|
_ = mySettings.SetSetting("ViewedContentSortDirection", "Ascending")
|
||||||
|
_ = mySettings.SetSetting("ViewedContentSortedStatus", "No")
|
||||||
|
_ = mySettings.SetSetting("ViewedContentViewIndex", "0")
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
return button, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByDirectionButton, err := getSortDirectionButtonWithIcon()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByRow := container.NewHBox(layout.NewSpacer(), sortingByLabel, sortByAttributeButton, sortByDirectionButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
appNetworkType, err := getAppNetworkType.GetAppNetworkType()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewedContentReady, err := viewedContent.GetViewedContentIsReadyStatus(appNetworkType)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewedContentReady == false){
|
||||||
|
|
||||||
|
progressPercentageBinding := binding.NewFloat()
|
||||||
|
sortingDetailsBinding := binding.NewString()
|
||||||
|
|
||||||
|
startUpdateViewedContentAndLoadingBarFunction := func(){
|
||||||
|
|
||||||
|
err := viewedContent.StartUpdatingViewedContent(appNetworkType)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sortingDetailsBindingSet := false
|
||||||
|
|
||||||
|
var encounteredError error
|
||||||
|
|
||||||
|
for{
|
||||||
|
|
||||||
|
pageHasChanged := checkIfPageHasChangedFunction()
|
||||||
|
if (pageHasChanged == true){
|
||||||
|
appMemory.SetMemoryEntry("StopBuildViewedContentYesNo", "Yes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buildEncounteredError, errorEncounteredString, buildIsStopped, contentIsReady, currentPercentageProgress, err := viewedContent.GetViewedContentBuildStatus(appNetworkType)
|
||||||
|
if (err != nil){
|
||||||
|
encounteredError = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildEncounteredError == true){
|
||||||
|
encounteredError = errors.New(errorEncounteredString)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildIsStopped == true){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentIsReady == true){
|
||||||
|
|
||||||
|
progressPercentageBinding.Set(1)
|
||||||
|
|
||||||
|
// We wait so that the loading bar will appear complete.
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
currentPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
progressPercentageBinding.Set(currentPercentageProgress)
|
||||||
|
|
||||||
|
if (currentPercentageProgress >= .50 && sortingDetailsBindingSet == false){
|
||||||
|
|
||||||
|
numberOfContents, err := viewedContent.GetNumberOfGeneratedViewedContents()
|
||||||
|
if (err != nil) {
|
||||||
|
encounteredError = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (numberOfContents != 0){
|
||||||
|
|
||||||
|
numberOfContentsString := helpers.ConvertIntToString(numberOfContents)
|
||||||
|
sortingDetailsBinding.Set("Sorting " + numberOfContentsString + " Contents...")
|
||||||
|
}
|
||||||
|
|
||||||
|
sortingDetailsBindingSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is only reached if an error is encountered during build
|
||||||
|
|
||||||
|
errorToShow := errors.New("Error encountered during build of viewed content: " + encounteredError.Error())
|
||||||
|
|
||||||
|
setErrorEncounteredPage(window, errorToShow, previousPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingLabel := getBoldLabelCentered("Loading Content...")
|
||||||
|
|
||||||
|
loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding))
|
||||||
|
|
||||||
|
loadingDetailsLabel := widget.NewLabelWithData(sortingDetailsBinding)
|
||||||
|
loadingDetailsLabel.TextStyle = getFyneTextStyle_Italic()
|
||||||
|
loadingDetailsLabelCentered := getWidgetCentered(loadingDetailsLabel)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), pageButtonsRow, widget.NewSeparator(), sortByRow, widget.NewSeparator(), loadingLabel, loadingBar, loadingDetailsLabelCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
|
||||||
|
go startUpdateViewedContentAndLoadingBarFunction()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getResultsContainer := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
getRefreshResultsButtonText := func()(string, error){
|
||||||
|
needsRefresh, err := viewedContent.CheckIfViewedContentNeedsRefresh()
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
if (needsRefresh == false){
|
||||||
|
return "Refresh Results", nil
|
||||||
|
}
|
||||||
|
return "Refresh Results - Updates Available!", nil
|
||||||
|
}
|
||||||
|
refreshButtonText, err := getRefreshResultsButtonText()
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
refreshResultsButton := getWidgetCentered(widget.NewButtonWithIcon(refreshButtonText, theme.ViewRefreshIcon(), func(){
|
||||||
|
_ = mySettings.SetSetting("ViewedContentGeneratedStatus", "No")
|
||||||
|
_ = mySettings.SetSetting("ViewedContentViewIndex", "0")
|
||||||
|
currentPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
viewedContentIsReady, currentViewedContentList, err := viewedContent.GetViewedContentList(appNetworkType)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (viewedContentIsReady == false){
|
||||||
|
return nil, errors.New("Viewed content is not ready after earlier check determined it was ready.")
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfViewedContents := len(currentViewedContentList)
|
||||||
|
|
||||||
|
if (numberOfViewedContents == 0){
|
||||||
|
|
||||||
|
noContentFoundLabel := getBoldLabelCentered("No content found.")
|
||||||
|
|
||||||
|
numberOfFilters, err := viewedContent.GetNumberOfActiveViewedContentFilters()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (numberOfFilters == 0){
|
||||||
|
noContentFoundWithRefreshButton := container.NewVBox(noContentFoundLabel, refreshResultsButton)
|
||||||
|
return noContentFoundWithRefreshButton, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfFiltersString := helpers.ConvertIntToString(numberOfFilters)
|
||||||
|
|
||||||
|
getActiveFiltersText := func()string{
|
||||||
|
if (numberOfFilters == 1){
|
||||||
|
return "active filter"
|
||||||
|
}
|
||||||
|
return "active filters"
|
||||||
|
}
|
||||||
|
|
||||||
|
activeFiltersText := getActiveFiltersText()
|
||||||
|
|
||||||
|
activeFiltersLabel := getLabelCentered(numberOfFiltersString + " " + activeFiltersText)
|
||||||
|
|
||||||
|
noContentFoundWithFiltersInfo := container.NewVBox(noContentFoundLabel, activeFiltersLabel, refreshResultsButton)
|
||||||
|
|
||||||
|
return noContentFoundWithFiltersInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentViewIndex := func()(int, error){
|
||||||
|
exists, viewIndexString, err := mySettings.GetSetting("ViewedContentViewIndex")
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
if (exists == false){
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
viewIndex, err := helpers.ConvertStringToInt(viewIndexString)
|
||||||
|
if (err != nil){
|
||||||
|
return 0, errors.New("MySettings invalid: Invalid ViewedContentViewIndex: " + viewIndexString)
|
||||||
|
}
|
||||||
|
if (viewIndex < 0) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
maximumViewIndex := numberOfViewedContents-1
|
||||||
|
if (viewIndex > maximumViewIndex){
|
||||||
|
|
||||||
|
return maximumViewIndex, nil
|
||||||
|
}
|
||||||
|
return viewIndex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
viewIndex, err := getCurrentViewIndex()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
getNavigateToBeginningButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (numberOfViewedContents <= 5 || viewIndex == 0){
|
||||||
|
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
goToBeginningButton := widget.NewButtonWithIcon("", theme.MediaSkipPreviousIcon(), func(){
|
||||||
|
_ = mySettings.SetSetting("ViewedContentViewIndex", "0")
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return goToBeginningButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getNavigateToEndButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
|
||||||
|
if (numberOfViewedContents <= 5){
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPageMinimumIndex := numberOfViewedContents - 5
|
||||||
|
|
||||||
|
if (viewIndex >= finalPageMinimumIndex){
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
goToEndButton := widget.NewButtonWithIcon("", theme.MediaSkipNextIcon(), func(){
|
||||||
|
finalPageIndexString := helpers.ConvertIntToString(finalPageMinimumIndex)
|
||||||
|
_ = mySettings.SetSetting("ViewedContentViewIndex", finalPageIndexString)
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return goToEndButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getNavigateLeftButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (numberOfViewedContents <= 5 || viewIndex == 0){
|
||||||
|
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
leftButton := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){
|
||||||
|
newIndex := helpers.ConvertIntToString(viewIndex-5)
|
||||||
|
_ = mySettings.SetSetting("ViewedContentViewIndex", newIndex)
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return leftButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getNavigateRightButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
|
||||||
|
if (numberOfViewedContents <= 5){
|
||||||
|
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPageMinimumIndex := numberOfViewedContents - 5
|
||||||
|
|
||||||
|
if (viewIndex >= finalPageMinimumIndex){
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
rightButton := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){
|
||||||
|
newIndex := helpers.ConvertIntToString(viewIndex+5)
|
||||||
|
_ = mySettings.SetSetting("ViewedContentViewIndex", newIndex)
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return rightButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewedContentInfoRow := func()*fyne.Container{
|
||||||
|
|
||||||
|
numberOfViewedContentsString := helpers.ConvertIntToString(numberOfViewedContents)
|
||||||
|
|
||||||
|
getResultOrResultsText := func()string{
|
||||||
|
if (numberOfViewedContents == 1){
|
||||||
|
return "Result"
|
||||||
|
}
|
||||||
|
return "Results"
|
||||||
|
}
|
||||||
|
|
||||||
|
resultOrResultsText := getResultOrResultsText()
|
||||||
|
|
||||||
|
numberOfViewedContentsLabel := getBoldLabel("Viewing " + numberOfViewedContentsString + " " + resultOrResultsText)
|
||||||
|
|
||||||
|
if (numberOfViewedContents <= 5){
|
||||||
|
viewedContentInfoRow := getWidgetCentered(numberOfViewedContentsLabel)
|
||||||
|
return viewedContentInfoRow
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToBeginningButton := getNavigateToBeginningButton()
|
||||||
|
navigateToEndButton := getNavigateToEndButton()
|
||||||
|
navigateLeftButton := getNavigateLeftButton()
|
||||||
|
navigateRightButton := getNavigateRightButton()
|
||||||
|
|
||||||
|
viewedContentInfoRow := container.NewHBox(layout.NewSpacer(), navigateToBeginningButton, navigateLeftButton, numberOfViewedContentsLabel, navigateRightButton, navigateToEndButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
return viewedContentInfoRow
|
||||||
|
}
|
||||||
|
|
||||||
|
viewedContentInfoRow := getViewedContentInfoRow()
|
||||||
|
|
||||||
|
viewIndexOnwardsViewedContentList := currentViewedContentList[viewIndex:]
|
||||||
|
|
||||||
|
contentResultsContainer := container.NewVBox()
|
||||||
|
|
||||||
|
if (viewIndex == 0){
|
||||||
|
contentResultsContainer.Add(refreshResultsButton)
|
||||||
|
contentResultsContainer.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyLabel1 := widget.NewLabel("")
|
||||||
|
contentHashTitle := getItalicLabelCentered("Content Hash")
|
||||||
|
contentTypeTitle := getItalicLabelCentered("Content Type")
|
||||||
|
featuredAttributeTitle := getItalicLabelCentered(sortByAttributeTitle)
|
||||||
|
emptyLabel2 := widget.NewLabel("")
|
||||||
|
|
||||||
|
contentIndexColumn := container.NewVBox(emptyLabel1, widget.NewSeparator())
|
||||||
|
contentHashColumn := container.NewVBox(contentHashTitle, widget.NewSeparator())
|
||||||
|
contentTypeColumn := container.NewVBox(contentTypeTitle, widget.NewSeparator())
|
||||||
|
featuredAttributeColumn := container.NewVBox(featuredAttributeTitle, widget.NewSeparator())
|
||||||
|
viewContentButtonsColumn := container.NewVBox(emptyLabel2, widget.NewSeparator())
|
||||||
|
|
||||||
|
for index, contentHashString := range viewIndexOnwardsViewedContentList{
|
||||||
|
|
||||||
|
resultIndex := viewIndex + index + 1
|
||||||
|
resultIndexString := helpers.ConvertIntToString(resultIndex)
|
||||||
|
|
||||||
|
resultIndexLabel := getBoldLabelCentered(resultIndexString + ".")
|
||||||
|
|
||||||
|
contentHash, err := encoding.DecodeHexStringToBytes(contentHashString)
|
||||||
|
if (err != nil){
|
||||||
|
return nil, errors.New("Viewed content list contains invalid contentHash: " + contentHashString)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, err := helpers.GetContentTypeFromContentHash(contentHash)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (contentType != "Profile" && contentType != "Message"){
|
||||||
|
return nil, errors.New("Viewed content list contains invalid contentHash: " + contentHashString)
|
||||||
|
}
|
||||||
|
|
||||||
|
getFeaturedAttributeValue := func()(string, error){
|
||||||
|
|
||||||
|
unknownTranslated := translate("Unknown")
|
||||||
|
|
||||||
|
if (currentSortByAttribute == "Controversy"){
|
||||||
|
|
||||||
|
controversyIsKnown, controversyRating, err := contentControversy.GetContentControversyRating(contentHash)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (controversyIsKnown == false){
|
||||||
|
return unknownTranslated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
controversyRatingString := helpers.ConvertInt64ToString(controversyRating)
|
||||||
|
return controversyRatingString, nil
|
||||||
|
}
|
||||||
|
if (currentSortByAttribute == "NumberOfReviewers"){
|
||||||
|
|
||||||
|
if (contentType == "Profile"){
|
||||||
|
|
||||||
|
if (len(contentHash) != 28){
|
||||||
|
return "", errors.New("GetContentTypeFromContentHash returning Profile for different length content hash.")
|
||||||
|
}
|
||||||
|
|
||||||
|
profileHash := [28]byte(contentHash)
|
||||||
|
|
||||||
|
profileMetadataIsKnown, downloadingRequiredData, numberOfReviewers, err := reviewStorage.GetNumberOfProfileReviewers(profileHash)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (profileMetadataIsKnown == false || downloadingRequiredData == false){
|
||||||
|
return unknownTranslated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfReviewersString := helpers.ConvertIntToString(numberOfReviewers)
|
||||||
|
|
||||||
|
return numberOfReviewersString, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
// contentType == "Message"
|
||||||
|
|
||||||
|
if (len(contentHash) != 26){
|
||||||
|
return "", errors.New("GetContentTypeFromContentHash returning Message for different length content hash.")
|
||||||
|
}
|
||||||
|
|
||||||
|
messageHash := [26]byte(contentHash)
|
||||||
|
|
||||||
|
messageMetadataIsKnown, downloadingRequiredData, numberOfReviewers, err := reviewStorage.GetNumberOfMessageReviewers(messageHash)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (messageMetadataIsKnown == false || downloadingRequiredData == false){
|
||||||
|
return unknownTranslated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfReviewersString := helpers.ConvertIntToString(numberOfReviewers)
|
||||||
|
return numberOfReviewersString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("Unknown featured attribute: " + currentSortByAttribute)
|
||||||
|
}
|
||||||
|
|
||||||
|
featuredAttributeValue, err := getFeaturedAttributeValue()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
featuredAttributeLabel := getBoldLabelCentered(featuredAttributeValue)
|
||||||
|
|
||||||
|
contentTypeLabel := getBoldLabelCentered(translate(contentType))
|
||||||
|
|
||||||
|
contentHashTrimmed, _, err := helpers.TrimAndFlattenString(contentHashString, 7)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
contentHashLabel := getBoldLabelCentered(contentHashTrimmed)
|
||||||
|
|
||||||
|
viewContentButton := widget.NewButtonWithIcon("View Details", theme.VisibilityIcon(), func(){
|
||||||
|
if (contentType == "Message"){
|
||||||
|
|
||||||
|
if (len(contentHash) != 26){
|
||||||
|
setErrorEncounteredPage(window, errors.New("GetContentTypeFromContentHash returning Message for different length content hash."), currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
messageHash := [26]byte(contentHash)
|
||||||
|
|
||||||
|
setViewMessageModerationDetailsPage(window, messageHash, currentPage)
|
||||||
|
|
||||||
|
} else if (contentType == "Profile"){
|
||||||
|
|
||||||
|
if (len(contentHash) != 28){
|
||||||
|
setErrorEncounteredPage(window, errors.New("GetContentTypeFromContentHash returning Profile for different length content hash."), currentPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profileHash := [28]byte(contentHash)
|
||||||
|
|
||||||
|
setViewProfileModerationDetailsPage(window, profileHash, currentPage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
contentIndexColumn.Add(resultIndexLabel)
|
||||||
|
contentHashColumn.Add(contentHashLabel)
|
||||||
|
contentTypeColumn.Add(contentTypeLabel)
|
||||||
|
featuredAttributeColumn.Add(featuredAttributeLabel)
|
||||||
|
viewContentButtonsColumn.Add(viewContentButton)
|
||||||
|
|
||||||
|
contentIndexColumn.Add(widget.NewSeparator())
|
||||||
|
contentHashColumn.Add(widget.NewSeparator())
|
||||||
|
contentTypeColumn.Add(widget.NewSeparator())
|
||||||
|
featuredAttributeColumn.Add(widget.NewSeparator())
|
||||||
|
viewContentButtonsColumn.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
if (index >= 4){
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gridInfoRowWithSeparator := container.NewVBox(viewedContentInfoRow, widget.NewSeparator())
|
||||||
|
|
||||||
|
resultsGrid := container.NewHBox(layout.NewSpacer(), contentIndexColumn, contentHashColumn, contentTypeColumn, featuredAttributeColumn, viewContentButtonsColumn, layout.NewSpacer())
|
||||||
|
|
||||||
|
contentResultsContainer.Add(resultsGrid)
|
||||||
|
|
||||||
|
viewedContentContainerScrollable := container.NewVScroll(contentResultsContainer)
|
||||||
|
|
||||||
|
resultsContainer := container.NewBorder(gridInfoRowWithSeparator, nil, nil, nil, viewedContentContainerScrollable)
|
||||||
|
|
||||||
|
return resultsContainer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsContainer, err := getResultsContainer()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator(), pageButtonsRow, widget.NewSeparator(), sortByRow, widget.NewSeparator())
|
||||||
|
|
||||||
|
page := container.NewBorder(header, nil, nil, nil, resultsContainer)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func setSelectViewedContentSortByAttributePage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", "ViewedContentSortBySelect")
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Select Sort By Attribute")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description := getLabelCentered("Select the attribute to sort the viewed content by.")
|
||||||
|
|
||||||
|
getSelectButton := func(attributeTitle string, attributeName string, sortDirection string) fyne.Widget{
|
||||||
|
|
||||||
|
button := widget.NewButton(attributeTitle, func(){
|
||||||
|
_ = mySettings.SetSetting("ViewedContentSortedStatus", "No")
|
||||||
|
_ = mySettings.SetSetting("ViewedContentSortByAttribute", attributeName)
|
||||||
|
_ = mySettings.SetSetting("ViewedContentSortDirection", sortDirection)
|
||||||
|
_ = mySettings.SetSetting("ViewedContentViewIndex", "0")
|
||||||
|
|
||||||
|
previousPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Add more attributes
|
||||||
|
|
||||||
|
controversyButton := getSelectButton("Controversy", "Controversy", "Ascending")
|
||||||
|
numberOfReviewersButton := getSelectButton("Number Of Reviewers", "NumberOfReviewers", "Descending")
|
||||||
|
|
||||||
|
buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, controversyButton, numberOfReviewersButton))
|
||||||
|
|
||||||
|
content := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), buttonsGrid)
|
||||||
|
|
||||||
|
setPageContent(content, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
631
gui/viewGeneticReferencesGui.go
Normal file
631
gui/viewGeneticReferencesGui.go
Normal file
|
@ -0,0 +1,631 @@
|
||||||
|
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// viewGeneticReferencesGui.go implements pages to display information about genetic diseases and traits
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
|
||||||
|
import "seekia/resources/geneticReferences/locusMetadata"
|
||||||
|
import "seekia/resources/geneticReferences/traits"
|
||||||
|
import "seekia/resources/geneticReferences/monogenicDiseases"
|
||||||
|
import "seekia/resources/geneticReferences/polygenicDiseases"
|
||||||
|
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
import "slices"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
|
||||||
|
func setViewMonogenicDiseaseDetailsPage(window fyne.Window, diseaseName string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setViewMonogenicDiseaseDetailsPage(window, diseaseName, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Monogenic Disease Details - " + diseaseName)
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
diseaseObject, err := monogenicDiseases.GetMonogenicDiseaseObject(diseaseName)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
diseaseGeneName := diseaseObject.GeneName
|
||||||
|
diseaseIsDominantOrRecessive := diseaseObject.DominantOrRecessive
|
||||||
|
diseaseDescription := diseaseObject.DiseaseDescription
|
||||||
|
diseaseReferencesMap := diseaseObject.References
|
||||||
|
|
||||||
|
diseaseNameLabel := widget.NewLabel("Disease Name:")
|
||||||
|
diseaseNameText := getBoldLabel(diseaseName)
|
||||||
|
diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameLabel, diseaseNameText, layout.NewSpacer())
|
||||||
|
|
||||||
|
geneNameLabel := widget.NewLabel("Gene Name:")
|
||||||
|
geneNameText := getBoldLabel(diseaseGeneName)
|
||||||
|
geneNameRow := container.NewHBox(layout.NewSpacer(), geneNameLabel, geneNameText, layout.NewSpacer())
|
||||||
|
|
||||||
|
dominantOrRecessiveLabel := widget.NewLabel("Dominant or Recessive?:")
|
||||||
|
dominantOrRecessiveText := getBoldLabel(diseaseIsDominantOrRecessive)
|
||||||
|
dominantOrRecessiveRow := container.NewHBox(layout.NewSpacer(), dominantOrRecessiveLabel, dominantOrRecessiveText, layout.NewSpacer())
|
||||||
|
|
||||||
|
diseaseDescriptionTrimmed, _, err := helpers.TrimAndFlattenString(diseaseDescription, 10)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
diseaseDescriptionLabel := widget.NewLabel("Description:")
|
||||||
|
diseaseDescriptionText := getBoldLabel(diseaseDescriptionTrimmed)
|
||||||
|
viewDiseaseDescriptionButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){
|
||||||
|
setViewTextPage(window, "Disease Description", diseaseDescription, false, currentPage)
|
||||||
|
})
|
||||||
|
diseaseDescriptionRow := container.NewHBox(layout.NewSpacer(), diseaseDescriptionLabel, diseaseDescriptionText, viewDiseaseDescriptionButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
viewReferencesButton := getWidgetCentered(widget.NewButtonWithIcon("View References", theme.ListIcon(), func(){
|
||||||
|
setViewGeneticAnalysisReferencesPage(window, "Monogenic Disease", diseaseReferencesMap, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), geneNameRow, widget.NewSeparator(), dominantOrRecessiveRow, widget.NewSeparator(), diseaseDescriptionRow, widget.NewSeparator(), viewReferencesButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setViewMonogenicDiseaseVariantDetailsPage(window fyne.Window, diseaseName string, variantIdentifier string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setViewMonogenicDiseaseVariantDetailsPage(window, diseaseName, variantIdentifier, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Viewing Monogenic Disease Variant Details")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
diseaseNameLabel := widget.NewLabel("Disease Name:")
|
||||||
|
diseaseNameText := getBoldLabel(diseaseName)
|
||||||
|
diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameLabel, diseaseNameText, layout.NewSpacer())
|
||||||
|
|
||||||
|
variantObject, err := monogenicDiseases.GetMonogenicDiseaseVariantObject(diseaseName, variantIdentifier)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
variantNamesList := variantObject.VariantNames
|
||||||
|
nucleotideChange := variantObject.NucleotideChange
|
||||||
|
aminoAcidChange := variantObject.AminoAcidChange
|
||||||
|
variantRSID := variantObject.VariantRSID
|
||||||
|
variantEffectIsMild := variantObject.EffectIsMild
|
||||||
|
referencesMap := variantObject.References
|
||||||
|
|
||||||
|
variantRSIDsList := []int64{variantRSID}
|
||||||
|
|
||||||
|
// We add aliases to variantRSIDsList
|
||||||
|
|
||||||
|
anyAliasesExist, rsidAliasesList, err := locusMetadata.GetRSIDAliases(variantRSID)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (anyAliasesExist == true){
|
||||||
|
variantRSIDsList = append(variantRSIDsList, rsidAliasesList...)
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariantNamesLabelText := func()string{
|
||||||
|
|
||||||
|
if(len(variantNamesList) == 1){
|
||||||
|
return "Variant Name:"
|
||||||
|
}
|
||||||
|
return "Variant Names:"
|
||||||
|
}
|
||||||
|
|
||||||
|
variantNamesLabelText := getVariantNamesLabelText()
|
||||||
|
|
||||||
|
variantNamesListString := strings.Join(variantNamesList, ", ")
|
||||||
|
variantNamesLabel := widget.NewLabel(variantNamesLabelText)
|
||||||
|
variantNamesText := getBoldLabel(variantNamesListString)
|
||||||
|
variantNamesRow := container.NewHBox(layout.NewSpacer(), variantNamesLabel, variantNamesText, layout.NewSpacer())
|
||||||
|
|
||||||
|
nucleotideChangeLabel := widget.NewLabel("Nucleotide Change:")
|
||||||
|
nucleotideChangeText := getBoldLabel(nucleotideChange)
|
||||||
|
nucleotideChangeRow := container.NewHBox(layout.NewSpacer(), nucleotideChangeLabel, nucleotideChangeText, layout.NewSpacer())
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), variantNamesRow, widget.NewSeparator(), nucleotideChangeRow, widget.NewSeparator())
|
||||||
|
|
||||||
|
if (aminoAcidChange != ""){
|
||||||
|
aminoAcidChangeLabel := widget.NewLabel("Amino Acid Change:")
|
||||||
|
aminoAcidChangeText := getBoldLabel(aminoAcidChange)
|
||||||
|
aminoAcidChangeRow := container.NewHBox(layout.NewSpacer(), aminoAcidChangeLabel, aminoAcidChangeText, layout.NewSpacer())
|
||||||
|
|
||||||
|
page.Add(aminoAcidChangeRow)
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariantRSIDsLabelText := func()string{
|
||||||
|
if (len(variantRSIDsList) == 1){
|
||||||
|
return "Variant rsID:"
|
||||||
|
}
|
||||||
|
return "Variant rsIDs:"
|
||||||
|
}
|
||||||
|
|
||||||
|
variantRSIDsLabelText := getVariantRSIDsLabelText()
|
||||||
|
|
||||||
|
variantRSIDStringsList := make([]string, 0, len(variantRSIDsList))
|
||||||
|
|
||||||
|
for _, variantRSID := range variantRSIDsList{
|
||||||
|
|
||||||
|
variantRSIDString := helpers.ConvertInt64ToString(variantRSID)
|
||||||
|
|
||||||
|
variantRSIDName := "rs" + variantRSIDString
|
||||||
|
|
||||||
|
variantRSIDStringsList = append(variantRSIDStringsList, variantRSIDName)
|
||||||
|
}
|
||||||
|
|
||||||
|
variantRSIDsListString := strings.Join(variantRSIDStringsList, ", ")
|
||||||
|
variantRSIDsLabel := widget.NewLabel(variantRSIDsLabelText)
|
||||||
|
variantRSIDsText := getBoldLabel(variantRSIDsListString)
|
||||||
|
variantRSIDsRow := container.NewHBox(layout.NewSpacer(), variantRSIDsLabel, variantRSIDsText, layout.NewSpacer())
|
||||||
|
page.Add(variantRSIDsRow)
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
effectIsMildString := helpers.ConvertBoolToYesOrNoString(variantEffectIsMild)
|
||||||
|
effectIsMildLabel := widget.NewLabel("Effect is Mild:")
|
||||||
|
effectIsMildText := getBoldLabel(effectIsMildString)
|
||||||
|
effectIsMildHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){
|
||||||
|
setVariantEffectIsMildExplainerPage(window, currentPage)
|
||||||
|
})
|
||||||
|
effectIsMildRow := container.NewHBox(layout.NewSpacer(), effectIsMildLabel, effectIsMildText, effectIsMildHelpButton, layout.NewSpacer())
|
||||||
|
page.Add(effectIsMildRow)
|
||||||
|
page.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
viewReferencesButton := getWidgetCentered(widget.NewButtonWithIcon("View References", theme.ListIcon(), func(){
|
||||||
|
setViewGeneticAnalysisReferencesPage(window, "Variant", referencesMap, currentPage)
|
||||||
|
}))
|
||||||
|
page.Add(viewReferencesButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setViewPolygenicDiseaseDetailsPage(window fyne.Window, diseaseName string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setViewPolygenicDiseaseDetailsPage(window, diseaseName, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Disease Details - " + diseaseName)
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
diseaseObject, err := polygenicDiseases.GetPolygenicDiseaseObject(diseaseName)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
diseaseDescription := diseaseObject.DiseaseDescription
|
||||||
|
effectedSex := diseaseObject.EffectedSex
|
||||||
|
diseaseReferencesMap := diseaseObject.References
|
||||||
|
|
||||||
|
diseaseNameLabel := widget.NewLabel("Disease Name:")
|
||||||
|
diseaseNameText := getBoldLabel(diseaseName)
|
||||||
|
diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameLabel, diseaseNameText, layout.NewSpacer())
|
||||||
|
|
||||||
|
getEffectedSexTextLabelText := func()string{
|
||||||
|
if (effectedSex == "Both"){
|
||||||
|
return "Male and Female"
|
||||||
|
}
|
||||||
|
return effectedSex
|
||||||
|
}
|
||||||
|
|
||||||
|
effectedSexTextLabelText := getEffectedSexTextLabelText()
|
||||||
|
|
||||||
|
effectedSexLabel := widget.NewLabel("Effected Sex:")
|
||||||
|
effectedSexText := getBoldLabel(effectedSexTextLabelText)
|
||||||
|
effectedSexRow := container.NewHBox(layout.NewSpacer(), effectedSexLabel, effectedSexText, layout.NewSpacer())
|
||||||
|
|
||||||
|
diseaseDescriptionTrimmed, _, err := helpers.TrimAndFlattenString(diseaseDescription, 10)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
diseaseDescriptionLabel := widget.NewLabel("Description:")
|
||||||
|
diseaseDescriptionText := getBoldLabel(diseaseDescriptionTrimmed)
|
||||||
|
viewDiseaseDescriptionButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){
|
||||||
|
setViewTextPage(window, "Disease Description", diseaseDescription, false, currentPage)
|
||||||
|
})
|
||||||
|
diseaseDescriptionRow := container.NewHBox(layout.NewSpacer(), diseaseDescriptionLabel, diseaseDescriptionText, viewDiseaseDescriptionButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
viewReferencesButton := getWidgetCentered(widget.NewButtonWithIcon("View References", theme.ListIcon(), func(){
|
||||||
|
setViewGeneticAnalysisReferencesPage(window, "Polygenic Disease", diseaseReferencesMap, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), effectedSexRow, widget.NewSeparator(), diseaseDescriptionRow, widget.NewSeparator(), viewReferencesButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setViewPolygenicDiseaseLocusDetailsPage(window fyne.Window, diseaseName string, locusIdentifier string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setViewPolygenicDiseaseLocusDetailsPage(window, diseaseName, locusIdentifier, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Viewing Locus Details")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
locusObject, err := polygenicDiseases.GetPolygenicDiseaseLocusObject(diseaseName, locusIdentifier)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
locusRSID := locusObject.LocusRSID
|
||||||
|
locusReferencesMap := locusObject.References
|
||||||
|
|
||||||
|
locusRSIDsList := []int64{locusRSID}
|
||||||
|
|
||||||
|
// We add aliases to locusRSIDsList
|
||||||
|
|
||||||
|
anyAliasesExist, rsidAliasesList, err := locusMetadata.GetRSIDAliases(locusRSID)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (anyAliasesExist == true){
|
||||||
|
locusRSIDsList = append(locusRSIDsList, rsidAliasesList...)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataExists, locusMetadataObject, err := locusMetadata.GetLocusMetadata(locusRSID)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (metadataExists == false){
|
||||||
|
setErrorEncounteredPage(window, errors.New("setViewPolygenicDiseaseLocusDetailsPage called with locusRSID missing from locusMetadata."), previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
locusGeneName := locusMetadataObject.GeneNamesList[0]
|
||||||
|
|
||||||
|
diseaseNameLabel := widget.NewLabel("Disease Name:")
|
||||||
|
diseaseNameText := getBoldLabel(diseaseName)
|
||||||
|
diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameLabel, diseaseNameText, layout.NewSpacer())
|
||||||
|
|
||||||
|
getLocusNamesLabelText := func()string{
|
||||||
|
|
||||||
|
if(len(locusRSIDsList) == 1){
|
||||||
|
return "Locus Name:"
|
||||||
|
}
|
||||||
|
return "Locus Names:"
|
||||||
|
}
|
||||||
|
|
||||||
|
locusNamesLabelText := getLocusNamesLabelText()
|
||||||
|
|
||||||
|
locusRSIDStringsList := make([]string, 0, len(locusRSIDsList))
|
||||||
|
|
||||||
|
for _, locusRSID := range locusRSIDsList{
|
||||||
|
|
||||||
|
locusRSIDString := helpers.ConvertInt64ToString(locusRSID)
|
||||||
|
|
||||||
|
locusRSIDName := "rs" + locusRSIDString
|
||||||
|
|
||||||
|
locusRSIDStringsList = append(locusRSIDStringsList, locusRSIDName)
|
||||||
|
}
|
||||||
|
|
||||||
|
locusNamesListString := strings.Join(locusRSIDStringsList, ", ")
|
||||||
|
locusNamesLabel := widget.NewLabel(locusNamesLabelText)
|
||||||
|
locusNamesText := getBoldLabel(locusNamesListString)
|
||||||
|
locusNamesRow := container.NewHBox(layout.NewSpacer(), locusNamesLabel, locusNamesText, layout.NewSpacer())
|
||||||
|
|
||||||
|
geneNameLabel := widget.NewLabel("Gene Name:")
|
||||||
|
geneNameText := getBoldLabel(locusGeneName)
|
||||||
|
geneNameRow := container.NewHBox(layout.NewSpacer(), geneNameLabel, geneNameText, layout.NewSpacer())
|
||||||
|
|
||||||
|
viewReferencesButton := getWidgetCentered(widget.NewButtonWithIcon("View References", theme.ListIcon(), func(){
|
||||||
|
setViewGeneticAnalysisReferencesPage(window, "Locus", locusReferencesMap, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
getBasePairsGrid := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
locusRiskWeightsMap := locusObject.RiskWeightsMap
|
||||||
|
locusBasePairProbabilitiesMap := locusObject.BasePairProbabilitiesMap
|
||||||
|
|
||||||
|
riskWeightLabel := getItalicLabelCentered("Risk Weight")
|
||||||
|
probabilityLabel := getItalicLabelCentered("Probability Of Weight")
|
||||||
|
|
||||||
|
riskWeightColumn := container.NewVBox(riskWeightLabel, widget.NewSeparator())
|
||||||
|
riskWeightProbabilityColumn := container.NewVBox(probabilityLabel, widget.NewSeparator())
|
||||||
|
|
||||||
|
// We create a new map with duplicates removed
|
||||||
|
|
||||||
|
locusBasePairProbabilitiesMap_DuplicatesRemoved := make(map[string]float64)
|
||||||
|
|
||||||
|
for basePair, basePairProbability := range locusBasePairProbabilitiesMap{
|
||||||
|
|
||||||
|
baseA, baseB, semicolonFound := strings.Cut(basePair, ";")
|
||||||
|
if (semicolonFound == false) {
|
||||||
|
return nil, errors.New("Invalid base pair found in locusBasePairProbabilitiesMap: " + basePair)
|
||||||
|
}
|
||||||
|
basePairDuplicate := baseB + ";" + baseA
|
||||||
|
|
||||||
|
existingProbabilityValue, exists := locusBasePairProbabilitiesMap_DuplicatesRemoved[basePairDuplicate]
|
||||||
|
if (exists == true){
|
||||||
|
|
||||||
|
// The duplicate has already been added.
|
||||||
|
// We make sure the probability values match
|
||||||
|
if (existingProbabilityValue != basePairProbability){
|
||||||
|
return nil, errors.New("locusBasePairProbabilitiesMap contains duplicate base pair with different value")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
locusBasePairProbabilitiesMap_DuplicatesRemoved[basePair] = basePairProbability
|
||||||
|
}
|
||||||
|
|
||||||
|
// All probabilities are mutually exclusive (you can only have 1 base pair for each genome locus)
|
||||||
|
// Thus, we can add them together to get a total probability for each risk weight
|
||||||
|
|
||||||
|
// Map structure: Risk Weight -> Probability of having weight
|
||||||
|
riskWeightProbabilitiesMap := make(map[int]float64)
|
||||||
|
|
||||||
|
for basePair, basePairProbability := range locusBasePairProbabilitiesMap_DuplicatesRemoved{
|
||||||
|
|
||||||
|
getBasePairRiskWeight := func()int{
|
||||||
|
|
||||||
|
basePairRiskWeight, exists := locusRiskWeightsMap[basePair]
|
||||||
|
if (exists == false){
|
||||||
|
// This base pair has no known weight. We treat it as a 0 weight.
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return basePairRiskWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
basePairRiskWeight := getBasePairRiskWeight()
|
||||||
|
|
||||||
|
riskWeightProbabilitiesMap[basePairRiskWeight] += basePairProbability
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we sort risk weights in order of least to greatest
|
||||||
|
allRiskWeightsList := helpers.GetListOfMapKeys(riskWeightProbabilitiesMap)
|
||||||
|
|
||||||
|
slices.Sort(allRiskWeightsList)
|
||||||
|
|
||||||
|
for _, riskWeight := range allRiskWeightsList{
|
||||||
|
|
||||||
|
riskWeightProbability, exists := riskWeightProbabilitiesMap[riskWeight]
|
||||||
|
if (exists == false){
|
||||||
|
return nil, errors.New("Risk weight probability not found in riskWeightProbabilitiesMap")
|
||||||
|
}
|
||||||
|
|
||||||
|
riskWeightString := helpers.ConvertIntToString(riskWeight)
|
||||||
|
|
||||||
|
riskWeightPercentageProbability := riskWeightProbability * 100
|
||||||
|
riskWeightProbabilityString := helpers.ConvertFloat64ToStringRounded(riskWeightPercentageProbability, 2)
|
||||||
|
riskWeightProbabilityFormatted := "~" + riskWeightProbabilityString + "%"
|
||||||
|
|
||||||
|
riskWeightText := getBoldLabelCentered(riskWeightString)
|
||||||
|
riskWeightProbabilityText := getBoldLabelCentered(riskWeightProbabilityFormatted)
|
||||||
|
|
||||||
|
riskWeightColumn.Add(riskWeightText)
|
||||||
|
riskWeightProbabilityColumn.Add(riskWeightProbabilityText)
|
||||||
|
|
||||||
|
riskWeightColumn.Add(widget.NewSeparator())
|
||||||
|
riskWeightProbabilityColumn.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
riskWeightHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){
|
||||||
|
setPolygenicDiseaseLocusRiskWeightExplainerPage(window, currentPage)
|
||||||
|
})
|
||||||
|
riskWeightColumn.Add(riskWeightHelpButton)
|
||||||
|
|
||||||
|
probabilityHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){
|
||||||
|
setPolygenicDiseaseLocusRiskWeightProbabilityExplainerPage(window, currentPage)
|
||||||
|
})
|
||||||
|
riskWeightProbabilityColumn.Add(probabilityHelpButton)
|
||||||
|
|
||||||
|
basePairsGrid := container.NewHBox(layout.NewSpacer(), riskWeightColumn, riskWeightProbabilityColumn, layout.NewSpacer())
|
||||||
|
|
||||||
|
return basePairsGrid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
basePairsGrid, err := getBasePairsGrid()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), locusNamesRow, widget.NewSeparator(), geneNameRow, widget.NewSeparator(), viewReferencesButton, widget.NewSeparator(), basePairsGrid)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setViewGeneticAnalysisReferencesPage(window fyne.Window, referencesTopic string, referencesMap map[string]string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setViewGeneticAnalysisReferencesPage(window, referencesTopic, referencesMap, previousPage)}
|
||||||
|
|
||||||
|
pageTitle := "Viewing " + referencesTopic + " References"
|
||||||
|
|
||||||
|
title := getPageTitleCentered(pageTitle)
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
referencesContainer := container.NewVBox()
|
||||||
|
|
||||||
|
referenceNamesList := helpers.GetListOfMapKeys(referencesMap)
|
||||||
|
|
||||||
|
// We sort the references so they always show up in the same order
|
||||||
|
slices.Sort(referenceNamesList)
|
||||||
|
|
||||||
|
for index, referenceName := range referenceNamesList{
|
||||||
|
|
||||||
|
referenceLink, exists := referencesMap[referenceName]
|
||||||
|
if (exists == false){
|
||||||
|
setErrorEncounteredPage(window, errors.New("referencesMap missing referenceName"), previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
indexString := helpers.ConvertIntToString(index+1)
|
||||||
|
|
||||||
|
indexLabel := getBoldLabel(indexString + ".")
|
||||||
|
|
||||||
|
referenceNameTrimmed, _, err := helpers.TrimAndFlattenString(referenceName, 35)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
referenceNameLabel := getItalicLabel(referenceNameTrimmed)
|
||||||
|
|
||||||
|
referenceViewNameButton := widget.NewButtonWithIcon("View Name", theme.VisibilityIcon(), func(){
|
||||||
|
setViewTextPage(window, "Viewing Reference Name", referenceName, false, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
referenceLinkButton := widget.NewButtonWithIcon("View Link", theme.VisibilityIcon(), func(){
|
||||||
|
setViewLinkPage(window, "Viewing Reference Link", referenceLink, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
referenceRow := container.NewHBox(layout.NewSpacer(), indexLabel, referenceNameLabel, referenceViewNameButton, referenceLinkButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
referencesContainer.Add(referenceRow)
|
||||||
|
referencesContainer.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), referencesContainer)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setViewTraitDetailsPage(window fyne.Window, traitName string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setViewTraitDetailsPage(window, traitName, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Trait Details - " + traitName)
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
traitObject, err := traits.GetTraitObject(traitName)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
traitDescription := traitObject.TraitDescription
|
||||||
|
traitReferencesMap := traitObject.References
|
||||||
|
|
||||||
|
traitNameLabel := widget.NewLabel("Trait Name:")
|
||||||
|
traitNameText := getBoldLabel(traitName)
|
||||||
|
traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameLabel, traitNameText, layout.NewSpacer())
|
||||||
|
|
||||||
|
traitDescriptionTrimmed, _, err := helpers.TrimAndFlattenString(traitDescription, 10)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
traitDescriptionLabel := widget.NewLabel("Description:")
|
||||||
|
traitDescriptionText := getBoldLabel(traitDescriptionTrimmed)
|
||||||
|
viewTraitDescriptionButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){
|
||||||
|
setViewTextPage(window, "Trait Description", traitDescription, false, currentPage)
|
||||||
|
})
|
||||||
|
traitDescriptionRow := container.NewHBox(layout.NewSpacer(), traitDescriptionLabel, traitDescriptionText, viewTraitDescriptionButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
viewReferencesButton := getWidgetCentered(widget.NewButtonWithIcon("View References", theme.ListIcon(), func(){
|
||||||
|
setViewGeneticAnalysisReferencesPage(window, "Trait", traitReferencesMap, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), traitNameRow, widget.NewSeparator(), traitDescriptionRow, widget.NewSeparator(), viewReferencesButton)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setViewTraitRuleDetailsPage(window fyne.Window, traitName string, ruleIdentifier string, previousPage func()){
|
||||||
|
|
||||||
|
currentPage := func(){setViewTraitRuleDetailsPage(window, traitName, ruleIdentifier, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Viewing Rule Details")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
traitNameLabel := widget.NewLabel("Trait Name:")
|
||||||
|
traitNameText := getBoldLabel(traitName)
|
||||||
|
traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameLabel, traitNameText, layout.NewSpacer())
|
||||||
|
|
||||||
|
ruleIdentifierLabel := widget.NewLabel("Rule Identifier:")
|
||||||
|
ruleIdentifierText := getBoldLabel(ruleIdentifier)
|
||||||
|
ruleIdentifierRow := container.NewHBox(layout.NewSpacer(), ruleIdentifierLabel, ruleIdentifierText, layout.NewSpacer())
|
||||||
|
|
||||||
|
traitRuleObject, err := traits.GetTraitRuleObject(traitName, ruleIdentifier)
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleOutcomePointsMap := traitRuleObject.OutcomePointsMap
|
||||||
|
ruleReferencesMap := traitRuleObject.References
|
||||||
|
|
||||||
|
viewReferencesButton := getWidgetCentered(widget.NewButtonWithIcon("View References", theme.ListIcon(), func(){
|
||||||
|
setViewGeneticAnalysisReferencesPage(window, "Rule", ruleReferencesMap, currentPage)
|
||||||
|
}))
|
||||||
|
|
||||||
|
ruleEffectsLabel := getBoldLabelCentered("Rule Effects:")
|
||||||
|
|
||||||
|
getRuleEffectsGrid := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
outcomeNameLabel := getItalicLabelCentered("Outcome Name")
|
||||||
|
outcomeEffectLabel := getItalicLabelCentered("Outcome Effect")
|
||||||
|
|
||||||
|
outcomeNameColumn := container.NewVBox(outcomeNameLabel, widget.NewSeparator())
|
||||||
|
outcomeEffectColumn := container.NewVBox(outcomeEffectLabel, widget.NewSeparator())
|
||||||
|
|
||||||
|
for outcomeName, outcomePointsEffect := range ruleOutcomePointsMap{
|
||||||
|
|
||||||
|
outcomeNameLabel := getBoldLabelCentered(outcomeName)
|
||||||
|
|
||||||
|
getOutcomeEffect := func()string{
|
||||||
|
|
||||||
|
outcomePointsEffectString := helpers.ConvertIntToString(outcomePointsEffect)
|
||||||
|
|
||||||
|
if (outcomePointsEffect < 0){
|
||||||
|
return outcomePointsEffectString
|
||||||
|
}
|
||||||
|
|
||||||
|
outcomeEffect := "+" + outcomePointsEffectString
|
||||||
|
return outcomeEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
outcomeEffect := getOutcomeEffect()
|
||||||
|
|
||||||
|
outcomeEffectLabel := getBoldLabelCentered(outcomeEffect)
|
||||||
|
|
||||||
|
outcomeNameColumn.Add(outcomeNameLabel)
|
||||||
|
outcomeEffectColumn.Add(outcomeEffectLabel)
|
||||||
|
|
||||||
|
outcomeNameColumn.Add(widget.NewSeparator())
|
||||||
|
outcomeEffectColumn.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
outcomeEffectHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){
|
||||||
|
setTraitRuleOutcomeEffectsExplainerPage(window, currentPage)
|
||||||
|
})
|
||||||
|
outcomeEffectColumn.Add(outcomeEffectHelpButton)
|
||||||
|
|
||||||
|
ruleEffectsGrid := container.NewHBox(layout.NewSpacer(), outcomeNameColumn, outcomeEffectColumn, layout.NewSpacer())
|
||||||
|
|
||||||
|
return ruleEffectsGrid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleEffectsGrid, err := getRuleEffectsGrid()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), traitNameRow, widget.NewSeparator(), ruleIdentifierRow, widget.NewSeparator(), viewReferencesButton, widget.NewSeparator(), ruleEffectsLabel, ruleEffectsGrid)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
593
gui/viewHostsGui.go
Normal file
593
gui/viewHostsGui.go
Normal file
|
@ -0,0 +1,593 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// viewHostsGui.go implements a page to browse hosts
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/data/binding"
|
||||||
|
|
||||||
|
import "seekia/internal/appMemory"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/identity"
|
||||||
|
import "seekia/internal/mySettings"
|
||||||
|
import "seekia/internal/network/appNetworkType/getAppNetworkType"
|
||||||
|
import "seekia/internal/network/viewedHosts"
|
||||||
|
import "seekia/internal/profiles/viewableProfiles"
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func setViewHostsPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", "ViewHosts")
|
||||||
|
|
||||||
|
checkIfPageHasChangedFunction := func()bool{
|
||||||
|
exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage")
|
||||||
|
if (exists == false || currentViewedPage != "ViewHosts"){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Check if moderator/host mode is enabled
|
||||||
|
// If not, many attributes cannot be displayed, and the list will be empty/outdated
|
||||||
|
// Require either mode to be enabled to view hosts
|
||||||
|
|
||||||
|
currentPage := func(){setViewHostsPage(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Viewing Hosts")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
filtersIcon, err := getFyneImageIcon("Desires")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filtersButton := widget.NewButton("Filters", func(){
|
||||||
|
//TODO
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
filtersButtonWithIcon := container.NewGridWithRows(2, filtersIcon, filtersButton)
|
||||||
|
|
||||||
|
statsIcon, err := getFyneImageIcon("Stats")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
statsButton := widget.NewButton("Stats", func(){
|
||||||
|
//TODO
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
statsButtonWithIcon := container.NewGridWithRows(2, statsIcon, statsButton)
|
||||||
|
|
||||||
|
pageButtonsRow := getContainerCentered(container.NewGridWithRows(1, filtersButtonWithIcon, statsButtonWithIcon))
|
||||||
|
|
||||||
|
currentSortByAttribute, err := viewedHosts.GetViewedHostsSortByAttribute()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sortingByLabel := getBoldLabel(translate("Sorting By:"))
|
||||||
|
|
||||||
|
getSortByAttributeTitle := func()string{
|
||||||
|
|
||||||
|
if (currentSortByAttribute == "BanAdvocates"){
|
||||||
|
result := translate("Ban Advocates")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result := translate(currentSortByAttribute)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByAttributeTitle := getSortByAttributeTitle()
|
||||||
|
|
||||||
|
sortByAttributeButton := widget.NewButton(sortByAttributeTitle, func(){
|
||||||
|
setSelectViewedHostsSortByAttributePage(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
getSortDirectionButtonWithIcon := func()(fyne.Widget, error){
|
||||||
|
|
||||||
|
currentSortDirection, err := viewedHosts.GetViewedHostsSortDirection()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
if (currentSortDirection == "Ascending"){
|
||||||
|
|
||||||
|
button := widget.NewButtonWithIcon(translate("Ascending"), theme.MoveUpIcon(), func(){
|
||||||
|
appMemory.SetMemoryEntry("StopBuildViewedHostsYesNo", "Yes")
|
||||||
|
mySettings.SetSetting("ViewedHostsSortDirection", "Descending")
|
||||||
|
mySettings.SetSetting("ViewedHostsSortedStatus", "No")
|
||||||
|
mySettings.SetSetting("ViewedHostsViewIndex", "0")
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
return button, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
button := widget.NewButtonWithIcon(translate("Descending"), theme.MoveDownIcon(), func(){
|
||||||
|
appMemory.SetMemoryEntry("StopBuildViewedHostsYesNo", "Yes")
|
||||||
|
mySettings.SetSetting("ViewedHostsSortDirection", "Ascending")
|
||||||
|
mySettings.SetSetting("ViewedHostsSortedStatus", "No")
|
||||||
|
mySettings.SetSetting("ViewedHostsViewIndex", "0")
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return button, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByDirectionButton, err := getSortDirectionButtonWithIcon()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByRow := container.NewHBox(layout.NewSpacer(), sortingByLabel, sortByAttributeButton, sortByDirectionButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
appNetworkType, err := getAppNetworkType.GetAppNetworkType()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewedHostsReady, err := viewedHosts.GetViewedHostsAreReadyStatus(appNetworkType)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewedHostsReady == false){
|
||||||
|
|
||||||
|
progressPercentageBinding := binding.NewFloat()
|
||||||
|
sortingDetailsBinding := binding.NewString()
|
||||||
|
|
||||||
|
startUpdateHostsAndProgressBarFunction := func(){
|
||||||
|
|
||||||
|
err := viewedHosts.StartUpdatingViewedHosts(appNetworkType)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sortingDetailsBindingSet := false
|
||||||
|
|
||||||
|
var encounteredError error
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
pageHasChanged := checkIfPageHasChangedFunction()
|
||||||
|
if (pageHasChanged == true){
|
||||||
|
appMemory.SetMemoryEntry("StopBuildViewedHostsYesNo", "Yes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buildEncounteredError, errorEncounteredString, buildIsStopped, hostsAreReady, currentPercentageProgress, err := viewedHosts.GetViewedHostsBuildStatus(appNetworkType)
|
||||||
|
if (err != nil){
|
||||||
|
encounteredError = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildEncounteredError == true){
|
||||||
|
encounteredError = errors.New(errorEncounteredString)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildIsStopped == true){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostsAreReady == true){
|
||||||
|
|
||||||
|
progressPercentageBinding.Set(1)
|
||||||
|
|
||||||
|
// We wait so that the loading bar will appear complete.
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
currentPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
progressPercentageBinding.Set(currentPercentageProgress)
|
||||||
|
|
||||||
|
if (currentPercentageProgress >= .50 && sortingDetailsBindingSet == false){
|
||||||
|
|
||||||
|
numberOfHosts, err := viewedHosts.GetNumberOfGeneratedViewedHosts()
|
||||||
|
if (err != nil) {
|
||||||
|
encounteredError = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (numberOfHosts != 0){
|
||||||
|
|
||||||
|
numberOfHostsString := helpers.ConvertIntToString(numberOfHosts)
|
||||||
|
sortingDetailsBinding.Set("Sorting " + numberOfHostsString + " Hosts...")
|
||||||
|
}
|
||||||
|
|
||||||
|
sortingDetailsBindingSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is only reached if an error is encountered during build
|
||||||
|
|
||||||
|
errorToShow := errors.New("Error encountered during build of viewed hosts: " + encounteredError.Error())
|
||||||
|
|
||||||
|
setErrorEncounteredPage(window, errorToShow, previousPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingLabel := getBoldLabelCentered("Loading Hosts...")
|
||||||
|
|
||||||
|
loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding))
|
||||||
|
|
||||||
|
loadingDetailsLabel := widget.NewLabelWithData(sortingDetailsBinding)
|
||||||
|
loadingDetailsLabel.TextStyle = getFyneTextStyle_Italic()
|
||||||
|
loadingDetailsLabelCentered := getWidgetCentered(loadingDetailsLabel)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), pageButtonsRow, widget.NewSeparator(), sortByRow, widget.NewSeparator(), loadingLabel, loadingBar, loadingDetailsLabelCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
|
||||||
|
go startUpdateHostsAndProgressBarFunction()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getResultsContainer := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
getRefreshResultsButtonText := func()(string, error){
|
||||||
|
needsRefresh, err := viewedHosts.CheckIfViewedHostsNeedsRefresh()
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
if (needsRefresh == false){
|
||||||
|
return "Refresh Results", nil
|
||||||
|
}
|
||||||
|
return "Refresh Results - Updates Available!", nil
|
||||||
|
}
|
||||||
|
refreshButtonText, err := getRefreshResultsButtonText()
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
refreshResultsButton := getWidgetCentered(widget.NewButtonWithIcon(refreshButtonText, theme.ViewRefreshIcon(), func(){
|
||||||
|
mySettings.SetSetting("ViewedHostsGeneratedStatus", "No")
|
||||||
|
mySettings.SetSetting("ViewedHostsViewIndex", "0")
|
||||||
|
currentPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
listIsReady, currentViewedHostsList, err := viewedHosts.GetViewedHostsList(appNetworkType)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (listIsReady == false){
|
||||||
|
return nil, errors.New("Viewed hosts list not ready after generation completed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfViewedHosts := len(currentViewedHostsList)
|
||||||
|
|
||||||
|
if (numberOfViewedHosts == 0){
|
||||||
|
|
||||||
|
noHostsFoundLabel := getBoldLabelCentered("No Hosts Found")
|
||||||
|
|
||||||
|
numberOfFilters, err := viewedHosts.GetNumberOfActiveHostFilters()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (numberOfFilters == 0){
|
||||||
|
noHostsFoundLabelWithRefreshButton := container.NewVBox(noHostsFoundLabel, refreshResultsButton)
|
||||||
|
return noHostsFoundLabelWithRefreshButton, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfFiltersString := helpers.ConvertIntToString(numberOfFilters)
|
||||||
|
|
||||||
|
getActiveFiltersText := func()string{
|
||||||
|
if (numberOfFilters == 1){
|
||||||
|
return "active filter"
|
||||||
|
}
|
||||||
|
return "active filters"
|
||||||
|
}
|
||||||
|
|
||||||
|
activeFiltersText := getActiveFiltersText()
|
||||||
|
|
||||||
|
activeFiltersLabel := getLabelCentered(numberOfFiltersString + " " + activeFiltersText)
|
||||||
|
|
||||||
|
noHostsFoundWithFiltersInfo := container.NewVBox(noHostsFoundLabel, activeFiltersLabel, refreshResultsButton)
|
||||||
|
|
||||||
|
return noHostsFoundWithFiltersInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentViewIndex := func()(int, error){
|
||||||
|
|
||||||
|
exists, viewIndexString, err := mySettings.GetSetting("ViewedHostsViewIndex")
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
if (exists == false){
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
viewIndex, err := helpers.ConvertStringToInt(viewIndexString)
|
||||||
|
if (err != nil){
|
||||||
|
return 0, errors.New("MySettings malformed: Invalid ViewedHostsViewIndex: " + viewIndexString)
|
||||||
|
}
|
||||||
|
if (viewIndex < 0) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
maximumViewIndex := numberOfViewedHosts-1
|
||||||
|
if (viewIndex > maximumViewIndex){
|
||||||
|
return maximumViewIndex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewIndex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
viewIndex, err := getCurrentViewIndex()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
getNavigateToBeginningButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (numberOfViewedHosts <= 5 || viewIndex == 0){
|
||||||
|
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
goToBeginningButton := widget.NewButtonWithIcon("", theme.MediaSkipPreviousIcon(), func(){
|
||||||
|
mySettings.SetSetting("ViewedHostsViewIndex", "0")
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return goToBeginningButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getNavigateToEndButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
|
||||||
|
if (numberOfViewedHosts <= 5){
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPageMinimumIndex := numberOfViewedHosts - 5
|
||||||
|
|
||||||
|
if (viewIndex >= finalPageMinimumIndex){
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
goToEndButton := widget.NewButtonWithIcon("", theme.MediaSkipNextIcon(), func(){
|
||||||
|
finalPageIndexString := helpers.ConvertIntToString(finalPageMinimumIndex)
|
||||||
|
mySettings.SetSetting("ViewedHostsViewIndex", finalPageIndexString)
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return goToEndButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getNavigateLeftButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (numberOfViewedHosts <= 5 || viewIndex == 0){
|
||||||
|
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
button := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){
|
||||||
|
newIndex := helpers.ConvertIntToString(viewIndex-5)
|
||||||
|
mySettings.SetSetting("ViewedHostsViewIndex", newIndex)
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
getNavigateRightButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
|
||||||
|
if (numberOfViewedHosts <= 5){
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPageMinimumIndex := numberOfViewedHosts - 5
|
||||||
|
|
||||||
|
if (viewIndex >= finalPageMinimumIndex){
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
button := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){
|
||||||
|
newIndex := helpers.ConvertIntToString(viewIndex+5)
|
||||||
|
mySettings.SetSetting("ViewedHostsViewIndex", newIndex)
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewingHostsInfoRow := func()*fyne.Container{
|
||||||
|
|
||||||
|
numberOfViewedHostsString := helpers.ConvertIntToString(numberOfViewedHosts)
|
||||||
|
|
||||||
|
getHostOrHostsText := func()string{
|
||||||
|
|
||||||
|
if (numberOfViewedHosts == 1){
|
||||||
|
return "Host"
|
||||||
|
}
|
||||||
|
return "Hosts"
|
||||||
|
}
|
||||||
|
|
||||||
|
hostOrHostsText := getHostOrHostsText()
|
||||||
|
|
||||||
|
numberOfViewedHostsLabel := getBoldLabel("Viewing " + numberOfViewedHostsString + " " + hostOrHostsText)
|
||||||
|
|
||||||
|
if (numberOfViewedHosts <= 5){
|
||||||
|
viewingHostsInfoRow := getWidgetCentered(numberOfViewedHostsLabel)
|
||||||
|
return viewingHostsInfoRow
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToBeginningButton := getNavigateToBeginningButton()
|
||||||
|
navigateToEndButton := getNavigateToEndButton()
|
||||||
|
navigateLeftButton := getNavigateLeftButton()
|
||||||
|
navigateRightButton := getNavigateRightButton()
|
||||||
|
|
||||||
|
viewingHostsInfoRow := container.NewHBox(layout.NewSpacer(), navigateToBeginningButton, navigateLeftButton, numberOfViewedHostsLabel, navigateRightButton, navigateToEndButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
return viewingHostsInfoRow
|
||||||
|
}
|
||||||
|
|
||||||
|
viewingHostsInfoRow := getViewingHostsInfoRow()
|
||||||
|
|
||||||
|
hostResultsContainer := container.NewVBox()
|
||||||
|
|
||||||
|
if (viewIndex == 0){
|
||||||
|
hostResultsContainer.Add(refreshResultsButton)
|
||||||
|
hostResultsContainer.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyLabelA := widget.NewLabel("")
|
||||||
|
nameLabel := getItalicLabelCentered("Name")
|
||||||
|
identityHashTitle := getItalicLabelCentered("Identity Hash")
|
||||||
|
featuredAttributeTitle := getItalicLabelCentered(sortByAttributeTitle)
|
||||||
|
emptyLabelB := widget.NewLabel("")
|
||||||
|
|
||||||
|
resultIndexColumn := container.NewVBox(emptyLabelA, widget.NewSeparator())
|
||||||
|
nameColumn := container.NewVBox(nameLabel, widget.NewSeparator())
|
||||||
|
identityHashColumn := container.NewVBox(identityHashTitle, widget.NewSeparator())
|
||||||
|
featuredAttributeColumn := container.NewVBox(featuredAttributeTitle, widget.NewSeparator())
|
||||||
|
viewButtonsColumn := container.NewVBox(emptyLabelB, widget.NewSeparator())
|
||||||
|
|
||||||
|
viewIndexOnwardsHostIdentityHashesList := currentViewedHostsList[viewIndex:]
|
||||||
|
|
||||||
|
for index, hostIdentityHash := range viewIndexOnwardsHostIdentityHashesList{
|
||||||
|
|
||||||
|
resultIndex := viewIndex + index + 1
|
||||||
|
resultIndexString := helpers.ConvertIntToString(resultIndex)
|
||||||
|
|
||||||
|
resultIndexBoldLabel := getBoldLabel(resultIndexString + ".")
|
||||||
|
|
||||||
|
profileExists, _, getAnyHostAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(hostIdentityHash, appNetworkType, true, true, true)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (profileExists == false) {
|
||||||
|
// Profile has expired since results were generated
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserName := func()(string, error){
|
||||||
|
exists, _, username, err := getAnyHostAttributeFunction("Username")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (exists == false) {
|
||||||
|
return "Anonymous", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
theirUsernameTrimmed, _, err := helpers.TrimAndFlattenString(username, 15)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
return theirUsernameTrimmed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTheirUsername, err := getUserName()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
userNameLabel := getBoldLabelCentered(currentTheirUsername)
|
||||||
|
|
||||||
|
hostIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(hostIdentityHash)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
theirIdentityHashTrimmed, _, err := helpers.TrimAndFlattenString(hostIdentityHashString, 10)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
theirIdentityHashLabel := getBoldLabelCentered(theirIdentityHashTrimmed)
|
||||||
|
|
||||||
|
getFeaturedAttributeValue := func()(string, error){
|
||||||
|
|
||||||
|
exists, _, featuredAttributeValue, err := getAnyHostAttributeFunction(currentSortByAttribute)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (exists == false){
|
||||||
|
return "Unknown", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return featuredAttributeValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
featuredAttributeValue, err := getFeaturedAttributeValue()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
featuredAttributeValueLabel := getBoldLabelCentered(featuredAttributeValue)
|
||||||
|
|
||||||
|
viewHostButton := widget.NewButtonWithIcon("View", theme.VisibilityIcon(), func(){
|
||||||
|
setViewHostDetailsPage(window, hostIdentityHash, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
resultIndexColumn.Add(resultIndexBoldLabel)
|
||||||
|
nameColumn.Add(userNameLabel)
|
||||||
|
identityHashColumn.Add(theirIdentityHashLabel)
|
||||||
|
featuredAttributeColumn.Add(featuredAttributeValueLabel)
|
||||||
|
viewButtonsColumn.Add(viewHostButton)
|
||||||
|
|
||||||
|
resultIndexColumn.Add(widget.NewSeparator())
|
||||||
|
nameColumn.Add(widget.NewSeparator())
|
||||||
|
identityHashColumn.Add(widget.NewSeparator())
|
||||||
|
featuredAttributeColumn.Add(widget.NewSeparator())
|
||||||
|
viewButtonsColumn.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
if (index >= 4){
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hostResultsGrid := container.NewHBox(layout.NewSpacer(), resultIndexColumn, nameColumn, identityHashColumn, featuredAttributeColumn, viewButtonsColumn, layout.NewSpacer())
|
||||||
|
|
||||||
|
hostResultsContainer.Add(hostResultsGrid)
|
||||||
|
|
||||||
|
viewedHostContainerScrollable := container.NewVScroll(hostResultsContainer)
|
||||||
|
viewedHostContainerBoxed := getWidgetBoxed(viewedHostContainerScrollable)
|
||||||
|
|
||||||
|
resultsContainer := container.NewBorder(viewingHostsInfoRow, nil, nil, nil, viewedHostContainerBoxed)
|
||||||
|
|
||||||
|
return resultsContainer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsContainer, err := getResultsContainer()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator(), pageButtonsRow, widget.NewSeparator(), sortByRow, widget.NewSeparator())
|
||||||
|
|
||||||
|
page := container.NewBorder(header, nil, nil, nil, resultsContainer)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setSelectViewedHostsSortByAttributePage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", "ViewedHostsSortBySelect")
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Select Sort By Attribute")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description := getLabelCentered("Select the attribute to sort the hosts by.")
|
||||||
|
|
||||||
|
getSelectButton := func(attributeTitle string, attributeName string, sortDirection string) fyne.Widget{
|
||||||
|
|
||||||
|
button := widget.NewButton(attributeTitle, func(){
|
||||||
|
mySettings.SetSetting("ViewedHostsSortedStatus", "No")
|
||||||
|
mySettings.SetSetting("ViewedHostsSortByAttribute", attributeName)
|
||||||
|
mySettings.SetSetting("ViewedHostsSortDirection", sortDirection)
|
||||||
|
mySettings.SetSetting("ViewedHostsViewIndex", "0")
|
||||||
|
|
||||||
|
previousPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Add more attributes
|
||||||
|
|
||||||
|
banAdvocatesButton := getSelectButton("Ban Advocates", "BanAdvocates", "Ascending")
|
||||||
|
|
||||||
|
buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, banAdvocatesButton))
|
||||||
|
|
||||||
|
content := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), buttonsGrid)
|
||||||
|
|
||||||
|
setPageContent(content, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
616
gui/viewModeratorsGui.go
Normal file
616
gui/viewModeratorsGui.go
Normal file
|
@ -0,0 +1,616 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
// viewModeratorsGui.go implements a page to browse moderators
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2"
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
import "fyne.io/fyne/v2/container"
|
||||||
|
import "fyne.io/fyne/v2/layout"
|
||||||
|
import "fyne.io/fyne/v2/theme"
|
||||||
|
import "fyne.io/fyne/v2/data/binding"
|
||||||
|
|
||||||
|
import "seekia/internal/appMemory"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/identity"
|
||||||
|
import "seekia/internal/moderation/viewedModerators"
|
||||||
|
import "seekia/internal/mySettings"
|
||||||
|
import "seekia/internal/network/appNetworkType/getAppNetworkType"
|
||||||
|
import "seekia/internal/profiles/viewableProfiles"
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
|
||||||
|
func setViewModeratorsPage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", "ViewModerators")
|
||||||
|
|
||||||
|
checkIfPageHasChangedFunction := func()bool{
|
||||||
|
exists, currentViewedPage := appMemory.GetMemoryEntry("CurrentViewedPage")
|
||||||
|
if (exists == false || currentViewedPage != "ViewModerators"){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Check if moderator/host mode is enabled
|
||||||
|
// If not, many attributes cannot be displayed, and the list will be empty/outdated
|
||||||
|
// Require either mode to be enabled to view moderators
|
||||||
|
|
||||||
|
currentPage := func(){setViewModeratorsPage(window, previousPage)}
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Viewing Moderators")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
filtersIcon, err := getFyneImageIcon("Desires")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filtersButton := widget.NewButton("Filters", func(){
|
||||||
|
//TODO
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
filtersButtonWithIcon := container.NewGridWithRows(2, filtersIcon, filtersButton)
|
||||||
|
|
||||||
|
statsIcon, err := getFyneImageIcon("Stats")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
statsButton := widget.NewButton("Stats", func(){
|
||||||
|
//TODO
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
statsButtonWithIcon := container.NewGridWithRows(2, statsIcon, statsButton)
|
||||||
|
|
||||||
|
contactsIcon, err := getFyneImageIcon("Contacts")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contactsButton := widget.NewButton("Contacts", func(){
|
||||||
|
setMyContactsPage(window, "Moderator", currentPage)
|
||||||
|
})
|
||||||
|
contactsButtonWithIcon := container.NewGridWithRows(2, contactsIcon, contactsButton)
|
||||||
|
|
||||||
|
controversyIcon, err := getFyneImageIcon("Controversy")
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controversyButton := widget.NewButton("Controversy", func(){
|
||||||
|
//TODO: A page to tune the controversy calculation
|
||||||
|
showUnderConstructionDialog(window)
|
||||||
|
})
|
||||||
|
controversyButtonWithIcon := container.NewGridWithRows(2, controversyIcon, controversyButton)
|
||||||
|
|
||||||
|
pageButtonsRow := getContainerCentered(container.NewGridWithRows(1, filtersButtonWithIcon, statsButtonWithIcon, contactsButtonWithIcon, controversyButtonWithIcon))
|
||||||
|
|
||||||
|
currentSortByAttribute, err := viewedModerators.GetViewedModeratorsSortByAttribute()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sortingByLabel := getBoldLabel(translate("Sorting By:"))
|
||||||
|
|
||||||
|
getSortByAttributeTitle := func()string{
|
||||||
|
|
||||||
|
if (currentSortByAttribute == "IdentityScore"){
|
||||||
|
result := translate("Identity Score")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
if (currentSortByAttribute == "NumberOfReviews"){
|
||||||
|
result := translate("Number Of Reviews")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSortByAttribute == "BanAdvocates"){
|
||||||
|
result := translate("Ban Advocates")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result := translate(currentSortByAttribute)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByAttributeTitle := getSortByAttributeTitle()
|
||||||
|
|
||||||
|
sortByAttributeButton := widget.NewButton(sortByAttributeTitle, func(){
|
||||||
|
setSelectViewedModeratorsSortByAttributePage(window, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
getSortDirectionButtonWithIcon := func()(fyne.Widget, error){
|
||||||
|
|
||||||
|
currentSortDirection, err := viewedModerators.GetViewedModeratorsSortDirection()
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
if (currentSortDirection == "Ascending"){
|
||||||
|
|
||||||
|
button := widget.NewButtonWithIcon(translate("Ascending"), theme.MoveUpIcon(), func(){
|
||||||
|
appMemory.SetMemoryEntry("StopBuildViewedModeratorsYesNo", "Yes")
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsSortDirection", "Descending")
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsSortedStatus", "No")
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsViewIndex", "0")
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return button, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
button := widget.NewButtonWithIcon(translate("Descending"), theme.MoveDownIcon(), func(){
|
||||||
|
appMemory.SetMemoryEntry("StopBuildViewedModeratorsYesNo", "Yes")
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsSortDirection", "Ascending")
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsSortedStatus", "No")
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsViewIndex", "0")
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return button, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByDirectionButton, err := getSortDirectionButtonWithIcon()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByRow := container.NewHBox(layout.NewSpacer(), sortingByLabel, sortByAttributeButton, sortByDirectionButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
appNetworkType, err := getAppNetworkType.GetAppNetworkType()
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewedModeratorsReady, err := viewedModerators.GetViewedModeratorsAreReadyStatus(appNetworkType)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewedModeratorsReady == false){
|
||||||
|
|
||||||
|
progressPercentageBinding := binding.NewFloat()
|
||||||
|
sortingStatusBinding := binding.NewString()
|
||||||
|
|
||||||
|
startUpdateModeratorsAndProgressBarProgress := func(){
|
||||||
|
|
||||||
|
err := viewedModerators.StartUpdatingViewedModerators(appNetworkType)
|
||||||
|
if (err != nil) {
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sortingStatusBindingSet := false
|
||||||
|
|
||||||
|
var encounteredError error
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
pageHasChanged := checkIfPageHasChangedFunction()
|
||||||
|
if (pageHasChanged == true){
|
||||||
|
appMemory.SetMemoryEntry("StopBuildViewedModeratorsYesNo", "Yes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buildEncounteredError, errorEncounteredString, buildIsStopped, viewedModeratorsAreReady, currentPercentageProgress, err := viewedModerators.GetViewedModeratorsBuildStatus(appNetworkType)
|
||||||
|
if (err != nil){
|
||||||
|
encounteredError = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (buildEncounteredError == true){
|
||||||
|
encounteredError = errors.New(errorEncounteredString)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildIsStopped == true){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewedModeratorsAreReady == true){
|
||||||
|
progressPercentageBinding.Set(1)
|
||||||
|
|
||||||
|
// We wait so that the loading bar will appear complete.
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
currentPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
progressPercentageBinding.Set(currentPercentageProgress)
|
||||||
|
|
||||||
|
if (currentPercentageProgress >= .50 && sortingStatusBindingSet == false){
|
||||||
|
|
||||||
|
numberOfModerators, err := viewedModerators.GetNumberOfGeneratedViewedModerators()
|
||||||
|
if (err != nil) {
|
||||||
|
encounteredError = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (numberOfModerators != 0){
|
||||||
|
numberOfModeratorsString := helpers.ConvertIntToString(numberOfModerators)
|
||||||
|
sortingStatusBinding.Set("Sorting " + numberOfModeratorsString + " Moderators...")
|
||||||
|
}
|
||||||
|
sortingStatusBindingSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
// This will only be reached if an error occurred
|
||||||
|
errorToShow := errors.New("Error encountered while generating viewed moderators: " + encounteredError.Error())
|
||||||
|
setErrorEncounteredPage(window, errorToShow, currentPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingLabel := getBoldLabelCentered("Loading Moderators...")
|
||||||
|
|
||||||
|
loadingBar := getWidgetCentered(widget.NewProgressBarWithData(progressPercentageBinding))
|
||||||
|
|
||||||
|
loadingDetailsLabel := widget.NewLabelWithData(sortingStatusBinding)
|
||||||
|
loadingDetailsLabel.TextStyle = getFyneTextStyle_Italic()
|
||||||
|
loadingDetailsLabelCentered := getWidgetCentered(loadingDetailsLabel)
|
||||||
|
|
||||||
|
page := container.NewVBox(title, backButton, widget.NewSeparator(), pageButtonsRow, widget.NewSeparator(), sortByRow, widget.NewSeparator(), loadingLabel, loadingBar, loadingDetailsLabelCentered)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
|
||||||
|
go startUpdateModeratorsAndProgressBarProgress()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getResultsContainer := func()(*fyne.Container, error){
|
||||||
|
|
||||||
|
getRefreshResultsButtonText := func()(string, error){
|
||||||
|
needsRefresh, err := viewedModerators.CheckIfViewedModeratorsNeedsRefresh()
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
if (needsRefresh == false){
|
||||||
|
return "Refresh Results", nil
|
||||||
|
}
|
||||||
|
return "Refresh Results - Updates Available!", nil
|
||||||
|
}
|
||||||
|
refreshButtonText, err := getRefreshResultsButtonText()
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
refreshResultsButton := getWidgetCentered(widget.NewButtonWithIcon(refreshButtonText, theme.ViewRefreshIcon(), func(){
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsGeneratedStatus", "No")
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsViewIndex", "0")
|
||||||
|
currentPage()
|
||||||
|
}))
|
||||||
|
|
||||||
|
listIsReady, currentViewedModeratorsList, err := viewedModerators.GetViewedModeratorsList(appNetworkType)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (listIsReady == false){
|
||||||
|
return nil, errors.New("Viewed moderators list is not ready after earlier status check determined it was.")
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfViewedModerators := len(currentViewedModeratorsList)
|
||||||
|
|
||||||
|
if (numberOfViewedModerators == 0){
|
||||||
|
|
||||||
|
noModeratorsFoundLabel := getBoldLabelCentered("No Moderators Found")
|
||||||
|
|
||||||
|
numberOfFilters, err := viewedModerators.GetNumberOfActiveModeratorFilters()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (numberOfFilters == 0){
|
||||||
|
noModeratorsFoundWithRefreshButton := container.NewVBox(noModeratorsFoundLabel, refreshResultsButton)
|
||||||
|
return noModeratorsFoundWithRefreshButton, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfFiltersString := helpers.ConvertIntToString(numberOfFilters)
|
||||||
|
|
||||||
|
getActiveFiltersText := func()string{
|
||||||
|
if (numberOfFilters == 1){
|
||||||
|
return "active filter"
|
||||||
|
}
|
||||||
|
return "active filters"
|
||||||
|
}
|
||||||
|
|
||||||
|
activeFiltersText := getActiveFiltersText()
|
||||||
|
|
||||||
|
activeFiltersLabel := getLabelCentered(numberOfFiltersString + " " + activeFiltersText)
|
||||||
|
|
||||||
|
noModeratorsFoundWithFiltersInfo := container.NewVBox(noModeratorsFoundLabel, activeFiltersLabel, refreshResultsButton)
|
||||||
|
|
||||||
|
return noModeratorsFoundWithFiltersInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentViewIndex := func()(int, error){
|
||||||
|
|
||||||
|
exists, viewIndexString, err := mySettings.GetSetting("ViewedModeratorsViewIndex")
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
if (exists == false){
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
viewIndex, err := helpers.ConvertStringToInt(viewIndexString)
|
||||||
|
if (err != nil || viewIndex < 0) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
maximumViewIndex := numberOfViewedModerators-1
|
||||||
|
if (viewIndex > maximumViewIndex){
|
||||||
|
|
||||||
|
return maximumViewIndex, nil
|
||||||
|
}
|
||||||
|
return viewIndex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
viewIndex, err := getCurrentViewIndex()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
getNavigateToBeginningButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (numberOfViewedModerators <= 5 || viewIndex == 0){
|
||||||
|
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
goToBeginningButton := widget.NewButtonWithIcon("", theme.MediaSkipPreviousIcon(), func(){
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsViewIndex", "0")
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return goToBeginningButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getNavigateToEndButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
|
||||||
|
if (numberOfViewedModerators <= 5){
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPageMinimumIndex := numberOfViewedModerators - 5
|
||||||
|
|
||||||
|
if (viewIndex >= finalPageMinimumIndex){
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
goToEndButton := widget.NewButtonWithIcon("", theme.MediaSkipNextIcon(), func(){
|
||||||
|
finalPageIndexString := helpers.ConvertIntToString(finalPageMinimumIndex)
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsViewIndex", finalPageIndexString)
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return goToEndButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getNavigateLeftButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
if (numberOfViewedModerators <= 5 || viewIndex == 0){
|
||||||
|
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
leftButton := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func(){
|
||||||
|
newIndex := helpers.ConvertIntToString(viewIndex-5)
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsViewIndex", newIndex)
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return leftButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getNavigateRightButton := func()fyne.Widget{
|
||||||
|
|
||||||
|
emptyButton := widget.NewButton(" ", nil)
|
||||||
|
|
||||||
|
if (numberOfViewedModerators <= 5){
|
||||||
|
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPageMinimumIndex := numberOfViewedModerators - 5
|
||||||
|
|
||||||
|
if (viewIndex >= finalPageMinimumIndex){
|
||||||
|
return emptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
rightButton := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func(){
|
||||||
|
newIndex := helpers.ConvertIntToString(viewIndex+5)
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsViewIndex", newIndex)
|
||||||
|
currentPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return rightButton
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewingModeratorsInfoRow := func()*fyne.Container{
|
||||||
|
|
||||||
|
numberOfViewedModeratorsString := helpers.ConvertIntToString(numberOfViewedModerators)
|
||||||
|
|
||||||
|
getModeratorOrModeratorsText := func()string{
|
||||||
|
if (numberOfViewedModerators == 1){
|
||||||
|
return "Moderator"
|
||||||
|
}
|
||||||
|
return "Moderators"
|
||||||
|
}
|
||||||
|
moderatorOrModeratorsText := getModeratorOrModeratorsText()
|
||||||
|
|
||||||
|
numberOfViewedModeratorsLabel := getBoldLabel("Viewing " + numberOfViewedModeratorsString + " " + moderatorOrModeratorsText)
|
||||||
|
|
||||||
|
if (numberOfViewedModerators <= 5){
|
||||||
|
viewingModeratorsInfoRow := getWidgetCentered(numberOfViewedModeratorsLabel)
|
||||||
|
return viewingModeratorsInfoRow
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToBeginningButton := getNavigateToBeginningButton()
|
||||||
|
navigateToEndButton := getNavigateToEndButton()
|
||||||
|
navigateLeftButton := getNavigateLeftButton()
|
||||||
|
navigateRightButton := getNavigateRightButton()
|
||||||
|
|
||||||
|
viewingModeratorsInfoRow := container.NewHBox(layout.NewSpacer(), navigateToBeginningButton, navigateLeftButton, numberOfViewedModeratorsLabel, navigateRightButton, navigateToEndButton, layout.NewSpacer())
|
||||||
|
|
||||||
|
return viewingModeratorsInfoRow
|
||||||
|
}
|
||||||
|
|
||||||
|
viewingModeratorsInfoRow := getViewingModeratorsInfoRow()
|
||||||
|
|
||||||
|
moderatorResultsContainer := container.NewVBox()
|
||||||
|
|
||||||
|
if (viewIndex == 0){
|
||||||
|
moderatorResultsContainer.Add(refreshResultsButton)
|
||||||
|
moderatorResultsContainer.Add(widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyLabelA := widget.NewLabel("")
|
||||||
|
nameLabel := getItalicLabelCentered("Name")
|
||||||
|
identityHashTitle := getItalicLabelCentered("Identity Hash")
|
||||||
|
featuredAttributeTitle := getItalicLabelCentered(sortByAttributeTitle)
|
||||||
|
emptyLabelB := widget.NewLabel("")
|
||||||
|
|
||||||
|
resultIndexColumn := container.NewVBox(emptyLabelA, widget.NewSeparator())
|
||||||
|
nameColumn := container.NewVBox(nameLabel, widget.NewSeparator())
|
||||||
|
identityHashColumn := container.NewVBox(identityHashTitle, widget.NewSeparator())
|
||||||
|
featuredAttributeColumn := container.NewVBox(featuredAttributeTitle, widget.NewSeparator())
|
||||||
|
viewButtonsColumn := container.NewVBox(emptyLabelB, widget.NewSeparator())
|
||||||
|
|
||||||
|
viewIndexOnwardsModeratorIdentityHashesList := currentViewedModeratorsList[viewIndex:]
|
||||||
|
|
||||||
|
for index, moderatorIdentityHash := range viewIndexOnwardsModeratorIdentityHashesList{
|
||||||
|
|
||||||
|
resultIndex := viewIndex + index + 1
|
||||||
|
resultIndexString := helpers.ConvertIntToString(resultIndex)
|
||||||
|
|
||||||
|
resultIndexBoldLabel := getBoldLabel(resultIndexString + ".")
|
||||||
|
|
||||||
|
profileExists, _, getAnyModeratorAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(moderatorIdentityHash, appNetworkType, true, true, true)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (profileExists == false) {
|
||||||
|
// Profile has been deleted since results were generated
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserName := func()(string, error){
|
||||||
|
exists, _, username, err := getAnyModeratorAttributeFunction("Username")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (exists == false) {
|
||||||
|
return "Anonymous", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
theirUsernameTrimmed, _, err := helpers.TrimAndFlattenString(username, 15)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
return theirUsernameTrimmed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTheirUsername, err := getUserName()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
userNameLabel := getBoldLabelCentered(currentTheirUsername)
|
||||||
|
|
||||||
|
moderatorIdentityHashString, _, err := identity.EncodeIdentityHashBytesToString(moderatorIdentityHash)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
theirIdentityHashTrimmed, _, err := helpers.TrimAndFlattenString(moderatorIdentityHashString, 10)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
theirIdentityHashLabel := getBoldLabelCentered(theirIdentityHashTrimmed)
|
||||||
|
|
||||||
|
getFeaturedAttributeValue := func()(string, error){
|
||||||
|
|
||||||
|
exists, _, featuredAttributeValue, err := getAnyModeratorAttributeFunction(currentSortByAttribute)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (exists == false){
|
||||||
|
return "Unknown", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return featuredAttributeValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
featuredAttributeValue, err := getFeaturedAttributeValue()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
featuredAttributeValueLabel := getBoldLabelCentered(featuredAttributeValue)
|
||||||
|
|
||||||
|
viewModeratorButton := widget.NewButtonWithIcon("View", theme.VisibilityIcon(), func(){
|
||||||
|
setViewModeratorDetailsPage(window, moderatorIdentityHash, currentPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
resultIndexColumn.Add(resultIndexBoldLabel)
|
||||||
|
nameColumn.Add(userNameLabel)
|
||||||
|
identityHashColumn.Add(theirIdentityHashLabel)
|
||||||
|
featuredAttributeColumn.Add(featuredAttributeValueLabel)
|
||||||
|
viewButtonsColumn.Add(viewModeratorButton)
|
||||||
|
|
||||||
|
resultIndexColumn.Add(widget.NewSeparator())
|
||||||
|
nameColumn.Add(widget.NewSeparator())
|
||||||
|
identityHashColumn.Add(widget.NewSeparator())
|
||||||
|
featuredAttributeColumn.Add(widget.NewSeparator())
|
||||||
|
viewButtonsColumn.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
if (index >= 4){
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moderatorResultsGrid := container.NewHBox(layout.NewSpacer(), resultIndexColumn, nameColumn, identityHashColumn, featuredAttributeColumn, viewButtonsColumn, layout.NewSpacer())
|
||||||
|
|
||||||
|
moderatorResultsContainer.Add(moderatorResultsGrid)
|
||||||
|
|
||||||
|
viewedModeratorContainerScrollable := container.NewVScroll(moderatorResultsContainer)
|
||||||
|
viewedModeratorContainerBoxed := getWidgetBoxed(viewedModeratorContainerScrollable)
|
||||||
|
|
||||||
|
resultsContainer := container.NewBorder(viewingModeratorsInfoRow, nil, nil, nil, viewedModeratorContainerBoxed)
|
||||||
|
|
||||||
|
return resultsContainer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsContainer, err := getResultsContainer()
|
||||||
|
if (err != nil){
|
||||||
|
setErrorEncounteredPage(window, err, previousPage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
header := container.NewVBox(title, backButton, widget.NewSeparator(), pageButtonsRow, widget.NewSeparator(), sortByRow, widget.NewSeparator())
|
||||||
|
|
||||||
|
page := container.NewBorder(header, nil, nil, nil, resultsContainer)
|
||||||
|
|
||||||
|
setPageContent(page, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func setSelectViewedModeratorsSortByAttributePage(window fyne.Window, previousPage func()){
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("CurrentViewedPage", "ViewedModeratorsSortBySelect")
|
||||||
|
|
||||||
|
title := getPageTitleCentered("Select Sort By Attribute")
|
||||||
|
|
||||||
|
backButton := getBackButtonCentered(previousPage)
|
||||||
|
|
||||||
|
description := getLabelCentered("Choose the attribute to sort the moderators by.")
|
||||||
|
|
||||||
|
getSelectButton := func(attributeTitle string, attributeName string, sortDirection string) fyne.Widget{
|
||||||
|
|
||||||
|
button := widget.NewButton(attributeTitle, func(){
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsSortedStatus", "No")
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsSortByAttribute", attributeName)
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsSortDirection", sortDirection)
|
||||||
|
_ = mySettings.SetSetting("ViewedModeratorsViewIndex", "0")
|
||||||
|
|
||||||
|
previousPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
identityScoreButton := getSelectButton("Identity Score", "IdentityScore", "Ascending")
|
||||||
|
banAdvocatesButton := getSelectButton("Ban Advocates", "BanAdvocates", "Descending")
|
||||||
|
controversyButton := getSelectButton("Controversy", "Controversy", "Ascending")
|
||||||
|
numberOfReviewsButton := getSelectButton("Number Of Reviews", "NumberOfReviews", "Descending")
|
||||||
|
|
||||||
|
buttonsGrid := getContainerCentered(container.NewGridWithColumns(1, identityScoreButton, banAdvocatesButton, controversyButton, numberOfReviewsButton))
|
||||||
|
|
||||||
|
content := container.NewVBox(title, backButton, widget.NewSeparator(), description, widget.NewSeparator(), buttonsGrid)
|
||||||
|
|
||||||
|
setPageContent(content, window)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
4660
gui/viewProfileGui.go
Normal file
4660
gui/viewProfileGui.go
Normal file
File diff suppressed because it is too large
Load diff
72
imported/geodist/geodist.go
Normal file
72
imported/geodist/geodist.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
|
||||||
|
// geodist is a package created by jftuga
|
||||||
|
// Repository: github.com/jftuga/geodist
|
||||||
|
// It is used to calculate distance between latitude/longitude points
|
||||||
|
|
||||||
|
package geodist
|
||||||
|
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
// The below function is adapted from github.com/jftuga/geodist
|
||||||
|
|
||||||
|
// HaversineDistance returns the distance (in miles) between two points of
|
||||||
|
// a given longitude and latitude relatively accurately (using a spherical
|
||||||
|
// approximation of the Earth) through the Haversin Distance Formula for
|
||||||
|
// great arc distance on a sphere with accuracy for small distances
|
||||||
|
//
|
||||||
|
// Point coordinates are supplied in degrees and converted into rad.
|
||||||
|
//
|
||||||
|
// https://en.wikipedia.org/wiki/Haversine_formula
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -float64: Distance in kilometers
|
||||||
|
// -error
|
||||||
|
func GetDistanceBetweenCoordinates(aCoordinateLatitude float64, aCoordinateLongitude float64, bCoordinateLatitude float64, bCoordinateLongitude float64) (float64, error) {
|
||||||
|
|
||||||
|
aLatitudeIsValid := helpers.VerifyLatitude(aCoordinateLatitude)
|
||||||
|
if (aLatitudeIsValid == false){
|
||||||
|
return 0, errors.New("GetDistanceBetweenCoordinates called with invalid aCoordinateLatitude.")
|
||||||
|
}
|
||||||
|
aLongitudeIsValid := helpers.VerifyLongitude(aCoordinateLongitude)
|
||||||
|
if (aLongitudeIsValid == false){
|
||||||
|
return 0, errors.New("GetDistanceBetweenCoordinates called with invalid aCoordinateLongitude.")
|
||||||
|
}
|
||||||
|
|
||||||
|
bLatitudeIsValid := helpers.VerifyLatitude(bCoordinateLatitude)
|
||||||
|
if (bLatitudeIsValid == false){
|
||||||
|
return 0, errors.New("GetDistanceBetweenCoordinates called with invalid bCoordinateLatitude.")
|
||||||
|
}
|
||||||
|
bLongitudeIsValid := helpers.VerifyLongitude(bCoordinateLongitude)
|
||||||
|
if (bLongitudeIsValid == false){
|
||||||
|
return 0, errors.New("GetDistanceBetweenCoordinates called with invalid bCoordinateLongitude.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to radians
|
||||||
|
|
||||||
|
piRad := math.Pi / 180
|
||||||
|
|
||||||
|
la1 := aCoordinateLatitude * piRad
|
||||||
|
lo1 := aCoordinateLongitude * piRad
|
||||||
|
la2 := bCoordinateLatitude * piRad
|
||||||
|
lo2 := bCoordinateLongitude * piRad
|
||||||
|
|
||||||
|
r := float64(6378100) // Earth radius in meters
|
||||||
|
|
||||||
|
// haversin(θ) function
|
||||||
|
hsin := func(theta float64) float64 {
|
||||||
|
result := math.Pow(math.Sin(theta/2), 2)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
h := hsin(la2-la1) + math.Cos(la1) * math.Cos(la2) * hsin(lo2-lo1)
|
||||||
|
|
||||||
|
meters := 2 * r * math.Asin(math.Sqrt(h))
|
||||||
|
kilometers := meters / 1000
|
||||||
|
|
||||||
|
return kilometers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
44
imported/geodist/geodist_test.go
Normal file
44
imported/geodist/geodist_test.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package geodist_test
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
import "seekia/imported/geodist"
|
||||||
|
|
||||||
|
func TestDistanceCalculations(t *testing.T){
|
||||||
|
|
||||||
|
// distance New York to San Diego: 3915340.577 m
|
||||||
|
// distance El Paso to Saint Louis: 1663833.491 m
|
||||||
|
|
||||||
|
newYorkLatitude := 40.7128
|
||||||
|
newYorkLongitude := 74.0060
|
||||||
|
|
||||||
|
sanDiegoLatitude := 32.7157
|
||||||
|
sanDiegoLongitude := 117.1611
|
||||||
|
|
||||||
|
elPasoLatitude := 31.7619
|
||||||
|
elPasoLongitude := 106.4850
|
||||||
|
|
||||||
|
stLouisLatitude := 38.6270
|
||||||
|
stLouisLongitude := 90.1994
|
||||||
|
|
||||||
|
kilometers, err := geodist.GetDistanceBetweenCoordinates(newYorkLatitude, newYorkLongitude, sanDiegoLatitude, sanDiegoLongitude)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Cannot compute distance between coordinates: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int(kilometers) != 3911){
|
||||||
|
t.Fatalf("Distance is invalid between New York and San Diego")
|
||||||
|
}
|
||||||
|
|
||||||
|
kilometers, err = geodist.GetDistanceBetweenCoordinates(elPasoLatitude, elPasoLongitude, stLouisLatitude, stLouisLongitude)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Cannot compute distance between coordinates: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int(kilometers) != 1663){
|
||||||
|
t.Fatalf("Distance is invalid between El Paso and St. Louis")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
113
imported/goeffects/cartoon.go
Normal file
113
imported/goeffects/cartoon.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package goeffects
|
||||||
|
|
||||||
|
// Package copied from https://github.com/markdaws/go-effects
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "runtime"
|
||||||
|
|
||||||
|
|
||||||
|
// NewCartoon returns an effect that renders images as if they are drawn like a cartoon.
|
||||||
|
// It works by rendering the input image using the OilPainting effect, then drawing lines
|
||||||
|
// ontop of the image based on the Sobel edge detection method. You will probably have to
|
||||||
|
// play with the opts values to get a good result. Some starting values are:
|
||||||
|
// BlurKernelSize: 21
|
||||||
|
// EdgeThreshold: 40
|
||||||
|
// OilFilterSize: 15
|
||||||
|
// OilLevels: 15
|
||||||
|
func NewCartoon(opts CTOpts) Effect {
|
||||||
|
return &cartoon{
|
||||||
|
opts: opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CTOpts options to pass to the Cartoon effect
|
||||||
|
type CTOpts struct {
|
||||||
|
// BlurKernelSize is the gaussian blur kernel size. You might need to blur
|
||||||
|
// the original input image to reduce the amount of noise you get in the edge
|
||||||
|
// detection phase. Set to 0 to skip blur, otherwise the number must be an
|
||||||
|
// odd number, the bigger the number the more blur
|
||||||
|
BlurKernelSize int
|
||||||
|
|
||||||
|
// EdgeThreshold is a number between 0 and 255 that specifies a cutoff point to
|
||||||
|
// determine if an intensity change is an edge. Make smaller to include more details
|
||||||
|
// as edges
|
||||||
|
EdgeThreshold int
|
||||||
|
|
||||||
|
// OilFilterSize specifies how bold the simulated strokes will be when turning the
|
||||||
|
// style towards a painting, something around 5,10,15 should work well
|
||||||
|
OilFilterSize int
|
||||||
|
|
||||||
|
// OilLevels is the number of levels that the oil painting style will bucket colors in
|
||||||
|
// to. Larger number to get more detail.
|
||||||
|
OilLevels int
|
||||||
|
|
||||||
|
// DebugPath is not empty is assumed to be a path where intermediate debug files can
|
||||||
|
// be written to, such as the gaussian blured image and the sobel edge detection. This
|
||||||
|
// can be useful for tweaking parameters
|
||||||
|
DebugPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
type cartoon struct {
|
||||||
|
opts CTOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply runs the image through the cartoon filter
|
||||||
|
func (c *cartoon) Apply(img *Image, numRoutines int) (*Image, error) {
|
||||||
|
if numRoutines == 0 {
|
||||||
|
numRoutines = runtime.GOMAXPROCS(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline := Pipeline{}
|
||||||
|
if c.opts.BlurKernelSize > 0 {
|
||||||
|
pipeline.Add(NewGaussian(c.opts.BlurKernelSize, 1), nil)
|
||||||
|
}
|
||||||
|
pipeline.Add(NewGrayscale(GSLUMINOSITY), nil)
|
||||||
|
pipeline.Add(NewSobel(c.opts.EdgeThreshold, false), nil)
|
||||||
|
edgeImg, err := pipeline.Run(img, numRoutines)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
edgePix := edgeImg.img.Pix
|
||||||
|
pf := func(ri, x, y, offset, inStride int, inPix, outPix []uint8) {
|
||||||
|
|
||||||
|
r := inPix[offset]
|
||||||
|
g := inPix[offset+1]
|
||||||
|
b := inPix[offset+2]
|
||||||
|
|
||||||
|
rEdge := edgePix[offset]
|
||||||
|
if rEdge == 255 {
|
||||||
|
r = 0
|
||||||
|
b = 0
|
||||||
|
g = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
outPix[offset] = r
|
||||||
|
outPix[offset+1] = g
|
||||||
|
outPix[offset+2] = b
|
||||||
|
outPix[offset+3] = 255
|
||||||
|
}
|
||||||
|
|
||||||
|
oil := NewOilPainting(c.opts.OilFilterSize, c.opts.OilLevels)
|
||||||
|
oilImg, err := oil.Apply(img, numRoutines)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &Image{
|
||||||
|
img: image.NewRGBA(image.Rectangle{
|
||||||
|
Min: image.Point{X: 0, Y: 0},
|
||||||
|
Max: image.Point{X: img.Width, Y: img.Height},
|
||||||
|
}),
|
||||||
|
Width: img.Width,
|
||||||
|
Height: img.Height,
|
||||||
|
|
||||||
|
// Have to take in to account pixels are lost in some of the effects around the edges,
|
||||||
|
// so so only have the area where the two rections intersect from the edge detection and
|
||||||
|
// the oil painting effect
|
||||||
|
Bounds: oilImg.Bounds.Intersect(edgeImg.Bounds),
|
||||||
|
}
|
||||||
|
runParallel(numRoutines, oilImg, out.Bounds, out, pf, 0)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
80
imported/goeffects/effects.go
Normal file
80
imported/goeffects/effects.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package goeffects
|
||||||
|
|
||||||
|
// Package copied from https://github.com/markdaws/go-effects
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// Effect interface for any effect type
|
||||||
|
type Effect interface {
|
||||||
|
// Apply applies the effect to the input image and returns an output image
|
||||||
|
Apply(img *Image, numRoutines int) (*Image, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type pixelFunc func(ri, x, y, offset, inStride int, inPix, outPix []uint8)
|
||||||
|
|
||||||
|
func runParallel(numRoutines int, inImg *Image, inBounds Rect, outImg *Image, pf pixelFunc, blockWidth int) {
|
||||||
|
w := inBounds.Width
|
||||||
|
h := inBounds.Height
|
||||||
|
|
||||||
|
minX := inBounds.X
|
||||||
|
minY := inBounds.Y
|
||||||
|
|
||||||
|
stride := inImg.img.Stride
|
||||||
|
inPix := inImg.img.Pix
|
||||||
|
outPix := outImg.img.Pix
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
xOffset := minX
|
||||||
|
|
||||||
|
var widthPerRoutine int
|
||||||
|
if blockWidth != 0 {
|
||||||
|
widthPerRoutine = blockWidth
|
||||||
|
} else {
|
||||||
|
widthPerRoutine = w / numRoutines
|
||||||
|
}
|
||||||
|
|
||||||
|
for r := 0; r < numRoutines; r++ {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
if r == numRoutines-1 {
|
||||||
|
widthPerRoutine = (minX + w) - xOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(ri, xStart, yStart, width, height int) {
|
||||||
|
for x := xStart; x < xStart+width; x++ {
|
||||||
|
for y := yStart; y < yStart+height; y++ {
|
||||||
|
offset := y*stride + x*4
|
||||||
|
pf(ri, x, y, offset, stride, inPix, outPix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}(r, xOffset, minY, widthPerRoutine, h)
|
||||||
|
|
||||||
|
xOffset += widthPerRoutine
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundToInt32(a float64) int32 {
|
||||||
|
if a < 0 {
|
||||||
|
return int32(a - 0.5)
|
||||||
|
}
|
||||||
|
return int32(a + 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rangeInt(i, minimum, maximum int) int {
|
||||||
|
|
||||||
|
result := min(max(i, minimum), maximum)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOddInt(i int) bool {
|
||||||
|
return i%2 != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func reset(s []int) {
|
||||||
|
for i := range s {
|
||||||
|
s[i] = 0
|
||||||
|
}
|
||||||
|
}
|
100
imported/goeffects/gaussian.go
Normal file
100
imported/goeffects/gaussian.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package goeffects
|
||||||
|
|
||||||
|
// Package copied from https://github.com/markdaws/go-effects
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
import "image"
|
||||||
|
import "math"
|
||||||
|
import "runtime"
|
||||||
|
|
||||||
|
|
||||||
|
type gaussian struct {
|
||||||
|
kernelSize int
|
||||||
|
sigma float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGaussian is an effect that applies a gaussian blur to the image
|
||||||
|
func NewGaussian(kernelSize int, sigma float64) Effect {
|
||||||
|
return &gaussian{
|
||||||
|
kernelSize: kernelSize,
|
||||||
|
sigma: sigma,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gaussian) Apply(img *Image, numRoutines int) (*Image, error) {
|
||||||
|
if !isOddInt(g.kernelSize) {
|
||||||
|
return nil, fmt.Errorf("kernel size must be odd")
|
||||||
|
}
|
||||||
|
|
||||||
|
if numRoutines == 0 {
|
||||||
|
numRoutines = runtime.GOMAXPROCS(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
kernel := gaussianKernel(g.kernelSize, g.sigma)
|
||||||
|
kernelOffset := (g.kernelSize - 1) / 2
|
||||||
|
pf := func(ri, x, y, offset, inStride int, inPix, outPix []uint8) {
|
||||||
|
var gr, gb, gg float64
|
||||||
|
for dy := -kernelOffset; dy <= kernelOffset; dy++ {
|
||||||
|
for dx := -kernelOffset; dx <= kernelOffset; dx++ {
|
||||||
|
pOffset := offset + (dx*4 + dy*inStride)
|
||||||
|
r := inPix[pOffset]
|
||||||
|
g := inPix[pOffset+1]
|
||||||
|
b := inPix[pOffset+2]
|
||||||
|
|
||||||
|
scale := kernel[dx+kernelOffset][dy+kernelOffset]
|
||||||
|
gr += scale * float64(r)
|
||||||
|
gg += scale * float64(g)
|
||||||
|
gb += scale * float64(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outPix[offset] = uint8(gr)
|
||||||
|
outPix[offset+1] = uint8(gg)
|
||||||
|
outPix[offset+2] = uint8(gb)
|
||||||
|
outPix[offset+3] = 255
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &Image{
|
||||||
|
img: image.NewRGBA(image.Rectangle{
|
||||||
|
Min: image.Point{X: 0, Y: 0},
|
||||||
|
Max: image.Point{X: img.Width, Y: img.Height},
|
||||||
|
}),
|
||||||
|
Width: img.Width,
|
||||||
|
Height: img.Height,
|
||||||
|
Bounds: Rect{
|
||||||
|
X: img.Bounds.X + kernelOffset,
|
||||||
|
Y: img.Bounds.Y + kernelOffset,
|
||||||
|
Width: img.Bounds.Width - 2*kernelOffset,
|
||||||
|
Height: img.Bounds.Height - 2*kernelOffset,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runParallel(numRoutines, img, out.Bounds, out, pf, 0)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gaussianKernel(dimension int, sigma float64) [][]float64 {
|
||||||
|
k := make([][]float64, dimension)
|
||||||
|
sum := 0.0
|
||||||
|
for x := 0; x < dimension; x++ {
|
||||||
|
k[x] = make([]float64, dimension)
|
||||||
|
for y := 0; y < dimension; y++ {
|
||||||
|
k[x][y] = gaussianXY(x, y, sigma)
|
||||||
|
sum += k[x][y]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scale := 1.0 / sum
|
||||||
|
for y := 0; y < dimension; y++ {
|
||||||
|
for x := 0; x < dimension; x++ {
|
||||||
|
k[x][y] *= scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
// expects x,y to be 0 at the center of the kernel
|
||||||
|
func gaussianXY(x, y int, sigma float64) float64 {
|
||||||
|
return ((1.0 / (2 * math.Pi * sigma * sigma)) * math.E) - (float64(x*x+y*y) / (2 * sigma * sigma))
|
||||||
|
}
|
207
imported/goeffects/goeffects.go
Normal file
207
imported/goeffects/goeffects.go
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
package goeffects
|
||||||
|
|
||||||
|
// goeffects.go provides functions to interface with go-effects
|
||||||
|
// Package copied from https://github.com/markdaws/go-effects/
|
||||||
|
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/imagery"
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "image/draw"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
|
||||||
|
func ApplyCartoonEffect(inputImage image.Image, effectStrength int)(image.Image, error){
|
||||||
|
|
||||||
|
if (inputImage == nil){
|
||||||
|
return nil, errors.New("ApplyCartoonEffect called with nil image.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectStrength < 0 || effectStrength > 100){
|
||||||
|
return nil, errors.New("ApplyCartoonEffect called with invalid effectStrength.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectStrength == 0) {
|
||||||
|
return inputImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
blurKernelSize, err := helpers.ScaleNumberProportionally(true, effectStrength, 0, 100, 1, 3)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (blurKernelSize % 2 == 0){
|
||||||
|
blurKernelSize += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeThreshold, err := helpers.ScaleNumberProportionally(false, effectStrength, 0, 100, 5, 200)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
oilFilterSize, err := helpers.ScaleNumberProportionally(true, effectStrength, 0, 100, 5, 20)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
oilLevels, err := helpers.ScaleNumberProportionally(true, effectStrength, 0, 100, 1, 3)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
options := CTOpts{
|
||||||
|
BlurKernelSize : blurKernelSize,
|
||||||
|
EdgeThreshold : edgeThreshold,
|
||||||
|
OilFilterSize : oilFilterSize,
|
||||||
|
OilLevels : oilLevels,
|
||||||
|
}
|
||||||
|
|
||||||
|
cartoonEffectObject := NewCartoon(options)
|
||||||
|
|
||||||
|
goeffectsImageObject, err := convertGolangImageObjectToGoeffectsImageObject(inputImage)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
resultGoeffectsImage, err := cartoonEffectObject.Apply(&goeffectsImageObject, 8)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
result := convertGoeffectsImageObjectToGolangImageObject(*resultGoeffectsImage)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplyPencilEffect(inputImage image.Image, effectStrength int)(image.Image, error){
|
||||||
|
|
||||||
|
if (inputImage == nil){
|
||||||
|
return nil, errors.New("ApplyPencilEffect called with nil image.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectStrength < 0 || effectStrength > 100){
|
||||||
|
return nil, errors.New("ApplyPencilEffect called with invalid effectStrength.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectStrength == 0) {
|
||||||
|
return inputImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
goeffectsImageObject, err := convertGolangImageObjectToGoeffectsImageObject(inputImage)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
blurAmount, err := helpers.ScaleNumberProportionally(true, effectStrength, 0, 100, 1, 20)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
if (blurAmount % 2 == 0) {
|
||||||
|
blurAmount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pencilEffectObject := NewPencil(blurAmount)
|
||||||
|
|
||||||
|
resultGoeffectsImage, err := pencilEffectObject.Apply(&goeffectsImageObject, 5)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
result := convertGoeffectsImageObjectToGolangImageObject(*resultGoeffectsImage)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplyWireframeEffect(inputImage image.Image, effectStrength int, lightMode bool)(image.Image, error){
|
||||||
|
|
||||||
|
if (inputImage == nil){
|
||||||
|
return nil, errors.New("ApplyWireframeEffect called with nil image.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectStrength < 0 || effectStrength > 100){
|
||||||
|
return nil, errors.New("ApplyWireframeEffect called with invalid effectStrength.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectStrength == 0) {
|
||||||
|
return inputImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
goeffectsImageObject, err := convertGolangImageObjectToGoeffectsImageObject(inputImage)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
const algorithm GSAlgo = 3
|
||||||
|
|
||||||
|
grayscaleEffectObject := NewGrayscale(algorithm)
|
||||||
|
|
||||||
|
grayscaleGoeffectsImage, err := grayscaleEffectObject.Apply(&goeffectsImageObject, 5)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
threshold, err := helpers.ScaleNumberProportionally(false, effectStrength, 0, 100, 10, 100)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
sobelEffectObject := NewSobel(threshold, lightMode)
|
||||||
|
|
||||||
|
resultGoeffectsImage, err := sobelEffectObject.Apply(grayscaleGoeffectsImage, 5)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
result := convertGoeffectsImageObjectToGolangImageObject(*resultGoeffectsImage)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplyOilPaintingEffect(inputImage image.Image, effectStrength int)(image.Image, error){
|
||||||
|
|
||||||
|
if (inputImage == nil){
|
||||||
|
return nil, errors.New("ApplyOilPaintingEffect called with nil image.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectStrength < 0 || effectStrength > 100){
|
||||||
|
return nil, errors.New("ApplyOilPaintingEffect called with invalid effectStrength.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectStrength == 0) {
|
||||||
|
return inputImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filterSize, err := helpers.ScaleNumberProportionally(true, effectStrength, 0, 100, 10, 30)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
levels, err := helpers.ScaleNumberProportionally(true, effectStrength, 0, 100, 10, 70)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
oilPaintingEffectObject := NewOilPainting(filterSize, levels)
|
||||||
|
|
||||||
|
goeffectsImageObject, err := convertGolangImageObjectToGoeffectsImageObject(inputImage)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
resultGoeffectsImage, err := oilPaintingEffectObject.Apply(&goeffectsImageObject, 5)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
result := convertGoeffectsImageObjectToGolangImageObject(*resultGoeffectsImage)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertGolangImageObjectToGoeffectsImageObject(inputImage image.Image)(Image, error){
|
||||||
|
|
||||||
|
newImageRGBA := image.NewRGBA(inputImage.Bounds())
|
||||||
|
draw.Draw(newImageRGBA, inputImage.Bounds(), inputImage, image.Point{}, draw.Over)
|
||||||
|
|
||||||
|
width, height, err := imagery.GetImageWidthAndHeightPixels(inputImage)
|
||||||
|
if (err != nil) {
|
||||||
|
var failedImage Image
|
||||||
|
return failedImage, err
|
||||||
|
}
|
||||||
|
|
||||||
|
goeffectsImage := Image{
|
||||||
|
img: newImageRGBA,
|
||||||
|
Width: width,
|
||||||
|
Height: height,
|
||||||
|
Bounds: Rect{X: 0, Y: 0, Width: width, Height: height},
|
||||||
|
}
|
||||||
|
|
||||||
|
return goeffectsImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertGoeffectsImageObjectToGolangImageObject(inputImage Image) image.Image {
|
||||||
|
|
||||||
|
imageRGBA := inputImage.img
|
||||||
|
|
||||||
|
imageWidth := inputImage.Width
|
||||||
|
imageHeight := inputImage.Height
|
||||||
|
|
||||||
|
newRectangle := image.Rect(0, 0, imageWidth, imageHeight)
|
||||||
|
|
||||||
|
newImage := image.NewRGBA(newRectangle)
|
||||||
|
|
||||||
|
draw.Draw(newImage, newImage.Bounds(), imageRGBA, image.Point{}, draw.Over)
|
||||||
|
|
||||||
|
return newImage
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
78
imported/goeffects/grayscale.go
Normal file
78
imported/goeffects/grayscale.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
package goeffects
|
||||||
|
|
||||||
|
// Package copied from https://github.com/markdaws/go-effects
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "runtime"
|
||||||
|
|
||||||
|
// GSAlgo the type of algorithm to use when converting an image to it's grayscale equivalent
|
||||||
|
type GSAlgo int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// GSLIGHTNESS is the average of the min and max r,g,b value
|
||||||
|
GSLIGHTNESS GSAlgo = iota
|
||||||
|
|
||||||
|
// GSAVERAGE is the average of the r,g,b values of each pixel
|
||||||
|
GSAVERAGE
|
||||||
|
|
||||||
|
// GSLUMINOSITY used a weighting for r,g,b based on how the human eye perceives colors
|
||||||
|
GSLUMINOSITY
|
||||||
|
)
|
||||||
|
|
||||||
|
type grayscale struct {
|
||||||
|
algo GSAlgo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs *grayscale) Apply(img *Image, numRoutines int) (*Image, error) {
|
||||||
|
if numRoutines == 0 {
|
||||||
|
numRoutines = runtime.GOMAXPROCS(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pf := func(ri, x, y, offset, inStride int, inPix, outPix []uint8) {
|
||||||
|
var r, g, b uint8 = inPix[offset], inPix[offset+1], inPix[offset+2]
|
||||||
|
switch gs.algo {
|
||||||
|
case GSLIGHTNESS:
|
||||||
|
maximum := float64(max(max(r, g), b))
|
||||||
|
minimum := float64(max(min(r, g), b))
|
||||||
|
r = uint8(maximum + minimum/2)
|
||||||
|
g = r
|
||||||
|
b = r
|
||||||
|
case GSAVERAGE:
|
||||||
|
r = (r + g + b) / 3
|
||||||
|
g = r
|
||||||
|
b = r
|
||||||
|
case GSLUMINOSITY:
|
||||||
|
r = uint8(0.21*float64(r) + 0.72*float64(g) + 0.07*float64(b))
|
||||||
|
g = r
|
||||||
|
b = r
|
||||||
|
}
|
||||||
|
outPix[offset] = r
|
||||||
|
outPix[offset+1] = g
|
||||||
|
outPix[offset+2] = b
|
||||||
|
outPix[offset+3] = 255
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &Image{
|
||||||
|
img: image.NewRGBA(image.Rectangle{
|
||||||
|
Min: image.Point{X: 0, Y: 0},
|
||||||
|
Max: image.Point{X: img.Width, Y: img.Height},
|
||||||
|
}),
|
||||||
|
Width: img.Width,
|
||||||
|
Height: img.Height,
|
||||||
|
Bounds: Rect{
|
||||||
|
X: img.Bounds.X,
|
||||||
|
Y: img.Bounds.Y,
|
||||||
|
Width: img.Bounds.Width,
|
||||||
|
Height: img.Bounds.Height,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runParallel(numRoutines, img, out.Bounds, out, pf, 0)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGrayscale renders the input image as a grayscale image. numRoutines specifies how many
|
||||||
|
// goroutines should be used to process the image in parallel, use 0 to let the library decide
|
||||||
|
func NewGrayscale(algo GSAlgo) Effect {
|
||||||
|
return &grayscale{algo: algo}
|
||||||
|
}
|
13
imported/goeffects/image.go
Normal file
13
imported/goeffects/image.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package goeffects
|
||||||
|
|
||||||
|
// Package copied from https://github.com/markdaws/go-effects
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
|
// Image wrapper around internal pixels
|
||||||
|
type Image struct {
|
||||||
|
img *image.RGBA
|
||||||
|
Bounds Rect
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
95
imported/goeffects/oilpainting.go
Normal file
95
imported/goeffects/oilpainting.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package goeffects
|
||||||
|
|
||||||
|
// Package copied from https://github.com/markdaws/go-effects
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "runtime"
|
||||||
|
|
||||||
|
|
||||||
|
type oilPainting struct {
|
||||||
|
filterSize int
|
||||||
|
levels int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOilPainting renders the input image as if it was painted like an oil painting. numRoutines specifies how many
|
||||||
|
// goroutines should be used to process the image in parallel, use 0 to let the library decide. filterSize specifies
|
||||||
|
// how bold the image should look, larger numbers equate to larger strokes, levels specifies how many buckets colors
|
||||||
|
// will be grouped in to, start with values 5,30 to see how that works.
|
||||||
|
|
||||||
|
func NewOilPainting(filterSize, levels int) Effect {
|
||||||
|
return &oilPainting{filterSize: filterSize, levels: levels}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (op *oilPainting) Apply(img *Image, numRoutines int) (*Image, error) {
|
||||||
|
levels := op.levels - 1
|
||||||
|
filterOffset := (op.filterSize - 1) / 2
|
||||||
|
|
||||||
|
if numRoutines == 0 {
|
||||||
|
numRoutines = runtime.GOMAXPROCS(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var iBin, rBin, gBin, bBin [][]int
|
||||||
|
iBin = make([][]int, numRoutines)
|
||||||
|
rBin = make([][]int, numRoutines)
|
||||||
|
gBin = make([][]int, numRoutines)
|
||||||
|
bBin = make([][]int, numRoutines)
|
||||||
|
for ri := 0; ri < numRoutines; ri++ {
|
||||||
|
iBin[ri] = make([]int, levels+1)
|
||||||
|
rBin[ri] = make([]int, levels+1)
|
||||||
|
gBin[ri] = make([]int, levels+1)
|
||||||
|
bBin[ri] = make([]int, levels+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pf := func(ri, x, y, offset, inStride int, inPix, outPix []uint8) {
|
||||||
|
reset(iBin[ri])
|
||||||
|
reset(rBin[ri])
|
||||||
|
reset(gBin[ri])
|
||||||
|
reset(bBin[ri])
|
||||||
|
|
||||||
|
var maxIntensity int
|
||||||
|
var maxIndex int
|
||||||
|
|
||||||
|
for fy := -filterOffset; fy <= filterOffset; fy++ {
|
||||||
|
for fx := -filterOffset; fx <= filterOffset; fx++ {
|
||||||
|
fOffset := offset + (fx*4 + fy*inStride)
|
||||||
|
r := inPix[fOffset]
|
||||||
|
g := inPix[fOffset+1]
|
||||||
|
b := inPix[fOffset+2]
|
||||||
|
ci := int(roundToInt32((float64(r+g+b) / 3.0 * float64(levels)) / 255.0))
|
||||||
|
iBin[ri][ci]++
|
||||||
|
rBin[ri][ci] += int(r)
|
||||||
|
gBin[ri][ci] += int(g)
|
||||||
|
bBin[ri][ci] += int(b)
|
||||||
|
|
||||||
|
if iBin[ri][ci] > maxIntensity {
|
||||||
|
maxIntensity = iBin[ri][ci]
|
||||||
|
maxIndex = ci
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outPix[offset] = uint8(rBin[ri][maxIndex] / maxIntensity)
|
||||||
|
outPix[offset+1] = uint8(gBin[ri][maxIndex] / maxIntensity)
|
||||||
|
outPix[offset+2] = uint8(bBin[ri][maxIndex] / maxIntensity)
|
||||||
|
outPix[offset+3] = 255
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &Image{
|
||||||
|
img: image.NewRGBA(image.Rectangle{
|
||||||
|
Min: image.Point{X: 0, Y: 0},
|
||||||
|
Max: image.Point{X: img.Width, Y: img.Height},
|
||||||
|
}),
|
||||||
|
Width: img.Width,
|
||||||
|
Height: img.Height,
|
||||||
|
Bounds: Rect{
|
||||||
|
X: img.Bounds.X + filterOffset,
|
||||||
|
Y: img.Bounds.Y + filterOffset,
|
||||||
|
Width: img.Bounds.Width - 2*filterOffset,
|
||||||
|
Height: img.Bounds.Height - 2*filterOffset,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
runParallel(numRoutines, img, out.Bounds, out, pf, 0)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
43
imported/goeffects/pencil.go
Normal file
43
imported/goeffects/pencil.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package goeffects
|
||||||
|
|
||||||
|
// Package copied from https://github.com/markdaws/go-effects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pencil struct {
|
||||||
|
blurFactor int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pencil) Apply(img *Image, numRoutines int) (*Image, error) {
|
||||||
|
if !isOddInt(p.blurFactor) {
|
||||||
|
return nil, fmt.Errorf("blurFactor must be odd")
|
||||||
|
}
|
||||||
|
|
||||||
|
if numRoutines == 0 {
|
||||||
|
numRoutines = runtime.GOMAXPROCS(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
inImg := img
|
||||||
|
if p.blurFactor != 0 {
|
||||||
|
var err error
|
||||||
|
gaussian := NewGaussian(p.blurFactor, 1)
|
||||||
|
inImg, err = gaussian.Apply(img, numRoutines)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sobel := NewSobel(-1, true)
|
||||||
|
out, err := sobel.Apply(inImg, numRoutines)
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPencil renders the input image as if it was drawn in pencil. It is simply
|
||||||
|
// an inverted Sobel image. You can specify the blurFactor, a value that must
|
||||||
|
// be odd, to blur the input image to get rid of the noise. This is the gaussian
|
||||||
|
// kernel size, larger numbers blur more but can significantly increase processing time.
|
||||||
|
func NewPencil(blurFactor int) Effect {
|
||||||
|
return &pencil{blurFactor: blurFactor}
|
||||||
|
}
|
37
imported/goeffects/pipeline.go
Normal file
37
imported/goeffects/pipeline.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package goeffects
|
||||||
|
|
||||||
|
// Package copied from https://github.com/markdaws/go-effects
|
||||||
|
|
||||||
|
|
||||||
|
// Pipeline allows multiple effects to be composed together easily
|
||||||
|
type Pipeline struct {
|
||||||
|
effects []item
|
||||||
|
}
|
||||||
|
|
||||||
|
type item struct {
|
||||||
|
effect Effect
|
||||||
|
callback func(*Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds an effect to the pipeline
|
||||||
|
func (p *Pipeline) Add(e Effect, callback func(*Image)) {
|
||||||
|
p.effects = append(p.effects, item{effect: e, callback: callback})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Run executes all of the effects in the order they were passed to the Add function
|
||||||
|
// on the input image and returns the results.
|
||||||
|
func (p *Pipeline) Run(img *Image, numRoutines int) (*Image, error) {
|
||||||
|
currentImg := img
|
||||||
|
for _, item := range p.effects {
|
||||||
|
outImg, err := item.effect.Apply(currentImg, numRoutines)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if item.callback != nil {
|
||||||
|
item.callback(outImg)
|
||||||
|
}
|
||||||
|
currentImg = outImg
|
||||||
|
}
|
||||||
|
return currentImg, nil
|
||||||
|
}
|
47
imported/goeffects/rect.go
Normal file
47
imported/goeffects/rect.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package goeffects
|
||||||
|
|
||||||
|
// Package copied from https://github.com/markdaws/go-effects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rect used for image bounds
|
||||||
|
type Rect struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a debug string
|
||||||
|
func (r Rect) String() string {
|
||||||
|
return fmt.Sprintf("X:%d, Y:%d, Width:%d, Height:%d", r.X, r.Y, r.Width, r.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intersect returns the intersection between two rectangles
|
||||||
|
func (r Rect) Intersect(r2 Rect) Rect {
|
||||||
|
x := max(r.X, r2.X)
|
||||||
|
num1 := min((r.X+r.Width), (r2.X+r2.Width))
|
||||||
|
|
||||||
|
y := max(r.Y, r2.Y)
|
||||||
|
num2 := min((r.Y+r.Height), (r2.Y+r2.Height))
|
||||||
|
if num1 >= x && num2 >= y {
|
||||||
|
return Rect{X: x, Y: y, Width: (num1 - x), Height: (num2 - y)}
|
||||||
|
}
|
||||||
|
return Rect{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if this is an empty rectangle
|
||||||
|
func (r Rect) IsEmpty() bool {
|
||||||
|
return r.Width == 0 || r.Height == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToImageRect returns an image.Rectangle instance initialized from this Rect
|
||||||
|
func (r Rect) ToImageRect() image.Rectangle {
|
||||||
|
return image.Rectangle{
|
||||||
|
Min: image.Point{X: r.X, Y: r.Y},
|
||||||
|
Max: image.Point{X: r.X + r.Width, Y: r.Y + r.Height},
|
||||||
|
}
|
||||||
|
}
|
90
imported/goeffects/sobel.go
Normal file
90
imported/goeffects/sobel.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
package goeffects
|
||||||
|
|
||||||
|
// Package copied from https://github.com/markdaws/go-effects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sobel struct {
|
||||||
|
threshold int
|
||||||
|
invert bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSobel the input image should be a grayscale image, the output will be a version of
|
||||||
|
// the input image with the Sobel edge detector applied to it. A value of -1 for threshold
|
||||||
|
// will return an image whos rgb values are the sobel intensity values, if 0 <= threshold <= 255
|
||||||
|
// then the rgb values will be 255 if the intensity is >= threshold and 0 if the intensity
|
||||||
|
// is < threshold
|
||||||
|
func NewSobel(threshold int, invert bool) Effect {
|
||||||
|
return &sobel{
|
||||||
|
threshold: threshold,
|
||||||
|
invert: invert,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sobel) Apply(img *Image, numRoutines int) (*Image, error) {
|
||||||
|
if numRoutines == 0 {
|
||||||
|
numRoutines = runtime.GOMAXPROCS(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
sobelX := [][]int{
|
||||||
|
{-1, 0, 1},
|
||||||
|
{-2, 0, 2},
|
||||||
|
{-1, 0, 1},
|
||||||
|
}
|
||||||
|
sobelY := [][]int{
|
||||||
|
{-1, -2, -1},
|
||||||
|
{0, 0, 0},
|
||||||
|
{1, 2, 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
pf := func(ri, x, y, offset, inStride int, inPix, outPix []uint8) {
|
||||||
|
var px, py int
|
||||||
|
for dy := -1; dy <= 1; dy++ {
|
||||||
|
for dx := -1; dx <= 1; dx++ {
|
||||||
|
pOffset := offset + (dx*4 + dy*inStride)
|
||||||
|
r := int(inPix[pOffset])
|
||||||
|
px += sobelX[dx+1][dy+1] * r
|
||||||
|
py += sobelY[dx+1][dy+1] * r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val := uint8(math.Sqrt(float64(px*px + py*py)))
|
||||||
|
if s.threshold != -1 {
|
||||||
|
if val >= uint8(s.threshold) {
|
||||||
|
val = 255
|
||||||
|
} else {
|
||||||
|
val = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.invert {
|
||||||
|
val = 255 - val
|
||||||
|
}
|
||||||
|
outPix[offset] = val
|
||||||
|
outPix[offset+1] = val
|
||||||
|
outPix[offset+2] = val
|
||||||
|
outPix[offset+3] = 255
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &Image{
|
||||||
|
img: image.NewRGBA(image.Rectangle{
|
||||||
|
Min: image.Point{X: 0, Y: 0},
|
||||||
|
Max: image.Point{X: img.Width, Y: img.Height},
|
||||||
|
}),
|
||||||
|
Width: img.Width,
|
||||||
|
Height: img.Height,
|
||||||
|
Bounds: Rect{
|
||||||
|
X: img.Bounds.X + 1,
|
||||||
|
Y: img.Bounds.Y + 1,
|
||||||
|
Width: img.Bounds.Width - 2,
|
||||||
|
Height: img.Bounds.Height - 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
runParallel(numRoutines, img, out.Bounds, out, pf, 0)
|
||||||
|
return out, nil
|
||||||
|
}
|
21
internal/allowedText/allowedText.go
Normal file
21
internal/allowedText/allowedText.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
// allowedText provides a function to check if a user-supplied text is UTF-8 encoded
|
||||||
|
// All profile and message text must be utf-8
|
||||||
|
|
||||||
|
package allowedText
|
||||||
|
|
||||||
|
// TODO: We may want to add other unicode characters to restrict for future profile versions
|
||||||
|
|
||||||
|
import "unicode/utf8"
|
||||||
|
|
||||||
|
|
||||||
|
func VerifyStringIsAllowed(input string)bool{
|
||||||
|
|
||||||
|
isAllowed := utf8.ValidString(input)
|
||||||
|
if (isAllowed == false){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
56
internal/appMemory/appMemory.go
Normal file
56
internal/appMemory/appMemory.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
|
||||||
|
// appMemory provides functions to read and write to the user app memory map.
|
||||||
|
// This map is lost when the application is closed, or the user is changed.
|
||||||
|
// An example of a memory entry is the CurrentViewedPage entry.
|
||||||
|
|
||||||
|
package appMemory
|
||||||
|
|
||||||
|
// Examples of memory entries include:
|
||||||
|
// -AppUser (name of user who is signed in)
|
||||||
|
// -CurrentViewedPage
|
||||||
|
// -StopBuildMyMatches
|
||||||
|
// -ViewedModeratorsReadyProgressStatus
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
var appMemoryMapMutex sync.RWMutex
|
||||||
|
|
||||||
|
var appMemoryMap map[string]string = make(map[string]string)
|
||||||
|
|
||||||
|
func ClearAppMemory(){
|
||||||
|
|
||||||
|
appMemoryMapMutex.Lock()
|
||||||
|
clear(appMemoryMap)
|
||||||
|
appMemoryMapMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetMemoryEntry(key string, value string){
|
||||||
|
|
||||||
|
appMemoryMapMutex.Lock()
|
||||||
|
appMemoryMap[key] = value
|
||||||
|
appMemoryMapMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMemoryEntry(key string)(bool, string){
|
||||||
|
|
||||||
|
appMemoryMapMutex.RLock()
|
||||||
|
value, exists := appMemoryMap[key]
|
||||||
|
appMemoryMapMutex.RUnlock()
|
||||||
|
if (exists == false){
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, value
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteMemoryEntry(key string){
|
||||||
|
|
||||||
|
appMemoryMapMutex.Lock()
|
||||||
|
delete(appMemoryMap, key)
|
||||||
|
appMemoryMapMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
518
internal/appUsers/appUsers.go
Normal file
518
internal/appUsers/appUsers.go
Normal file
|
@ -0,0 +1,518 @@
|
||||||
|
|
||||||
|
// appUsers provides functions to sign Seekia users in and out, and to manage Seekia users
|
||||||
|
// These are the users that are displayed in the gui upon starting Seekia
|
||||||
|
// Each user has its own data folder, allowing each user to operate different identities.
|
||||||
|
|
||||||
|
package appUsers
|
||||||
|
|
||||||
|
// The functions in this package are not safe for concurrency
|
||||||
|
// The GUI should prevent any concurrent use of these functions
|
||||||
|
|
||||||
|
import "seekia/resources/geneticReferences/locusMetadata"
|
||||||
|
import "seekia/resources/geneticReferences/monogenicDiseases"
|
||||||
|
import "seekia/resources/geneticReferences/polygenicDiseases"
|
||||||
|
import "seekia/resources/geneticReferences/traits"
|
||||||
|
import "seekia/resources/worldLanguages"
|
||||||
|
import "seekia/resources/worldLocations"
|
||||||
|
|
||||||
|
import "seekia/internal/appMemory"
|
||||||
|
import "seekia/internal/backgroundJobs"
|
||||||
|
import "seekia/internal/desires/myLocalDesires"
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
import "seekia/internal/genetics/myAnalyses"
|
||||||
|
import "seekia/internal/genetics/myCouples"
|
||||||
|
import "seekia/internal/genetics/myGenomes"
|
||||||
|
import "seekia/internal/genetics/myPeople"
|
||||||
|
import "seekia/internal/localFilesystem"
|
||||||
|
import "seekia/internal/logger"
|
||||||
|
import "seekia/internal/messaging/myChatConversations"
|
||||||
|
import "seekia/internal/messaging/myChatFilters"
|
||||||
|
import "seekia/internal/messaging/myChatKeys"
|
||||||
|
import "seekia/internal/messaging/myChatMessages"
|
||||||
|
import "seekia/internal/messaging/myCipherKeys"
|
||||||
|
import "seekia/internal/messaging/myConversationIndexes"
|
||||||
|
import "seekia/internal/messaging/myMessageQueue"
|
||||||
|
import "seekia/internal/messaging/myReadStatus"
|
||||||
|
import "seekia/internal/messaging/mySecretInboxes"
|
||||||
|
import "seekia/internal/messaging/peerChatKeys"
|
||||||
|
import "seekia/internal/messaging/peerDevices"
|
||||||
|
import "seekia/internal/messaging/peerSecretInboxes"
|
||||||
|
import "seekia/internal/messaging/sendMessages"
|
||||||
|
import "seekia/internal/moderation/myHiddenContent"
|
||||||
|
import "seekia/internal/moderation/mySkippedContent"
|
||||||
|
import "seekia/internal/moderation/viewedContent"
|
||||||
|
import "seekia/internal/moderation/viewedModerators"
|
||||||
|
import "seekia/internal/myBlockedUsers"
|
||||||
|
import "seekia/internal/myContacts"
|
||||||
|
import "seekia/internal/myDatastores/myList"
|
||||||
|
import "seekia/internal/myDatastores/myMap"
|
||||||
|
import "seekia/internal/myDatastores/myMapList"
|
||||||
|
import "seekia/internal/myIgnoredUsers"
|
||||||
|
import "seekia/internal/myLikedUsers"
|
||||||
|
import "seekia/internal/myMatches"
|
||||||
|
import "seekia/internal/myMatchScore"
|
||||||
|
import "seekia/internal/mySeedPhrases"
|
||||||
|
import "seekia/internal/mySettings"
|
||||||
|
import "seekia/internal/network/myBroadcasts"
|
||||||
|
import "seekia/internal/network/myMateCriteria"
|
||||||
|
import "seekia/internal/network/peerServer"
|
||||||
|
import "seekia/internal/network/viewedHosts"
|
||||||
|
import "seekia/internal/profiles/myLocalProfiles"
|
||||||
|
import "seekia/internal/profiles/profileFormat"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
import "os"
|
||||||
|
import "strings"
|
||||||
|
import "sync"
|
||||||
|
import "time"
|
||||||
|
import "unicode"
|
||||||
|
|
||||||
|
import goFilepath "path/filepath"
|
||||||
|
|
||||||
|
// This bool is set to true if we have initialized application variables
|
||||||
|
// These only need to be initialized once for all users each time the application is started
|
||||||
|
var applicationVariablesInitialized bool = false
|
||||||
|
|
||||||
|
func SignInToAppUser(userName string, startBackgroundJobs bool)error{
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("AppUser", userName)
|
||||||
|
|
||||||
|
userDirectoryPath, err := localFilesystem.GetAppUserFolderPath()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
_, err = localFilesystem.CreateFolder(userDirectoryPath)
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myMap.InitializeMyMapsFolder()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myList.InitializeMyListsFolder()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myMapList.InitializeMyMapListsFolder()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = mySeedPhrases.InitializeMySeedPhrasesDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myBroadcasts.InitializeMyBroadcastsFolders()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myLocalDesires.InitializeMyDesiresDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myMateCriteria.InitializeMyCriteriaDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myMatchScore.InitializeMyMatchScorePointsDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = mySettings.InitializeMySettingsDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myLocalProfiles.InitializeMyLocalProfileDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myMatches.InitializeMyMatchesDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myContacts.InitializeMyContactDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myIgnoredUsers.InitializeMyIgnoredUsersDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myBlockedUsers.InitializeMyBlockedUsersDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = mySecretInboxes.InitializeMySecretInboxesDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myChatKeys.InitializeMyChatKeysDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myCipherKeys.InitializeMyMessageCipherKeysDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myReadStatus.InitializeMyReadStatusDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myConversationIndexes.InitializeMyConversationIndexesDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myChatFilters.InitializeMyChatFiltersDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myLikedUsers.InitializeMyLikedUsersDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myChatConversations.InitializeMyChatConversationsDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myChatMessages.InitializeMyChatMessageDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myMessageQueue.InitializeMyMessageQueueDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = peerChatKeys.InitializePeerChatKeysDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = peerSecretInboxes.InitializePeerSecretInboxesDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = peerDevices.InitializePeerDevicesDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = sendMessages.InitializeSentMessagesDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myGenomes.InitializeMyGenomeDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myGenomes.CreateUserGenomesFolder()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myPeople.InitializeMyGenomePeopleDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myCouples.InitializeMyGenomeCouplesDatastore()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myAnalyses.InitializeMyAnalysesDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myAnalyses.CreateMyAnalysesFolder()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myAnalyses.PruneOldAnalyses()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = viewedContent.InitializeViewedContentDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = logger.InitializeMyLogDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = viewedModerators.InitializeViewedModeratorsDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = viewedHosts.InitializeViewedHostsDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = mySkippedContent.InitializeMySkippedContentDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myHiddenContent.InitializeMyHiddenContentDatastores()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
if (applicationVariablesInitialized == false){
|
||||||
|
|
||||||
|
// This only needs to be done once per application startup
|
||||||
|
|
||||||
|
err := worldLocations.InitializeWorldLocationsVariables()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = worldLanguages.InitializeWorldLanguageVariables()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = locusMetadata.InitializeLocusMetadataVariables()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
monogenicDiseases.InitializeMonogenicDiseaseVariables()
|
||||||
|
|
||||||
|
polygenicDiseases.InitializePolygenicDiseaseVariables()
|
||||||
|
|
||||||
|
traits.InitializeTraitVariables()
|
||||||
|
|
||||||
|
err = profileFormat.InitializeProfileFormatVariables()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
applicationVariablesInitialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startBackgroundJobs == true){
|
||||||
|
|
||||||
|
err = backgroundJobs.StartBackgroundJobs()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func SignOutOfAppUser()error{
|
||||||
|
|
||||||
|
var signOutWaitgroup sync.WaitGroup
|
||||||
|
|
||||||
|
stopBackgroundJobs := func(){
|
||||||
|
|
||||||
|
err := backgroundJobs.StopBackgroundJobs()
|
||||||
|
if (err != nil) {
|
||||||
|
//TODO: Log and show to user
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
signOutWaitgroup.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForSendsToComplete := func(){
|
||||||
|
|
||||||
|
sendMessages.WaitForPendingSendsToComplete()
|
||||||
|
|
||||||
|
signOutWaitgroup.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
stopPeerServer := func(){
|
||||||
|
|
||||||
|
err := peerServer.StopPeerServer()
|
||||||
|
if (err != nil){
|
||||||
|
//TODO: Log and show to user
|
||||||
|
}
|
||||||
|
|
||||||
|
signOutWaitgroup.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Wait for manual downloads and manual broadcasts to finish
|
||||||
|
|
||||||
|
signOutWaitgroup.Add(3)
|
||||||
|
|
||||||
|
go stopBackgroundJobs()
|
||||||
|
go waitForSendsToComplete()
|
||||||
|
go stopPeerServer()
|
||||||
|
|
||||||
|
//TODO: Wait for other tasks that use userData folders
|
||||||
|
|
||||||
|
signOutWaitgroup.Wait()
|
||||||
|
|
||||||
|
// We simulate some time. This will be removed once the above functions are implemented
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
appMemory.ClearAppMemory()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyAppUserNameCharactersAreAllowed(userName string)bool{
|
||||||
|
|
||||||
|
// The user name becomes the name of a folder on the filesystem
|
||||||
|
// Thus, we only allow letters and numbers
|
||||||
|
|
||||||
|
const digitsList = "0123456789"
|
||||||
|
|
||||||
|
for _, character := range userName {
|
||||||
|
|
||||||
|
isLetter := unicode.IsLetter(character)
|
||||||
|
if (isLetter == true){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
isDigit := strings.Contains(digitsList, string(character))
|
||||||
|
if (isDigit == true){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func GetAppUsersList()([]string, error){
|
||||||
|
|
||||||
|
appUsersDirectory, err := localFilesystem.GetAppUsersDataFolderPath()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
filesystemList, err := os.ReadDir(appUsersDirectory)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
allUsersList := make([]string, 0, len(filesystemList))
|
||||||
|
|
||||||
|
for _, filesystemObject := range filesystemList{
|
||||||
|
|
||||||
|
filepathIsDirectory := filesystemObject.IsDir()
|
||||||
|
if (filepathIsDirectory == false){
|
||||||
|
return nil, errors.New("UserData folder is corrupt: Contains non-folder: " + filesystemObject.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
folderName := filesystemObject.Name()
|
||||||
|
|
||||||
|
isValid := VerifyAppUserNameCharactersAreAllowed(folderName)
|
||||||
|
if (isValid == false){
|
||||||
|
return nil, errors.New("UserData folder is corrupt: Contains invalid folder name: " + folderName)
|
||||||
|
}
|
||||||
|
|
||||||
|
allUsersList = append(allUsersList, folderName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allUsersList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: App user exists
|
||||||
|
// -string: User name
|
||||||
|
func GetCurrentAppUserName()(bool, string){
|
||||||
|
|
||||||
|
exists, appUserName := appMemory.GetMemoryEntry("AppUser")
|
||||||
|
if (exists == false){
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, appUserName
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Name is a duplicate
|
||||||
|
// -error
|
||||||
|
func CreateAppUser(newUserName string)(bool, error){
|
||||||
|
|
||||||
|
if (newUserName == ""){
|
||||||
|
return false, errors.New("CreateAppUser called with empty user name.")
|
||||||
|
}
|
||||||
|
if (len(newUserName) > 30){
|
||||||
|
return false, errors.New("CreateAppUser called with user name that is too long: " + newUserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
isAllowed := VerifyAppUserNameCharactersAreAllowed(newUserName)
|
||||||
|
if (isAllowed == false){
|
||||||
|
return false, errors.New("CreateAppUser called with user name contains unallowed characters: " + newUserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
appUsersList, err := GetAppUsersList()
|
||||||
|
if (err != nil){ return false, err }
|
||||||
|
|
||||||
|
for _, existingUser := range appUsersList{
|
||||||
|
if (existingUser == newUserName){
|
||||||
|
// User name already exists
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appUsersDirectory, err := localFilesystem.GetAppUsersDataFolderPath()
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
userFolderpath := goFilepath.Join(appUsersDirectory, newUserName)
|
||||||
|
|
||||||
|
_, err = localFilesystem.CreateFolder(userFolderpath)
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
|
||||||
|
// We set the IsIgnored desire
|
||||||
|
// This desire is initialized upon user creation, so ignored users will be hidden
|
||||||
|
// The user can disable this desire
|
||||||
|
// We have to sign in to do this
|
||||||
|
|
||||||
|
err = SignInToAppUser(newUserName, false)
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
err = myLocalDesires.SetDesire("IsIgnored_FilterAll", "Yes")
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
// Desire value is a "+" delimited list of base64 choices which we desire
|
||||||
|
// "No" == We desire users who are not ignored
|
||||||
|
noBase64 := encoding.EncodeBytesToBase64String([]byte("No"))
|
||||||
|
|
||||||
|
err = myLocalDesires.SetDesire("IsIgnored", noBase64)
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
// We clear app memory to "sign out" of app user
|
||||||
|
appMemory.ClearAppMemory()
|
||||||
|
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: User is found
|
||||||
|
// -error
|
||||||
|
func RenameAppUser(userName string, newUserName string)(bool, error){
|
||||||
|
|
||||||
|
err := SignOutOfAppUser()
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
appUsersDirectory, err := localFilesystem.GetAppUsersDataFolderPath()
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
currentUserFolderpath := goFilepath.Join(appUsersDirectory, userName)
|
||||||
|
newUserFolderpath := goFilepath.Join(appUsersDirectory, newUserName)
|
||||||
|
|
||||||
|
err = os.Rename(currentUserFolderpath, newUserFolderpath)
|
||||||
|
if (err != nil){
|
||||||
|
folderDoesNotExist := os.IsNotExist(err)
|
||||||
|
if (folderDoesNotExist == true){
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: User found
|
||||||
|
// -error
|
||||||
|
func DeleteAppUser(userName string)(bool, error){
|
||||||
|
|
||||||
|
err := SignOutOfAppUser()
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
appUsersDirectory, err := localFilesystem.GetAppUsersDataFolderPath()
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
userFolderPath := goFilepath.Join(appUsersDirectory, userName)
|
||||||
|
|
||||||
|
folderExists, err := localFilesystem.DeleteAllFolderContents(userFolderPath)
|
||||||
|
if (err != nil){ return false, err }
|
||||||
|
if (folderExists == false){
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
folderExists, err = localFilesystem.DeleteFileOrFolder(userFolderPath)
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (folderExists == false){
|
||||||
|
return false, errors.New("User data folder not found after call to DeleteAllFolderContents")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// We use this to partially sign in to the first app user when testing packages that need it
|
||||||
|
// An example is myMap, which requires an app user to be signed in to use it
|
||||||
|
// We don't need to sign the user out after calling this function, because we only use this for testing
|
||||||
|
func InitializeAppUserForTests()error{
|
||||||
|
|
||||||
|
appUsersList, err := GetAppUsersList()
|
||||||
|
if (err != nil){ return err }
|
||||||
|
|
||||||
|
if (len(appUsersList) == 0){
|
||||||
|
return errors.New("SignInToAppUserForTests called when no app users exist.")
|
||||||
|
}
|
||||||
|
|
||||||
|
userName := appUsersList[0]
|
||||||
|
|
||||||
|
appMemory.SetMemoryEntry("AppUser", userName)
|
||||||
|
|
||||||
|
userDirectoryPath, err := localFilesystem.GetAppUserFolderPath()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
_, err = localFilesystem.CreateFolder(userDirectoryPath)
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myMap.InitializeMyMapsFolder()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myList.InitializeMyListsFolder()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
err = myMapList.InitializeMyMapListsFolder()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
55
internal/appValues/appValues.go
Normal file
55
internal/appValues/appValues.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
|
||||||
|
// appValues provides functions to get Seekia app values
|
||||||
|
// These are constants that can be different for each Seekia app version
|
||||||
|
|
||||||
|
package appValues
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func GetSeekiaVersion()float32{
|
||||||
|
|
||||||
|
version := float32(0.50)
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGeneticAnalysisVersion()int{
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMessageVersion()int{
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetProfileVersion(profileType string)(int, error){
|
||||||
|
|
||||||
|
if (profileType == "Mate"){
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
if (profileType == "Host"){
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
if (profileType == "Moderator"){
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
return 0, errors.New("GetProfileVersion called with invalid profile type: " + profileType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetReviewVersion()int{
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetReportVersion()int{
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maximum side length in pixels
|
||||||
|
func GetStandardImageMaximumSideLength()int{
|
||||||
|
return 800
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStandardImageMaximumBytes()int{
|
||||||
|
return 20000
|
||||||
|
}
|
||||||
|
|
||||||
|
|
646
internal/backgroundJobs/backgroundJobs.go
Normal file
646
internal/backgroundJobs/backgroundJobs.go
Normal file
|
@ -0,0 +1,646 @@
|
||||||
|
|
||||||
|
// backgroundJobs provides functions to start and stop the background jobs loop
|
||||||
|
// This is a loop that runs various jobs in the background periodically
|
||||||
|
// The backgroundJobs loop must be started when a user signs in, and stopped when they sign out
|
||||||
|
|
||||||
|
package backgroundJobs
|
||||||
|
|
||||||
|
// TODO: Add these jobs:
|
||||||
|
// -Remove old moderator reviews (where moderator has updated their review)(within reviews list)
|
||||||
|
// -Prune old profiles (if they are outdated and not reported/moderated with ban reviews)
|
||||||
|
// -Import broadcasted reviews and profiles into database (needed if restoring from old device/database is deleted)
|
||||||
|
// -Broadcast content (rebroadcast content in the background on a set period)
|
||||||
|
// -Find reviews with cipher keys that do not hash to the message cipherKeyHash, messages that are not decryptable
|
||||||
|
// when using a cipherKey that hashes to the message's cipherKeyHash, and messages that are invalid upon being decrypted.
|
||||||
|
// Ban the moderators who approved the messages (if ModeratorMode is enabled)
|
||||||
|
// Also, automatically ban these kinds of messages.
|
||||||
|
// -Delete messages whose metadata we already have (for moderation, if HostMode is disabled and ModeratorMode is enabled)
|
||||||
|
// -Prune invalid message reports (whose cipher key's hash does not match message's cipherKeyHash)
|
||||||
|
// -Prune unfunded/expired identities/profiles/reports/messages
|
||||||
|
// -Prune old profiles/reviews once they have been replaced, if we know they are not reported or banned
|
||||||
|
// We have to keep content that has been banned for some time, even if it has been replaced with newer content by its author
|
||||||
|
// This is necessary so the moderators can review the content
|
||||||
|
// An example is a mate user who shares unruleful content in a profile, then changes their profile to something ruleful
|
||||||
|
// -Prune content we have already reviewed (in moderatorMode)
|
||||||
|
// Prune user profiles who no longer fulfill my downloads criteria (if space is running out and host/moderator mode is disabled)
|
||||||
|
// -Delete a mate user's older profiles if they have a newer profile that does not
|
||||||
|
// fulfill our criteria, even if that newer profile is not viewable yet (for users who are not within our host/moderator range)
|
||||||
|
// -Prune database of different networkType content
|
||||||
|
// -Moderators should automatically ban other moderators who review content from a different networkType
|
||||||
|
// For example, a review is created on Mainnet for a message which belongs to Testnet1
|
||||||
|
// The Seekia app will not allow this to happen, so any moderator who does it must be malicious.
|
||||||
|
|
||||||
|
//TODO: If a user disables a mode, we should be able to stop all of the jobs associated with that mode
|
||||||
|
// We will do this by using the checkIfStopped function which we will pass to all of the networkJobs
|
||||||
|
// We will also use the checkIfStopped function to stop networkJobs upon application closure and user signout.
|
||||||
|
|
||||||
|
import "seekia/internal/appMemory"
|
||||||
|
import "seekia/internal/databaseJobs"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/logger"
|
||||||
|
import "seekia/internal/moderation/bannedModeratorConsensus"
|
||||||
|
import "seekia/internal/moderation/enabledModerators"
|
||||||
|
import "seekia/internal/moderation/verdictHistory"
|
||||||
|
import "seekia/internal/myRanges"
|
||||||
|
import "seekia/internal/mySettings"
|
||||||
|
import "seekia/internal/network/appNetworkType/getAppNetworkType"
|
||||||
|
import "seekia/internal/network/enabledHosts"
|
||||||
|
import "seekia/internal/network/networkJobs"
|
||||||
|
import "seekia/internal/network/peerServer"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
import "sync"
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
var loopIsRunningMutex sync.RWMutex
|
||||||
|
var loopIsRunning bool
|
||||||
|
|
||||||
|
var taskCompletionTimesMapMutex sync.RWMutex
|
||||||
|
// Map Structure: Task name -> Last time task was completed
|
||||||
|
var taskCompletionTimesMap map[string]int64 = make(map[string]int64)
|
||||||
|
|
||||||
|
var runningTasksMapMutex sync.RWMutex
|
||||||
|
// Map Structure: Task Name -> Task is running
|
||||||
|
var runningTasksMap map[string]bool = make(map[string]bool)
|
||||||
|
|
||||||
|
func setLoopIsRunningStatus(newStatus bool){
|
||||||
|
|
||||||
|
loopIsRunningMutex.Lock()
|
||||||
|
loopIsRunning = newStatus
|
||||||
|
loopIsRunningMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func StopBackgroundJobs()error{
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
// This needs to be run whenever the user is changed
|
||||||
|
// This will be tricky to implement, because some tasks take a long time
|
||||||
|
// We need to add isStopped checking into many functions such as sendRequests
|
||||||
|
// This function should wait for all tasks to stop.
|
||||||
|
|
||||||
|
setLoopIsRunningStatus(false)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartBackgroundJobs()error{
|
||||||
|
|
||||||
|
exists, currentUserName := appMemory.GetMemoryEntry("AppUser")
|
||||||
|
if (exists == false){
|
||||||
|
return errors.New("StartBackgroundJobs called when appUser is not signed in.")
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIfUserHasChanged := func()bool{
|
||||||
|
|
||||||
|
exists, userName := appMemory.GetMemoryEntry("AppUser")
|
||||||
|
if (exists == false || currentUserName != userName){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
appNetworkType, err := getAppNetworkType.GetAppNetworkType()
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
checkIfAppNetworkTypeHasChanged := func()(bool, error){
|
||||||
|
|
||||||
|
currentAppNetworkType, err := getAppNetworkType.GetAppNetworkType()
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (appNetworkType != currentAppNetworkType){
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoopIsRunningStatus(true)
|
||||||
|
|
||||||
|
runBackgroundJobsLoop := func(){
|
||||||
|
|
||||||
|
//TODO: Stagger jobs, so we don't start them all at the same time
|
||||||
|
// If we start them all at the same time, it could result in fingerprinting and privacy leaks
|
||||||
|
|
||||||
|
for{
|
||||||
|
|
||||||
|
userHasChanged := checkIfUserHasChanged()
|
||||||
|
if (userHasChanged == true){
|
||||||
|
// This should not happen
|
||||||
|
// User should only be changed after this loop has stopped
|
||||||
|
logger.AddLogError("BackgroundJobs", errors.New("App user changed before backgroundJobs loop is stopped."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appNetworkTypeHasChanged, err := checkIfAppNetworkTypeHasChanged()
|
||||||
|
if (err != nil){
|
||||||
|
logger.AddLogError("BackgroundJobs", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (appNetworkTypeHasChanged == true){
|
||||||
|
// This should not happen
|
||||||
|
// App network type should only be changed after this loop has stopped
|
||||||
|
logger.AddLogError("BackgroundJobs", errors.New("App network type changed before backgroundJobs loop is stopped."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type taskStruct struct{
|
||||||
|
|
||||||
|
// A task name is a jobName with a suffix representing the task process
|
||||||
|
TaskName string
|
||||||
|
|
||||||
|
// The name of the Job which this task will run
|
||||||
|
JobName string
|
||||||
|
|
||||||
|
// Seconds between each completed run of the task to wait before we run it again
|
||||||
|
TimeBetweenTasks int
|
||||||
|
}
|
||||||
|
|
||||||
|
// For example, _1 is the first process, _2 is the second, etc...
|
||||||
|
getTasksList := func()([]taskStruct, error){
|
||||||
|
|
||||||
|
tasksList := make([]taskStruct, 0)
|
||||||
|
|
||||||
|
addTasksToList := func(jobName string, timeBetweenTasks int, numberOfProcesses int){
|
||||||
|
|
||||||
|
for i:=1; i <= numberOfProcesses; i++{
|
||||||
|
|
||||||
|
processString := helpers.ConvertIntToString(i)
|
||||||
|
|
||||||
|
taskName := jobName + "_" + processString
|
||||||
|
|
||||||
|
newTaskObject := taskStruct{
|
||||||
|
|
||||||
|
TaskName: taskName,
|
||||||
|
JobName: jobName,
|
||||||
|
TimeBetweenTasks: timeBetweenTasks,
|
||||||
|
}
|
||||||
|
|
||||||
|
tasksList = append(tasksList, newTaskObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Choose better timeBetweenTasks and numberOfProcesses values
|
||||||
|
|
||||||
|
addTasksToList("UpdateEnabledHosts", 60, 1)
|
||||||
|
|
||||||
|
addTasksToList("DownloadParameters", 120, 1)
|
||||||
|
|
||||||
|
// Database Jobs:
|
||||||
|
|
||||||
|
addTasksToList("UpdateDatabaseMateIdentityProfilesLists", 300, 1)
|
||||||
|
addTasksToList("UpdateDatabaseHostIdentityProfilesLists", 300, 1)
|
||||||
|
addTasksToList("UpdateDatabaseModeratorIdentityProfilesLists", 300, 1)
|
||||||
|
|
||||||
|
addTasksToList("PruneMateProfileMetadata", 300, 1)
|
||||||
|
addTasksToList("PruneHostProfileMetadata", 300, 1)
|
||||||
|
addTasksToList("PruneModeratorProfileMetadata", 300, 1)
|
||||||
|
addTasksToList("PruneMessageMetadata", 300, 1)
|
||||||
|
|
||||||
|
// Host Jobs:
|
||||||
|
|
||||||
|
getHostModeIsEnabledStatus := func()(bool, error){
|
||||||
|
|
||||||
|
exists, hostModeStatus, err := mySettings.GetSetting("HostModeOnOffStatus")
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (exists == true && hostModeStatus == "On"){
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hostModeIsEnabled, err := getHostModeIsEnabledStatus()
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
if (hostModeIsEnabled == true){
|
||||||
|
|
||||||
|
addTasksToList("AdjustMyHostRanges", 120, 1)
|
||||||
|
|
||||||
|
addTasksToList("DownloadMateProfilesToHost", 60, 1)
|
||||||
|
addTasksToList("DownloadHostProfilesToHost", 60, 1)
|
||||||
|
addTasksToList("DownloadModeratorProfilesToHost", 60, 1)
|
||||||
|
|
||||||
|
addTasksToList("DownloadMessagesToHost", 60, 1)
|
||||||
|
addTasksToList("DownloadReviewsToHost", 60, 1)
|
||||||
|
addTasksToList("DownloadReportsToHost", 60, 1)
|
||||||
|
|
||||||
|
addTasksToList("StartPeerServer", 3, 1)
|
||||||
|
} else {
|
||||||
|
addTasksToList("StopPeerServer", 3, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moderator Jobs:
|
||||||
|
|
||||||
|
getModeratorModeIsEnabledStatus := func()(bool, error){
|
||||||
|
|
||||||
|
exists, moderatorModeStatus, err := mySettings.GetSetting("ModeratorModeOnOffStatus")
|
||||||
|
if (err != nil){ return false, err }
|
||||||
|
if (exists == true && moderatorModeStatus == "On"){
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
moderatorModeIsEnabled, err := getModeratorModeIsEnabledStatus()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (moderatorModeIsEnabled == true){
|
||||||
|
|
||||||
|
addTasksToList("AdjustMyModeratorRanges", 120, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostModeIsEnabled == true || moderatorModeIsEnabled == true){
|
||||||
|
|
||||||
|
addTasksToList("UpdateEnabledModerators", 60, 1)
|
||||||
|
addTasksToList("UpdateBannedModerators", 60, 1)
|
||||||
|
|
||||||
|
addTasksToList("RecordIdentityVerdictHistories", 120, 1)
|
||||||
|
addTasksToList("RecordProfileVerdictHistories", 120, 1)
|
||||||
|
addTasksToList("RecordMessageVerdictHistories", 120, 1)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
addTasksToList("DownloadMyMateMessages", 1, 3)
|
||||||
|
if (moderatorModeIsEnabled == true){
|
||||||
|
addTasksToList("DownloadMyModeratorMessages", 1, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Add tasks from networkJobs.go, and more tasks:
|
||||||
|
|
||||||
|
// networkJobs.DownloadAllNewestViewableUserProfiles
|
||||||
|
// networkJobs.DownloadMateProfilesToBrowse
|
||||||
|
// networkJobs.DownloadMateOutlierProfiles
|
||||||
|
// networkJobs.DownloadProfilesToModerate
|
||||||
|
// networkJobs.DownloadMessagesToModerate
|
||||||
|
// networkJobs.DownloadMyInboxMessages
|
||||||
|
// networkJobs.DownloadModeratorIdentityBanningReviews
|
||||||
|
|
||||||
|
// networkJobs.DownloadIdentityReviewsToHost
|
||||||
|
// networkJobs.DownloadMessageReviewsToHost
|
||||||
|
// networkJobs.DownloadIdentityReviewsForModeration
|
||||||
|
// networkJobs.DownloadMessageReviewsForModeration
|
||||||
|
// networkJobs.DownloadIdentityReportsToHost
|
||||||
|
// networkJobs.DownloadMessageReportsToHost
|
||||||
|
// networkJobs.DownloadIdentityReportsForModeration
|
||||||
|
// networkJobs.DownloadMessageReportsForModeration
|
||||||
|
|
||||||
|
// networkJobs.DownloadHostViewableStatuses
|
||||||
|
// networkJobs.DownloadModeratorProfileViewableStatuses
|
||||||
|
// networkJobs.DownloadMateViewableStatusesForBrowsing
|
||||||
|
// networkJobs.DownloadMateOutlierViewableStatuses
|
||||||
|
|
||||||
|
// If local blockchain is not enabled and host/moderator mode is enabled:
|
||||||
|
// networkJobs.DownloadModeratorIdentityDeposits
|
||||||
|
|
||||||
|
// myMessageQueue.AttemptToSendMessagesInQueue
|
||||||
|
|
||||||
|
// myBroadcasts.PruneMyBroadcastedReviews
|
||||||
|
|
||||||
|
return tasksList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tasksList, err := getTasksList()
|
||||||
|
if (err != nil) {
|
||||||
|
logger.AddLogError("BackgroundJobs", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, taskObject := range tasksList{
|
||||||
|
|
||||||
|
taskName := taskObject.TaskName
|
||||||
|
jobName := taskObject.JobName
|
||||||
|
taskWaitTime := taskObject.TimeBetweenTasks
|
||||||
|
|
||||||
|
runningTasksMapMutex.RLock()
|
||||||
|
taskIsRunning, exists := runningTasksMap[taskName]
|
||||||
|
runningTasksMapMutex.RUnlock()
|
||||||
|
if (exists == true && taskIsRunning == true){
|
||||||
|
// Task is already running.
|
||||||
|
// We skip it.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
taskCompletionTimesMapMutex.RLock()
|
||||||
|
taskLastCompletionTime, exists := taskCompletionTimesMap[taskName]
|
||||||
|
taskCompletionTimesMapMutex.RUnlock()
|
||||||
|
if (exists == true){
|
||||||
|
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
timeElapsedFromLastCompletion := currentTime - taskLastCompletionTime
|
||||||
|
if (timeElapsedFromLastCompletion < int64(taskWaitTime)){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we run the task
|
||||||
|
// We get the task's job function
|
||||||
|
|
||||||
|
//TODO: Add a CheckIfStoppedFunction that we pass to many jobs
|
||||||
|
// This will return true if the user is signing out/closing Seekia
|
||||||
|
|
||||||
|
getJobFunction := func()(func()error, error){
|
||||||
|
|
||||||
|
if (jobName == "UpdateEnabledModerators"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := enabledModerators.UpdateEnabledModeratorsList(appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "UpdateBannedModerators"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := bannedModeratorConsensus.UpdateBannedModeratorsList(appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "UpdateEnabledHosts"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := enabledHosts.UpdateEnabledHostsList(appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "AdjustMyHostRanges"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := myRanges.AdjustMyRanges("Host")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "AdjustMyModeratorRanges"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := myRanges.AdjustMyRanges("Moderator")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobName == "UpdateDatabaseMateIdentityProfilesLists"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := databaseJobs.UpdateDatabaseIdentityProfilesLists("Mate")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "UpdateDatabaseHostIdentityProfilesLists"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := databaseJobs.UpdateDatabaseIdentityProfilesLists("Host")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "UpdateDatabaseModeratorIdentityProfilesLists"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := databaseJobs.UpdateDatabaseIdentityProfilesLists("Moderator")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "PruneMateProfileMetadata"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := databaseJobs.PruneProfileMetadata("Mate")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "PruneHostProfileMetadata"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := databaseJobs.PruneProfileMetadata("Host")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "PruneModeratorProfileMetadata"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := databaseJobs.PruneProfileMetadata("Moderator")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "PruneMessageMetadata"){
|
||||||
|
|
||||||
|
jobFunction := databaseJobs.PruneMessageMetadata
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobName == "DownloadParameters"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := networkJobs.DownloadParameters(appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "DownloadMateProfilesToHost"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := networkJobs.DownloadProfilesToHost("Mate", appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "DownloadHostProfilesToHost"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := networkJobs.DownloadProfilesToHost("Host", appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "DownloadModeratorProfilesToHost"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := networkJobs.DownloadProfilesToHost("Moderator", appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "DownloadMessagesToHost"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := networkJobs.DownloadMessagesToHost(appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "StartPeerServer"){
|
||||||
|
|
||||||
|
jobFunction := peerServer.StartPeerServer
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "StopPeerServer"){
|
||||||
|
|
||||||
|
jobFunction := peerServer.StopPeerServer
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "RecordIdentityVerdictHistories"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := verdictHistory.RecordIdentityVerdictsToHistoryMap(appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "RecordProfileVerdictHistories"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := verdictHistory.RecordProfileVerdictsToHistoryMap(appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
if (jobName == "RecordMessageVerdictHistories"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := verdictHistory.RecordMessageVerdictsToHistoryMap(appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobName == "DownloadMyMateMessages"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := networkJobs.DownloadMyInboxMessages("Mate", appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobName == "DownloadMyModeratorMessages"){
|
||||||
|
|
||||||
|
jobFunction := func()error{
|
||||||
|
|
||||||
|
err := networkJobs.DownloadMyInboxMessages("Moderator", appNetworkType)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobFunction, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("Tasks map contains unknown jobName: " + jobName)
|
||||||
|
}
|
||||||
|
|
||||||
|
jobFunction, err := getJobFunction()
|
||||||
|
if (err != nil){
|
||||||
|
logger.AddLogError("BackgroundJobs", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runningTasksMapMutex.Lock()
|
||||||
|
runningTasksMap[taskName] = true
|
||||||
|
runningTasksMapMutex.Unlock()
|
||||||
|
|
||||||
|
startTaskFunction := func(){
|
||||||
|
|
||||||
|
err := jobFunction()
|
||||||
|
if (err != nil){
|
||||||
|
logger.AddLogError("BackgroundJobs", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
runningTasksMapMutex.Lock()
|
||||||
|
runningTasksMap[taskName] = false
|
||||||
|
runningTasksMapMutex.Unlock()
|
||||||
|
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
|
||||||
|
taskCompletionTimesMapMutex.Lock()
|
||||||
|
taskCompletionTimesMap[taskName] = currentTime
|
||||||
|
taskCompletionTimesMapMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
go startTaskFunction()
|
||||||
|
}
|
||||||
|
|
||||||
|
loopIsRunningMutex.RLock()
|
||||||
|
currentLoopIsRunningStatus := loopIsRunning
|
||||||
|
loopIsRunningMutex.RUnlock()
|
||||||
|
|
||||||
|
if (currentLoopIsRunningStatus == false){
|
||||||
|
// Background jobs loop has been stopped.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go runBackgroundJobsLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
2869
internal/badgerDatabase/badgerDatabase.go
Normal file
2869
internal/badgerDatabase/badgerDatabase.go
Normal file
File diff suppressed because it is too large
Load diff
140
internal/badgerDatabase/badgerDatabase_test.go
Normal file
140
internal/badgerDatabase/badgerDatabase_test.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package badgerDatabase
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/localFilesystem"
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func TestDatabase(t *testing.T) {
|
||||||
|
|
||||||
|
err := localFilesystem.InitializeAppDatastores()
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to initialize app datastores: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = startDatabase()
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to start database: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
testKey, err := helpers.GetNewRandomBytes(16)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get new random hex string: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setDatabaseEntry(testKey, []byte("InitialValue"))
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to add test key: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, retrievedValue, err := getDatabaseValue(testKey)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to get test key value: " + err.Error())
|
||||||
|
}
|
||||||
|
if (exists == false){
|
||||||
|
t.Fatalf("Failed to find test key value.")
|
||||||
|
}
|
||||||
|
if (string(retrievedValue) != "InitialValue"){
|
||||||
|
t.Fatalf("Retrieved value does not match.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setDatabaseEntry(testKey, []byte("NewValue"))
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to set value: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, keyValue, err := getDatabaseValue(testKey)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to get test key value: " + err.Error())
|
||||||
|
}
|
||||||
|
if (string(keyValue) != "NewValue"){
|
||||||
|
t.Fatalf("Failed to overwrite key.")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValueFunction := func(entryExists bool, entryValue []byte)(bool, bool, []byte, error){
|
||||||
|
|
||||||
|
if (entryExists == false){
|
||||||
|
return false, false, nil, errors.New("Entry should exist during update.")
|
||||||
|
}
|
||||||
|
if (string(entryValue) != "NewValue"){
|
||||||
|
return false, false, nil, errors.New("Value is unexpected during update: " + string(entryValue))
|
||||||
|
}
|
||||||
|
return true, true, []byte("UpdatedValue"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateDatabaseEntry(testKey, updateValueFunction)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to updateDatabaseEntry: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, keyValue, err = getDatabaseValue(testKey)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to get test key value: " + err.Error())
|
||||||
|
}
|
||||||
|
if (string(keyValue) != "UpdatedValue"){
|
||||||
|
t.Fatalf("updateDatabaseEntry failed: Unexpected value: " + string(keyValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = deleteDatabaseEntry(testKey)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to delete key: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, _, err = getDatabaseValue(testKey)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to get test key value: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists == true){
|
||||||
|
t.Fatalf("Key we deleted still exists.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we test concurrency
|
||||||
|
|
||||||
|
var encounteredErrorMutex sync.Mutex
|
||||||
|
var encounteredError error
|
||||||
|
|
||||||
|
var tasksWaitgroup sync.WaitGroup
|
||||||
|
|
||||||
|
for i:=0; i < 100; i++{
|
||||||
|
|
||||||
|
newValue := []byte{byte(i)}
|
||||||
|
|
||||||
|
newFunc := func(){
|
||||||
|
|
||||||
|
err = setDatabaseEntry(testKey, newValue)
|
||||||
|
if (err != nil){
|
||||||
|
encounteredErrorMutex.Lock()
|
||||||
|
encounteredError = err
|
||||||
|
encounteredErrorMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := deleteDatabaseEntry(testKey)
|
||||||
|
if (err != nil){
|
||||||
|
encounteredErrorMutex.Lock()
|
||||||
|
encounteredError = err
|
||||||
|
encounteredErrorMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasksWaitgroup.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasksWaitgroup.Add(1)
|
||||||
|
go newFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasksWaitgroup.Wait()
|
||||||
|
|
||||||
|
if (encounteredError != nil){
|
||||||
|
t.Fatalf("Failed to set/delete database entry concurrently: " + encounteredError.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
StopDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
701
internal/byteRange/byteRange.go
Normal file
701
internal/byteRange/byteRange.go
Normal file
|
@ -0,0 +1,701 @@
|
||||||
|
|
||||||
|
// byteRange provides functions to read and generate byte ranges
|
||||||
|
// Byte ranges are used to describe what range of identity hashes/inboxes to retrieve from a host
|
||||||
|
// Hosts share the range of identities/inboxes they host on their profile.
|
||||||
|
|
||||||
|
package byteRange
|
||||||
|
|
||||||
|
import "seekia/internal/identity"
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
import "math/big"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// This is used to retrieve the minimum and maximum identity hash ranges
|
||||||
|
// This is used when a server request does not need to filter based on a range
|
||||||
|
func GetMinimumMaximumIdentityHashBounds()([16]byte, [16]byte){
|
||||||
|
|
||||||
|
// minimumBound == "aaaaaaaaaaaaaaaaaaaaaaaam"
|
||||||
|
minimumBound := [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||||
|
|
||||||
|
// maximumBound == "777777777777777777777777r"
|
||||||
|
maximumBound := [16]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 3}
|
||||||
|
|
||||||
|
return minimumBound, maximumBound
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used to retrieve the minimum and maximum inbox ranges
|
||||||
|
// This is used when a server request does not need to filter based on a range
|
||||||
|
func GetMinimumMaximumInboxBounds()([10]byte, [10]byte){
|
||||||
|
|
||||||
|
// minimumBound == "aaaaaaaaaaaaaaaa"
|
||||||
|
minimumBound := [10]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
|
||||||
|
// maximumBound == "7777777777777777"
|
||||||
|
maximumBound := [10]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255}
|
||||||
|
|
||||||
|
return minimumBound, maximumBound
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckIfIdentityHashIsWithinRange(rangeStart [16]byte, rangeEnd [16]byte, inputIdentityHash [16]byte)(bool, error){
|
||||||
|
|
||||||
|
isValid, err := identity.VerifyIdentityHash(inputIdentityHash, false, "")
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (isValid == false){
|
||||||
|
inputIdentityHashHex := encoding.EncodeBytesToHexString(inputIdentityHash[:])
|
||||||
|
return false, errors.New("CheckIfIdentityHashIsWithinRange called with invalid inputIdentityHash: " + inputIdentityHashHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeStart == inputIdentityHash || rangeEnd == inputIdentityHash){
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if (rangeStart == rangeEnd){
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isWithinRange, err := checkIfBytesAreWithinRange(rangeStart[:], rangeEnd[:], inputIdentityHash[:])
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
return isWithinRange, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckIfInboxIsWithinRange(rangeStart [10]byte, rangeEnd [10]byte, inputInbox [10]byte)(bool, error){
|
||||||
|
|
||||||
|
if (rangeStart == inputInbox || rangeEnd == inputInbox){
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if (rangeStart == rangeEnd){
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isWithinRange, err := checkIfBytesAreWithinRange(rangeStart[:], rangeEnd[:], inputInbox[:])
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
return isWithinRange, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkIfBytesAreWithinRange(rangeStart []byte, rangeEnd []byte, inputSlice []byte)(bool, error){
|
||||||
|
|
||||||
|
// We check to see if inputSlice is between both values
|
||||||
|
|
||||||
|
slicesAreEqual, comparisonA, err := compareByteSlices(rangeStart, inputSlice)
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (slicesAreEqual == true){
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slicesAreEqual, comparisonB, err := compareByteSlices(rangeEnd, inputSlice)
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (slicesAreEqual == true){
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comparisonA != comparisonB){
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Any values found
|
||||||
|
// -[][16]byte: List of values that are within range
|
||||||
|
// -error
|
||||||
|
func GetAllIdentityHashesInListWithinRange(rangeStart [16]byte, rangeEnd [16]byte, inputList [][16]byte)(bool, [][16]byte, error){
|
||||||
|
|
||||||
|
identitiesWithinRangeList := make([][16]byte, 0)
|
||||||
|
|
||||||
|
for _, identityHash := range inputList{
|
||||||
|
|
||||||
|
isValid, err := identity.VerifyIdentityHash(identityHash, false, "")
|
||||||
|
if (err != nil) { return false, nil, err }
|
||||||
|
if (isValid == false){
|
||||||
|
identityHashHex := encoding.EncodeBytesToHexString(identityHash[:])
|
||||||
|
return false, nil, errors.New("GetAllIdentityHashesInListWithinRange called with inputList containing invalid identityHash: " + identityHashHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
isWithinRange, err := checkIfBytesAreWithinRange(rangeStart[:], rangeEnd[:], identityHash[:])
|
||||||
|
if (err != nil) { return false, nil, err }
|
||||||
|
if (isWithinRange == true){
|
||||||
|
identitiesWithinRangeList = append(identitiesWithinRangeList, identityHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(identitiesWithinRangeList) == 0){
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, identitiesWithinRangeList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Any inboxes found
|
||||||
|
// -[][10]byte: List of inboxes that are within range
|
||||||
|
// -error
|
||||||
|
func GetAllInboxesInListWithinRange(rangeStart [10]byte, rangeEnd [10]byte, inputList [][10]byte)(bool, [][10]byte, error){
|
||||||
|
|
||||||
|
inboxesWithinRangeList := make([][10]byte, 0)
|
||||||
|
|
||||||
|
for _, inbox := range inputList{
|
||||||
|
|
||||||
|
isWithinRange, err := checkIfBytesAreWithinRange(rangeStart[:], rangeEnd[:], inbox[:])
|
||||||
|
if (err != nil) { return false, nil, err }
|
||||||
|
if (isWithinRange == true){
|
||||||
|
inboxesWithinRangeList = append(inboxesWithinRangeList, inbox)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(inboxesWithinRangeList) == 0){
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, inboxesWithinRangeList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function will find the shared range from two ranges
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Any intersection found
|
||||||
|
// -[16]byte: Intersection range start
|
||||||
|
// -[16]byte: Intersection range end
|
||||||
|
// -error
|
||||||
|
func GetIdentityIntersectionRangeFromTwoRanges(range1Start [16]byte, range1End [16]byte, range2Start [16]byte, range2End [16]byte)(bool, [16]byte, [16]byte, error){
|
||||||
|
|
||||||
|
anyIntersectionFound, intersectionRangeStart, intersectionRangeEnd, err := getIntersectionRangeFromTwoRanges(range1Start[:], range1End[:], range2Start[:], range2End[:])
|
||||||
|
if (err != nil) { return false, [16]byte{}, [16]byte{}, err }
|
||||||
|
if (anyIntersectionFound == false){
|
||||||
|
return false, [16]byte{}, [16]byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(intersectionRangeStart) != 16){
|
||||||
|
return false, [16]byte{}, [16]byte{}, errors.New("getIntersectionRangeFromTwoRanges returning invalid length range start.")
|
||||||
|
}
|
||||||
|
if (len(intersectionRangeEnd) != 16){
|
||||||
|
return false, [16]byte{}, [16]byte{}, errors.New("getIntersectionRangeFromTwoRanges returning invalid length range end.")
|
||||||
|
}
|
||||||
|
|
||||||
|
intersectionRangeStartArray := [16]byte(intersectionRangeStart)
|
||||||
|
intersectionRangeEndArray := [16]byte(intersectionRangeEnd)
|
||||||
|
|
||||||
|
return true, intersectionRangeStartArray, intersectionRangeEndArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function will find the shared range from two ranges
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Any intersection found
|
||||||
|
// -[10]byte: Intersection range start
|
||||||
|
// -[10]byte: Intersection range end
|
||||||
|
// -error
|
||||||
|
func GetInboxIntersectionRangeFromTwoRanges(range1Start [10]byte, range1End [10]byte, range2Start [10]byte, range2End [10]byte)(bool, [10]byte, [10]byte, error){
|
||||||
|
|
||||||
|
anyIntersectionFound, intersectionRangeStart, intersectionRangeEnd, err := getIntersectionRangeFromTwoRanges(range1Start[:], range1End[:], range2Start[:], range2End[:])
|
||||||
|
if (err != nil) { return false, [10]byte{}, [10]byte{}, err }
|
||||||
|
if (anyIntersectionFound == false){
|
||||||
|
return false, [10]byte{}, [10]byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(intersectionRangeStart) != 10){
|
||||||
|
return false, [10]byte{}, [10]byte{}, errors.New("getIntersectionRangeFromTwoRanges returning invalid length rangeStart.")
|
||||||
|
}
|
||||||
|
if (len(intersectionRangeEnd) != 10){
|
||||||
|
return false, [10]byte{}, [10]byte{}, errors.New("getIntersectionRangeFromTwoRanges returning invalid length rangeEnd.")
|
||||||
|
}
|
||||||
|
|
||||||
|
intersectionRangeStartArray := [10]byte(intersectionRangeStart)
|
||||||
|
intersectionRangeEndArray := [10]byte(intersectionRangeEnd)
|
||||||
|
|
||||||
|
return true, intersectionRangeStartArray, intersectionRangeEndArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// This function will find the shared range from two ranges
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Any intersection found
|
||||||
|
// -[]byte: Intersection range start
|
||||||
|
// -[]byte: Intersection range end
|
||||||
|
// -error
|
||||||
|
func getIntersectionRangeFromTwoRanges(range1Start []byte, range1End []byte, range2Start []byte, range2End []byte)(bool, []byte, []byte, error){
|
||||||
|
|
||||||
|
if (string(range1Start) == string(range1End) && string(range2Start) == string(range2End)){
|
||||||
|
|
||||||
|
// The intersection is either 1 value, or none
|
||||||
|
if (string(range1Start) == string(range2Start)){
|
||||||
|
|
||||||
|
return true, range1Start, range1Start, nil
|
||||||
|
}
|
||||||
|
return false, nil, nil, nil
|
||||||
|
}
|
||||||
|
if (string(range1Start) == string(range1End)){
|
||||||
|
|
||||||
|
// We know range2Start != range2End
|
||||||
|
|
||||||
|
isInRange, err := checkIfBytesAreWithinRange(range2Start, range2End, range1Start)
|
||||||
|
if (err != nil) { return false, nil, nil, err }
|
||||||
|
if (isInRange == true){
|
||||||
|
// Intersection is a single value
|
||||||
|
return true, range1Start, range1Start, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil, nil, nil
|
||||||
|
}
|
||||||
|
if (string(range2Start) == string(range2End)){
|
||||||
|
|
||||||
|
// We know range1Start != range1End
|
||||||
|
|
||||||
|
isInRange, err := checkIfBytesAreWithinRange(range1Start, range1End, range2Start)
|
||||||
|
if (err != nil) { return false, nil, nil, err }
|
||||||
|
if (isInRange == true){
|
||||||
|
// Intersection is a single value
|
||||||
|
return true, range2Start, range2Start, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -[]byte: Smaller range bound
|
||||||
|
// -[]byte: Larger range bound
|
||||||
|
// -error
|
||||||
|
getSmallerAndLargerRangeBounds := func(bound1 []byte, bound2 []byte)([]byte, []byte, error){
|
||||||
|
|
||||||
|
boundsAreEqual, latterIsLarger, err := compareByteSlices(bound1, bound2)
|
||||||
|
if (err != nil){ return nil, nil, err }
|
||||||
|
if (boundsAreEqual == true){
|
||||||
|
return nil, nil, errors.New("compareByteSlices returning bounds are equal after we already checked.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latterIsLarger == true){
|
||||||
|
|
||||||
|
return bound1, bound2, nil
|
||||||
|
}
|
||||||
|
return bound2, bound1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
smallerBound1, largerBound1, err := getSmallerAndLargerRangeBounds(range1Start, range1End)
|
||||||
|
if (err != nil) { return false, nil, nil, err }
|
||||||
|
|
||||||
|
smallerBound2, largerBound2, err := getSmallerAndLargerRangeBounds(range2Start, range2End)
|
||||||
|
if (err != nil) { return false, nil, nil, err }
|
||||||
|
|
||||||
|
boundsAreEqual, latterIsLarger, err := compareByteSlices(largerBound1, smallerBound2)
|
||||||
|
if (err != nil) { return false, nil, nil, err }
|
||||||
|
if (boundsAreEqual == true){
|
||||||
|
// The intersection is only this 1 value
|
||||||
|
return true, largerBound1, smallerBound2, nil
|
||||||
|
}
|
||||||
|
if (latterIsLarger == true){
|
||||||
|
// There is no intersection
|
||||||
|
return false, nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is some overlap between the ranges
|
||||||
|
|
||||||
|
getIntersectionSmallerBound := func()([]byte, error){
|
||||||
|
|
||||||
|
// We return the greater of the two bounds
|
||||||
|
|
||||||
|
boundsAreEqual, latterIsLarger, err := compareByteSlices(smallerBound1, smallerBound2)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (boundsAreEqual == true){
|
||||||
|
return smallerBound2, nil
|
||||||
|
}
|
||||||
|
if (latterIsLarger == true){
|
||||||
|
return smallerBound2, nil
|
||||||
|
}
|
||||||
|
return smallerBound1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
intersectionSmallerBound, err := getIntersectionSmallerBound()
|
||||||
|
if (err != nil) { return false, nil, nil, err }
|
||||||
|
|
||||||
|
getIntersectionLargerBound := func()([]byte, error){
|
||||||
|
|
||||||
|
// We return the smaller of the two bounds
|
||||||
|
|
||||||
|
boundsAreEqual, latterIsLarger, err := compareByteSlices(largerBound1, largerBound2)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
if (boundsAreEqual == true){
|
||||||
|
return largerBound1, nil
|
||||||
|
}
|
||||||
|
if (latterIsLarger == true){
|
||||||
|
return largerBound1, nil
|
||||||
|
}
|
||||||
|
return largerBound2, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
intersectionLargerBound, err := getIntersectionLargerBound()
|
||||||
|
if (err != nil) { return false, nil, nil, err }
|
||||||
|
|
||||||
|
return true, intersectionSmallerBound, intersectionLargerBound, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEstimatedIdentitySubrangeQuantity(fullRangeStart [16]byte, fullRangeEnd [16]byte, fullRangeQuantity int64, subrangeStart [16]byte, subrangeEnd [16]byte)(int64, error){
|
||||||
|
|
||||||
|
estimatedIdentitiesWithinRange, err := getEstimatedSubrangeQuantity(fullRangeStart[:], fullRangeEnd[:], fullRangeQuantity, subrangeStart[:], subrangeEnd[:])
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
|
||||||
|
return estimatedIdentitiesWithinRange, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEstimatedInboxSubrangeQuantity(fullRangeStart [10]byte, fullRangeEnd [10]byte, fullRangeQuantity int64, subrangeStart [10]byte, subrangeEnd [10]byte)(int64, error){
|
||||||
|
|
||||||
|
estimatedInboxesWithinRange, err := getEstimatedSubrangeQuantity(fullRangeStart[:], fullRangeEnd[:], fullRangeQuantity, subrangeStart[:], subrangeEnd[:])
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
|
||||||
|
return estimatedInboxesWithinRange, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function takes a range and its item quantity, and a subset range, and returns the estimated number of items within the subset range
|
||||||
|
// This is used to determine how many items will be in a request, so that the requestor can know the maximum range to request
|
||||||
|
// This is because requests and responses have a maximum size, so the requestor must only request enough items to not go over the limit
|
||||||
|
|
||||||
|
// Outputs:
|
||||||
|
// -int64: Estimated number of items in subrange
|
||||||
|
// -error
|
||||||
|
func getEstimatedSubrangeQuantity(fullRangeStart []byte, fullRangeEnd []byte, fullRangeQuantityInt64 int64, subRangeStart []byte, subRangeEnd []byte)(int64, error){
|
||||||
|
|
||||||
|
// First we make sure the subrange is a valid subrange of the full range.
|
||||||
|
|
||||||
|
isInRange, err := checkIfBytesAreWithinRange(fullRangeStart, fullRangeEnd, subRangeStart)
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
if (isInRange == false){
|
||||||
|
return 0, errors.New("getEstimatedSubrangeQuantity called with a start bound out of range.")
|
||||||
|
}
|
||||||
|
isInRange, err = checkIfBytesAreWithinRange(fullRangeStart, fullRangeEnd, subRangeEnd)
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
if (isInRange == false){
|
||||||
|
return 0, errors.New("getEstimatedSubrangeQuantity called with a end bound out of range.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullRangeQuantityInt64 == 0){
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, fullRangeLength, err := getRangeLength(fullRangeStart, fullRangeEnd)
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
|
||||||
|
fullRangeQuantity := big.NewInt(fullRangeQuantityInt64)
|
||||||
|
|
||||||
|
_, _, _, subRangeLength, err := getRangeLength(subRangeStart, subRangeEnd)
|
||||||
|
if (err != nil){ return 0, err }
|
||||||
|
|
||||||
|
// FullRangeLength/FullRangeQuantity = SubrangeLength/SubrangeQuantity
|
||||||
|
// We solve for SubrangeQuantity
|
||||||
|
// SubrangeQuantity/SubrangeLength = FullRangeQuantity/FullRangeLength
|
||||||
|
// SubrangeQuantity = (FullRangeQuantity * SubrangeLength)/FullRangeLength
|
||||||
|
|
||||||
|
numerator := new(big.Int)
|
||||||
|
numerator.Mul(fullRangeQuantity, subRangeLength)
|
||||||
|
|
||||||
|
subrangeQuantity := new(big.Int)
|
||||||
|
subrangeQuantity.Div(numerator, fullRangeLength)
|
||||||
|
|
||||||
|
isInt64 := subrangeQuantity.IsInt64()
|
||||||
|
if (isInt64 == false){
|
||||||
|
// Number is too large to represent as int64
|
||||||
|
// This should never happen in practice, so host must be malicious
|
||||||
|
// We will return maximum int64 value
|
||||||
|
return 9223372036854775807, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := subrangeQuantity.Int64()
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentitySubrange struct{
|
||||||
|
|
||||||
|
SubrangeStart [16]byte
|
||||||
|
SubrangeEnd [16]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type InboxSubrange struct{
|
||||||
|
|
||||||
|
SubrangeStart [10]byte
|
||||||
|
SubrangeEnd [10]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitIdentityRangeIntoEqualSubranges(fullRangeStart [16]byte, fullRangeEnd [16]byte, fullRangeQuantity int64, maximumIdentitiesPerSubrange int64)([]IdentitySubrange, error){
|
||||||
|
|
||||||
|
subrangeObjectsList, err := splitRangeIntoEqualSubranges(16, fullRangeStart[:], fullRangeEnd[:], fullRangeQuantity, maximumIdentitiesPerSubrange)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
identitySubrangesList := make([]IdentitySubrange, 0, len(subrangeObjectsList))
|
||||||
|
|
||||||
|
for _, subrangeObject := range subrangeObjectsList{
|
||||||
|
|
||||||
|
subrangeStart := subrangeObject.SubrangeStart
|
||||||
|
subrangeEnd := subrangeObject.SubrangeEnd
|
||||||
|
|
||||||
|
if (len(subrangeStart) != 16){
|
||||||
|
return nil, errors.New("splitRangeIntoEqualSubranges returning invalid subrangeObject with invalid length subrangeStart.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(subrangeEnd) != 16){
|
||||||
|
return nil, errors.New("splitRangeIntoEqualSubranges returning invalid subrangeObject with invalid length subrangeEnd.")
|
||||||
|
}
|
||||||
|
|
||||||
|
subrangeStartArray := [16]byte(subrangeStart)
|
||||||
|
subrangeEndArray := [16]byte(subrangeEnd)
|
||||||
|
|
||||||
|
identitySubrangeObject := IdentitySubrange{
|
||||||
|
|
||||||
|
SubrangeStart: subrangeStartArray,
|
||||||
|
SubrangeEnd: subrangeEndArray,
|
||||||
|
}
|
||||||
|
|
||||||
|
identitySubrangesList = append(identitySubrangesList, identitySubrangeObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
return identitySubrangesList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitInboxRangeIntoEqualSubranges(fullRangeStart [10]byte, fullRangeEnd [10]byte, fullRangeQuantity int64, maximumInboxesPerSubrange int64)([]InboxSubrange, error){
|
||||||
|
|
||||||
|
subrangeObjectsList, err := splitRangeIntoEqualSubranges(10, fullRangeStart[:], fullRangeEnd[:], fullRangeQuantity, maximumInboxesPerSubrange)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
inboxSubrangesList := make([]InboxSubrange, 0, len(subrangeObjectsList))
|
||||||
|
|
||||||
|
for _, subrangeObject := range subrangeObjectsList{
|
||||||
|
|
||||||
|
subrangeStart := subrangeObject.SubrangeStart
|
||||||
|
subrangeEnd := subrangeObject.SubrangeEnd
|
||||||
|
|
||||||
|
if (len(subrangeStart) != 10){
|
||||||
|
return nil, errors.New("splitRangeIntoEqualSubranges returning invalid subrangeObject with invalid length subrangeStart.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(subrangeEnd) != 10){
|
||||||
|
return nil, errors.New("splitRangeIntoEqualSubranges returning invalid subrangeObject with invalid length subrangeEnd.")
|
||||||
|
}
|
||||||
|
|
||||||
|
subrangeStartArray := [10]byte(subrangeStart)
|
||||||
|
subrangeEndArray := [10]byte(subrangeEnd)
|
||||||
|
|
||||||
|
inboxSubrangeObject := InboxSubrange{
|
||||||
|
|
||||||
|
SubrangeStart: subrangeStartArray,
|
||||||
|
SubrangeEnd: subrangeEndArray,
|
||||||
|
}
|
||||||
|
|
||||||
|
inboxSubrangesList = append(inboxSubrangesList, inboxSubrangeObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
return inboxSubrangesList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type SubrangeObject struct{
|
||||||
|
|
||||||
|
SubrangeStart []byte
|
||||||
|
SubrangeEnd []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -[]subrangeObject
|
||||||
|
// -error
|
||||||
|
func splitRangeIntoEqualSubranges(subrangeBoundBytesLength int, fullRangeStart []byte, fullRangeEnd []byte, fullRangeQuantity int64, maximumItemsPerSubrange int64)([]SubrangeObject, error){
|
||||||
|
|
||||||
|
if (subrangeBoundBytesLength != 10 && subrangeBoundBytesLength != 16){
|
||||||
|
return nil, errors.New("splitRangeIntoEqualSubranges called with invalid subrangeBoundBytesLength.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maximumItemsPerSubrange == 0 || fullRangeQuantity == 0){
|
||||||
|
return nil, errors.New("Cannot create subranges: 0 quantity or 0 maximum items")
|
||||||
|
}
|
||||||
|
|
||||||
|
boundsAreEqual, fullRangeStartInt, fullRangeEndInt, fullRangeLength, err := getRangeLength(fullRangeStart, fullRangeEnd)
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
if (fullRangeQuantity <= maximumItemsPerSubrange || boundsAreEqual == true){
|
||||||
|
|
||||||
|
subrangeObject := SubrangeObject{
|
||||||
|
|
||||||
|
SubrangeStart: fullRangeStart,
|
||||||
|
SubrangeEnd: fullRangeEnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
newSubrangeObjectList := []SubrangeObject{subrangeObject}
|
||||||
|
|
||||||
|
return newSubrangeObjectList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsPerSubrange := math.Floor(float64(fullRangeQuantity)/float64(maximumItemsPerSubrange))
|
||||||
|
|
||||||
|
if (itemsPerSubrange > 100000000){
|
||||||
|
return nil, errors.New("Items per subrange is too large.")
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsPerSubrangeInt := big.NewInt(int64(itemsPerSubrange))
|
||||||
|
|
||||||
|
// SubrangeIncrement is the integer count of each subrange
|
||||||
|
getSubrangeIncrement := func()(*big.Int, error){
|
||||||
|
|
||||||
|
subrangeIncrement := new(big.Int)
|
||||||
|
subrangeIncrement.Div(fullRangeLength, itemsPerSubrangeInt)
|
||||||
|
|
||||||
|
zeroInt := big.NewInt(0)
|
||||||
|
compareResult := subrangeIncrement.Cmp(zeroInt)
|
||||||
|
if (compareResult == -1){
|
||||||
|
// This should not happen
|
||||||
|
return nil, errors.New("Subrange increment is negative.")
|
||||||
|
}
|
||||||
|
if (compareResult == 0){
|
||||||
|
// Increment is zero
|
||||||
|
// We need increment to be at least 1
|
||||||
|
oneInt := big.NewInt(1)
|
||||||
|
return oneInt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return subrangeIncrement, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
subrangeIncrement, err := getSubrangeIncrement()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
subrangeObjectsList := make([]SubrangeObject, 0)
|
||||||
|
|
||||||
|
index := fullRangeStartInt
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
subrangeStart := make([]byte, subrangeBoundBytesLength)
|
||||||
|
index.FillBytes(subrangeStart)
|
||||||
|
|
||||||
|
// Outputs:
|
||||||
|
// -bool: Is the final subrange
|
||||||
|
// -*big.Int: Subrange end int
|
||||||
|
getSubrangeEndInt := func()(bool, *big.Int){
|
||||||
|
|
||||||
|
nextBound := new(big.Int)
|
||||||
|
nextBound.Add(index, subrangeIncrement)
|
||||||
|
|
||||||
|
comparisonResult := nextBound.Cmp(fullRangeEndInt)
|
||||||
|
if (comparisonResult == -1){
|
||||||
|
// This subrange will not take us to the end
|
||||||
|
// Another bound remains.
|
||||||
|
return false, nextBound
|
||||||
|
}
|
||||||
|
// This bound takes us either exactly to the end, or exceeds the final value
|
||||||
|
// Either way, return the final value
|
||||||
|
return true, fullRangeEndInt
|
||||||
|
}
|
||||||
|
|
||||||
|
isFinalSubrange, subrangeEndInt := getSubrangeEndInt()
|
||||||
|
|
||||||
|
subrangeEnd := make([]byte, subrangeBoundBytesLength)
|
||||||
|
subrangeEndInt.FillBytes(subrangeEnd)
|
||||||
|
|
||||||
|
newSubrangeObject := SubrangeObject{
|
||||||
|
|
||||||
|
SubrangeStart: subrangeStart,
|
||||||
|
SubrangeEnd: subrangeEnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
subrangeObjectsList = append(subrangeObjectsList, newSubrangeObject)
|
||||||
|
|
||||||
|
if (isFinalSubrange == true){
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
index.Set(subrangeEndInt)
|
||||||
|
|
||||||
|
oneInt := big.NewInt(1)
|
||||||
|
|
||||||
|
index.Add(index, oneInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return subrangeObjectsList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Slices are equal
|
||||||
|
// -bool: Latter is larger
|
||||||
|
// -error
|
||||||
|
func compareByteSlices(sliceA []byte, sliceB []byte)(bool, bool, error){
|
||||||
|
|
||||||
|
if (len(sliceA) != len(sliceB)){
|
||||||
|
return false, false, errors.New("compareByteSlices called with mismatched length slices.")
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, sliceAByte := range sliceA{
|
||||||
|
|
||||||
|
sliceBByte := sliceB[index]
|
||||||
|
|
||||||
|
if (sliceAByte == sliceBByte){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (sliceAByte < sliceBByte){
|
||||||
|
return false, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slices are identical
|
||||||
|
|
||||||
|
return true, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Range bounds are equal
|
||||||
|
// -*big.Int: Range start
|
||||||
|
// -*big.Int: Range end
|
||||||
|
// -*big.Int: Range length (will be 1 if ranges are equal)
|
||||||
|
// -error
|
||||||
|
func getRangeLength(rangeStart []byte, rangeEnd []byte)(bool, *big.Int, *big.Int, *big.Int, error){
|
||||||
|
|
||||||
|
if (len(rangeStart) != len(rangeEnd)){
|
||||||
|
return false, nil, nil, nil, errors.New("getRangeLength called with differing length range bounds.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range length: (absolute value of the difference between both bounds) +1
|
||||||
|
// We add 1 to avoid 0 values. A range where rangeStart == rangeEnd contains 1 value.
|
||||||
|
|
||||||
|
rangeStartInt := new(big.Int)
|
||||||
|
rangeStartInt.SetBytes(rangeStart)
|
||||||
|
|
||||||
|
rangeEndInt := new(big.Int)
|
||||||
|
rangeEndInt.SetBytes(rangeEnd)
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Range bounds are equal
|
||||||
|
// -*big.Int: Lesser range bound
|
||||||
|
// -*big.Int: Greater range bound
|
||||||
|
getSmallerAndLargerRangeBounds := func()(bool, *big.Int, *big.Int){
|
||||||
|
|
||||||
|
comparisonResult := rangeStartInt.Cmp(rangeEndInt)
|
||||||
|
if (comparisonResult == -1){
|
||||||
|
return false, rangeStartInt, rangeEndInt
|
||||||
|
}
|
||||||
|
if (comparisonResult == 0){
|
||||||
|
return true, rangeStartInt, rangeEndInt
|
||||||
|
}
|
||||||
|
// comparisonResult == 1
|
||||||
|
return false, rangeEndInt, rangeStartInt
|
||||||
|
}
|
||||||
|
|
||||||
|
boundsAreEqual, smallerRangeBound, largerRangeBound := getSmallerAndLargerRangeBounds()
|
||||||
|
if (boundsAreEqual == true){
|
||||||
|
|
||||||
|
rangeLength := big.NewInt(1)
|
||||||
|
|
||||||
|
return true, smallerRangeBound, largerRangeBound, rangeLength, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := new(big.Int)
|
||||||
|
result.Sub(largerRangeBound, smallerRangeBound)
|
||||||
|
|
||||||
|
oneInt := big.NewInt(1)
|
||||||
|
|
||||||
|
result.Add(result, oneInt)
|
||||||
|
|
||||||
|
return false, smallerRangeBound, largerRangeBound, result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
419
internal/byteRange/byteRange_test.go
Normal file
419
internal/byteRange/byteRange_test.go
Normal file
|
@ -0,0 +1,419 @@
|
||||||
|
package byteRange
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
import "seekia/internal/messaging/inbox"
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
|
||||||
|
func TestCompareIdentityBounds(t *testing.T){
|
||||||
|
|
||||||
|
testBound1, testBound2 := GetMinimumMaximumIdentityHashBounds()
|
||||||
|
|
||||||
|
areEqual, latterIsLarger, err := compareByteSlices(testBound1[:], testBound2[:])
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("compareByteSlices failed test 1 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (areEqual == true){
|
||||||
|
t.Fatalf("compareByteSlices failed test 1a.")
|
||||||
|
}
|
||||||
|
if (latterIsLarger == false){
|
||||||
|
t.Fatalf("compareByteSlices failed test 1b.")
|
||||||
|
}
|
||||||
|
|
||||||
|
areEqual, latterIsLarger, err = compareByteSlices(testBound2[:], testBound1[:])
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("compareByteSlices failed test 2 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (areEqual == true){
|
||||||
|
t.Fatalf("compareByteSlices failed test 2a.")
|
||||||
|
}
|
||||||
|
if (latterIsLarger == true){
|
||||||
|
t.Fatalf("compareByteSlices failed test 2b.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareInboxBounds(t *testing.T){
|
||||||
|
|
||||||
|
testBound1 := "5555555555555555"
|
||||||
|
testBound2 := "7777777777777777"
|
||||||
|
|
||||||
|
testBound1Bytes, err := inbox.ReadInboxString(testBound1)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound1 is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
testBound2Bytes, err := inbox.ReadInboxString(testBound2)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound2 is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
areEqual, latterIsLarger, err := compareByteSlices(testBound1Bytes[:], testBound2Bytes[:])
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("compareByteSlices failed test 3 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (areEqual == true){
|
||||||
|
t.Fatalf("compareByteSlices failed test 3a.")
|
||||||
|
}
|
||||||
|
if (latterIsLarger == false){
|
||||||
|
t.Fatalf("compareByteSlices failed test 3b.")
|
||||||
|
}
|
||||||
|
|
||||||
|
areEqual, latterIsLarger, err = compareByteSlices(testBound2Bytes[:], testBound1Bytes[:])
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("compareByteSlices failed test 4 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (areEqual == true){
|
||||||
|
t.Fatalf("compareByteSlices failed test 4a.")
|
||||||
|
}
|
||||||
|
if (latterIsLarger == true){
|
||||||
|
t.Fatalf("compareByteSlices failed test 4b.")
|
||||||
|
}
|
||||||
|
|
||||||
|
testBound3 := "beraceaware22222"
|
||||||
|
testBound4 := "beraceaware33333"
|
||||||
|
|
||||||
|
testBound3Bytes, err := inbox.ReadInboxString(testBound3)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound3 is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
testBound4Bytes, err := inbox.ReadInboxString(testBound4)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound4 is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
areEqual, latterIsLarger, err = compareByteSlices(testBound3Bytes[:], testBound4Bytes[:])
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("compareByteSlices failed test 5 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (areEqual == true){
|
||||||
|
t.Fatalf("compareByteSlices failed test 5a.")
|
||||||
|
}
|
||||||
|
if (latterIsLarger == false){
|
||||||
|
t.Fatalf("compareByteSlices failed test 5b.")
|
||||||
|
}
|
||||||
|
|
||||||
|
areEqual, latterIsLarger, err = compareByteSlices(testBound4Bytes[:], testBound3Bytes[:])
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("compareByteSlices failed test 6 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (areEqual == true){
|
||||||
|
t.Fatalf("compareByteSlices failed test 6a.")
|
||||||
|
}
|
||||||
|
if (latterIsLarger == true){
|
||||||
|
t.Fatalf("compareByteSlices failed test 6b.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestInboxIsWithinRangeFunction(t *testing.T){
|
||||||
|
|
||||||
|
testRangeStartString := "aaaaaaaaaaaaaaaa"
|
||||||
|
testRangeEndString := "5555555555555555"
|
||||||
|
|
||||||
|
testRangeStart, err := inbox.ReadInboxString(testRangeStartString)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testRangeStart is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
testRangeEnd, err := inbox.ReadInboxString(testRangeEndString)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testRangeEnd is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
inboxAString := "ffffffffffffffff"
|
||||||
|
inboxBString := "6666666666666666"
|
||||||
|
|
||||||
|
inboxA, err := inbox.ReadInboxString(inboxAString)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("inboxAString is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
inboxB, err := inbox.ReadInboxString(inboxBString)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("inboxBString is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
isWithinRange, err := CheckIfInboxIsWithinRange(testRangeStart, testRangeEnd, inboxA)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("CheckIfInboxIsWithinRange failed test 1 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (isWithinRange == false){
|
||||||
|
t.Fatalf("CheckIfInboxIsWithinRange failed test 1.")
|
||||||
|
}
|
||||||
|
|
||||||
|
isWithinRange, err = CheckIfInboxIsWithinRange(testRangeStart, testRangeEnd, inboxB)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("CheckIfInboxIsWithinRange failed test 2 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (isWithinRange == true){
|
||||||
|
t.Fatalf("CheckIfInboxIsWithinRange failed test 2.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetInboxesWithinRangeFunction(t *testing.T){
|
||||||
|
|
||||||
|
testRangeStartString := "aaaaaaaaaaaaaaaa"
|
||||||
|
testRangeEndString := "5555555555555555"
|
||||||
|
|
||||||
|
testRangeStart, err := inbox.ReadInboxString(testRangeStartString)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testRangeStart is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
testRangeEnd, err := inbox.ReadInboxString(testRangeEndString)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testRangeEnd is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
inRangeInbox1String := "aaaaaaaaaaaaaaaa"
|
||||||
|
inRangeInbox2String := "bbbbbbbbbbbbbbbb"
|
||||||
|
inRangeInbox3String := "cccccccccccccccc"
|
||||||
|
inRangeInbox4String := "beraceaware23456"
|
||||||
|
|
||||||
|
inRangeInbox1, err := inbox.ReadInboxString(inRangeInbox1String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("inRangeInbox1String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
inRangeInbox2, err := inbox.ReadInboxString(inRangeInbox2String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("inRangeInbox2String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
inRangeInbox3, err := inbox.ReadInboxString(inRangeInbox3String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("inRangeInbox3String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
inRangeInbox4, err := inbox.ReadInboxString(inRangeInbox4String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("inRangeInbox4String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
outOfRangeInbox1String := "65432beraceaware"
|
||||||
|
outOfRangeInbox2String := "7777777777777777"
|
||||||
|
|
||||||
|
outOfRangeInbox1, err := inbox.ReadInboxString(outOfRangeInbox1String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("outOfRangeInbox1String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
outOfRangeInbox2, err := inbox.ReadInboxString(outOfRangeInbox2String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("outOfRangeInbox2String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
inputList := [][10]byte{inRangeInbox1, outOfRangeInbox1, inRangeInbox2, outOfRangeInbox2, inRangeInbox3, inRangeInbox4}
|
||||||
|
|
||||||
|
expectedOutputList := [][10]byte{inRangeInbox1, inRangeInbox2, inRangeInbox3, inRangeInbox4}
|
||||||
|
|
||||||
|
anyInboxesFound, inRangeInboxesList, err := GetAllInboxesInListWithinRange(testRangeStart, testRangeEnd, inputList)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("GetAllInboxesInListWithinRange failed with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (anyInboxesFound == false){
|
||||||
|
t.Fatalf("GetAllInboxesInListWithinRange failed to return any inboxes.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(inRangeInboxesList) != 4){
|
||||||
|
t.Fatalf("GetAllInboxesInListWithinRange returning invalid length result.")
|
||||||
|
}
|
||||||
|
|
||||||
|
areEqual := slices.Equal(inRangeInboxesList, expectedOutputList)
|
||||||
|
if (areEqual == false){
|
||||||
|
t.Fatalf("GetAllInboxesInListWithinRange returning invalid in range values list.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestGetInboxIntersectionRangeFunction(t *testing.T){
|
||||||
|
|
||||||
|
testBound1String := "aaaaaaaaaaaaaaaa"
|
||||||
|
testBound2String := "bbbbbbbbbbbbbbbb"
|
||||||
|
testBound3String := "cccccccccccccccc"
|
||||||
|
testBound4String := "dddddddddddddddd"
|
||||||
|
|
||||||
|
testBound1, err := inbox.ReadInboxString(testBound1String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound1String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
testBound2, err := inbox.ReadInboxString(testBound2String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound2String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
testBound3, err := inbox.ReadInboxString(testBound3String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound3String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
testBound4, err := inbox.ReadInboxString(testBound4String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound4String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
anyIntersectionFound, _, _, err := GetInboxIntersectionRangeFromTwoRanges(testBound1, testBound2, testBound3, testBound4)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("GetInboxIntersectionRangeFromTwoRanges failed test 1 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (anyIntersectionFound == true){
|
||||||
|
t.Fatalf("GetInboxIntersectionRangeFromTwoRanges failed test 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
anyIntersectionFound, intersectionRangeStart, intersectionRangeEnd, err := GetInboxIntersectionRangeFromTwoRanges(testBound1, testBound3, testBound2, testBound4)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("GetInboxIntersectionRangeFromTwoRanges failed test 2 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (anyIntersectionFound == false){
|
||||||
|
t.Fatalf("GetInboxIntersectionRangeFromTwoRanges failed test 2: No intersection found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersectionRangeStart != testBound2 || intersectionRangeEnd != testBound3){
|
||||||
|
t.Fatalf("GetInboxIntersectionRangeFromTwoRanges failed test 2.")
|
||||||
|
}
|
||||||
|
|
||||||
|
anyIntersectionFound, intersectionRangeStart, intersectionRangeEnd, err = GetInboxIntersectionRangeFromTwoRanges(testBound1, testBound4, testBound2, testBound3)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("GetInboxIntersectionRangeFromTwoRanges failed test 3 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (anyIntersectionFound == false){
|
||||||
|
t.Fatalf("GetInboxIntersectionRangeFromTwoRanges failed test 3: No intersection found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersectionRangeStart != testBound2 || intersectionRangeEnd != testBound3){
|
||||||
|
t.Fatalf("GetInboxIntersectionRangeFromTwoRanges failed test 3.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetInboxEstimatedItemsInSubrange(t *testing.T){
|
||||||
|
|
||||||
|
testBound1String := "aaaaaaaaaaaaaaaa" // == 0
|
||||||
|
testBound2String := "bbbbbbbbbbbbbbbb" // == 1
|
||||||
|
testBound3String := "cccccccccccccccc" // == 2
|
||||||
|
testBound4String := "dddddddddddddddd" // == 3
|
||||||
|
testBound5String := "eeeeeeeeeeeeeeee" // == 4
|
||||||
|
testBound6String := "ffffffffffffffff" // == 5
|
||||||
|
testBound7String := "gggggggggggggggg" // == 6
|
||||||
|
|
||||||
|
testBound1, err := inbox.ReadInboxString(testBound1String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound1String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
testBound2, err := inbox.ReadInboxString(testBound2String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound2String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
testBound3, err := inbox.ReadInboxString(testBound3String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound3String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
testBound4, err := inbox.ReadInboxString(testBound4String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound4String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
testBound5, err := inbox.ReadInboxString(testBound5String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound5String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
testBound6, err := inbox.ReadInboxString(testBound6String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound6String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
testBound7, err := inbox.ReadInboxString(testBound7String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound7String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
estimatedItems, err := GetEstimatedInboxSubrangeQuantity(testBound1, testBound7, 100, testBound2, testBound5)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("GetEstimatedInboxSubrangeQuantity failed test 1 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (estimatedItems != 50){
|
||||||
|
t.Fatalf("GetEstimatedInboxSubrangeQuantity failed test 1.")
|
||||||
|
}
|
||||||
|
|
||||||
|
estimatedItems, err = GetEstimatedInboxSubrangeQuantity(testBound1, testBound6, 100, testBound1, testBound2)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("GetEstimatedInboxSubrangeQuantity failed test 2 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (estimatedItems != 20){
|
||||||
|
t.Fatalf("GetEstimatedInboxSubrangeQuantity failed test 2.")
|
||||||
|
}
|
||||||
|
|
||||||
|
estimatedItems, err = GetEstimatedInboxSubrangeQuantity(testBound1, testBound6, 100, testBound3, testBound4)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("GetEstimatedInboxSubrangeQuantity failed test 3 with error: " + err.Error())
|
||||||
|
}
|
||||||
|
if (estimatedItems != 20){
|
||||||
|
t.Fatalf("GetEstimatedInboxSubrangeQuantity failed test 3.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestSplitIntoSubrangesFunction(t *testing.T){
|
||||||
|
|
||||||
|
testBound1String := "aaaaaaaaaaaaaaaa"
|
||||||
|
testBound2String := "cccccccccccccccc"
|
||||||
|
|
||||||
|
testBound1, err := inbox.ReadInboxString(testBound1String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound1String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
testBound2, err := inbox.ReadInboxString(testBound2String)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("testBound2String is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
subrangesList, err := SplitInboxRangeIntoEqualSubranges(testBound1, testBound2, 100, 50)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("SplitInboxRangeIntoEqualSubranges failed with error: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(subrangesList) != 2){
|
||||||
|
t.Fatalf("SplitInboxRangeIntoEqualSubranges failed: Subranges list not 2 items in length.")
|
||||||
|
}
|
||||||
|
|
||||||
|
subrange1StartString := "aaaaaaaaaaaaaaaa"
|
||||||
|
subrange1EndString := "bbbbbbbbbbbbbbbb"
|
||||||
|
|
||||||
|
subrange2StartString := "bbbbbbbbbbbbbbbc"
|
||||||
|
subrange2EndString := "cccccccccccccccc"
|
||||||
|
|
||||||
|
subrange1Start, err := inbox.ReadInboxString(subrange1StartString)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("subrange1StartString is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
subrange1End, err := inbox.ReadInboxString(subrange1EndString)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("subrange1EndString is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
subrange2Start, err := inbox.ReadInboxString(subrange2StartString)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("subrange2StartString is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
subrange2End, err := inbox.ReadInboxString(subrange2EndString)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("subrange2EndString is invalid: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
subrange1 := InboxSubrange{
|
||||||
|
SubrangeStart: subrange1Start,
|
||||||
|
SubrangeEnd: subrange1End,
|
||||||
|
}
|
||||||
|
|
||||||
|
subrange2 := InboxSubrange{
|
||||||
|
|
||||||
|
SubrangeStart: subrange2Start,
|
||||||
|
SubrangeEnd: subrange2End,
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedSubrangesList := []InboxSubrange{subrange1, subrange2}
|
||||||
|
|
||||||
|
areEqual := slices.Equal(subrangesList, expectedSubrangesList)
|
||||||
|
if (areEqual == false){
|
||||||
|
t.Fatalf("SplitInboxRangeIntoEqualSubranges failed: Expected map list does not match.")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
225
internal/contentMetadata/contentMetadata.go
Normal file
225
internal/contentMetadata/contentMetadata.go
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
|
||||||
|
// contentMetadata provides functions to store and retrieve metadata about profiles and messages
|
||||||
|
// Content metadata is stored in the database, so the content can be deleted and the metadata can still be retained
|
||||||
|
|
||||||
|
package contentMetadata
|
||||||
|
|
||||||
|
// Storing content metadata in the database enables:
|
||||||
|
// 1. We do not have to calculate the ProfileIsCanonical status and the attribute hashes for the same profile more than once
|
||||||
|
// 2. Hosts/Moderators can delete banned messages and still keep the cipher key hash to verify reviews for the messages
|
||||||
|
// 3. Hosts can delete banned profiles/messages and still be aware if those profile's/message's reviews are within their range
|
||||||
|
// Knowing if a profile's/message's reviews are within your range requires knowing the profile author/message inbox
|
||||||
|
// 4. Hosts can delete banned profiles and still be aware of why those profiles were banned (if their attributes were banned)
|
||||||
|
// We need to keep track of the profile's attribute hashes to be able to determine the profile's verdict consensus
|
||||||
|
// 5. It is faster to retrieve metadata than to read it from the profile/message
|
||||||
|
// This is because we do not need to read the entire message/profile into memory
|
||||||
|
|
||||||
|
// Profile metadata:
|
||||||
|
// -Profile version
|
||||||
|
// -Profile network type
|
||||||
|
// -Profile identity Hash
|
||||||
|
// -Profile broadcast time
|
||||||
|
// -Profile is disabled status
|
||||||
|
// -Profile is canonical status
|
||||||
|
// -Profile attribute hashes map
|
||||||
|
// Message metadata:
|
||||||
|
// -Message version
|
||||||
|
// -Message network type
|
||||||
|
// -Message size in bytes
|
||||||
|
// -Message Inbox
|
||||||
|
// -Message Cipher key hash
|
||||||
|
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
import "seekia/internal/badgerDatabase"
|
||||||
|
import "seekia/internal/profiles/readProfiles"
|
||||||
|
import "seekia/internal/messaging/readMessages"
|
||||||
|
|
||||||
|
import messagepack "github.com/vmihailenco/msgpack/v5"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Profile metadata exists
|
||||||
|
// -int: Profile Version
|
||||||
|
// -byte: Profile network type (1 == Mainnet, 2 == Testnet1)
|
||||||
|
// -[16]byte: Profile author identity Hash
|
||||||
|
// -int64: Profile broadcast time
|
||||||
|
// -bool: Profile is disabled
|
||||||
|
// -bool: Profile is canonical
|
||||||
|
// -map[int][27]byte: Map of Attribute identifier -> Attribute hash
|
||||||
|
// -error
|
||||||
|
func GetProfileMetadata(profileHash [28]byte)(bool, int, byte, [16]byte, int64, bool, bool, map[int][27]byte, error){
|
||||||
|
|
||||||
|
metadataExists, profileMetadata, err := badgerDatabase.GetProfileMetadata(profileHash)
|
||||||
|
if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
if (metadataExists == true){
|
||||||
|
|
||||||
|
type profileMetadataStruct struct{
|
||||||
|
ProfileVersion int
|
||||||
|
NetworkType byte
|
||||||
|
IdentityHash [16]byte
|
||||||
|
BroadcastTime int64
|
||||||
|
IsDisabled bool
|
||||||
|
IsCanonical bool
|
||||||
|
AttributeHashesMap map[int][27]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var profileMetadataObject profileMetadataStruct
|
||||||
|
|
||||||
|
err := encoding.DecodeMessagePackBytes(false, profileMetadata, &profileMetadataObject)
|
||||||
|
if (err != nil) {
|
||||||
|
return false, 0, 0, [16]byte{}, 0, false, false, nil, errors.New("Database malformed: contains invalid profile metadata: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
profileVersion := profileMetadataObject.ProfileVersion
|
||||||
|
profileNetworkType := profileMetadataObject.NetworkType
|
||||||
|
profileIdentityHash := profileMetadataObject.IdentityHash
|
||||||
|
profileBroadcastTime := profileMetadataObject.BroadcastTime
|
||||||
|
profileIsDisabled := profileMetadataObject.IsDisabled
|
||||||
|
profileIsCanonical := profileMetadataObject.IsCanonical
|
||||||
|
attributeHashesMap := profileMetadataObject.AttributeHashesMap
|
||||||
|
|
||||||
|
if (profileIsDisabled == true){
|
||||||
|
emptyMap := make(map[int][27]byte)
|
||||||
|
return true, profileVersion, profileNetworkType, profileIdentityHash, profileBroadcastTime, true, true, emptyMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, profileVersion, profileNetworkType, profileIdentityHash, profileBroadcastTime, false, profileIsCanonical, attributeHashesMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
profileType, _, err := readProfiles.ReadProfileHashMetadata(profileHash)
|
||||||
|
if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
|
||||||
|
profileExists, profileBytes, err := badgerDatabase.GetUserProfile(profileType, profileHash)
|
||||||
|
if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
if (profileExists == false){
|
||||||
|
return false, 0, 0, [16]byte{}, 0, false, false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ableToRead, profileHash_Retrieved, profileVersion, profileNetworkType, profileIdentityHash, profileBroadcastTime, profileIsDisabled, rawProfileMap, err := readProfiles.ReadProfileAndHash(false, profileBytes)
|
||||||
|
if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
if (ableToRead == false){
|
||||||
|
return false, 0, 0, [16]byte{}, 0, false, false, nil, errors.New("Database corrupt: Contains invalid profile: " + err.Error())
|
||||||
|
}
|
||||||
|
if (profileHash != profileHash_Retrieved){
|
||||||
|
return false, 0, 0, [16]byte{}, 0, false, false, nil, errors.New("Database corrupt: Profile hash does not match profile entry key")
|
||||||
|
}
|
||||||
|
|
||||||
|
profileAttributeHashesMap, profileIsCanonical, err := readProfiles.GetProfileAttributeHashesMap(profileIdentityHash, profileVersion, profileNetworkType, rawProfileMap)
|
||||||
|
if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
|
||||||
|
profileVersionEncoded, err := encoding.EncodeMessagePackBytes(profileVersion)
|
||||||
|
if (err != nil){ return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
|
||||||
|
profileNetworkTypeEncoded, err := encoding.EncodeMessagePackBytes(profileNetworkType)
|
||||||
|
if (err != nil){ return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
|
||||||
|
identityHashEncoded, err := encoding.EncodeMessagePackBytes(profileIdentityHash)
|
||||||
|
if (err != nil){ return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
|
||||||
|
broadcastTimeEncoded, err := encoding.EncodeMessagePackBytes(profileBroadcastTime)
|
||||||
|
if (err != nil){ return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
|
||||||
|
isDisabledEncoded, err := encoding.EncodeMessagePackBytes(profileIsDisabled)
|
||||||
|
if (err != nil){ return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
|
||||||
|
isCanonicalEncoded, err := encoding.EncodeMessagePackBytes(profileIsCanonical)
|
||||||
|
if (err != nil){ return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
|
||||||
|
attributeHashesMapEncoded, err := encoding.EncodeMessagePackBytes(profileAttributeHashesMap)
|
||||||
|
if (err != nil){ return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
|
||||||
|
profileMetadataSlice := []messagepack.RawMessage{profileVersionEncoded, profileNetworkTypeEncoded, identityHashEncoded, broadcastTimeEncoded, isDisabledEncoded, isCanonicalEncoded, attributeHashesMapEncoded}
|
||||||
|
|
||||||
|
profileMetadataBytes, err := encoding.EncodeMessagePackBytes(profileMetadataSlice)
|
||||||
|
if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
|
||||||
|
err = badgerDatabase.AddProfileMetadata(profileHash, profileMetadataBytes)
|
||||||
|
if (err != nil) { return false, 0, 0, [16]byte{}, 0, false, false, nil, err }
|
||||||
|
|
||||||
|
return true, profileVersion, profileNetworkType, profileIdentityHash, profileBroadcastTime, profileIsDisabled, profileIsCanonical, profileAttributeHashesMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Message metadata exists
|
||||||
|
// -int: Message version
|
||||||
|
// -byte: Message network type (1 == Mainnet, 2 == Testnet1)
|
||||||
|
// -int: Message size in bytes
|
||||||
|
// -[10]byte: Message inbox
|
||||||
|
// -[25]byte: Message cipher key hash
|
||||||
|
// -error
|
||||||
|
func GetMessageMetadata(messageHash [26]byte)(bool, int, byte, int, [10]byte, [25]byte, error){
|
||||||
|
|
||||||
|
exists, messageMetadata, err := badgerDatabase.GetMessageMetadata(messageHash)
|
||||||
|
if (err != nil) { return false, 0, 0, 0, [10]byte{}, [25]byte{}, err }
|
||||||
|
if (exists == true){
|
||||||
|
|
||||||
|
type messageMetadataStruct struct{
|
||||||
|
MessageVersion int
|
||||||
|
MessageNetworkType byte
|
||||||
|
MessageSize int
|
||||||
|
MessageInbox [10]byte
|
||||||
|
MessageCipherKeyHash [25]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageMetadataObject messageMetadataStruct
|
||||||
|
|
||||||
|
err := encoding.DecodeMessagePackBytes(false, messageMetadata, &messageMetadataObject)
|
||||||
|
if (err != nil) {
|
||||||
|
return false, 0, 0, 0, [10]byte{}, [25]byte{}, errors.New("Database malformed: contains invalid message metadata: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
messageVersion := messageMetadataObject.MessageVersion
|
||||||
|
messageNetworkType := messageMetadataObject.MessageNetworkType
|
||||||
|
messageSize := messageMetadataObject.MessageSize
|
||||||
|
messageInbox := messageMetadataObject.MessageInbox
|
||||||
|
messageCipherKeyHash := messageMetadataObject.MessageCipherKeyHash
|
||||||
|
|
||||||
|
return true, messageVersion, messageNetworkType, messageSize, messageInbox, messageCipherKeyHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
messageExists, messageBytes, err := badgerDatabase.GetChatMessage(messageHash)
|
||||||
|
if (err != nil) { return false, 0, 0, 0, [10]byte{}, [25]byte{}, err }
|
||||||
|
if (messageExists == false){
|
||||||
|
return false, 0, 0, 0, [10]byte{}, [25]byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ableToRead, messageHash_Retrieved, messageVersion, messageNetworkType, messageInbox, _, _, _, messageCipherKeyHash, _, _, err := readMessages.ReadChatMessagePublicDataAndHash(false, messageBytes)
|
||||||
|
if (err != nil) { return false, 0, 0, 0, [10]byte{}, [25]byte{}, err }
|
||||||
|
if (ableToRead == false){
|
||||||
|
return false, 0, 0, 0, [10]byte{}, [25]byte{}, errors.New("Database corrupt: Contains invalid message.")
|
||||||
|
}
|
||||||
|
if (messageHash != messageHash_Retrieved){
|
||||||
|
return false, 0, 0, 0, [10]byte{}, [25]byte{}, errors.New("Database corrupt: Chat message entry key does not match message hash.")
|
||||||
|
}
|
||||||
|
|
||||||
|
messageSize := len(messageBytes)
|
||||||
|
|
||||||
|
messageVersionEncoded, err := encoding.EncodeMessagePackBytes(messageVersion)
|
||||||
|
if (err != nil) { return false, 0, 0, 0, [10]byte{}, [25]byte{}, err }
|
||||||
|
|
||||||
|
messageNetworkTypeEncoded, err := encoding.EncodeMessagePackBytes(messageNetworkType)
|
||||||
|
if (err != nil) { return false, 0, 0, 0, [10]byte{}, [25]byte{}, err }
|
||||||
|
|
||||||
|
messageSizeEncoded, err := encoding.EncodeMessagePackBytes(messageSize)
|
||||||
|
if (err != nil) { return false, 0, 0, 0, [10]byte{}, [25]byte{}, err }
|
||||||
|
|
||||||
|
messageInboxEncoded, err := encoding.EncodeMessagePackBytes(messageInbox)
|
||||||
|
if (err != nil) { return false, 0, 0, 0, [10]byte{}, [25]byte{}, err }
|
||||||
|
|
||||||
|
messageCipherKeyHashEncoded, err := encoding.EncodeMessagePackBytes(messageCipherKeyHash)
|
||||||
|
if (err != nil) { return false, 0, 0, 0, [10]byte{}, [25]byte{}, err }
|
||||||
|
|
||||||
|
messageSlice := []messagepack.RawMessage{messageVersionEncoded, messageNetworkTypeEncoded, messageSizeEncoded, messageInboxEncoded, messageCipherKeyHashEncoded}
|
||||||
|
|
||||||
|
newMessageMetadata, err := encoding.EncodeMessagePackBytes(messageSlice)
|
||||||
|
if (err != nil) { return false, 0, 0, 0, [10]byte{}, [25]byte{}, err }
|
||||||
|
|
||||||
|
err = badgerDatabase.AddMessageMetadata(messageHash, newMessageMetadata)
|
||||||
|
if (err != nil) { return false, 0, 0, 0, [10]byte{}, [25]byte{}, err }
|
||||||
|
|
||||||
|
return true, messageVersion, messageNetworkType, messageSize, messageInbox, messageCipherKeyHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
159
internal/convertCurrencies/convertCurrencies.go
Normal file
159
internal/convertCurrencies/convertCurrencies.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
|
||||||
|
// convertCurrencies provides functions to converting between different currencies
|
||||||
|
// Currency rate data is sourced from the network parameters. The admin(s) update the rates manually.
|
||||||
|
|
||||||
|
package convertCurrencies
|
||||||
|
|
||||||
|
// All currency rates use 1 kilogram of gold as the exchange pair
|
||||||
|
// The crypto/gold rates are used to calculate identity scores, so the parameters must contain historical gold rates.
|
||||||
|
// The parameters do not contain historical fiat rates.
|
||||||
|
// Fiat is only used to display message/profile/report funding costs and user income/wealth in a user's currency
|
||||||
|
|
||||||
|
// Each networkType has its own parameters, and thus its own currency exchange rates.
|
||||||
|
// The currency exchange rates might be updated less often for test networks than for Mainnet.
|
||||||
|
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
//TODO: Complete package
|
||||||
|
// Each function will get its data from the getParameters package
|
||||||
|
// We have to consider if we should replace kilograms/grams with a better unit (milligrams?)
|
||||||
|
|
||||||
|
func CheckIfExchangeRatesAreDownloaded(networkType byte)(bool, error){
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts cryptocurrency atomic units to whole units
|
||||||
|
// For example, an Ethereum atomic unit is called Wei and a whole unit is called an Ether
|
||||||
|
func ConvertCryptocurrencyAtomicUnitsToWholeUnits(cryptocurrencyName string, inputAtomicUnits int64)(float64, error){
|
||||||
|
|
||||||
|
if (cryptocurrencyName == "Ethereum"){
|
||||||
|
|
||||||
|
// 1 Ether = 10^18 Wei = 1,000,000,000,000,000,000
|
||||||
|
|
||||||
|
ethereumAmount := float64(inputAtomicUnits) / float64(1000000000000000000)
|
||||||
|
|
||||||
|
return ethereumAmount, nil
|
||||||
|
|
||||||
|
} else if (cryptocurrencyName == "Cardano"){
|
||||||
|
|
||||||
|
// 1 ADA = 1,000,000 Lovelace
|
||||||
|
|
||||||
|
cardanoAmount := float64(inputAtomicUnits) / float64(1000000)
|
||||||
|
|
||||||
|
return cardanoAmount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, errors.New("ConvertCryptocurrencyAtomicUnitsToWholeUnits called with invalid cryptocurrencyName: " + cryptocurrencyName)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Parameters exist
|
||||||
|
// -float64: Output Currency units
|
||||||
|
// -error
|
||||||
|
func ConvertCryptoAtomicUnitsToAnyCurrency(networkType byte, inputCryptocurrency string, inputAtomicUnits int64, outputCurrencyCode string)(bool, float64, error){
|
||||||
|
|
||||||
|
isValid := helpers.VerifyNetworkType(networkType)
|
||||||
|
if (isValid == false){
|
||||||
|
networkTypeString := helpers.ConvertByteToString(networkType)
|
||||||
|
return false, 0, errors.New("ConvertCryptoAtomicUnitsToAnyCurrency called with invalid networkType: " + networkTypeString)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputCryptocurrency != "Ethereum" && inputCryptocurrency != "Cardano"){
|
||||||
|
return false, 0, errors.New("ConvertCryptoAtomicUnitsToAnyCurrency called with invalid inputCryptocurrency: " + inputCryptocurrency)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
|
||||||
|
result := float64(inputAtomicUnits/2)
|
||||||
|
|
||||||
|
return true, result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Parameters exist
|
||||||
|
// -float64: Output Cryptocurrency atomic units
|
||||||
|
// -error
|
||||||
|
func ConvertAnyCurrencyToCryptoAtomicUnits(networkType byte, inputCurrencyCode string, inputCurrencyAmount float64, outputCryptocurrency string)(bool, int64, error){
|
||||||
|
|
||||||
|
isValid := helpers.VerifyNetworkType(networkType)
|
||||||
|
if (isValid == false){
|
||||||
|
networkTypeString := helpers.ConvertByteToString(networkType)
|
||||||
|
return false, 0, errors.New("ConvertAnyCurrencyToCryptoAtomicUnits called with invalid networkType: " + networkTypeString)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputCryptocurrency != "Ethereum" && outputCryptocurrency != "Cardano"){
|
||||||
|
return false, 0, errors.New("ConvertAnyCurrencyToCryptoAtomicUnits called with invalid outputCryptocurrency: " + outputCryptocurrency)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
|
||||||
|
result := int64(inputCurrencyAmount*2)
|
||||||
|
|
||||||
|
return true, result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function will still return a valid amount, even if parameters are missing
|
||||||
|
// It will use a fallback currency exchange rate, coded within the client
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Parameters exist
|
||||||
|
// -float64: Currency value in kilograms of gold
|
||||||
|
// -error
|
||||||
|
func ConvertCurrencyToKilogramsOfGold(networkType byte, inputCurrencyCode string, inputValue float64)(bool, float64, error){
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
|
||||||
|
result := inputValue/3
|
||||||
|
|
||||||
|
return true, result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function will still return a valid amount, even if parameters are missing
|
||||||
|
// It will use a fallback currency exchange rate, coded within the client
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Parameters exist
|
||||||
|
// -float64: Value in input currency
|
||||||
|
// -error:
|
||||||
|
func ConvertKilogramsOfGoldToAnyCurrency(networkType byte, inputKilogramsAmount float64, outputCurrencyCode string)(bool, float64, error){
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
|
||||||
|
result := inputKilogramsAmount*3000
|
||||||
|
|
||||||
|
return true, result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Parameters exist
|
||||||
|
// -float64: Crypto atomic units amount converted to kilograms of gold
|
||||||
|
// -error
|
||||||
|
func ConvertCryptoAtomicUnitsToKilogramsOfGold(networkType byte, inputCryptocurrency string, depositTime int64, cryptoAtomicUnitsAmount int64)(bool, float64, error){
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
// This function will use the parameters which contain historical data
|
||||||
|
|
||||||
|
result := float64(cryptoAtomicUnitsAmount/500)
|
||||||
|
|
||||||
|
return true, result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Parameters exist
|
||||||
|
// -int64: Input kilograms amount converted to crypto atomic units
|
||||||
|
// -error
|
||||||
|
func ConvertKilogramsOfGoldToCryptoAtomicUnits(networkType byte, inputKilogramsAmount float64, outputCryptocurrency string)(bool, int64, error){
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
|
||||||
|
result := int64(inputKilogramsAmount/1000)
|
||||||
|
|
||||||
|
return true, result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
66
internal/convertCurrencies/convertCurrencies_test.go
Normal file
66
internal/convertCurrencies/convertCurrencies_test.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package convertCurrencies_test
|
||||||
|
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
import "seekia/internal/convertCurrencies"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
|
||||||
|
|
||||||
|
func TestConvertCurrencies(t *testing.T){
|
||||||
|
|
||||||
|
{
|
||||||
|
ethereumWeiAmount := int64(1234567899876543210)
|
||||||
|
|
||||||
|
etherAmount, err := convertCurrencies.ConvertCryptocurrencyAtomicUnitsToWholeUnits("Ethereum", ethereumWeiAmount)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("ConvertCryptocurrencyAtomicUnitsToWholeUnits failed: " + err.Error())
|
||||||
|
}
|
||||||
|
if (etherAmount != 1.23456789987654321){
|
||||||
|
etherAmountString := helpers.ConvertFloat64ToString(etherAmount)
|
||||||
|
t.Fatalf("Unexpected Ether amount: " + etherAmountString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
ethereumWeiAmount := int64(123456)
|
||||||
|
|
||||||
|
etherAmount, err := convertCurrencies.ConvertCryptocurrencyAtomicUnitsToWholeUnits("Ethereum", ethereumWeiAmount)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("ConvertCryptocurrencyAtomicUnitsToWholeUnits failed: " + err.Error())
|
||||||
|
}
|
||||||
|
if (etherAmount != 0.000000000000123456){
|
||||||
|
etherAmountString := helpers.ConvertFloat64ToString(etherAmount)
|
||||||
|
t.Fatalf("Unexpected Ether amount: " + etherAmountString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
cardanoLovelaceAmount := int64(1234567)
|
||||||
|
|
||||||
|
adaAmount, err := convertCurrencies.ConvertCryptocurrencyAtomicUnitsToWholeUnits("Cardano", cardanoLovelaceAmount)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("ConvertCryptocurrencyAtomicUnitsToWholeUnits failed: " + err.Error())
|
||||||
|
}
|
||||||
|
if (adaAmount != 1.2345670){
|
||||||
|
adaAmountString := helpers.ConvertFloat64ToString(adaAmount)
|
||||||
|
t.Fatalf("Unexpected ADA amount: " + adaAmountString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
cardanoLovelaceAmount := int64(123456789987654321)
|
||||||
|
|
||||||
|
adaAmount, err := convertCurrencies.ConvertCryptocurrencyAtomicUnitsToWholeUnits("Cardano", cardanoLovelaceAmount)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("ConvertCryptocurrencyAtomicUnitsToWholeUnits failed: " + err.Error())
|
||||||
|
}
|
||||||
|
if (adaAmount != 123456789987.6543274){
|
||||||
|
adaAmountString := helpers.ConvertFloat64ToString(adaAmount)
|
||||||
|
t.Fatalf("Unexpected ADA amount: " + adaAmountString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
193
internal/createCharts/createCharts.go
Normal file
193
internal/createCharts/createCharts.go
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
|
||||||
|
// charts provides functions to create charts
|
||||||
|
// These are used to show statistics about users
|
||||||
|
// See userStatistics.go to see how input statistics are created
|
||||||
|
|
||||||
|
package createCharts
|
||||||
|
|
||||||
|
import "seekia/internal/profiles/userStatistics"
|
||||||
|
|
||||||
|
import goChart "github.com/wcharczuk/go-chart/v2"
|
||||||
|
import "github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Inputs:
|
||||||
|
// -string: Chart title
|
||||||
|
// -[]userStatistics.StatisticsItem: Statistics items list
|
||||||
|
// -func(float64)(string, error): formatYAxisValuesFunction
|
||||||
|
// -This will take values such as 1000000 and turn them to "1 million"
|
||||||
|
// -bool: Y-axis units exist
|
||||||
|
// -string: Y-axis units (example: " Users")
|
||||||
|
// Outputs:
|
||||||
|
// -image.Image
|
||||||
|
// -error
|
||||||
|
func CreateBarChart(chartTitle string, chartStatisticsItemsList []userStatistics.StatisticsItem, formatYAxisValuesFunction func(float64)(string, error), yAxisUnitsProvided bool, yAxisUnits string)(image.Image, error){
|
||||||
|
|
||||||
|
if (len(chartStatisticsItemsList) == 0) {
|
||||||
|
return nil, errors.New("CreateBarChart called with empty chartStatisticsItemsList")
|
||||||
|
}
|
||||||
|
|
||||||
|
chartItemsList := make([]goChart.Value, 0, len(chartStatisticsItemsList))
|
||||||
|
|
||||||
|
for _, statisticsItem := range chartStatisticsItemsList{
|
||||||
|
|
||||||
|
itemLabel := statisticsItem.LabelFormatted
|
||||||
|
|
||||||
|
itemValue := statisticsItem.Value
|
||||||
|
|
||||||
|
// We make sure this function does not error
|
||||||
|
_, err := formatYAxisValuesFunction(itemValue)
|
||||||
|
if (err != nil){
|
||||||
|
return nil, errors.New("Invalid chartStatisticsItemsList: Item value is invalid. Reason: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
newChartValue := goChart.Value{
|
||||||
|
Label: itemLabel,
|
||||||
|
Value: itemValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
chartItemsList = append(chartItemsList, newChartValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(chartItemsList) == 1){
|
||||||
|
|
||||||
|
// This package cannot create bar charts with only 1 item
|
||||||
|
// Thus, we must add an empty item
|
||||||
|
|
||||||
|
newChartValue := goChart.Value{
|
||||||
|
Style: goChart.Style{
|
||||||
|
StrokeColor: drawing.ColorWhite,
|
||||||
|
FillColor: drawing.ColorWhite,
|
||||||
|
},
|
||||||
|
Label: "",
|
||||||
|
Value: .001,
|
||||||
|
}
|
||||||
|
|
||||||
|
chartItemsList = append(chartItemsList, newChartValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
chartStyleObject := goChart.Style{
|
||||||
|
Padding: goChart.Box{
|
||||||
|
Top: 40,
|
||||||
|
Bottom: 0,
|
||||||
|
Left: 0,
|
||||||
|
Right: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
titleStyleObject := goChart.Style{
|
||||||
|
Padding: goChart.Box{
|
||||||
|
Top: 10,
|
||||||
|
Bottom: 30,
|
||||||
|
},
|
||||||
|
FontSize: 15,
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
}
|
||||||
|
|
||||||
|
yAxisObject := goChart.YAxis{
|
||||||
|
ValueFormatter: func(v interface{}) string {
|
||||||
|
|
||||||
|
valueFloat64 := v.(float64)
|
||||||
|
|
||||||
|
valueFormatted, err := formatYAxisValuesFunction(valueFloat64)
|
||||||
|
if (err != nil){
|
||||||
|
return "ERROR"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yAxisUnitsProvided == false){
|
||||||
|
return valueFormatted
|
||||||
|
}
|
||||||
|
|
||||||
|
result := valueFormatted + yAxisUnits
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
barChartObject := goChart.BarChart{
|
||||||
|
Title: chartTitle,
|
||||||
|
TitleStyle: titleStyleObject,
|
||||||
|
Background: chartStyleObject,
|
||||||
|
Height: 500,
|
||||||
|
BarWidth: 60,
|
||||||
|
Bars: chartItemsList,
|
||||||
|
YAxis: yAxisObject,
|
||||||
|
}
|
||||||
|
|
||||||
|
collector := &goChart.ImageWriter{}
|
||||||
|
barChartObject.Render(goChart.PNG, collector)
|
||||||
|
|
||||||
|
goImage, err := collector.Image()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
return goImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateDonutChart(chartTitle string, chartStatisticsItemsList []userStatistics.StatisticsItem)(image.Image, error){
|
||||||
|
|
||||||
|
if (len(chartStatisticsItemsList) == 0) {
|
||||||
|
|
||||||
|
return nil, errors.New("CreateDonutChart called with empty chartStatisticsItemsList")
|
||||||
|
}
|
||||||
|
|
||||||
|
chartItemsList := make([]goChart.Value, 0, len(chartStatisticsItemsList))
|
||||||
|
|
||||||
|
for _, statisticsItem := range chartStatisticsItemsList{
|
||||||
|
|
||||||
|
itemLabel := statisticsItem.LabelFormatted
|
||||||
|
|
||||||
|
// Value is always a number representing the percentage of the donut
|
||||||
|
itemValue := statisticsItem.Value
|
||||||
|
|
||||||
|
newChartValue := goChart.Value{
|
||||||
|
Label: itemLabel,
|
||||||
|
Value: itemValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
chartItemsList = append(chartItemsList, newChartValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
chartStyleObject := goChart.Style{
|
||||||
|
Padding: goChart.Box{
|
||||||
|
Top: 20,
|
||||||
|
Bottom: 0,
|
||||||
|
Left: 0,
|
||||||
|
Right: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
titleStyleObject := goChart.Style{
|
||||||
|
FontSize: 15,
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
}
|
||||||
|
|
||||||
|
donutChartObject := goChart.DonutChart{
|
||||||
|
Title: chartTitle,
|
||||||
|
TitleStyle: titleStyleObject,
|
||||||
|
Background: chartStyleObject,
|
||||||
|
Height: 500,
|
||||||
|
Values: chartItemsList,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(chartItemsList) == 1){
|
||||||
|
|
||||||
|
// Default is transparent, we need to add color
|
||||||
|
sliceStyleObject := goChart.Style{
|
||||||
|
FillColor: drawing.ColorRed,
|
||||||
|
}
|
||||||
|
|
||||||
|
donutChartObject.SliceStyle = sliceStyleObject
|
||||||
|
}
|
||||||
|
|
||||||
|
collector := &goChart.ImageWriter{}
|
||||||
|
donutChartObject.Render(goChart.PNG, collector)
|
||||||
|
|
||||||
|
goImage, err := collector.Image()
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
return goImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
373
internal/cryptocurrency/cardanoAddress/cardanoAddress.go
Normal file
373
internal/cryptocurrency/cardanoAddress/cardanoAddress.go
Normal file
|
@ -0,0 +1,373 @@
|
||||||
|
package cardanoAddress
|
||||||
|
|
||||||
|
|
||||||
|
// Much of the code in this file is taken from btcd
|
||||||
|
// Repository: github.com/btcsuite/btcd
|
||||||
|
// Soure file: /btcutil/bech32/bech32.go
|
||||||
|
|
||||||
|
import "seekia/internal/cryptography/blake3"
|
||||||
|
import "seekia/internal/identity"
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
import "crypto/rand"
|
||||||
|
import "strings"
|
||||||
|
import "slices"
|
||||||
|
|
||||||
|
|
||||||
|
func GetIdentityScoreCardanoAddressFromIdentityHash(identityHash [16]byte)(string, error){
|
||||||
|
|
||||||
|
isValid, err := identity.VerifyIdentityHash(identityHash, true, "Moderator")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (isValid == false){
|
||||||
|
identityHashHex := encoding.EncodeBytesToHexString(identityHash[:])
|
||||||
|
return "", errors.New("GetIdentityScoreCardanoAddressFromIdentityHash called with invalid identity hash: " + identityHashHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
hashInputSuffix, err := encoding.DecodeBase32StringToBytes("cardanoidentityscoreaddresshashsaltbytes")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
hashInput := slices.Concat(identityHash[:], hashInputSuffix)
|
||||||
|
|
||||||
|
pseudoRandomBytes, err := blake3.GetBlake3HashAsBytes(28, hashInput)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
resultAddress, err := getCardanoAddressFromBytes(pseudoRandomBytes)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
return resultAddress, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCreditAccountCardanoAddressFromAccountPublicKey(publicKey [32]byte)(string, error){
|
||||||
|
|
||||||
|
hashInputSuffix, err := encoding.DecodeBase32StringToBytes("cardanocreditaccountsalt")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
hashInput := slices.Concat(publicKey[:], hashInputSuffix)
|
||||||
|
|
||||||
|
pseudoRandomBytes, err := blake3.GetBlake3HashAsBytes(28, hashInput)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
resultAddress, err := getCardanoAddressFromBytes(pseudoRandomBytes)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
return resultAddress, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMemoCardanoAddressFromMemoHash(memoHash [32]byte)(string, error){
|
||||||
|
|
||||||
|
hashInputSuffix, err := encoding.DecodeBase32StringToBytes("memocardanoaddresshashinputbytes")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
hashInput := slices.Concat(memoHash[:], hashInputSuffix)
|
||||||
|
|
||||||
|
pseudoRandomBytes, err := blake3.GetBlake3HashAsBytes(28, hashInput)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
resultAddress, err := getCardanoAddressFromBytes(pseudoRandomBytes)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
return resultAddress, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNewRandomCardanoAddress()(string, error){
|
||||||
|
|
||||||
|
randomBytes := make([]byte, 28)
|
||||||
|
_, err := rand.Read(randomBytes[:])
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
address, err := getCardanoAddressFromBytes(randomBytes)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
return address, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// bech32Charset is the set of characters used in the data section of bech32 strings.
|
||||||
|
// Note that this is ordered, such that for a given charset[i], i is the binary value of the character.
|
||||||
|
const bech32Charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||||
|
|
||||||
|
// gen encodes the generator polynomial for the bech32 BCH checksum.
|
||||||
|
var gen = []int{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
|
||||||
|
|
||||||
|
|
||||||
|
func getCardanoAddressFromBytes(paymentKeyHash []byte)(string, error){
|
||||||
|
|
||||||
|
if (len(paymentKeyHash) != 28){
|
||||||
|
// This should be 228 bits, or 28 bytes
|
||||||
|
return "", errors.New("getCardanoAddressFromBytes called with invalid length paymentKeyHash.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We first construct the address header (1 byte)
|
||||||
|
// First 4 bytes: 0110 = Enterprise Address with PaymentKeyHash
|
||||||
|
// Last 4 bytes: 0001 = Mainnet
|
||||||
|
// Header bits == 01100001
|
||||||
|
// 1100001 in binary == 97
|
||||||
|
|
||||||
|
addressBytes := []byte{97}
|
||||||
|
addressBytes = append(addressBytes, paymentKeyHash...)
|
||||||
|
|
||||||
|
convertedAddressBytes, err := convertBits(addressBytes, 8, 5, true)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
addressString := "addr1"
|
||||||
|
|
||||||
|
// Now we encode the rest of the address as bech32
|
||||||
|
|
||||||
|
for _, addressByte := range convertedAddressBytes {
|
||||||
|
|
||||||
|
if (addressByte >= 32) {
|
||||||
|
return "", errors.New("convertBits returning out-of-range byte.")
|
||||||
|
}
|
||||||
|
|
||||||
|
addressByteCharacter := bech32Charset[addressByte]
|
||||||
|
|
||||||
|
addressString += string(addressByteCharacter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we compute the checksum
|
||||||
|
|
||||||
|
polymod := bech32Polymod(convertedAddressBytes, nil) ^ 1
|
||||||
|
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
|
||||||
|
b := byte((polymod >> uint(5*(5-i))) & 31)
|
||||||
|
|
||||||
|
// This can't fail, given we explicitly cap the previous b byte by the first 31 bits.
|
||||||
|
character := bech32Charset[b]
|
||||||
|
|
||||||
|
addressString += string(character)
|
||||||
|
}
|
||||||
|
|
||||||
|
return addressString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is partially adopted from btcd
|
||||||
|
// Repository: github.com/btcsuite/btcd
|
||||||
|
// File: /btcutil/bech32/bech32.go
|
||||||
|
|
||||||
|
// This function verifies that a Cardano address in an Enterprise address, encoded in bech32
|
||||||
|
// This is the format for all Seekia Cardano addresses
|
||||||
|
func VerifyCardanoSeekiaAddress(address string)bool{
|
||||||
|
|
||||||
|
if (len(address) != 58){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
addressWithoutPrefix, hasPrefix := strings.CutPrefix(address, "addr1")
|
||||||
|
if (hasPrefix == false){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len([]rune(addressWithoutPrefix)) != 53){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedAddress := make([]byte, 0, 53)
|
||||||
|
|
||||||
|
for _, addressCharacter := range addressWithoutPrefix{
|
||||||
|
|
||||||
|
index := strings.IndexByte(bech32Charset, byte(addressCharacter))
|
||||||
|
if (index < 0) {
|
||||||
|
|
||||||
|
// This character is not in the bech32 charset.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedAddress = append(decodedAddress, byte(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedAddressWithoutChecksum := decodedAddress[:47]
|
||||||
|
decodedAddressChecksum := decodedAddress[47:]
|
||||||
|
|
||||||
|
polymod := bech32Polymod(decodedAddressWithoutChecksum, decodedAddressChecksum)
|
||||||
|
|
||||||
|
if (polymod != 1){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
convertedBytes, err := convertBits(decodedAddressWithoutChecksum, 5, 8, false)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (convertedBytes[0] != 97){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// This function is taken from btcd
|
||||||
|
// Repository: github.com/btcsuite/btcd
|
||||||
|
// File: /btcutil/bech32/bech32.go
|
||||||
|
|
||||||
|
// bech32Polymod calculates the BCH checksum for a given HumanReadablePart, values and checksum data.
|
||||||
|
// Checksum is optional, and if nil a 0 checksum is assumed.
|
||||||
|
//
|
||||||
|
// Values and checksum (if provided) MUST be encoded as 5 bits per element (base
|
||||||
|
// 32), otherwise the results are undefined.
|
||||||
|
//
|
||||||
|
// For more details on the polymod calculation, please refer to BIP 173.
|
||||||
|
func bech32Polymod(values []byte, checksum []byte) int {
|
||||||
|
|
||||||
|
humanReadablePart := "addr"
|
||||||
|
humanReadablePartLength := 4
|
||||||
|
|
||||||
|
chk := 1
|
||||||
|
|
||||||
|
// Account for the high bits of the HumanReadablePart in the checksum.
|
||||||
|
for i := 0; i < humanReadablePartLength; i++ {
|
||||||
|
b := chk >> 25
|
||||||
|
hiBits := int(humanReadablePart[i]) >> 5
|
||||||
|
chk = (chk&0x1ffffff)<<5 ^ hiBits
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
if (b>>uint(i))&1 == 1 {
|
||||||
|
chk ^= gen[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account for the separator (0) between high and low bits of the HumanReadablePart.
|
||||||
|
// x^0 == x, so we eliminate the redundant xor used in the other rounds.
|
||||||
|
b := chk >> 25
|
||||||
|
chk = (chk & 0x1ffffff) << 5
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
if (b>>uint(i))&1 == 1 {
|
||||||
|
chk ^= gen[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account for the low bits of the HumanReadablePart.
|
||||||
|
for i := 0; i < humanReadablePartLength; i++ {
|
||||||
|
b := chk >> 25
|
||||||
|
loBits := int(humanReadablePart[i]) & 31
|
||||||
|
chk = (chk&0x1ffffff)<<5 ^ loBits
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
if (b>>uint(i))&1 == 1 {
|
||||||
|
chk ^= gen[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account for the values.
|
||||||
|
for _, v := range values {
|
||||||
|
b := chk >> 25
|
||||||
|
chk = (chk&0x1ffffff)<<5 ^ int(v)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
if (b>>uint(i))&1 == 1 {
|
||||||
|
chk ^= gen[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checksum == nil) {
|
||||||
|
// A nil checksum is used during encoding, so assume all bytes are zero.
|
||||||
|
// x^0 == x, so we eliminate the redundant xor used in the other rounds.
|
||||||
|
for v := 0; v < 6; v++ {
|
||||||
|
b := chk >> 25
|
||||||
|
chk = (chk & 0x1ffffff) << 5
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
if (b>>uint(i))&1 == 1 {
|
||||||
|
chk ^= gen[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Checksum is provided during decoding, so use it.
|
||||||
|
for _, v := range checksum {
|
||||||
|
b := chk >> 25
|
||||||
|
chk = (chk&0x1ffffff)<<5 ^ int(v)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
if (b>>uint(i))&1 == 1 {
|
||||||
|
chk ^= gen[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chk
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// This function is taken from btcd
|
||||||
|
// Repository: github.com/btcsuite/btcd
|
||||||
|
// File: /btcutil/bech32/bech32.go
|
||||||
|
|
||||||
|
// ConvertBits converts a byte slice where each byte is encoding fromBits bits,
|
||||||
|
// to a byte slice where each byte is encoding toBits bits.
|
||||||
|
func convertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) {
|
||||||
|
|
||||||
|
if fromBits < 1 || fromBits > 8 || toBits < 1 || toBits > 8 {
|
||||||
|
return nil, errors.New("convertBits called with invalid bit groups.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the maximum size the resulting array can have after base
|
||||||
|
// conversion, so that we can size it a single time. This might be off
|
||||||
|
// by a byte depending on whether padding is used or not and if the input
|
||||||
|
// data is a multiple of both fromBits and toBits, but we ignore that and
|
||||||
|
// just size it to the maximum possible.
|
||||||
|
maxSize := len(data)*int(fromBits)/int(toBits) + 1
|
||||||
|
|
||||||
|
// The final bytes, each byte encoding toBits bits.
|
||||||
|
regrouped := make([]byte, 0, maxSize)
|
||||||
|
|
||||||
|
// Keep track of the next byte we create and how many bits we have
|
||||||
|
// added to it out of the toBits goal.
|
||||||
|
nextByte := byte(0)
|
||||||
|
filledBits := uint8(0)
|
||||||
|
|
||||||
|
for _, b := range data {
|
||||||
|
|
||||||
|
// Discard unused bits.
|
||||||
|
b <<= 8 - fromBits
|
||||||
|
|
||||||
|
// How many bits remaining to extract from the input data.
|
||||||
|
remFromBits := fromBits
|
||||||
|
for remFromBits > 0 {
|
||||||
|
// How many bits remaining to be added to the next byte.
|
||||||
|
remToBits := toBits - filledBits
|
||||||
|
|
||||||
|
// The number of bytes to next extract is the minimum of remFromBits and remToBits.
|
||||||
|
toExtract := remFromBits
|
||||||
|
if remToBits < toExtract {
|
||||||
|
toExtract = remToBits
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the next bits to nextByte, shifting the already added bits to the left.
|
||||||
|
nextByte = (nextByte << toExtract) | (b >> (8 - toExtract))
|
||||||
|
|
||||||
|
// Discard the bits we just extracted and get ready for next iteration.
|
||||||
|
b <<= toExtract
|
||||||
|
remFromBits -= toExtract
|
||||||
|
filledBits += toExtract
|
||||||
|
|
||||||
|
// If the nextByte is completely filled, we add it to
|
||||||
|
// our regrouped bytes and start on the next byte.
|
||||||
|
if filledBits == toBits {
|
||||||
|
regrouped = append(regrouped, nextByte)
|
||||||
|
filledBits = 0
|
||||||
|
nextByte = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We pad any unfinished group if specified.
|
||||||
|
if pad && filledBits > 0 {
|
||||||
|
nextByte <<= toBits - filledBits
|
||||||
|
regrouped = append(regrouped, nextByte)
|
||||||
|
filledBits = 0
|
||||||
|
nextByte = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any incomplete group must be <= 4 bits, and all zeroes.
|
||||||
|
if filledBits > 0 && (filledBits > 4 || nextByte != 0) {
|
||||||
|
return nil, errors.New("Invalid incomplete group")
|
||||||
|
}
|
||||||
|
|
||||||
|
return regrouped, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
package cardanoAddress_test
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
import "seekia/internal/cryptocurrency/cardanoAddress"
|
||||||
|
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
import "seekia/internal/identity"
|
||||||
|
|
||||||
|
|
||||||
|
func TestCardanoAddressFunctions(t *testing.T) {
|
||||||
|
|
||||||
|
{
|
||||||
|
testIdentityHashString := "wgkxplfvju7yhpoadvxju4kmfdr"
|
||||||
|
|
||||||
|
testIdentityHash, _, err := identity.ReadIdentityHashString(testIdentityHashString)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to read testIdentityHashString: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
identityAddress, err := cardanoAddress.GetIdentityScoreCardanoAddressFromIdentityHash(testIdentityHash)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get Cardano identity score address: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identityAddress != "addr1vxy7mhvmk0zthqryd0cyrqvw9xryv5ky235vqn5ektqyrzc4w9459"){
|
||||||
|
t.Fatalf("GetIdentityScoreCardanoAddressFromIdentityHash not producing expected identity address: " + identityAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
testPublicKey := "667ff88775bec9dbe163b1a8d3f60a69334fa594ce17c008249f59efb6607cff"
|
||||||
|
|
||||||
|
testPublicKeyBytes, err := encoding.DecodeHexStringToBytes(testPublicKey)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to decode testPublicKey: Not Hex: " + testPublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(testPublicKeyBytes) != 32){
|
||||||
|
t.Fatalf("testPublicKey is invalid: Invalid length.")
|
||||||
|
}
|
||||||
|
|
||||||
|
testPublicKeyArray := [32]byte(testPublicKeyBytes)
|
||||||
|
|
||||||
|
accountAddress, err := cardanoAddress.GetCreditAccountCardanoAddressFromAccountPublicKey(testPublicKeyArray)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get Cardano account address: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountAddress != "addr1v9cc856hgzqpl63dgjd7cd2nqrna02wdfdeel0zh65ary0cyfeu9k"){
|
||||||
|
t.Fatalf("Cardano credit account address does not match expected value: " + accountAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
testMemoHash := "db61e89bb23f5c9964393eaa0510491c5ea5ac14b26bcc33cdfed1b6a45900b0"
|
||||||
|
|
||||||
|
testMemoHashBytes, err := encoding.DecodeHexStringToBytes(testMemoHash)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to decode testPublicKey: Not Hex: " + testMemoHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(testMemoHashBytes) != 32){
|
||||||
|
t.Fatalf("testMemoHashBytes is invalid: Invalid length.")
|
||||||
|
}
|
||||||
|
|
||||||
|
testMemoHashArray := [32]byte(testMemoHashBytes)
|
||||||
|
|
||||||
|
memoAddress, err := cardanoAddress.GetMemoCardanoAddressFromMemoHash(testMemoHashArray)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get Cardano account address: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memoAddress != "addr1v90ldkkj5ga0wsgcj4pvndl2v24n05xv9k6wmzuwfrmk8eqz8kru9"){
|
||||||
|
t.Fatalf("Cardano memo address does not match expected value: " + memoAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i:=0; i < 1000; i++{
|
||||||
|
|
||||||
|
newAddress, err := cardanoAddress.GetNewRandomCardanoAddress()
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get random Cardano address: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid := cardanoAddress.VerifyCardanoSeekiaAddress(newAddress)
|
||||||
|
if (isValid == false){
|
||||||
|
t.Fatalf("Randomly generated Cardano address is invalid.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
25
internal/cryptocurrency/cardanoNode/cardanoNode.go
Normal file
25
internal/cryptocurrency/cardanoNode/cardanoNode.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
// cardanoNode provides functions to access a local Cardano node
|
||||||
|
|
||||||
|
package cardanoNode
|
||||||
|
|
||||||
|
//TODO: Complete this package
|
||||||
|
// We should start by using daedalus as the node, and possibly add support for other node implementations later.
|
||||||
|
// We might need to use an indexer to access the information faster
|
||||||
|
// We need to be able to access all transactions to an address, when they were sent, and the amount in ETH of each transaction
|
||||||
|
// We should also include the fee paid so that users can get credit/score for the amount paid in fees
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Able to get data from node
|
||||||
|
// -map[int64]int64: Map of Deposit time -> Amount deposited (in gwei or wei?)
|
||||||
|
// -error
|
||||||
|
func GetAddressDeposits(address string)(bool, map[string]string, error){
|
||||||
|
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
203
internal/cryptocurrency/ethereumAddress/ethereumAddress.go
Normal file
203
internal/cryptocurrency/ethereumAddress/ethereumAddress.go
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
|
||||||
|
// ethereumAddress provides functions to derive Ethereum addresses for identity hashes, credit account keys, and memo hashes
|
||||||
|
|
||||||
|
package ethereumAddress
|
||||||
|
|
||||||
|
import "seekia/internal/cryptography/blake3"
|
||||||
|
import "seekia/internal/identity"
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
|
||||||
|
import "golang.org/x/crypto/sha3"
|
||||||
|
|
||||||
|
import "crypto/rand"
|
||||||
|
import "encoding/hex"
|
||||||
|
import "slices"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
|
||||||
|
func GetIdentityScoreEthereumAddressFromIdentityHash(identityHash [16]byte)(string, error){
|
||||||
|
|
||||||
|
isValid, err := identity.VerifyIdentityHash(identityHash, true, "Moderator")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (isValid == false){
|
||||||
|
identityHashHex := encoding.EncodeBytesToHexString(identityHash[:])
|
||||||
|
return "", errors.New("GetIdentityScoreEthereumAddressFromIdentityHash called with invalid identity hash: " + identityHashHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
hashInputSuffix, err := encoding.DecodeBase32StringToBytes("ethereum")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
hashInput := slices.Concat(identityHash[:], hashInputSuffix)
|
||||||
|
|
||||||
|
pseudoRandomBytes, err := blake3.GetBlake3HashAsBytes(20, hashInput)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
resultAddress, err := getEthereumAddressFromBytes(pseudoRandomBytes)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
return resultAddress, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCreditAccountEthereumAddressFromAccountPublicKey(publicKey [32]byte)(string, error){
|
||||||
|
|
||||||
|
hashInputSuffix, err := encoding.DecodeBase32StringToBytes("ethereum")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
hashInput := slices.Concat(publicKey[:], hashInputSuffix)
|
||||||
|
|
||||||
|
pseudoRandomBytes, err := blake3.GetBlake3HashAsBytes(20, hashInput)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
resultAddress, err := getEthereumAddressFromBytes(pseudoRandomBytes)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
return resultAddress, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMemoEthereumAddressFromMemoHash(memoHash [32]byte)(string, error){
|
||||||
|
|
||||||
|
hashInputSuffix, err := encoding.DecodeBase32StringToBytes("memoethereumaddr")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
hashInput := slices.Concat(memoHash[:], hashInputSuffix)
|
||||||
|
|
||||||
|
pseudoRandomBytes, err := blake3.GetBlake3HashAsBytes(20, hashInput)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
resultAddress, err := getEthereumAddressFromBytes(pseudoRandomBytes)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
return resultAddress, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func GetNewRandomEthereumAddress()(string, error){
|
||||||
|
|
||||||
|
randomBytes := make([]byte, 20)
|
||||||
|
_, err := rand.Read(randomBytes[:])
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
address, err := getEthereumAddressFromBytes(randomBytes)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
return address, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Below code is adopted from go-ethereum
|
||||||
|
// github.com/ethereum/go-ethereum/common/types.go
|
||||||
|
|
||||||
|
func getEthereumAddressFromBytes(inputBytes []byte) (string, error){
|
||||||
|
|
||||||
|
if (len(inputBytes) != 20) {
|
||||||
|
return "", errors.New("getEthereumAddressFromBytes called with invalid inputBytes: Invalid length.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var addressArray [42]byte
|
||||||
|
copy(addressArray[:2], "0x")
|
||||||
|
|
||||||
|
hex.Encode(addressArray[2:], inputBytes)
|
||||||
|
|
||||||
|
addressLowercase := string(addressArray[:])
|
||||||
|
|
||||||
|
addressString, err := convertLowercaseEthereumAddressToChecksumAddress(addressLowercase)
|
||||||
|
if (err != nil){ return "", err }
|
||||||
|
|
||||||
|
return addressString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyEthereumAddress(input string)bool{
|
||||||
|
|
||||||
|
if (len(input) != 42){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (input[0] != '0'){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (input[1] != 'x' && input[1] != 'X'){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
addressWithoutPrefix := input[2:]
|
||||||
|
|
||||||
|
addressWithoutPrefixCharacterBytes := []byte(addressWithoutPrefix)
|
||||||
|
|
||||||
|
// We verify all characters are hexadecimal
|
||||||
|
for _, char := range addressWithoutPrefixCharacterBytes{
|
||||||
|
|
||||||
|
if ('0' <= char && char <= '9'){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ('a' <= char && char <= 'f'){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ('A' <= char && char <= 'F'){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
addressWithoutPrefixBytes, err := hex.DecodeString(addressWithoutPrefix)
|
||||||
|
if (err != nil){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(addressWithoutPrefixBytes) != 20){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we compute checksum and compare with input
|
||||||
|
|
||||||
|
var expectedAddressArray [42]byte
|
||||||
|
copy(expectedAddressArray[:2], "0x")
|
||||||
|
hex.Encode(expectedAddressArray[2:], addressWithoutPrefixBytes)
|
||||||
|
|
||||||
|
expectedAddressLowercase := string(expectedAddressArray[:])
|
||||||
|
|
||||||
|
expectedAddressString, _ := convertLowercaseEthereumAddressToChecksumAddress(expectedAddressLowercase)
|
||||||
|
|
||||||
|
if (input != expectedAddressString){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function performs the checksum
|
||||||
|
// An ethereum address checksum is performed by changing some characters to uppercase
|
||||||
|
// Input:
|
||||||
|
// -[]byte: A hex encoded ethereum address with all lowercase characters
|
||||||
|
// Output:
|
||||||
|
// -string: Input address with checksum performed
|
||||||
|
// -error
|
||||||
|
func convertLowercaseEthereumAddressToChecksumAddress(inputAddress string)(string, error){
|
||||||
|
|
||||||
|
inputAddressBytes := []byte(inputAddress)
|
||||||
|
|
||||||
|
if (len(inputAddressBytes) != 42){
|
||||||
|
return "", errors.New("convertLowercaseEthereumAddressToChecksumAddress called with invalid address.")
|
||||||
|
}
|
||||||
|
|
||||||
|
sha := sha3.NewLegacyKeccak256()
|
||||||
|
sha.Write(inputAddressBytes[2:])
|
||||||
|
hash := sha.Sum(nil)
|
||||||
|
|
||||||
|
for i := 2; i < len(inputAddressBytes); i++ {
|
||||||
|
|
||||||
|
hashByte := hash[(i-2)/2]
|
||||||
|
if (i%2 == 0) {
|
||||||
|
hashByte = hashByte >> 4
|
||||||
|
} else {
|
||||||
|
hashByte &= 0xf
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputAddressBytes[i] > '9' && hashByte > 7) {
|
||||||
|
inputAddressBytes[i] -= 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addressString := string(inputAddressBytes)
|
||||||
|
|
||||||
|
return addressString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
package ethereumAddress_test
|
||||||
|
|
||||||
|
import "seekia/internal/cryptocurrency/ethereumAddress"
|
||||||
|
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
import "seekia/internal/identity"
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestEthereumAddressFunctions(t *testing.T) {
|
||||||
|
|
||||||
|
{
|
||||||
|
testIdentityHashString := "wgkxplfvju7yhpoadvxju4kmfdr"
|
||||||
|
|
||||||
|
testIdentityHash, _, err := identity.ReadIdentityHashString(testIdentityHashString)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to read testIdentityHashString: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
identityAddress, err := ethereumAddress.GetIdentityScoreEthereumAddressFromIdentityHash(testIdentityHash)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get ethereum identity score address: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identityAddress != "0x74cFc329DBEe18c791f15869019b9B3954Db1869"){
|
||||||
|
t.Fatalf("GetIdentityScoreEthereumAddressFromIdentityHash not producing expected identity address: " + identityAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
testPublicKey := "667ff88775bec9dbe163b1a8d3f60a69334fa594ce17c008249f59efb6607cff"
|
||||||
|
|
||||||
|
testPublicKeyBytes, err := encoding.DecodeHexStringToBytes(testPublicKey)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to decode testPublicKey: Not Hex: " + testPublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(testPublicKeyBytes) != 32){
|
||||||
|
t.Fatalf("testPublicKey is invalid: Invalid length.")
|
||||||
|
}
|
||||||
|
|
||||||
|
testPublicKeyArray := [32]byte(testPublicKeyBytes)
|
||||||
|
|
||||||
|
accountAddress, err := ethereumAddress.GetCreditAccountEthereumAddressFromAccountPublicKey(testPublicKeyArray)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get ethereum account address: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accountAddress != "0x37b477424E10BB84cef4D04C7F1E36AcB46b9efa"){
|
||||||
|
t.Fatalf("Ethereum credit account address does not match expected value: " + accountAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
testMemoHash := "db61e89bb23f5c9964393eaa0510491c5ea5ac14b26bcc33cdfed1b6a45900b0"
|
||||||
|
|
||||||
|
testMemoHashBytes, err := encoding.DecodeHexStringToBytes(testMemoHash)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to decode testPublicKey: Not Hex: " + testMemoHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(testMemoHashBytes) != 32){
|
||||||
|
t.Fatalf("testMemoHashBytes is invalid: Invalid length.")
|
||||||
|
}
|
||||||
|
|
||||||
|
testMemoHashArray := [32]byte(testMemoHashBytes)
|
||||||
|
|
||||||
|
memoAddress, err := ethereumAddress.GetMemoEthereumAddressFromMemoHash(testMemoHashArray)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get ethereum account address: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memoAddress != "0x666b01036557dF68866E7Ab852897A4D6499ea07"){
|
||||||
|
t.Fatalf("Ethereum memo address does not match expected value: " + memoAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i:=0; i < 1000; i++{
|
||||||
|
|
||||||
|
newAddress, err := ethereumAddress.GetNewRandomEthereumAddress()
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get random Ethereum address: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid := ethereumAddress.VerifyEthereumAddress(newAddress)
|
||||||
|
if (isValid == false){
|
||||||
|
t.Fatalf("Randomly generated Ethereum address is invalid.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
25
internal/cryptocurrency/ethereumNode/ethereumNode.go
Normal file
25
internal/cryptocurrency/ethereumNode/ethereumNode.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
// ethereumNode provides functions to access a local ethereum node
|
||||||
|
|
||||||
|
package ethereumNode
|
||||||
|
|
||||||
|
//TODO: Complete this package
|
||||||
|
// We should start by using go-ethereum as the node, and possibly add support for other node implementations later.
|
||||||
|
// We might need to use an indexer to access the information faster
|
||||||
|
// We need to be able to access all transactions to an address, when they were sent, and the amount in ETH of each transaction
|
||||||
|
// We should also include the fee paid so that users can get credit/score for the amount paid in fees
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Able to get data from node
|
||||||
|
// -map[int64]int64: Map of Deposit time -> Amount deposited (in gwei or wei?)
|
||||||
|
// -error
|
||||||
|
func GetAddressDeposits(address string)(bool, map[string]string, error){
|
||||||
|
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
72
internal/cryptography/blake3/blake3.go
Normal file
72
internal/cryptography/blake3/blake3.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
|
||||||
|
// blake3 provides functions to hash bytes with the blake3 hash function
|
||||||
|
|
||||||
|
package blake3
|
||||||
|
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
|
||||||
|
import zeeboBlake3 "github.com/zeebo/blake3"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
|
||||||
|
func GetBlake3HashAsBytes(hashLengthBytes int, data []byte)([]byte, error){
|
||||||
|
|
||||||
|
if (hashLengthBytes < 1 || hashLengthBytes > 64){
|
||||||
|
return nil, errors.New("GetBlake3HashAsBytes called with invalid blake3 hash length.")
|
||||||
|
}
|
||||||
|
if (len(data) == 0){
|
||||||
|
return nil, errors.New("GetBlake3HashAsBytes called with empty bytes.")
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher := zeeboBlake3.New()
|
||||||
|
hasher.Write(data)
|
||||||
|
|
||||||
|
digestObject := hasher.Digest()
|
||||||
|
|
||||||
|
output := make([]byte, hashLengthBytes)
|
||||||
|
|
||||||
|
_, err := digestObject.Read(output)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get32ByteBlake3Hash(data []byte)([32]byte, error){
|
||||||
|
|
||||||
|
if (len(data) == 0){
|
||||||
|
return [32]byte{}, errors.New("Get32ByteBlake3Hash called with empty bytes.")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := zeeboBlake3.Sum256(data)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBlake3HashAsHexString(hashLengthBytes int, data []byte)(string, error){
|
||||||
|
|
||||||
|
hashResult, err := GetBlake3HashAsBytes(hashLengthBytes, data)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
result := encoding.EncodeBytesToHexString(hashResult[:])
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBlake3HashAsBase32String(hashLengthBytes int, data []byte)(string, error){
|
||||||
|
|
||||||
|
remainder := hashLengthBytes % 5
|
||||||
|
if (remainder != 0){
|
||||||
|
return "", errors.New("GetBlake3HashAsBase32String called with invalid hash length.")
|
||||||
|
}
|
||||||
|
|
||||||
|
hashResult, err := GetBlake3HashAsBytes(hashLengthBytes, data)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
base32Result := encoding.EncodeBytesToBase32String(hashResult)
|
||||||
|
|
||||||
|
return base32Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
36
internal/cryptography/blake3/blake3_test.go
Normal file
36
internal/cryptography/blake3/blake3_test.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package blake3_test
|
||||||
|
|
||||||
|
import "seekia/internal/cryptography/blake3"
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestHashes(t *testing.T) {
|
||||||
|
|
||||||
|
testData := []byte("CureRacialLoneliness.BeautifyTheHumanSpecies.Seekia:BeRaceAware.")
|
||||||
|
|
||||||
|
expectedHashResult := "ea3c0d045257e361dd15b59c1934195990d9a03c634bc56373ce612d30d755cf5c6be2bfbeacbdf3086edcd224c01e2e3d40edcd4da4e536b6cc7d9e6296ca4e"
|
||||||
|
|
||||||
|
hashResult, err := blake3.GetBlake3HashAsHexString(64, testData)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to get 64 byte blake3 hash: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashResult != expectedHashResult){
|
||||||
|
t.Fatalf("Blake3 512 bits hash result is not expected: " + hashResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_, err = blake3.GetBlake3HashAsBase32String(32, testData)
|
||||||
|
if (err == nil) {
|
||||||
|
t.Fatalf("Failed to get correct length error on base32 hash.")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputHex, err := blake3.GetBlake3HashAsHexString(16, testData)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to get 16 byte blake3 hash: " + err.Error())
|
||||||
|
}
|
||||||
|
if (outputHex != "ea3c0d045257e361dd15b59c19341959"){
|
||||||
|
t.Fatalf("16 byte blake3 hash provides invalid output: " + outputHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
153
internal/cryptography/chaPolyShrink/chaPolyShrink.go
Normal file
153
internal/cryptography/chaPolyShrink/chaPolyShrink.go
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
|
||||||
|
|
||||||
|
// chaPolyShrink provides functions to encrypt or decrypt bytes using ChaPolyShrink
|
||||||
|
// ChaPolyShrink first compresses bytes using zlib, adds padding to obscure size of encrypted bytes, and ciphers data using chaPoly
|
||||||
|
|
||||||
|
package chaPolyShrink
|
||||||
|
|
||||||
|
import "encoding/binary"
|
||||||
|
import "golang.org/x/crypto/chacha20poly1305"
|
||||||
|
import "compress/zlib"
|
||||||
|
import "crypto/rand"
|
||||||
|
import "errors"
|
||||||
|
import "bytes"
|
||||||
|
import "io"
|
||||||
|
import "slices"
|
||||||
|
|
||||||
|
// A paddingSize is specified, which adds padding so that resulting encrypted result size is divisible by paddingSize
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -[]byte: ChaPoly Encrypted bytes
|
||||||
|
// -error
|
||||||
|
func EncryptChaPolyShrink(input []byte, key [32]byte, nonce [24]byte, compressContent bool, paddingSize int, includeAdditionalData bool, additionalData [32]byte)([]byte, error){
|
||||||
|
|
||||||
|
if (paddingSize > 10000000 || paddingSize < 0) {
|
||||||
|
return nil, errors.New("EncryptChaPolyShrink called with invalid paddingSize.")
|
||||||
|
}
|
||||||
|
|
||||||
|
getCompressionStrength := func()int{
|
||||||
|
if (compressContent == false){
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 9
|
||||||
|
}
|
||||||
|
|
||||||
|
compressionStrength := getCompressionStrength()
|
||||||
|
|
||||||
|
var compressedBuffer bytes.Buffer
|
||||||
|
|
||||||
|
compressorWriter, err := zlib.NewWriterLevel(&compressedBuffer, compressionStrength)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
compressorWriter.Write(input)
|
||||||
|
compressorWriter.Close()
|
||||||
|
|
||||||
|
compressedBytes := compressedBuffer.Bytes()
|
||||||
|
|
||||||
|
compressedBytesLength := len(compressedBytes)
|
||||||
|
|
||||||
|
getNeededPaddingLength := func()int{
|
||||||
|
|
||||||
|
if (paddingSize == 0){
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compressedBytesLength < paddingSize){
|
||||||
|
paddingLength := paddingSize - compressedBytesLength
|
||||||
|
return paddingLength
|
||||||
|
}
|
||||||
|
|
||||||
|
remainder := compressedBytesLength % paddingSize
|
||||||
|
|
||||||
|
if (remainder == 0){
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
paddingLengthInBytes := paddingSize - remainder
|
||||||
|
|
||||||
|
return paddingLengthInBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
neededPaddingLength := getNeededPaddingLength()
|
||||||
|
|
||||||
|
paddingLengthUint32 := uint32(neededPaddingLength)
|
||||||
|
|
||||||
|
paddingLengthHeader := make([]byte, 4)
|
||||||
|
|
||||||
|
binary.LittleEndian.PutUint32(paddingLengthHeader, paddingLengthUint32)
|
||||||
|
|
||||||
|
paddingBytes := make([]byte, neededPaddingLength)
|
||||||
|
_, err = rand.Read(paddingBytes[:])
|
||||||
|
if (err != nil){ return nil, err }
|
||||||
|
|
||||||
|
compressedBytesWithPaddingAndHeader := slices.Concat(paddingLengthHeader, paddingBytes, compressedBytes)
|
||||||
|
|
||||||
|
getAdditionalDataParameter := func()[]byte{
|
||||||
|
if (includeAdditionalData == false){
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := additionalData[:]
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
additionalDataParameter := getAdditionalDataParameter()
|
||||||
|
|
||||||
|
cipherObject, err := chacha20poly1305.NewX(key[:])
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
encryptedMessage := cipherObject.Seal(nil, nonce[:], compressedBytesWithPaddingAndHeader, additionalDataParameter)
|
||||||
|
|
||||||
|
return encryptedMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Able to decrypt
|
||||||
|
// -[]byte: Decrypted bytes
|
||||||
|
// -error (will return error if inputs are invalid)
|
||||||
|
func DecryptChaPolyShrink(inputBytes []byte, key [32]byte, nonce [24]byte, additionalDataExists bool, additionalData [32]byte)(bool, []byte, error){
|
||||||
|
|
||||||
|
getAdditionalDataParameter := func()[]byte{
|
||||||
|
if (additionalDataExists == false){
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := additionalData[:]
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
additionalDataParameter := getAdditionalDataParameter()
|
||||||
|
|
||||||
|
cipherObject, err := chacha20poly1305.NewX(key[:])
|
||||||
|
if (err != nil) { return false, nil, err }
|
||||||
|
|
||||||
|
decryptedBytes, err := cipherObject.Open(nil, nonce[:], inputBytes, additionalDataParameter)
|
||||||
|
if (err != nil) {
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
paddingHeader := decryptedBytes[:4]
|
||||||
|
|
||||||
|
paddingBytesUint32 := binary.LittleEndian.Uint32(paddingHeader[:])
|
||||||
|
|
||||||
|
endOfPaddingIndex := 4 + paddingBytesUint32
|
||||||
|
|
||||||
|
compressedBytes := decryptedBytes[endOfPaddingIndex:]
|
||||||
|
compressedBytesReader := bytes.NewReader(compressedBytes)
|
||||||
|
|
||||||
|
decompressedReader, err := zlib.NewReader(compressedBytesReader)
|
||||||
|
if (err != nil) {
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
decompressedBytes, err := io.ReadAll(decompressedReader)
|
||||||
|
if (err != nil) {
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, decompressedBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
137
internal/cryptography/chaPolyShrink/chaPolyShrink_test.go
Normal file
137
internal/cryptography/chaPolyShrink/chaPolyShrink_test.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
package chaPolyShrink_test
|
||||||
|
|
||||||
|
import "seekia/internal/cryptography/chaPolyShrink"
|
||||||
|
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestChaPolyShrink(t *testing.T) {
|
||||||
|
|
||||||
|
stringToEncrypt := "Seekia"
|
||||||
|
|
||||||
|
for i := 0; i < 1000; i++{
|
||||||
|
|
||||||
|
stringToEncrypt += "BeRaceAware"
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesToEncrypt := []byte(stringToEncrypt)
|
||||||
|
|
||||||
|
chaPolyKey, err := helpers.GetNewRandom32ByteArray()
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get new random 32 byte array: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
chaPolyNonce, err := helpers.GetNewRandom24ByteArray()
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get new random 24 byte array: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
encryptedBytes, err := chaPolyShrink.EncryptChaPolyShrink(bytesToEncrypt, chaPolyKey, chaPolyNonce, true, 100, false, [32]byte{})
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to encrypt chaPolyShrink: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ableToDecrypt, decryptedBytes, err := chaPolyShrink.DecryptChaPolyShrink(encryptedBytes, chaPolyKey, chaPolyNonce, false, [32]byte{})
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to decrypt chaPolyShrink: " + err.Error())
|
||||||
|
}
|
||||||
|
if (ableToDecrypt == false) {
|
||||||
|
t.Fatalf("Failed to decrypt chaPolyShrink.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringToEncrypt != string(decryptedBytes)){
|
||||||
|
t.Fatalf("Decrypted chaPolyShrink string does not match.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Now we test with no padding
|
||||||
|
|
||||||
|
encryptedBytes, err := chaPolyShrink.EncryptChaPolyShrink(bytesToEncrypt, chaPolyKey, chaPolyNonce, true, 0, false, [32]byte{})
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to encrypt chaPolyShrink: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ableToDecrypt, decryptedBytes, err := chaPolyShrink.DecryptChaPolyShrink(encryptedBytes, chaPolyKey, chaPolyNonce, false, [32]byte{})
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to decrypt chaPolyShrink: " + err.Error())
|
||||||
|
}
|
||||||
|
if (ableToDecrypt == false) {
|
||||||
|
t.Fatalf("Failed to decrypt chaPolyShrink.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringToEncrypt != string(decryptedBytes)){
|
||||||
|
t.Fatalf("Decrypted chaPolyShrink string does not match.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// We test with no compression
|
||||||
|
|
||||||
|
encryptedBytes, err := chaPolyShrink.EncryptChaPolyShrink(bytesToEncrypt, chaPolyKey, chaPolyNonce, false, 10000, false, [32]byte{})
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to encrypt chaPolyShrink: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ableToDecrypt, decryptedBytes, err := chaPolyShrink.DecryptChaPolyShrink(encryptedBytes, chaPolyKey, chaPolyNonce, false, [32]byte{})
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to decrypt chaPolyShrink: " + err.Error())
|
||||||
|
}
|
||||||
|
if (ableToDecrypt == false) {
|
||||||
|
t.Fatalf("Failed to decrypt chaPolyShrink.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringToEncrypt != string(decryptedBytes)){
|
||||||
|
t.Fatalf("Decrypted chaPolyShrink string does not match.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// We test with additional data
|
||||||
|
|
||||||
|
additionalData, err := helpers.GetNewRandom32ByteArray()
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get new random 32 byte array: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedBytes, err := chaPolyShrink.EncryptChaPolyShrink(bytesToEncrypt, chaPolyKey, chaPolyNonce, false, 10000, true, additionalData)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to encrypt chaPolyShrink: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ableToDecrypt, decryptedBytes, err := chaPolyShrink.DecryptChaPolyShrink(encryptedBytes, chaPolyKey, chaPolyNonce, true, additionalData)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to decrypt chaPolyShrink: " + err.Error())
|
||||||
|
}
|
||||||
|
if (ableToDecrypt == false) {
|
||||||
|
t.Fatalf("Failed to decrypt chaPolyShrink.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringToEncrypt != string(decryptedBytes)){
|
||||||
|
t.Fatalf("Decrypted chaPolyShrink string does not match.")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// We test decryption with invalid additionalData
|
||||||
|
|
||||||
|
invalidAdditionalData, err := helpers.GetNewRandom32ByteArray()
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get new random 32 byte array: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ableToDecrypt, _, err = chaPolyShrink.DecryptChaPolyShrink(encryptedBytes, chaPolyKey, chaPolyNonce, true, invalidAdditionalData)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to decrypt chaPolyShrink: " + err.Error())
|
||||||
|
}
|
||||||
|
if (ableToDecrypt == true) {
|
||||||
|
t.Fatalf("Able to decrypt chaPolyShrink with invalidAdditionalData.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
87
internal/cryptography/edwardsKeys/edwardsKeys.go
Normal file
87
internal/cryptography/edwardsKeys/edwardsKeys.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
|
||||||
|
// edwardsKeys provides functions to generate ed25519 keys and verify ed25519 signatures
|
||||||
|
// Identity keys and credit account keys are edwardsKeys
|
||||||
|
|
||||||
|
package edwardsKeys
|
||||||
|
|
||||||
|
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
|
||||||
|
import goEd25519 "crypto/ed25519"
|
||||||
|
|
||||||
|
|
||||||
|
func VerifyPublicKeyHex(publicKeyHex string)bool{
|
||||||
|
|
||||||
|
publicKeyBytes, err := encoding.DecodeHexStringToBytes(publicKeyHex)
|
||||||
|
if (err != nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(publicKeyBytes) != 32) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function signs content with provided private key
|
||||||
|
func CreateSignature(privateKey [64]byte, contentHash [32]byte)[64]byte{
|
||||||
|
|
||||||
|
signature := goEd25519.Sign(privateKey[:], contentHash[:])
|
||||||
|
|
||||||
|
if (len(signature) != 64){
|
||||||
|
panic("goEd25519.Sign returning invalid signatureLength")
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureArray := [64]byte(signature)
|
||||||
|
|
||||||
|
return signatureArray
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Signature is valid
|
||||||
|
func VerifySignature(publicKey [32]byte, signature [64]byte, contentHash [32]byte)bool{
|
||||||
|
|
||||||
|
isValid := goEd25519.Verify(publicKey[:], contentHash[:], signature[:])
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNewRandomPublicAndPrivateEdwardsKeys()([32]byte, [64]byte, error){
|
||||||
|
|
||||||
|
publicKeyObject, privateKeyObject, err := goEd25519.GenerateKey(nil)
|
||||||
|
if (err != nil) { return [32]byte{}, [64]byte{}, err }
|
||||||
|
|
||||||
|
publicKeyArray := [32]byte(publicKeyObject)
|
||||||
|
privateKeyArray := [64]byte(privateKeyObject)
|
||||||
|
|
||||||
|
return publicKeyArray, privateKeyArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -[32]byte: Public key
|
||||||
|
// -[64]byte: Private key
|
||||||
|
// -error
|
||||||
|
func GetSeededEdwardsPublicAndPrivateKeys(seedBytes [32]byte)([32]byte, [64]byte){
|
||||||
|
|
||||||
|
privateKeyObject := goEd25519.NewKeyFromSeed(seedBytes[:])
|
||||||
|
|
||||||
|
goPublicKeyObject := privateKeyObject.Public()
|
||||||
|
|
||||||
|
publicKeyObject := goPublicKeyObject.(goEd25519.PublicKey)
|
||||||
|
|
||||||
|
if (len(publicKeyObject) != 32){
|
||||||
|
panic("publicKeyObject is not 32 bytes long.")
|
||||||
|
}
|
||||||
|
if (len(privateKeyObject) != 64){
|
||||||
|
panic("privateKeyObject is not 64 bytes long.")
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKeyArray := [32]byte(publicKeyObject)
|
||||||
|
privateKeyArray := [64]byte(privateKeyObject)
|
||||||
|
|
||||||
|
return publicKeyArray, privateKeyArray
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
92
internal/cryptography/edwardsKeys/edwardsKeys_test.go
Normal file
92
internal/cryptography/edwardsKeys/edwardsKeys_test.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package edwardsKeys_test
|
||||||
|
|
||||||
|
import "seekia/internal/cryptography/edwardsKeys"
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
|
||||||
|
import "crypto/rand"
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSignAndVerify(t *testing.T) {
|
||||||
|
|
||||||
|
testPublicKey, testPrivateKey, err := edwardsKeys.GetNewRandomPublicAndPrivateEdwardsKeys()
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to derive random Edwards identity keys: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashToSign [32]byte
|
||||||
|
_, err = rand.Read(hashToSign[:])
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("rand.Read failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
signature := edwardsKeys.CreateSignature(testPrivateKey, hashToSign)
|
||||||
|
|
||||||
|
isValid := edwardsKeys.VerifySignature(testPublicKey, signature, hashToSign)
|
||||||
|
if (isValid == false) {
|
||||||
|
t.Fatalf("Failed to verify edwards signature.")
|
||||||
|
}
|
||||||
|
|
||||||
|
var randomHash [32]byte
|
||||||
|
_, err = rand.Read(randomHash[:])
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("rand.Read failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid = edwardsKeys.VerifySignature(testPublicKey, signature, randomHash)
|
||||||
|
if (isValid == true) {
|
||||||
|
t.Fatalf("Failed to detect invalid signature.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeriveKeys(t *testing.T){
|
||||||
|
|
||||||
|
testSeed := "b69ca6c703030f7aa18dad0e327117920521e7925394d31ebd1a18a9933e6e74"
|
||||||
|
|
||||||
|
testSeedBytes, err := encoding.DecodeHexStringToBytes(testSeed)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to decode test seed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(testSeedBytes) != 32){
|
||||||
|
t.Fatalf("Invalid testSeedBytes: Not 32 bytes long.")
|
||||||
|
}
|
||||||
|
|
||||||
|
testSeedArray := [32]byte(testSeedBytes)
|
||||||
|
|
||||||
|
expectedIdentityPublicKeyHex := "95a89a662cbe202d1de7376c58dbaaddf89300334b4fba6dcf5732e82ca0da0c"
|
||||||
|
|
||||||
|
expectedIdentityPublicKeyBytes, err := encoding.DecodeHexStringToBytes(expectedIdentityPublicKeyHex)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to decode expectedIdentityPublicKeyHex: " + err.Error())
|
||||||
|
}
|
||||||
|
if (len(expectedIdentityPublicKeyBytes) != 32){
|
||||||
|
t.Fatalf("Invalid expectedIdentityPublicKeyBytes: Not 32 bytes long.")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedIdentityPublicKeyArray := [32]byte(expectedIdentityPublicKeyBytes)
|
||||||
|
|
||||||
|
expectedIdentityPrivateKeyHex := "b69ca6c703030f7aa18dad0e327117920521e7925394d31ebd1a18a9933e6e7495a89a662cbe202d1de7376c58dbaaddf89300334b4fba6dcf5732e82ca0da0c"
|
||||||
|
|
||||||
|
expectedIdentityPrivateKeyBytes, err := encoding.DecodeHexStringToBytes(expectedIdentityPrivateKeyHex)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to decode expectedIdentityPrivateKeyHex: " + err.Error())
|
||||||
|
}
|
||||||
|
if (len(expectedIdentityPrivateKeyBytes) != 64){
|
||||||
|
t.Fatalf("Invalid expectedIdentityPrivateKeyBytes: Not 32 bytes long.")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedIdentityPrivateKeyArray := [64]byte(expectedIdentityPrivateKeyBytes)
|
||||||
|
|
||||||
|
identityPublicKey, identityPrivateKey := edwardsKeys.GetSeededEdwardsPublicAndPrivateKeys(testSeedArray)
|
||||||
|
|
||||||
|
if (identityPublicKey != expectedIdentityPublicKeyArray){
|
||||||
|
t.Fatalf("Derived invalid identity public key.")
|
||||||
|
}
|
||||||
|
if (identityPrivateKey != expectedIdentityPrivateKeyArray){
|
||||||
|
t.Fatalf("Derived invalid identity private key.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
108
internal/cryptography/kyber/kyber.go
Normal file
108
internal/cryptography/kyber/kyber.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
|
||||||
|
// kyber provides functions to encrypt and decrypt data using Kyber
|
||||||
|
|
||||||
|
package kyber
|
||||||
|
|
||||||
|
// Public key is 1568 bytes long
|
||||||
|
// Private key is 1536 bytes long
|
||||||
|
// Encrypted key is 1568 bytes long
|
||||||
|
|
||||||
|
// When the keys need to be encoded as strings:
|
||||||
|
// Kyber Public keys are encoded Base64
|
||||||
|
// Kyber private keys are encoded Hex
|
||||||
|
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
|
||||||
|
import "crypto/rand"
|
||||||
|
|
||||||
|
import "github.com/cloudflare/circl/pke/kyber/kyber1024"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func VerifyKyberPublicKeyString(inputKey string)bool{
|
||||||
|
|
||||||
|
_, err := ReadKyberPublicKeyString(inputKey)
|
||||||
|
if (err != nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadKyberPublicKeyString(inputKey string)([1568]byte, error){
|
||||||
|
|
||||||
|
decodedBytes, err := encoding.DecodeBase64StringToBytes(inputKey)
|
||||||
|
if (err != nil) {
|
||||||
|
return [1568]byte{}, errors.New("ReadKyberPublicKeyString called with invalid key: " + inputKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(decodedBytes) != 1568){
|
||||||
|
return [1568]byte{}, errors.New("ReadKyberPublicKeyString called with invalid key: " + inputKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey := [1568]byte(decodedBytes)
|
||||||
|
|
||||||
|
return publicKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNewRandomPublicKyberKey()([1568]byte, error){
|
||||||
|
|
||||||
|
var kyberPublicKeyArray [1568]byte
|
||||||
|
|
||||||
|
_, err := rand.Read(kyberPublicKeyArray[:])
|
||||||
|
if (err != nil) { return [1568]byte{}, err }
|
||||||
|
|
||||||
|
return kyberPublicKeyArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNewRandomPublicPrivateKyberKeys()([1568]byte, [1536]byte, error){
|
||||||
|
|
||||||
|
publicKeyObject, privateKeyObject, err := kyber1024.GenerateKey(nil)
|
||||||
|
if (err != nil) { return [1568]byte{}, [1536]byte{}, err }
|
||||||
|
|
||||||
|
var publicKeyArray [1568]byte
|
||||||
|
publicKeyObject.Pack(publicKeyArray[:])
|
||||||
|
|
||||||
|
var privateKeyArray [1536]byte
|
||||||
|
privateKeyObject.Pack(privateKeyArray[:])
|
||||||
|
|
||||||
|
return publicKeyArray, privateKeyArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -[1568]byte: Kyber Encrypted key bytes
|
||||||
|
// -error
|
||||||
|
func EncryptKeyWithKyber(publicKyberKey [1568]byte, keyToEncrypt [32]byte)([1568]byte, error){
|
||||||
|
|
||||||
|
randomSeed := make([]byte, 32)
|
||||||
|
|
||||||
|
_, err := rand.Read(randomSeed[:])
|
||||||
|
if (err != nil) { return [1568]byte{}, err }
|
||||||
|
|
||||||
|
var publicKeyObject kyber1024.PublicKey
|
||||||
|
|
||||||
|
publicKeyObject.Unpack(publicKyberKey[:])
|
||||||
|
|
||||||
|
var encryptedResult [1568]byte
|
||||||
|
|
||||||
|
publicKeyObject.EncryptTo(encryptedResult[:], keyToEncrypt[:], randomSeed)
|
||||||
|
|
||||||
|
return encryptedResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -[32]byte: Decrypted key
|
||||||
|
// -error: Will return err if inputs are invalid.
|
||||||
|
func DecryptKyberEncryptedKey(inputEncryptedKey [1568]byte, privateKey [1536]byte)([32]byte, error){
|
||||||
|
|
||||||
|
var privateKeyObject kyber1024.PrivateKey
|
||||||
|
privateKeyObject.Unpack(privateKey[:])
|
||||||
|
|
||||||
|
var decryptedKey [32]byte
|
||||||
|
|
||||||
|
privateKeyObject.DecryptTo(decryptedKey[:], inputEncryptedKey[:])
|
||||||
|
|
||||||
|
return decryptedKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
37
internal/cryptography/kyber/kyber_test.go
Normal file
37
internal/cryptography/kyber/kyber_test.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package kyber_test
|
||||||
|
|
||||||
|
import "seekia/internal/cryptography/kyber"
|
||||||
|
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestEncryptDecryptKyber(t *testing.T) {
|
||||||
|
|
||||||
|
testPublicKey, testPrivateKey, err := kyber.GetNewRandomPublicPrivateKyberKeys()
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to derive Kyber keys: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
keyToEncrypt, err := helpers.GetNewRandom32ByteArray()
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get new random 32 byte array: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedBytes, err := kyber.EncryptKeyWithKyber(testPublicKey, keyToEncrypt)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to encrypt Kyber: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedKey, err := kyber.DecryptKyberEncryptedKey(encryptedBytes, testPrivateKey)
|
||||||
|
if (err != nil) {
|
||||||
|
t.Fatalf("Failed to decrypt Kyber: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decryptedKey != keyToEncrypt){
|
||||||
|
t.Fatalf("Kyber decrypted key does not match.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
100
internal/cryptography/nacl/nacl.go
Normal file
100
internal/cryptography/nacl/nacl.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
|
||||||
|
// nacl provides functions to encrypt and decrypt data using Nacl
|
||||||
|
|
||||||
|
package nacl
|
||||||
|
|
||||||
|
// Public/Private keys are 32 bytes long
|
||||||
|
// Encrypted key is 80 bytes long
|
||||||
|
|
||||||
|
// When the keys need to be encoded as strings:
|
||||||
|
// Nacl public keys are encoded Base64
|
||||||
|
// Nacl private keys are encoded hex
|
||||||
|
|
||||||
|
import "seekia/internal/encoding"
|
||||||
|
|
||||||
|
import "golang.org/x/crypto/nacl/box"
|
||||||
|
|
||||||
|
import "crypto/rand"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func VerifyNaclPublicKeyString(inputKey string)bool{
|
||||||
|
|
||||||
|
_, err := ReadNaclPublicKeyString(inputKey)
|
||||||
|
if (err != nil){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadNaclPublicKeyString(inputKey string)([32]byte, error){
|
||||||
|
|
||||||
|
decodedBytes, err := encoding.DecodeBase64StringToBytes(inputKey)
|
||||||
|
if (err != nil) {
|
||||||
|
return [32]byte{}, errors.New("ReadNaclPublicKeyString called with invalid key: " + inputKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(decodedBytes) != 32){
|
||||||
|
return [32]byte{}, errors.New("ReadNaclPublicKeyString called with invalid key: " + inputKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey := [32]byte(decodedBytes)
|
||||||
|
|
||||||
|
return publicKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNewRandomPublicNaclKey()([32]byte, error){
|
||||||
|
|
||||||
|
var naclPublicKeyArray [32]byte
|
||||||
|
|
||||||
|
_, err := rand.Read(naclPublicKeyArray[:])
|
||||||
|
if (err != nil) { return [32]byte{}, err }
|
||||||
|
|
||||||
|
return naclPublicKeyArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNewRandomPublicPrivateNaclKeys()([32]byte, [32]byte, error){
|
||||||
|
|
||||||
|
publicKeyArray, privateKeyArray, err := box.GenerateKey(rand.Reader)
|
||||||
|
if (err != nil) { return [32]byte{}, [32]byte{}, err }
|
||||||
|
|
||||||
|
return *publicKeyArray, *privateKeyArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -[80]byte: Nacl Encrypted Key
|
||||||
|
// -error
|
||||||
|
func EncryptKeyWithNacl(recipientPublicKey [32]byte, keyToEncrypt [32]byte)([80]byte, error){
|
||||||
|
|
||||||
|
encryptedKeyBytes, err := box.SealAnonymous(nil, keyToEncrypt[:], &recipientPublicKey, nil)
|
||||||
|
if (err != nil) { return [80]byte{}, err }
|
||||||
|
|
||||||
|
if (len(encryptedKeyBytes) != 80){
|
||||||
|
return [80]byte{}, errors.New("box.SealAnonymous returning invalid length encryptedKey.")
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedKeyArray := [80]byte(encryptedKeyBytes)
|
||||||
|
|
||||||
|
return encryptedKeyArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Able to decrypt
|
||||||
|
// -[32]byte: Decrypted key
|
||||||
|
// -error
|
||||||
|
func DecryptNaclEncryptedKey(encryptedKey [80]byte, recipientPublicKey [32]byte, recipientPrivateKey [32]byte)(bool, [32]byte, error) {
|
||||||
|
|
||||||
|
decryptedKey, success := box.OpenAnonymous(nil, encryptedKey[:], &recipientPublicKey, &recipientPrivateKey)
|
||||||
|
if (success == false) {
|
||||||
|
return false, [32]byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(decryptedKey) != 32){
|
||||||
|
return false, [32]byte{}, errors.New("Invalid Nacl encrypted key length after decrypt.")
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedKeyArray := [32]byte(decryptedKey)
|
||||||
|
|
||||||
|
return true, decryptedKeyArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
40
internal/cryptography/nacl/nacl_test.go
Normal file
40
internal/cryptography/nacl/nacl_test.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package nacl_test
|
||||||
|
|
||||||
|
import "seekia/internal/cryptography/nacl"
|
||||||
|
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestEncryptDecryptNacl(t *testing.T) {
|
||||||
|
|
||||||
|
testPublicKey, testPrivateKey, err := nacl.GetNewRandomPublicPrivateNaclKeys()
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to derive Nacl keys: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
keyToEncrypt, err := helpers.GetNewRandom32ByteArray()
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to get new random 32 byte array: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedBytes, err := nacl.EncryptKeyWithNacl(testPublicKey, keyToEncrypt)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to encrypt Nacl: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ableToDecrypt, decryptedKey, err := nacl.DecryptNaclEncryptedKey(encryptedBytes, testPublicKey, testPrivateKey)
|
||||||
|
if (err != nil){
|
||||||
|
t.Fatalf("Failed to decrypt Nacl: " + err.Error())
|
||||||
|
}
|
||||||
|
if (ableToDecrypt == false){
|
||||||
|
t.Fatalf("Failed to decrypt Nacl.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyToEncrypt != decryptedKey){
|
||||||
|
t.Fatalf("Nacl decrypted key does not match.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
49
internal/databaseJobs/databaseJobs.go
Normal file
49
internal/databaseJobs/databaseJobs.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
|
||||||
|
// databaseJobs provides functions to perform database jobs
|
||||||
|
// These are jobs that must be performed in the background
|
||||||
|
// An example is pruning the database of content we no longer need to store
|
||||||
|
// These functions are called by backgroundJobs
|
||||||
|
|
||||||
|
package databaseJobs
|
||||||
|
|
||||||
|
//TODO: Build this package
|
||||||
|
// We need to add more jobs:
|
||||||
|
// -Update inbox messages lists
|
||||||
|
// -Update reviews/reports lists
|
||||||
|
// -Delete all of a Mate user's profiles if their newest profile does not fulfill our desires/criteria (and they are not an outlier/in our host range/in our moderation range)
|
||||||
|
// -This is needed, because we will not attempt to retrieve the viewable statuses of profiles whose author's newest profile does not fulfill our desires/criteria
|
||||||
|
// -Without this job, we will continue to show a user Mate profiles as matches for users whose newest profiles do not fulfill our desires,
|
||||||
|
// because those newer profiles cannot be shown to the user due to not having their viewable status downloaded
|
||||||
|
// -The decision to use our desires/criteria depends on if we are in desires pruning mode or not.
|
||||||
|
|
||||||
|
func UpdateDatabaseIdentityProfilesLists(identityType string)error{
|
||||||
|
|
||||||
|
//TODO: Will update the identityProfileHashes lists within badgerDatabase
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func PruneProfileMetadata(profileType string)error{
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// Delete all metadata for...
|
||||||
|
// -Profiles whose author's identity is expired
|
||||||
|
// -profiles with no reviews that are not downloaded
|
||||||
|
|
||||||
|
// If one identity has more than X profile metadatas which cannot be deleted due to above rules
|
||||||
|
// Delete the profiles with the least reviews first? deal with canonical profiles as well
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func PruneMessageMetadata()error{
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// Delete all metadata for...
|
||||||
|
// -Messages that are expired
|
||||||
|
// -Messages with neither reviews nor reports that are not downloaded
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
1119
internal/desires/mateDesires/mateDesires.go
Normal file
1119
internal/desires/mateDesires/mateDesires.go
Normal file
File diff suppressed because it is too large
Load diff
331
internal/desires/myDesireStatistics/myDesireStatistics.go
Normal file
331
internal/desires/myDesireStatistics/myDesireStatistics.go
Normal file
|
@ -0,0 +1,331 @@
|
||||||
|
|
||||||
|
// myDesireStatistics provides functions to calculate a user's mate desire statistics
|
||||||
|
// These are statistics about a user's mate desires.
|
||||||
|
// For each desire, we calculate:
|
||||||
|
// -The number/percentage of all downloaded profiles who pass the desire
|
||||||
|
// -The number/percentage of a user's matches who pass the desire (excluding the desire)
|
||||||
|
|
||||||
|
package myDesireStatistics
|
||||||
|
|
||||||
|
//TODO: Add desire statistics about how many users fulfill each desire
|
||||||
|
// This would enable users to view fulfillment statistics without requiring them to enable FilterAll and RequireResponse for each desire
|
||||||
|
|
||||||
|
import "seekia/internal/appMemory"
|
||||||
|
import "seekia/internal/badgerDatabase"
|
||||||
|
import "seekia/internal/desires/myMateDesires"
|
||||||
|
import "seekia/internal/helpers"
|
||||||
|
import "seekia/internal/myBlockedUsers"
|
||||||
|
import "seekia/internal/myIdentity"
|
||||||
|
import "seekia/internal/profiles/viewableProfiles"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -int64: Number of downloaded mate identities with a viewable profile
|
||||||
|
// -int64: Number of mate identities whose viewable profile passes desire
|
||||||
|
// -float64: Percentage of all mate identities whose viewable profile passes desire
|
||||||
|
// -int64: Number of matches (Excluding input desire)
|
||||||
|
// -int64: Number of matches
|
||||||
|
// -float64: Percentage of all matches (excluding input desire) that pass input desire
|
||||||
|
// -error
|
||||||
|
func GetMyDesireStatistics(desireName string, networkType byte)(int64, int64, float64, int64, int64, float64, error){
|
||||||
|
|
||||||
|
isValid := helpers.VerifyNetworkType(networkType)
|
||||||
|
if (isValid == false){
|
||||||
|
networkTypeString := helpers.ConvertByteToString(networkType)
|
||||||
|
return 0, 0, 0, 0, 0, 0, errors.New("GetMyDesireStatistics called with invalid networkType: " + networkTypeString)
|
||||||
|
}
|
||||||
|
|
||||||
|
myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Mate")
|
||||||
|
if (err != nil) { return 0, 0, 0, 0, 0, 0, err }
|
||||||
|
|
||||||
|
mateIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Mate")
|
||||||
|
if (err != nil) { return 0, 0, 0, 0, 0, 0, err }
|
||||||
|
|
||||||
|
numberOfMateIdentities := int64(0)
|
||||||
|
numberOfMateIdentitiesWhoPassDesire := int64(0)
|
||||||
|
numberOfMatches := int64(0)
|
||||||
|
numberOfMatchesExcludingDesire := int64(0)
|
||||||
|
|
||||||
|
for _, peerIdentityHash := range mateIdentityHashesList{
|
||||||
|
|
||||||
|
if (myIdentityExists == true && peerIdentityHash == myIdentityHash){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
userIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(peerIdentityHash)
|
||||||
|
if (err != nil) { return 0, 0, 0, 0, 0, 0, err }
|
||||||
|
if (userIsBlocked == true){
|
||||||
|
// We don't include blocked users in our statistics calculation
|
||||||
|
// This is because "Blocked" is not a desire, so it won't be shown on the Desire Statistics page
|
||||||
|
// We show the user how many users they have blocked on the Match Statistics page
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
profileExists, _, getAnyProfileAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(peerIdentityHash, networkType, true, false, true)
|
||||||
|
if (err != nil) { return 0, 0, 0, 0, 0, 0, err }
|
||||||
|
if (profileExists == false) {
|
||||||
|
// Profile must have been deleted, or user's profile is not viewable
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, _, _, err := getAnyProfileAttributeFunction("Disabled")
|
||||||
|
if (err != nil) { return 0, 0, 0, 0, 0, 0, err }
|
||||||
|
if (exists == true){
|
||||||
|
// Profile is disabled
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfMateIdentities += 1
|
||||||
|
|
||||||
|
userPassesMyDesire, err := myMateDesires.CheckIfMateProfilePassesMyDesire(desireName, getAnyProfileAttributeFunction)
|
||||||
|
if (err != nil) { return 0, 0, 0, 0, 0, 0, err }
|
||||||
|
if (userPassesMyDesire == true){
|
||||||
|
numberOfMateIdentitiesWhoPassDesire += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
userIsAMatchExcludingDesire, err := myMateDesires.CheckIfMateProfilePassesAllMyDesires(true, desireName, getAnyProfileAttributeFunction)
|
||||||
|
if (err != nil) { return 0, 0, 0, 0, 0, 0, err }
|
||||||
|
if (userIsAMatchExcludingDesire == false){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
numberOfMatchesExcludingDesire += 1
|
||||||
|
|
||||||
|
if (userPassesMyDesire == true){
|
||||||
|
numberOfMatches += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPercentageOfMateIdentitiesWhoPassDesire := func()float64{
|
||||||
|
if (numberOfMateIdentities == 0){
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
percentageOfMateIdentitiesWhoPassDesire := 100 * (float64(numberOfMateIdentitiesWhoPassDesire)/float64(numberOfMateIdentities))
|
||||||
|
|
||||||
|
return percentageOfMateIdentitiesWhoPassDesire
|
||||||
|
}
|
||||||
|
|
||||||
|
percentageOfMateIdentitiesWhoPassDesire := getPercentageOfMateIdentitiesWhoPassDesire()
|
||||||
|
|
||||||
|
getPercentageOfMatchesWhoPassDesire := func()float64{
|
||||||
|
if (numberOfMatchesExcludingDesire == 0){
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
percentageOfMatchesWhoPassDesire := 100 * (float64(numberOfMatches)/float64(numberOfMatchesExcludingDesire))
|
||||||
|
|
||||||
|
return percentageOfMatchesWhoPassDesire
|
||||||
|
}
|
||||||
|
|
||||||
|
percentageOfMatchesWhoPassDesire := getPercentageOfMatchesWhoPassDesire()
|
||||||
|
|
||||||
|
return numberOfMateIdentities, numberOfMateIdentitiesWhoPassDesire, percentageOfMateIdentitiesWhoPassDesire, numberOfMatchesExcludingDesire, numberOfMatches, percentageOfMatchesWhoPassDesire, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type DesireStatisticsItem struct{
|
||||||
|
|
||||||
|
// Example: "Age"
|
||||||
|
DesireName string
|
||||||
|
|
||||||
|
// This is the number of mate identities who pass the desire
|
||||||
|
// Their newest viewable profile is used to check if they pass the desire
|
||||||
|
NumberOfUsersWhoPassDesire int64
|
||||||
|
|
||||||
|
// This is the percentage of mate identities who pass the desire
|
||||||
|
// Blocked and disabled users are excluded from the denominator.
|
||||||
|
PercentageOfUsersWhoPassDesire float64
|
||||||
|
|
||||||
|
// This is the number of matches the user would have if this desire was excluded
|
||||||
|
NumberOfDesireExcludedMatches int64
|
||||||
|
|
||||||
|
// This is the percentage of desire-excluded matches who pass this desire
|
||||||
|
PercentageOfDesireExcludedMatchesWhoPassDesire float64
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -int64: Number of mate identities
|
||||||
|
// -int64: Number of matches
|
||||||
|
// -[]DesireStatisticsItem
|
||||||
|
// -error
|
||||||
|
func GetAllMyDesireStatistics(progressIdentifier string, networkType byte)(int64, int64, []DesireStatisticsItem, error){
|
||||||
|
|
||||||
|
isValid := helpers.VerifyNetworkType(networkType)
|
||||||
|
if (isValid == false){
|
||||||
|
networkTypeString := helpers.ConvertByteToString(networkType)
|
||||||
|
return 0, 0, nil, errors.New("GetAllMyDesireStatistics called with invalid networkType: " + networkTypeString)
|
||||||
|
}
|
||||||
|
|
||||||
|
myIdentityExists, myIdentityHash, err := myIdentity.GetMyIdentityHash("Mate")
|
||||||
|
if (err != nil) { return 0, 0, nil, err }
|
||||||
|
|
||||||
|
mateIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes("Mate")
|
||||||
|
if (err != nil) { return 0, 0, nil, err }
|
||||||
|
|
||||||
|
numberOfPeerIdentitiesString := helpers.ConvertIntToString(len(mateIdentityHashesList))
|
||||||
|
|
||||||
|
numberOfMateIdentities := int64(0)
|
||||||
|
numberOfMatches := int64(0)
|
||||||
|
|
||||||
|
//Map Structure: Desire name -> Number of mate identities who pass desire
|
||||||
|
numberOfMateIdentitiesWhoPassDesireMap := make(map[string]int64)
|
||||||
|
|
||||||
|
//Map Structure: Desire name -> Number of matches the user would have excluding the desire
|
||||||
|
numberOfDesireExcludedMatchesMap := make(map[string]int64)
|
||||||
|
|
||||||
|
for index, peerIdentityHash := range mateIdentityHashesList{
|
||||||
|
|
||||||
|
progressIndex := helpers.ConvertIntToString(index + 1)
|
||||||
|
newProgressString := "Calculated " + progressIndex + "/" + numberOfPeerIdentitiesString + " Users"
|
||||||
|
appMemory.SetMemoryEntry(progressIdentifier, newProgressString)
|
||||||
|
|
||||||
|
if (myIdentityExists == true && peerIdentityHash == myIdentityHash){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
userIsBlocked, _, _, _, err := myBlockedUsers.CheckIfUserIsBlocked(peerIdentityHash)
|
||||||
|
if (err != nil) { return 0, 0, nil, err }
|
||||||
|
if (userIsBlocked == true){
|
||||||
|
// We don't include blocked users in our statistics calculation
|
||||||
|
// This is because "Blocked" is not a desire, so it won't be shown on the Desire Statistics page
|
||||||
|
// We show the user how many users they have blocked on the Match Statistics page
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
profileExists, _, getAnyProfileAttributeFunction, err := viewableProfiles.GetRetrieveAnyNewestViewableUserProfileAttributeFunction(peerIdentityHash, networkType, true, false, true)
|
||||||
|
if (err != nil) { return 0, 0, nil, err }
|
||||||
|
if (profileExists == false) {
|
||||||
|
// Profile must have been deleted, or user's profile is not viewable
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, _, _, err := getAnyProfileAttributeFunction("Disabled")
|
||||||
|
if (err != nil) { return 0, 0, nil, err }
|
||||||
|
if (exists == true){
|
||||||
|
// Profile is disabled
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfMateIdentities += 1
|
||||||
|
|
||||||
|
userPassesMyDesiresMap, err := myMateDesires.GetMateProfilePassesAllMyDesiresMap(getAnyProfileAttributeFunction)
|
||||||
|
if (err != nil) { return 0, 0, nil, err }
|
||||||
|
|
||||||
|
getUserIsAMatchBool := func()bool{
|
||||||
|
|
||||||
|
for _, userPassesDesire := range userPassesMyDesiresMap{
|
||||||
|
if (userPassesDesire == false){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
userIsAMatch := getUserIsAMatchBool()
|
||||||
|
if (userIsAMatch == true){
|
||||||
|
numberOfMatches += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for desireName, userPassesDesire := range userPassesMyDesiresMap{
|
||||||
|
|
||||||
|
if (userPassesDesire == true){
|
||||||
|
|
||||||
|
numberOfMateIdentitiesWhoPassDesireMap[desireName] += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we find out if user would be a match excluding the current desire
|
||||||
|
|
||||||
|
getUserIsAMatchExcludingCurrentDesireBool := func()bool{
|
||||||
|
|
||||||
|
if (userIsAMatch == true){
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for desireNameInner, userPassesDesireInner := range userPassesMyDesiresMap{
|
||||||
|
|
||||||
|
if (desireNameInner == desireName){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (userPassesDesireInner == false){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
userIsAMatchExcludingCurrentDesire := getUserIsAMatchExcludingCurrentDesireBool()
|
||||||
|
if (userIsAMatchExcludingCurrentDesire == true){
|
||||||
|
numberOfDesireExcludedMatchesMap[desireName] += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allMyDesiresList := myMateDesires.GetAllMyDesiresList(false)
|
||||||
|
|
||||||
|
allMyDesireStatisticsItemsList := make([]DesireStatisticsItem, 0, len(allMyDesiresList))
|
||||||
|
|
||||||
|
for _, desireName := range allMyDesiresList{
|
||||||
|
|
||||||
|
getNumberOfMateIdentitiesWhoPassDesire := func()int64{
|
||||||
|
|
||||||
|
numberOfMateIdentitiesWhoPassDesire, exists := numberOfMateIdentitiesWhoPassDesireMap[desireName]
|
||||||
|
if (exists == false) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return numberOfMateIdentitiesWhoPassDesire
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfMateIdentitiesWhoPassDesire := getNumberOfMateIdentitiesWhoPassDesire()
|
||||||
|
|
||||||
|
getNumberOfDesireExcludedMatches := func()int64{
|
||||||
|
|
||||||
|
numberOfDesireExcludedMatches, exists := numberOfDesireExcludedMatchesMap[desireName]
|
||||||
|
if (exists == false) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return numberOfDesireExcludedMatches
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfDesireExcludedMatches := getNumberOfDesireExcludedMatches()
|
||||||
|
|
||||||
|
getPercentageOfMateIdentitiesWhoPassDesire := func()float64{
|
||||||
|
if (numberOfMateIdentities == 0){
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
percentageOfMateIdentitiesWhoPassDesire := 100 * (float64(numberOfMateIdentitiesWhoPassDesire)/float64(numberOfMateIdentities))
|
||||||
|
|
||||||
|
return percentageOfMateIdentitiesWhoPassDesire
|
||||||
|
}
|
||||||
|
|
||||||
|
percentageOfMateIdentitiesWhoPassDesire := getPercentageOfMateIdentitiesWhoPassDesire()
|
||||||
|
|
||||||
|
getPercentageOfDesireExcludedMatchesWhoPassDesire := func()float64{
|
||||||
|
if (numberOfDesireExcludedMatches == 0){
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
percentageOfDesireExcludedMatchesWhoPassDesire := 100 * (float64(numberOfMatches)/float64(numberOfDesireExcludedMatches))
|
||||||
|
|
||||||
|
return percentageOfDesireExcludedMatchesWhoPassDesire
|
||||||
|
}
|
||||||
|
|
||||||
|
percentageOfDesireExcludedMatchesWhoPassDesire := getPercentageOfDesireExcludedMatchesWhoPassDesire()
|
||||||
|
|
||||||
|
newDesireStatisticsItem := DesireStatisticsItem{
|
||||||
|
DesireName: desireName,
|
||||||
|
NumberOfUsersWhoPassDesire: numberOfMateIdentitiesWhoPassDesire,
|
||||||
|
PercentageOfUsersWhoPassDesire: percentageOfMateIdentitiesWhoPassDesire,
|
||||||
|
NumberOfDesireExcludedMatches: numberOfDesireExcludedMatches,
|
||||||
|
PercentageOfDesireExcludedMatchesWhoPassDesire: percentageOfDesireExcludedMatchesWhoPassDesire,
|
||||||
|
}
|
||||||
|
|
||||||
|
allMyDesireStatisticsItemsList = append(allMyDesireStatisticsItemsList, newDesireStatisticsItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return numberOfMateIdentities, numberOfMatches, allMyDesireStatisticsItemsList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
81
internal/desires/myLocalDesires/myLocalDesires.go
Normal file
81
internal/desires/myLocalDesires/myLocalDesires.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
|
||||||
|
// myLocalDesires provides functions to manage a user's mate desires
|
||||||
|
|
||||||
|
package myLocalDesires
|
||||||
|
|
||||||
|
import "seekia/internal/myDatastores/myMap"
|
||||||
|
import "seekia/internal/mySettings"
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var myDesiresMapDatastore *myMap.MyMap
|
||||||
|
|
||||||
|
// This function must be called whenever we sign in to an app user
|
||||||
|
func InitializeMyDesiresDatastore()error{
|
||||||
|
|
||||||
|
newMyDesiresMapDatastore, err := myMap.CreateNewMap("MyDesires")
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
myDesiresMapDatastore = newMyDesiresMapDatastore
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Outputs:
|
||||||
|
// -bool: Desire exists
|
||||||
|
// -string: Desire data
|
||||||
|
// -error
|
||||||
|
func GetDesire(desireName string)(bool, string, error){
|
||||||
|
|
||||||
|
if (desireName == ""){
|
||||||
|
return false, "", errors.New("GetDesire called with empty desireName.")
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, desireValue, err := myDesiresMapDatastore.GetMapEntry(desireName)
|
||||||
|
if (err != nil) { return false, "", err }
|
||||||
|
if (exists == false){
|
||||||
|
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, desireValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func SetDesire(desireName string, content string)error{
|
||||||
|
|
||||||
|
if (desireName == ""){
|
||||||
|
return errors.New("SetDesire called with empty desireName.")
|
||||||
|
}
|
||||||
|
if (content == ""){
|
||||||
|
return errors.New("SetDesire called with empty content.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := myDesiresMapDatastore.SetMapEntry(desireName, content)
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
// Matches must be regenerated
|
||||||
|
err = mySettings.SetSetting("MatchesGeneratedStatus", "No")
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteDesire(desireName string)error{
|
||||||
|
|
||||||
|
if (desireName == ""){
|
||||||
|
return errors.New("DeleteDesire called with empty desireName.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := myDesiresMapDatastore.DeleteMapEntry(desireName)
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
// Matches must be regenerated
|
||||||
|
err = mySettings.SetSetting("MatchesGeneratedStatus", "No")
|
||||||
|
if (err != nil) { return err }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
216
internal/desires/myMateDesires/myMateDesires.go
Normal file
216
internal/desires/myMateDesires/myMateDesires.go
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
|
||||||
|
|
||||||
|
// myMateDesires provides functions to check if a peer passes a user's mate desires
|
||||||
|
|
||||||
|
package myMateDesires
|
||||||
|
|
||||||
|
// There is a difference between "Passing" and "Fulfilling"
|
||||||
|
// All users will pass desires for which a user has not enabled FilterAll
|
||||||
|
// A user Fulfills a desire if the user has responded and their attribute values fulfill our desires
|
||||||
|
// Checking if a profile fulfills a desire is only needed when calculating match scores
|
||||||
|
|
||||||
|
import "seekia/internal/desires/myLocalDesires"
|
||||||
|
import "seekia/internal/desires/mateDesires"
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
|
||||||
|
// This is aa list of all desire names we check when testing if a user passes our desires
|
||||||
|
// It does not include desires only used in criterias
|
||||||
|
var allMyDesiresList []string
|
||||||
|
|
||||||
|
// We use this function to initialize the allMyDesiresList
|
||||||
|
func init(){
|
||||||
|
|
||||||
|
allDesiresList := mateDesires.GetAllDesiresList(false)
|
||||||
|
|
||||||
|
allMyDesiresList = make([]string, 0)
|
||||||
|
|
||||||
|
for _, desireName := range allDesiresList{
|
||||||
|
|
||||||
|
if (desireName == "23andMe_AncestryComposition_Restrictive"){
|
||||||
|
// This desire exists along with the non-restrictive version
|
||||||
|
// We will only check either desire once, depending on if restrictive mode is enabled
|
||||||
|
// Thus, we don't want to check it twice
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
desireIsCriteriaOnly := mateDesires.CheckIfDesireIsCriteriaOnly(desireName)
|
||||||
|
if (desireIsCriteriaOnly == true){
|
||||||
|
// These are desires that are only used for criteria, when querying hosts
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
allMyDesiresList = append(allMyDesiresList, desireName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllMyDesiresList(copyList bool)[]string{
|
||||||
|
|
||||||
|
if (copyList == false){
|
||||||
|
// List only needs to be copied if we are sorting and/or editing its elements afterwards
|
||||||
|
return allMyDesiresList
|
||||||
|
}
|
||||||
|
|
||||||
|
listCopy := slices.Clone(allMyDesiresList)
|
||||||
|
|
||||||
|
return listCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckIfMateProfilePassesAllMyDesires(excludeADesire bool, desireToExclude string, getAnyProfileAttributeFunction func(string)(bool, int, string, error))(bool, error){
|
||||||
|
|
||||||
|
for _, desireName := range allMyDesiresList{
|
||||||
|
|
||||||
|
if (excludeADesire == true && desireName == desireToExclude){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
userPassesDesire, err := CheckIfMateProfilePassesMyDesire(desireName, getAnyProfileAttributeFunction)
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (userPassesDesire == false){
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// This function returns a map describing the desires the user passes
|
||||||
|
//Outputs:
|
||||||
|
// -map[string]bool: Desire Name -> Profile passes desire bool
|
||||||
|
// -error
|
||||||
|
func GetMateProfilePassesAllMyDesiresMap(getAnyProfileAttributeFunction func(string)(bool, int, string, error))(map[string]bool, error){
|
||||||
|
|
||||||
|
passesAllMyDesiresMap := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, desireName := range allMyDesiresList{
|
||||||
|
|
||||||
|
userPassesDesire, err := CheckIfMateProfilePassesMyDesire(desireName, getAnyProfileAttributeFunction)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
passesAllMyDesiresMap[desireName] = userPassesDesire
|
||||||
|
}
|
||||||
|
|
||||||
|
return passesAllMyDesiresMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function will see if a profile passes a user's desire
|
||||||
|
// This involves considering the FilterAll and RequireResponse settings
|
||||||
|
//Outputs:
|
||||||
|
// -bool: User passes my desire
|
||||||
|
// -error
|
||||||
|
func CheckIfMateProfilePassesMyDesire(desireName string, getAnyProfileAttributeFunction func(string)(bool, int, string, error))(bool, error){
|
||||||
|
|
||||||
|
desireAllowsResponseRequired, attributeNameToCheck := mateDesires.CheckIfDesireAllowsRequireResponse(desireName)
|
||||||
|
if (desireAllowsResponseRequired == true){
|
||||||
|
|
||||||
|
getRequireResponseBool := func()(bool, error){
|
||||||
|
|
||||||
|
exists, currentResponseRequired, err := myLocalDesires.GetDesire(desireName + "_RequireResponse")
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (exists == true && currentResponseRequired == "Yes"){
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
requireResponseBool, err := getRequireResponseBool()
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
profileContainsAttribute, _, _, err := getAnyProfileAttributeFunction(attributeNameToCheck)
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (profileContainsAttribute == false){
|
||||||
|
|
||||||
|
// The profile is missing this attribute
|
||||||
|
|
||||||
|
if (requireResponseBool == true){
|
||||||
|
// User does not have a response, but we require one
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// User does not have a response, and we do not require one
|
||||||
|
// User passes this desire
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, currentFilterAllSetting, err := myLocalDesires.GetDesire(desireName + "_FilterAll")
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (exists == false || currentFilterAllSetting != "Yes"){
|
||||||
|
// We do not have filterAll enabled
|
||||||
|
// All users will pass the desire
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
myDesireExists, desireStatusIsKnown, desireIsFulfilled, err := CheckIfMateProfileFulfillsMyDesire(desireName, getAnyProfileAttributeFunction)
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
if (myDesireExists == false){
|
||||||
|
// We have no desire specified
|
||||||
|
// All users will pass this desire (they already fulfilled our RequireResponse desire, if we had one)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if (desireStatusIsKnown == false){
|
||||||
|
|
||||||
|
// This will only happen if the user did not respond
|
||||||
|
// We already checked for this
|
||||||
|
|
||||||
|
return false, errors.New("CheckIfMateProfileFulfillsMyDesire claims attribute response does not exist, but we already checked.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return desireIsFulfilled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Ouputs:
|
||||||
|
// -bool: My Desire exists
|
||||||
|
// -bool: Desire status is known
|
||||||
|
// -bool: Profile fullfills my desire (FilterAll and ResponseRequired are both ignored)
|
||||||
|
// -error
|
||||||
|
func CheckIfMateProfileFulfillsMyDesire(inputDesireName string, getAnyProfileAttributeFunction func(string)(bool, int, string, error))(bool, bool, bool, error){
|
||||||
|
|
||||||
|
getDesireName := func()(string, error){
|
||||||
|
|
||||||
|
if (inputDesireName == "23andMe_AncestryComposition" || inputDesireName == "23andMe_AncestryComposition_Restrictive"){
|
||||||
|
|
||||||
|
settingExists, restrictiveModeEnabled, err := myLocalDesires.GetDesire("23andMe_AncestryComposition_RestrictiveModeEnabled")
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
if (settingExists == true && restrictiveModeEnabled == "Yes"){
|
||||||
|
return "23andMe_AncestryComposition_Restrictive", nil
|
||||||
|
}
|
||||||
|
return "23andMe_AncestryComposition", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputDesireName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
desireName, err := getDesireName()
|
||||||
|
if (err != nil) { return false, false, false, err }
|
||||||
|
|
||||||
|
getMyDesireFunction := func(inputDesire string)(bool, string, error){
|
||||||
|
|
||||||
|
exists, desireValue, err := myLocalDesires.GetDesire(inputDesire)
|
||||||
|
|
||||||
|
return exists, desireValue, err
|
||||||
|
}
|
||||||
|
|
||||||
|
anyDesireExists, desireIsValid, attributeResponseExists, desireIsFulfilled, err := mateDesires.CheckIfMateProfileFulfillsDesire(desireName, getMyDesireFunction, getAnyProfileAttributeFunction)
|
||||||
|
if (err != nil) { return false, false, false, err }
|
||||||
|
if (anyDesireExists == false){
|
||||||
|
// We have no desire (ignoring RequireResponse, which may exist)
|
||||||
|
return false, false, false, nil
|
||||||
|
}
|
||||||
|
if (desireIsValid == false){
|
||||||
|
return false, false, false, errors.New("MyLocalDesires is malformed: Contains invalid " + desireName + " desire.")
|
||||||
|
}
|
||||||
|
if (attributeResponseExists == false){
|
||||||
|
// User did not respond.
|
||||||
|
return true, false, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, true, desireIsFulfilled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
306
internal/encoding/encoding.go
Normal file
306
internal/encoding/encoding.go
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
|
||||||
|
// encoding provides functions to encode and decode data in hex, base32, base64, and MessagePack
|
||||||
|
|
||||||
|
package encoding
|
||||||
|
|
||||||
|
// See what encodings are used for which types of data in Specification.md
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
import "encoding/hex"
|
||||||
|
import "encoding/base64"
|
||||||
|
import "encoding/base32"
|
||||||
|
import "strings"
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
import messagepack "github.com/vmihailenco/msgpack/v5"
|
||||||
|
|
||||||
|
|
||||||
|
func DecodeHexStringToBytes(hexInput string)([]byte, error){
|
||||||
|
|
||||||
|
decodedBytes, err := hex.DecodeString(hexInput)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
return decodedBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeBytesToHexString(input []byte)string{
|
||||||
|
|
||||||
|
result := hex.EncodeToString(input)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const base32Charset = "abcdefghijklmnopqrstuvwxyz234567"
|
||||||
|
|
||||||
|
func EncodeBytesToBase32String(input []byte)string{
|
||||||
|
|
||||||
|
newEncodingObject := base32.NewEncoding(base32Charset)
|
||||||
|
|
||||||
|
result := newEncodingObject.EncodeToString(input)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeBase32StringToBytes(input string)([]byte, error){
|
||||||
|
|
||||||
|
// We have to check for newlines because golang's base32 allows them
|
||||||
|
|
||||||
|
for _, element := range input{
|
||||||
|
|
||||||
|
if (element == '\r' || element == '\n') {
|
||||||
|
return nil, errors.New("Invalid base32 input: contains newline.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newEncodingObject := base32.NewEncoding(base32Charset)
|
||||||
|
|
||||||
|
decodedBytes, err := newEncodingObject.DecodeString(input)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
return decodedBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifies string is a base32 string
|
||||||
|
// Outputs:
|
||||||
|
// -bool: String is base32 charset only
|
||||||
|
// -string: Character that is not base32
|
||||||
|
func VerifyStringContainsOnlyBase32Charset(input string)(bool, string){
|
||||||
|
|
||||||
|
// This iterates through the string's runes
|
||||||
|
for _, element := range input {
|
||||||
|
|
||||||
|
characterString := string(element)
|
||||||
|
|
||||||
|
isBase32Character := strings.ContainsAny(characterString, base32Charset)
|
||||||
|
if (isBase32Character == false) {
|
||||||
|
return false, characterString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeBytesToBase64String(input []byte) string {
|
||||||
|
|
||||||
|
result := base64.URLEncoding.EncodeToString(input)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeBase64StringToBytes(base64Input string) ([]byte, error) {
|
||||||
|
|
||||||
|
decodedBytes, err := base64.URLEncoding.DecodeString(base64Input)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
return decodedBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeBase64StringToUnicodeString(base64Input string)(string, error){
|
||||||
|
|
||||||
|
decodedBytes, err := DecodeBase64StringToBytes(base64Input)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
decodedString := string(decodedBytes)
|
||||||
|
|
||||||
|
return decodedString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will omit empty values
|
||||||
|
func EncodeMessagePackBytes(input interface{}) ([]byte, error) {
|
||||||
|
|
||||||
|
encoder := messagepack.GetEncoder()
|
||||||
|
encoder.SetOmitEmpty(true)
|
||||||
|
encoder.UseCompactInts(true)
|
||||||
|
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
encoder.Reset(&buffer)
|
||||||
|
|
||||||
|
err := encoder.Encode(input)
|
||||||
|
bytes := buffer.Bytes()
|
||||||
|
|
||||||
|
messagepack.PutEncoder(encoder)
|
||||||
|
|
||||||
|
if (err != nil) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func DecodeRawMessagePackToString(data messagepack.RawMessage)(string, error){
|
||||||
|
|
||||||
|
var outputString string
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputString)
|
||||||
|
if (err != nil) { return "", err }
|
||||||
|
|
||||||
|
return outputString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeRawMessagePackToByte(data messagepack.RawMessage)(byte, error){
|
||||||
|
|
||||||
|
var outputByte byte
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputByte)
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
|
||||||
|
return outputByte, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeRawMessagePackToBytes(data messagepack.RawMessage)([]byte, error){
|
||||||
|
|
||||||
|
var outputBytes []byte
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputBytes)
|
||||||
|
if (err != nil) { return nil, err }
|
||||||
|
|
||||||
|
return outputBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used to decode inboxes
|
||||||
|
func DecodeRawMessagePackTo10ByteArray(data messagepack.RawMessage)([10]byte, error){
|
||||||
|
|
||||||
|
var outputArray [10]byte
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputArray)
|
||||||
|
if (err != nil) { return [10]byte{}, err }
|
||||||
|
|
||||||
|
return outputArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used to decode device identifiers
|
||||||
|
func DecodeRawMessagePackTo11ByteArray(data messagepack.RawMessage)([11]byte, error){
|
||||||
|
|
||||||
|
var outputArray [11]byte
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputArray)
|
||||||
|
if (err != nil) { return [11]byte{}, err }
|
||||||
|
|
||||||
|
return outputArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used to decode identity hashes
|
||||||
|
func DecodeRawMessagePackTo16ByteArray(data messagepack.RawMessage)([16]byte, error){
|
||||||
|
|
||||||
|
var outputArray [16]byte
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputArray)
|
||||||
|
if (err != nil) { return [16]byte{}, err }
|
||||||
|
|
||||||
|
return outputArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeRawMessagePackTo22ByteArray(data messagepack.RawMessage)([22]byte, error){
|
||||||
|
|
||||||
|
var outputArray [22]byte
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputArray)
|
||||||
|
if (err != nil) { return [22]byte{}, err }
|
||||||
|
|
||||||
|
return outputArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeRawMessagePackTo24ByteArray(data messagepack.RawMessage)([24]byte, error){
|
||||||
|
|
||||||
|
var outputArray [24]byte
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputArray)
|
||||||
|
if (err != nil) { return [24]byte{}, err }
|
||||||
|
|
||||||
|
return outputArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used to decode message cipher key hashes
|
||||||
|
func DecodeRawMessagePackTo25ByteArray(data messagepack.RawMessage)([25]byte, error){
|
||||||
|
|
||||||
|
var outputArray [25]byte
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputArray)
|
||||||
|
if (err != nil) { return [25]byte{}, err }
|
||||||
|
|
||||||
|
return outputArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeRawMessagePackTo32ByteArray(data messagepack.RawMessage)([32]byte, error){
|
||||||
|
|
||||||
|
var outputArray [32]byte
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputArray)
|
||||||
|
if (err != nil) { return [32]byte{}, err }
|
||||||
|
|
||||||
|
return outputArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeRawMessagePackTo64ByteArray(data messagepack.RawMessage)([64]byte, error){
|
||||||
|
|
||||||
|
var outputArray [64]byte
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputArray)
|
||||||
|
if (err != nil) { return [64]byte{}, err }
|
||||||
|
|
||||||
|
return outputArray, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeRawMessagePackToBool(data messagepack.RawMessage)(bool, error){
|
||||||
|
|
||||||
|
var outputBool bool
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputBool)
|
||||||
|
if (err != nil) { return false, err }
|
||||||
|
|
||||||
|
return outputBool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeRawMessagePackToInt(data messagepack.RawMessage)(int, error){
|
||||||
|
|
||||||
|
var outputInt int
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputInt)
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
|
||||||
|
return outputInt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeRawMessagePackToInt64(data messagepack.RawMessage)(int64, error){
|
||||||
|
|
||||||
|
var outputInt int64
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputInt)
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
|
||||||
|
return outputInt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeRawMessagePackToFloat64(data messagepack.RawMessage)(float64, error){
|
||||||
|
|
||||||
|
var outputFloat float64
|
||||||
|
|
||||||
|
err := DecodeMessagePackBytes(false, data, &outputFloat)
|
||||||
|
if (err != nil) { return 0, err }
|
||||||
|
|
||||||
|
return outputFloat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func DecodeMessagePackBytes(allowUnknownFields bool, data []byte, output interface{}) error {
|
||||||
|
|
||||||
|
decoder := messagepack.GetDecoder()
|
||||||
|
|
||||||
|
//TODO: Make allowUnknownFields also reject ignored fields
|
||||||
|
|
||||||
|
decoder.DisallowUnknownFields(!allowUnknownFields)
|
||||||
|
|
||||||
|
newReader := bytes.NewReader(data)
|
||||||
|
|
||||||
|
decoder.Reset(newReader)
|
||||||
|
|
||||||
|
err := decoder.Decode(output)
|
||||||
|
|
||||||
|
messagepack.PutDecoder(decoder)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue