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:
Simon Sarasova 2024-04-11 13:51:56 +00:00
commit 63a27d2fb8
No known key found for this signature in database
GPG key ID: EEDA4103C9C36944
4053 changed files with 276722 additions and 0 deletions

73
Changelog.md Normal file
View 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
View 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
View 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
View 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)

View 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)

View 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.

View 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.

File diff suppressed because it is too large Load diff

152
documentation/User Guide.md Normal file
View 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
View 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 persons 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 users desires. For example, if a users 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 desires 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

Binary file not shown.

80
go.mod Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

41
gui/adminGui.go Normal file
View 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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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)
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

3256
gui/chatGui.go Normal file

File diff suppressed because it is too large Load diff

1089
gui/contactsGui.go Normal file

File diff suppressed because it is too large Load diff

921
gui/createIdentityGui.go Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

333
gui/desiresGui_Lifestyle.go Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

438
gui/downloadGui.go Normal file
View 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

File diff suppressed because it is too large Load diff

1101
gui/helpGui.go Normal file

File diff suppressed because it is too large Load diff

780
gui/hostGui.go Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

2118
gui/matchesGui.go Normal file

File diff suppressed because it is too large Load diff

4855
gui/moderatorGui.go Normal file

File diff suppressed because it is too large Load diff

1161
gui/peerActionsGui.go Normal file

File diff suppressed because it is too large Load diff

529
gui/questionnaireGui.go Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

624
gui/startupGui.go Normal file
View 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

File diff suppressed because it is too large Load diff

117
gui/syncGui.go Normal file
View 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
View 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
View 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)
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

671
gui/viewContentGui.go Normal file
View 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)
}

View 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
View 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
View 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

File diff suppressed because it is too large Load diff

View 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
}

View 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")
}
}

View 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
}

View 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
}
}

View 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))
}

View 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
}

View 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}
}

View 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
}

View 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
}

View 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}
}

View 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
}

View 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},
}
}

View 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
}

View 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
}

View 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()
}

View 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
}

View 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
}

View 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
}

File diff suppressed because it is too large Load diff

View 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()
}

View 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
}

View 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.")
}
}

View 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
}

View 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
}

View 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)
}
}
}

View 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
}

View 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
}

View file

@ -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.")
}
}
}

View 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
}

View 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
}

View file

@ -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.")
}
}
}

View 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
}

View 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
}

View 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)
}
}

View 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
}

View 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.")
}
}
}

View 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
}

View 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.")
}
}

View 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
}

View 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.")
}
}

View 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
}

View 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.")
}
}

View 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
}

File diff suppressed because it is too large Load diff

View 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
}

View 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
}

View 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
}

View 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